DevOps & Skalierung

OpenTelemetry-Tracing für CAPTCHA-Lösungspipelines

Eine CAPTCHA-Lösung über die CaptchaAI-API dauert je nach Typ von wenigen Sekunden bis über eine Minute. Wird eine Pipeline langsam, lautet die entscheidende Frage: In welcher Phase geht die Zeit verloren – bei der Übermittlung an in.php, beim Polling von res.php oder in der Netzwerklatenz dazwischen? Genau diese Frage beantwortet OpenTelemetry (OTel). Sie instrumentieren Ihre Lösungspipeline einmal und exportieren die Traces anschließend nach Jaeger, Zipkin, Datadog oder jedes andere OTel-kompatible Backend – jede Phase erscheint als eigener, messbarer Span.

Der große Vorteil: OTel ist herstellerneutral. Sie legen sich nicht auf ein Monitoring-Produkt fest, sondern instrumentieren gegen einen offenen Standard und entscheiden später, wohin die Daten fließen. Für Teams in der DACH-Region, die häufig zwischen selbst gehosteten Werkzeugen (Jaeger, Zipkin) und kommerziellen Plattformen wechseln, ist das ein handfester Vorteil.

Warum verteiltes Tracing statt einfacher Logs?

Logs sagen Ihnen, dass eine Lösung 40 Sekunden gedauert hat. Ein Trace zeigt Ihnen, warum: ob 35 Sekunden im Polling steckten, weil der CAPTCHA-Typ langsam war, oder ob die eigentliche Latenz im vorgelagerten Scraping-Schritt lag. Bei einer CAPTCHA-Pipeline mit vielen parallelen Workern und einem externen Lösungsdienst ist diese Aufschlüsselung nach Phasen der schnellste Weg zur Ursache.

Drei Signale liefern in der Praxis den größten Nutzen: die reine Lösungszeit pro Typ, die Anzahl der nötigen Poll-Durchläufe und die Fehlerquote nach Fehlercode. Alle drei fallen bei sauberer Instrumentierung automatisch als Span-Attribute an – ohne dass Sie ein separates Metrik-System pflegen müssen.

Aufbau eines Traces

Ein Trace bildet die Eltern-Kind-Beziehung der Spans ab: Der übergeordnete Solve CAPTCHA-Span umschließt die Phasen Übermittlung, Polling und Token-Einbau. Jeder Poll-Versuch wird als eigener Kind-Span erfasst, sodass Sie im Backend genau sehen, beim wievielten Durchlauf die Lösung eintraf.

[Scrape Page]
  └── [Solve CAPTCHA]                    ← Parent span
        ├── [Submit Task]                ← HTTP POST to in.php
        ├── [Poll Result]               ← Repeated GET to res.php
        │     ├── [Poll Attempt 1]       ← CAPCHA_NOT_READY
        │     ├── [Poll Attempt 2]       ← CAPCHA_NOT_READY
        │     └── [Poll Attempt 3]       ← OK (solution)
        └── [Apply Token]               ← Inject into form

Python: OpenTelemetry einrichten

Pakete installieren

Für eine Python-Pipeline benötigen Sie die OTel-API, das SDK, den OTLP-Exporter und die automatische Instrumentierung der requests-Bibliothek. Letztere erfasst jeden HTTP-Aufruf an in.php und res.php ganz ohne zusätzlichen Code.

pip install opentelemetry-api opentelemetry-sdk \
    opentelemetry-exporter-otlp \
    opentelemetry-instrumentation-requests

Tracer konfigurieren und die Lösung instrumentieren

Der folgende Code richtet einen TracerProvider mit OTLP-Export ein und umschließt die eigentliche Lösung mit drei verschachtelten Spans: captcha.solve als Elternteil, darunter captcha.submit für die Übermittlung und captcha.poll für die Abfrageschleife. Jeder Poll-Durchlauf setzt zusätzlich das Attribut captcha.poll.ready, sodass Sie im Trace den genauen Moment sehen, in dem das Token bereitsteht.

import os
import time
import requests
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
    OTLPSpanExporter,
)
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.trace import StatusCode

# Configure provider
resource = Resource.create({"service.name": "captcha-pipeline"})
provider = TracerProvider(resource=resource)

# Export to OTel Collector (or Jaeger/Zipkin directly)
exporter = OTLPSpanExporter(
    endpoint=os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT",
                            "http://localhost:4317")
)
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)

# Auto-instrument requests library
RequestsInstrumentor().instrument()

tracer = trace.get_tracer("captchaai.solver")
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
session = requests.Session()


def solve_captcha(sitekey, pageurl, captcha_type="recaptcha_v2"):
    """Solve a CAPTCHA with full OpenTelemetry tracing."""
    with tracer.start_as_current_span(
        "captcha.solve",
        attributes={
            "captcha.type": captcha_type,
            "captcha.target_url": pageurl,
        }
    ) as solve_span:

        # Submit phase
        with tracer.start_as_current_span("captcha.submit") as submit_span:
            resp = session.post("https://ocr.captchaai.com/in.php", data={
                "key": API_KEY,
                "method": "userrecaptcha",
                "googlekey": sitekey,
                "pageurl": pageurl,
                "json": 1
            })
            data = resp.json()
            submit_span.set_attribute("http.status_code", resp.status_code)

            if data.get("status") != 1:
                error = data.get("request", "UNKNOWN")
                submit_span.set_status(StatusCode.ERROR, error)
                submit_span.set_attribute("captcha.error", error)
                solve_span.set_status(StatusCode.ERROR, error)
                return {"error": error}

            captcha_id = data["request"]
            submit_span.set_attribute("captcha.id", captcha_id)
            solve_span.set_attribute("captcha.id", captcha_id)

        # Poll phase
        with tracer.start_as_current_span("captcha.poll") as poll_span:
            poll_count = 0
            poll_start = time.time()

            for _ in range(60):
                time.sleep(5)
                poll_count += 1

                with tracer.start_as_current_span(
                    f"captcha.poll.attempt",
                    attributes={"captcha.poll.number": poll_count}
                ) as attempt_span:
                    result = session.get(
                        "https://ocr.captchaai.com/res.php",
                        params={
                            "key": API_KEY,
                            "action": "get",
                            "id": captcha_id,
                            "json": 1
                        }
                    ).json()

                    if result.get("status") == 1:
                        attempt_span.set_attribute("captcha.poll.ready", True)
                        elapsed = time.time() - poll_start
                        poll_span.set_attribute("captcha.poll.count", poll_count)
                        poll_span.set_attribute(
                            "captcha.poll.duration_s", round(elapsed, 2)
                        )
                        solve_span.set_attribute(
                            "captcha.solve_time_s", round(elapsed, 2)
                        )
                        solve_span.set_status(StatusCode.OK)
                        return {
                            "solution": result["request"],
                            "elapsed": elapsed,
                            "polls": poll_count
                        }

                    if result.get("request") != "CAPCHA_NOT_READY":
                        error = result.get("request", "UNKNOWN")
                        attempt_span.set_status(StatusCode.ERROR, error)
                        poll_span.set_status(StatusCode.ERROR, error)
                        solve_span.set_status(StatusCode.ERROR, error)
                        return {"error": error}

                    attempt_span.set_attribute("captcha.poll.ready", False)

            poll_span.set_attribute("captcha.poll.count", poll_count)
            poll_span.set_status(StatusCode.ERROR, "TIMEOUT")
            solve_span.set_status(StatusCode.ERROR, "TIMEOUT")
            return {"error": "TIMEOUT"}

Node.js: dieselbe Pipeline instrumentieren

Pakete installieren

In Node.js übernimmt das NodeSDK die Einrichtung. Die HttpInstrumentation erfasst die Axios-Aufrufe automatisch, sodass Sie sich auf die fachlichen Spans konzentrieren können.

npm install @opentelemetry/api @opentelemetry/sdk-node \
    @opentelemetry/sdk-trace-node \
    @opentelemetry/exporter-trace-otlp-grpc \
    @opentelemetry/instrumentation-http

Umsetzung in Node.js

Wichtig ist hier das saubere Beenden jedes Spans im finally-Block: Vergisst man span.end(), fehlen im Backend Kind-Spans oder der Trace bleibt unvollständig. startActiveSpan propagiert den Kontext automatisch, damit Submit- und Poll-Spans korrekt unter dem captcha.solve-Span einsortiert werden.

const { NodeSDK } = require("@opentelemetry/sdk-node");
const { OTLPTraceExporter } = require("@opentelemetry/exporter-trace-otlp-grpc");
const { HttpInstrumentation } = require("@opentelemetry/instrumentation-http");
const { trace, SpanStatusCode } = require("@opentelemetry/api");
const axios = require("axios");

// Initialize SDK
const sdk = new NodeSDK({
  serviceName: "captcha-pipeline",
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4317",
  }),
  instrumentations: [new HttpInstrumentation()],
});
sdk.start();

const tracer = trace.getTracer("captchaai.solver");
const API_KEY = process.env.CAPTCHAAI_API_KEY;

async function solveCaptchaWithTracing(sitekey, pageurl, captchaType = "recaptcha_v2") {
  return tracer.startActiveSpan("captcha.solve", {
    attributes: { "captcha.type": captchaType, "captcha.target_url": pageurl },
  }, async (solveSpan) => {
    try {
      // Submit
      const captchaId = await tracer.startActiveSpan(
        "captcha.submit",
        async (submitSpan) => {
          try {
            const resp = await axios.post("https://ocr.captchaai.com/in.php", null, {
              params: {
                key: API_KEY, method: "userrecaptcha",
                googlekey: sitekey, pageurl, json: 1,
              },
            });

            if (resp.data.status !== 1) {
              submitSpan.setStatus({ code: SpanStatusCode.ERROR, message: resp.data.request });
              throw new Error(resp.data.request);
            }

            submitSpan.setAttribute("captcha.id", resp.data.request);
            return resp.data.request;
          } finally {
            submitSpan.end();
          }
        }
      );

      solveSpan.setAttribute("captcha.id", captchaId);

      // Poll
      return await tracer.startActiveSpan("captcha.poll", async (pollSpan) => {
        try {
          let pollCount = 0;
          const pollStart = Date.now();

          for (let i = 0; i < 60; i++) {
            await new Promise((r) => setTimeout(r, 5000));
            pollCount++;

            const result = await tracer.startActiveSpan(
              "captcha.poll.attempt",
              { attributes: { "captcha.poll.number": pollCount } },
              async (attemptSpan) => {
                try {
                  const resp = await axios.get("https://ocr.captchaai.com/res.php", {
                    params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
                  });
                  attemptSpan.setAttribute("captcha.poll.ready", resp.data.status === 1);
                  return resp.data;
                } finally {
                  attemptSpan.end();
                }
              }
            );

            if (result.status === 1) {
              const elapsed = (Date.now() - pollStart) / 1000;
              pollSpan.setAttribute("captcha.poll.count", pollCount);
              solveSpan.setAttribute("captcha.solve_time_s", elapsed);
              solveSpan.setStatus({ code: SpanStatusCode.OK });
              return { solution: result.request, elapsed, polls: pollCount };
            }

            if (result.request !== "CAPCHA_NOT_READY") {
              throw new Error(result.request);
            }
          }
          throw new Error("TIMEOUT");
        } catch (err) {
          pollSpan.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
          throw err;
        } finally {
          pollSpan.end();
        }
      });
    } catch (err) {
      solveSpan.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
      return { error: err.message };
    } finally {
      solveSpan.end();
    }
  });
}

module.exports = { solveCaptchaWithTracing };

Traces zentral sammeln: OTel Collector

Statt jeden Worker direkt an ein Backend anzubinden, empfiehlt sich der OTel Collector als zentrale Zwischenstation. Er nimmt Traces per OTLP entgegen, bündelt sie im Batch und leitet sie an Jaeger, Datadog, New Relic oder ein anderes Ziel weiter. Der Vorteil: Ändert sich das Backend, passen Sie nur die Collector-Konfiguration an – nicht den Code auf Dutzenden Workern. In einem GitLab-CI-Setup, wie es in vielen deutschen Unternehmen üblich ist, lässt sich der Collector als eigener Service neben den Solver-Workern betreiben, etwa auf einem Hetzner-Server.

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  batch:
    timeout: 5s

exporters:
  jaeger:
    endpoint: jaeger:14250
    tls:
      insecure: true
  # Or export to Datadog, New Relic, etc.

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [jaeger]

Welche Span-Attribute Sie auswerten

Diese Attribute fallen bei der Instrumentierung oben automatisch an und beantworten im Backend die typischen Betriebsfragen:

Span-Attribut Beispielwert Aussage
captcha.type recaptcha_v2 Welcher CAPTCHA-Typ verbraucht die meiste Zeit?
captcha.solve_time_s 24.5 Tatsächliche Lösungslatenz in Sekunden
captcha.poll.count 5 Wie viele Abfragen bis zur Lösung nötig waren
captcha.error ERROR_WRONG_CAPTCHA_ID Fehler nach Typ aufgeschlüsselt
captcha.id 73519... Einzelne Lösungsversuche gezielt nachverfolgen

Tracing im Produktivbetrieb

Im Entwicklungsbetrieb dürfen Sie jede Lösung tracen. In Produktion arbeiten Sie mit Sampling – etwa 10 % der Traces reichen für ein statistisch belastbares Bild, während die Speicherkosten niedrig bleiben. Fehler sollten Sie dagegen lückenlos erfassen, denn genau dort steckt der diagnostische Wert. Der Overhead ist vernachlässigbar: OTel exportiert asynchron im Batch, ein Span kostet Mikrosekunden. Gemessen an einer Lösungszeit von mehreren Sekunden bis über einer Minute fällt das nicht ins Gewicht.

Ein Wort zur Kardinalität: captcha.id ist als Span-Attribut wertvoll, taugt aber nicht als Metrik-Label. Millionen eindeutiger IDs sprengen jedes Metrik-Backend. Nutzen Sie stattdessen captcha.type und Fehlercodes für Aggregationen und reservieren Sie die ID für die Einzelfall-Analyse im Trace.

Wenn Ihre Pipeline echte Ziel-URLs verarbeitet, denken Sie an die DSGVO: URLs und IP-Adressen können personenbezogene Daten enthalten. Prüfen Sie, welche Attribute Sie in Traces schreiben, und schwärzen Sie sensible Werte vor dem Export – Beobachtbarkeit und Datensparsamkeit schließen sich nicht aus.

Da CaptchaAI Thread-basiert abrechnet – ab 15 $/Monat im Tarif BASIC mit 5 Threads – hilft das Tracing zusätzlich bei der Kapazitätsplanung: Sie sehen an der Verteilung der Lösungszeiten, wie viele Threads Ihre Last tatsächlich braucht.

Häufige Fehler beim Tracing

Problem Ursache Lösung
Keine Traces im Backend sichtbar OTel Collector läuft nicht oder Endpoint stimmt nicht docker ps prüfen, Endpoint-URL und Port 4317 gegenprüfen
Kind-Spans fehlen Ein Span wurde nicht sauber beendet span.end() konsequent im finally-Block aufrufen
Trace ist fragmentiert Kontext wird nicht weitergereicht startActiveSpan verwenden, um den Kontext automatisch zu propagieren
Warnung wegen hoher Kardinalität Zu viele eindeutige Attributwerte captcha.id nicht als Metrik-Label verwenden, nur als Span-Attribut

FAQ

Welche Span-Attribute liefern den größten Mehrwert?

captcha.type, captcha.solve_time_s und captcha.poll.count. Aus diesen drei Werten leiten Sie ab, welcher CAPTCHA-Typ am langsamsten ist, wie hoch die reale Latenz liegt und ob Ihr Polling-Intervall gut gewählt ist. Fehlercodes über captcha.error runden das Bild ab.

Lässt sich OpenTelemetry mit Prometheus und Grafana kombinieren?

Ja. OTel liefert die verteilten Traces, Prometheus die aggregierten Metriken – beide ergänzen sich. Viele Teams tracen mit OTel und exportieren Zähler wie Lösungen pro Minute oder die Erfolgsquote zusätzlich nach Prometheus, um sie in Grafana-Dashboards darzustellen.

Für welche CAPTCHA-Typen funktioniert das Tracing?

Für alle von CaptchaAI unterstützten Typen. Die Instrumentierung ist typ-unabhängig: reCAPTCHA v2/v3, Cloudflare Turnstile und Challenge, GeeTest v3 sowie Bild-, Grid- und BLS-CAPTCHAs werden identisch getract; CaptchaFox, Friendly Captcha und Lemin befinden sich in der Beta. Sie ändern lediglich das captcha.type-Attribut.

Wie vermeide ich Warnungen wegen hoher Kardinalität?

Verwenden Sie hochvariable Werte wie captcha.id ausschließlich als Span-Attribut, nie als Metrik-Label. Für Metriken eignen sich Attribute mit begrenztem Wertebereich – Typ, Fehlercode, Erfolg/Misserfolg –, die sich sauber aggregieren lassen.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.