Tutorials

Strukturierte Protokollierung für CAPTCHA-Vorgänge

Strukturierte JSON-Logs sind der schnellste Weg, CAPTCHA-Vorgänge in der Produktion nachzuvollziehen: Jeder Eintrag trägt Aufgaben-ID, CAPTCHA-Typ, Lösungszeit und Fehlercode – filterbar, durchsuchbar und alarmierbar. Eine Zeile wie "Error solving captcha" sagt dagegen nichts über die betroffene Aufgabe, den Typ oder den Grund des Fehlers.

Sobald mehr als eine Handvoll Lösungen pro Minute durch Ihre Pipeline laufen, ist genau das der Unterschied zwischen „in fünf Minuten behoben" und „eine Stunde in Log-Dateien gescrollt". Dieses Tutorial richtet strukturiertes Logging zweimal ein – in Python mit structlog und in Node.js mit pino – und zeigt anschließend, welche Felder Sie brauchen, wie Sie danach filtern und ab wann ein Alarm sinnvoll ist.


Klartext-Logs gegen strukturiertes JSON

Klartext Strukturiertes JSON
Captcha solved in 12.3s {"event":"captcha_solved","task_id":"abc123","type":"recaptcha_v2","solve_time_ms":12300}
Schwer zu parsen Maschinenlesbar
Nur Grep-Suche Filtern nach jedem beliebigen Feld
Keine Verknüpfung Aufgaben-ID verbindet Übermittlung → Statusabfrage → Einfügen

Der entscheidende Gewinn ist die Verknüpfung: Ein einzelner Lösungsvorgang besteht aus Übermittlung, mehreren Statusabfragen und einem Ergebnis. Trägt jede dieser Zeilen dieselbe task_id, rekonstruieren Sie den kompletten Ablauf mit einer einzigen Suche.

Wer die Logs aus einer GitLab-CI-Pipeline oder von einem Hetzner-Worker an Grafana Loki oder einen ELK-Stack schickt, durchsucht jeden Datensatz nach event, captcha_type oder task_id – statt Textzeilen per grep und Regex zu zerlegen. Das ist besonders bei mehreren parallelen Worker-Prozessen wertvoll, wo Klartext-Logs ineinanderlaufen und sich kaum noch einzelnen Vorgängen zuordnen lassen.


Python: strukturierte Logs mit structlog

structlog hängt eine Kette von Prozessoren aneinander: TimeStamper ergänzt einen ISO-Zeitstempel, add_log_level schreibt das Log-Level als Feld, und JSONRenderer gibt am Ende gültiges JSON aus. Damit wird aus jedem Log-Aufruf automatisch eine maschinenlesbare Zeile:

import structlog
import time

structlog.configure(
    processors=[
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.add_log_level,
        structlog.processors.JSONRenderer(),
    ],
    logger_factory=structlog.PrintLoggerFactory(),
)

log = structlog.get_logger()

Den Lösungs-Lebenszyklus protokollieren

Binden Sie den Kontext (Typ, Ziel-URL, gekürzter Sitekey) früh mit log.bind(). Ab dann trägt jede Zeile dieselben Felder, ohne dass Sie sie erneut übergeben müssen. Sobald die Aufgaben-ID vorliegt, binden Sie auch die task_id – so verknüpft sie alle Folgezeilen zu einem Vorgang:

import requests

API_KEY = "YOUR_API_KEY"


def solve_captcha(captcha_type, sitekey, page_url, proxy=None):
    solve_log = log.bind(
        captcha_type=captcha_type,
        site_url=page_url,
        sitekey=sitekey[:12] + "...",
    )

    # Submit
    start = time.time()
    solve_log.info("captcha_submit_start")

    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": page_url,
        "json": "1",
    }).json()

    if resp["status"] != 1:
        solve_log.error("captcha_submit_failed", error=resp["request"])
        return None

    task_id = resp["request"]
    submit_ms = int((time.time() - start) * 1000)
    solve_log = solve_log.bind(task_id=task_id)
    solve_log.info("captcha_submitted", submit_ms=submit_ms)

    # Poll
    for attempt in range(24):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get", "id": task_id, "json": "1"
        }).json()

        if result["status"] == 1:
            solve_ms = int((time.time() - start) * 1000)
            solve_log.info(
                "captcha_solved",
                solve_time_ms=solve_ms,
                poll_attempts=attempt + 1,
                token_length=len(result["request"]),
            )
            return result["request"]

        if result["request"] != "CAPCHA_NOT_READY":
            solve_log.error(
                "captcha_solve_failed",
                error=result["request"],
                poll_attempts=attempt + 1,
            )
            return None

    solve_log.warning("captcha_solve_timeout", poll_attempts=24)
    return None

Ausgabe:

{"event":"captcha_submit_start","captcha_type":"recaptcha_v2","site_url":"https://example.com","sitekey":"6Le-wvkSAAAA...","timestamp":"2025-07-15T10:30:00Z","level":"info"}
{"event":"captcha_submitted","task_id":"71845302","submit_ms":245,"timestamp":"2025-07-15T10:30:00Z","level":"info"}
{"event":"captcha_solved","task_id":"71845302","solve_time_ms":18230,"poll_attempts":4,"token_length":580,"timestamp":"2025-07-15T10:30:18Z","level":"info"}

Node.js: JSON-Logs mit pino

In Node.js übernimmt pino dieselbe Aufgabe – JSON-Ausgabe mit ISO-Zeitstempel:

const pino = require('pino');

const log = pino({
  level: 'info',
  timestamp: pino.stdTimeFunctions.isoTime,
});

Den Lösungs-Lebenszyklus protokollieren

Statt log.bind() nutzt pino log.child(). Die Schrittfolge – übermitteln, abfragen, auswerten – bleibt identisch:

const axios = require('axios');

const API_KEY = 'YOUR_API_KEY';

async function solveCaptcha(captchaType, sitekey, pageUrl) {
  const taskLog = log.child({
    captchaType,
    siteUrl: pageUrl,
    sitekey: sitekey.substring(0, 12) + '...',
  });

  const start = Date.now();
  taskLog.info('captcha_submit_start');

  const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
    params: {
      key: API_KEY, method: 'userrecaptcha',
      googlekey: sitekey, pageurl: pageUrl, json: 1,
    },
  });

  if (submit.data.status !== 1) {
    taskLog.error({ error: submit.data.request }, 'captcha_submit_failed');
    return null;
  }

  const taskId = submit.data.request;
  const boundLog = taskLog.child({ taskId });
  boundLog.info({ submitMs: Date.now() - start }, 'captcha_submitted');

  for (let attempt = 1; attempt <= 24; attempt++) {
    await new Promise(r => setTimeout(r, 5000));
    const poll = await axios.get('https://ocr.captchaai.com/res.php', {
      params: { key: API_KEY, action: 'get', id: taskId, json: 1 },
    });

    if (poll.data.status === 1) {
      boundLog.info({
        solveTimeMs: Date.now() - start,
        pollAttempts: attempt,
        tokenLength: poll.data.request.length,
      }, 'captcha_solved');
      return poll.data.request;
    }

    if (poll.data.request !== 'CAPCHA_NOT_READY') {
      boundLog.error({ error: poll.data.request, pollAttempts: attempt }, 'captcha_solve_failed');
      return null;
    }
  }

  boundLog.warn({ pollAttempts: 24 }, 'captcha_solve_timeout');
  return null;
}

Referenz der Protokollfelder

Einheitliche Feldnamen sind die Voraussetzung für verlässliche Dashboards:

Feld Typ Beschreibung
event String Ereignisname: captcha_submitted, captcha_solved usw.
task_id String CaptchaAI-Aufgaben-ID zur Korrelation
captcha_type String recaptcha_v2, turnstile, image usw.
site_url String URL der Zielseite
solve_time_ms Integer Gesamtzeit von der Übermittlung bis zur Lösung
poll_attempts Integer Anzahl der durchgeführten Statusabfragen
error String Fehlercode von CaptchaAI
token_length Integer Länge des zurückgegebenen Tokens

Welche Ereignisse gehören ins Log – und welche nicht

Ein sauberes Log protokolliert genau vier Momente pro Vorgang: den Start der Übermittlung, die erfolgreiche Übermittlung mit Aufgaben-ID, das gelöste Ergebnis und – falls es dazu kommt – den Fehler oder das Timeout. Jede einzelne Statusabfrage zu protokollieren, wirkt zunächst gründlich, überschwemmt aber das Log mit CAPCHA_NOT_READY-Zeilen, die keine Erkenntnis liefern.

Die Zuordnung der Log-Level folgt der gleichen Logik: info für den normalen Verlauf, warning für ein Timeout und error für einen echten Fehlschlag. So bleibt eine Filterung nach level == "error" aussagekräftig und schlägt nicht bei Ereignissen an, die im Normalbetrieb erwartbar sind.

Vorgänge über verteilte Worker hinweg korrelieren

In einem Fleet aus mehreren Worker-Prozessen – etwa in Docker-Containern oder Kubernetes-Pods – reicht die task_id allein oft nicht aus, um nachzuvollziehen, welcher Prozess einen Vorgang bearbeitet hat. Binden Sie in diesem Fall zusätzlich eine worker_id und, falls vorhanden, eine übergeordnete run_id Ihres Batch-Laufs. Beide Felder kosten kaum Speicher, machen aber aus vielen ineinanderlaufenden Log-Strömen wieder eine nachvollziehbare, pro Worker filterbare Kette.


Logs filtern und Alarme auslösen

Alle Fehler der letzten Stunde finden

Da jede Zeile gültiges JSON ist, filtert jq direkt:

# With jq
cat captcha.log | jq 'select(.level == "error" and .event == "captcha_solve_failed")'

Alarm bei erhöhter Fehlerquote

Der folgende Monitor über ein gleitendes Fenster schlägt an, sobald die Fehlerquote 20 % überschreitet:

# Count errors vs successes in a rolling window
from collections import deque

class ErrorRateMonitor:
    def __init__(self, window_size=100, threshold=0.2):
        self.results = deque(maxlen=window_size)
        self.threshold = threshold

    def record(self, success):
        self.results.append(success)
        if len(self.results) >= 50:
            error_rate = 1 - sum(self.results) / len(self.results)
            if error_rate > self.threshold:
                log.warning(
                    "captcha_error_rate_high",
                    error_rate=round(error_rate, 3),
                    window=len(self.results),
                )

Zum Datenschutz: site_url und Proxy-Angaben können nach DSGVO personenbezogen sein. Kürzen Sie Sitekeys und protokollieren Sie niemals API-Schlüssel oder Token.


Häufige Probleme beheben

Problem Ursache Lösung
Logs zu ausführlich Jede Statusabfrage wird protokolliert Nur Übermittlung, Lösung und Fehler protokollieren
Ereignisse nicht korrelierbar Fehlende Aufgaben-ID task_id früh mit log.bind() oder log.child() binden
Logs nicht durchsuchbar Reines Textformat Mit structlog oder pino auf JSON umstellen
Sensible Daten im Log Vollständiger API-Schlüssel wird protokolliert API-Schlüssel nie protokollieren, Sitekeys kürzen

Häufige Fragen

Warum JSON-Logs statt Klartext für CAPTCHA-Workflows?

Weil jedes Feld einzeln durchsuchbar wird – Sie filtern nach captcha_type, error oder task_id und bauen daraus Alarme.

Verursacht ausführliches Logging bei CaptchaAI Zusatzkosten?

Nein. CaptchaAI rechnet pro gleichzeitigem Thread ab, nicht pro Lösung – bei BASIC (15 $/Monat, 5 Threads) mit unbegrenzten Lösungen pro Thread. Ihr Log-Volumen betrifft nur den eigenen Speicher.

Wie sende ich die Logs an ein zentrales System wie Grafana Loki?

Schreiben Sie eine JSON-Zeile pro Ereignis nach stdout und lassen Sie einen Agenten (Promtail, Fluent Bit oder Vector) sie einsammeln – die Felder stehen dann sofort als Labels bereit.

Wie behalte ich zusammengehörige Ereignisse im Blick?

Binden Sie die task_id direkt nach der Übermittlung mit log.bind() (Python) oder log.child() (Node.js). Danach trägt jede Folgezeile dieselbe ID, und der gesamte Vorgang erscheint als eine durchsuchbare Kette. In verteilten Setups ergänzen Sie zusätzlich eine worker_id.

Bremst strukturiertes Logging die Lösung aus?

Nein, der Effekt ist vernachlässigbar. structlog und pino serialisieren eine Zeile in Mikrosekunden, während die eigentliche Lösungszeit von Netzwerk und CAPTCHA-Verarbeitung bestimmt wird. Solange Sie nicht jede einzelne Statusabfrage protokollieren, fällt das Logging in der Bilanz nicht ins Gewicht.


Beobachtbare CAPTCHA-Workflows mit CaptchaAI aufbauen

Holen Sie sich Ihren API-Schlüssel unter captchaai.com und protokollieren Sie Ihren ersten Lösungs-Lebenszyklus strukturiert.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.