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
- CAPTCHA-Ergebnisse in PostgreSQL speichern
- Ein CaptchaAI-Nutzungs-Dashboard aufbauen
- Lösungsraten mit Prometheus und Grafana überwachen