Tutorials

Logging Terstruktur untuk Operasi CAPTCHA

Satu baris log yang layak dipakai untuk operasi CAPTCHA berisi minimal empat hal: task_id, jenis CAPTCHA, durasi solve dalam milidetik, dan kode error dari API. Jika log Anda hanya berbunyi "Error solving captcha", pertanyaan paling dasar pun tidak terjawab — solve mana yang gagal dan apakah masalahnya di sisi Anda atau di sisi target.

Bagi developer scraping lepas dan agensi price-monitoring di Indonesia, konsekuensinya bukan sekadar soal debugging. Ketika klien menanyakan kenapa job semalam hanya menghasilkan separuh data, log JSON dengan task_id dan solve_time_ms adalah bukti yang bisa ditunjukkan dalam hitungan menit. Panduan ini membahas field apa yang perlu dicatat, cara memasangnya di Python (structlog) dan Node.js (pino), lalu cara memfilter dan memasang alert.


Field JSON yang wajib ada di setiap peristiwa solve

Sebelum menyentuh library, tetapkan dulu kontrak field-nya supaya query yang Anda tulis hari ini masih jalan enam bulan lagi:

Teks biasa JSON terstruktur
Captcha solved in 12.3s {"event":"captcha_solved","task_id":"abc123","type":"recaptcha_v2","solve_time_ms":12300}
Sulit diurai program Langsung terbaca mesin
Hanya bisa dicari dengan grep Bisa difilter per field
Tidak ada korelasi task_id merangkai kirim → polling → token

Kolom kanan itulah target Anda. Perhatikan solve_time_ms disimpan sebagai angka, bukan string "12.3s": begitu jadi angka, Anda bisa menghitung p95, membandingkan reCAPTCHA v2 dengan Turnstile, atau memicu alert saat median melonjak — tanpa parsing tambahan. Karena reCAPTCHA v2, reCAPTCHA v3, Turnstile, dan GeeTest v3 punya karakter waktu penyelesaian berbeda, captcha_type di setiap baris membuat perbedaan itu terlihat alih-alih tenggelam di rata-rata gabungan.


Pasang structlog di worker Python

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()

Konfigurasi di atas menambahkan timestamp ISO 8601, menyisipkan level log sebagai field tersendiri, lalu merender semuanya sebagai satu baris JSON — format yang langsung ditelan jq, Loki, atau agent log aggregator lain tanpa parser khusus.

Catat siklus hidup solve dari kirim sampai token siap

Kode di bawah mengikuti alur empat langkah standar CaptchaAI: kirim task ke in.php → simpan task ID → polling ke res.php → pakai token. Kuncinya log.bind(): begitu task_id diikat, setiap baris berikutnya otomatis membawanya.

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

Perhatikan sitekey=sitekey[:12] + "...": sitekey dipotong agar tetap terkorelasi tanpa menyimpan nilai penuh. API key tidak pernah masuk log.

Contoh output yang dihasilkan:

{"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"}

Tiga baris itu bercerita utuh: submit selesai dalam 245 ms, solve tuntas 18.230 ms setelah pengiriman, dan hanya butuh 4 kali polling. Tanpa task_id yang sama di ketiganya, korelasi otomatis seperti ini mustahil.


Pasang pino di worker Node.js

const pino = require('pino');

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

pino menulis JSON secara bawaan dengan overhead kecil, aman untuk worker yang memproses ribuan task per jam. Padanan log.bind() di sini adalah log.child().

Catat siklus hidup solve dengan child logger

Urutannya identik dengan versi Python: taskLog membawa konteks permintaan, lalu boundLog menambahkan taskId begitu API mengembalikannya.

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;
}

Referensi field log CaptchaAI

Delapan field ini cukup untuk hampir semua pertanyaan operasional. Pakai nama yang persis sama di seluruh worker — begitu satu service menulis solveTime dan yang lain solve_time_ms, dashboard Anda pecah dua.

Field Tipe Keterangan
event string Nama peristiwa: captcha_submitted, captcha_solved, dsb.
task_id string ID task CaptchaAI, kunci korelasi
captcha_type string recaptcha_v2, turnstile, image, dsb.
site_url string URL halaman target
solve_time_ms integer Waktu dari kirim sampai token diterima
poll_attempts integer Jumlah request polling
error string Kode error dari CaptchaAI
token_length integer Panjang token hasil solve

Kalau worker Anda berjalan di beberapa region — misalnya AWS ap-southeast-3 (Jakarta) dan ap-southeast-1 (Singapura) — tambahkan field region. Perbedaan latency langsung terlihat saat solve_time_ms dibandingkan per region, jadi solver tidak disalahkan untuk masalah rute jaringan.


Filter log dan pasang alert error rate

Dua pola berikut menutup kebutuhan harian: query manual saat investigasi, dan alert otomatis saat pola kegagalan muncul.

Cari semua kegagalan dari file log

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

Karena setiap baris adalah JSON valid, jq menyaring per field tanpa regex rapuh. Ubah select(...) sesuai kebutuhan, misalnya per captcha_type.

Alert saat error rate melewati ambang

Satu kegagalan itu normal; yang perlu membangunkan Anda adalah pergeseran pola, misalnya kegagalan naik dari 3% ke 25%. Monitor berikut menghitung rasio pada jendela bergulir dan berbunyi hanya setelah sampelnya cukup:

# 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),
                )

Pakai log untuk mengukur pemakaian thread

CaptchaAI menagih per thread — satu thread berarti satu CAPTCHA yang sedang diproses — dan setiap paket memberi solve tanpa batas selama bulan berjalan. Biaya Anda ditentukan oleh berapa banyak solve yang berjalan bersamaan, bukan totalnya sebulan. Di sinilah solve_time_ms berubah dari angka debugging jadi angka perencanaan kapasitas.

Hitungannya sederhana: kalau rata-rata solve_time_ms sekitar 15.000 dan Anda perlu menyelesaikan 1.000 CAPTCHA dalam satu jam, kebutuhan konkurensi Anda kira-kira 1000 × 15 ÷ 3600 ≈ 4–5 thread — masuk paket BASIC ($15/bulan, 5 thread). Kalau volume naik jadi 10.000 per jam, kebutuhannya bergeser ke kisaran ADVANCE ($90/bulan, 50 thread). Tanpa log durasi per solve, ukuran paket hanya jadi tebakan.


Masalah umum dan cara memperbaikinya

Masalah Penyebab Perbaikan
Log terlalu berisik Setiap percobaan polling dicatat Catat hanya kirim, berhasil, dan gagal
Baris log tidak terkorelasi task_id belum diikat Ikat lewat log.bind() atau log.child()
Tidak bisa dicari per field Masih format teks biasa Pindah ke JSON dengan structlog atau pino
Data sensitif tercatat API key lengkap masuk log Jangan catat API key; potong sitekey
Durasi solve tidak wajar Timer dimulai setelah request Ambil start sebelum request ke in.php

Pertanyaan yang sering muncul

Berapa lama log solve CAPTCHA sebaiknya disimpan?

Untuk debugging harian, 14–30 hari biasanya cukup. Kalau log dipakai sebagai bukti laporan ke klien, simpan ringkasan agregat harian (jumlah solve, rata-rata durasi, jumlah error) lebih lama dan buang baris mentahnya lebih cepat.

Apakah structured logging memperlambat worker saya?

Dampaknya kecil dibanding waktu tunggu jaringan: satu solve memakan detik, menulis satu baris JSON memakan mikrodetik. Yang benar-benar memperlambat adalah mencatat setiap percobaan polling ke disk.

Apakah aman mencatat site_url lengkap?

Aman selama URL tidak mengandung parameter sesi atau data pribadi; potong query string yang membawa identitas pengguna sebelum menulis log. UU Pelindungan Data Pribadi (UU 27/2022) menjadikan penyaringan data pribadi dari log sebagai praktik yang wajar.

Bagaimana cara mengukur tingkat keberhasilan per jenis CAPTCHA?

Kelompokkan peristiwa captcha_solved dan captcha_solve_failed berdasarkan field captcha_type, lalu hitung rasionya per jendela waktu. Karena captcha_type sudah ada di setiap baris, query ini tidak butuh perubahan kode.

Tipe CAPTCHA apa saja yang perlu masuk field captcha_type?

Isi dengan tipe yang memang didukung: reCAPTCHA v2 dan v3, Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3, serta CAPTCHA gambar dan grid. hCaptcha dan FunCaptcha tidak didukung saat ini, dan GeeTest v4 masih berstatus segera hadir. Satu skema log cukup untuk semua tipe.


Mulai catat setiap solve dengan task ID

Ambil API key Anda di captchaai.com, tempelkan ke contoh structlog atau pino di atas, lalu jalankan satu solve percobaan. Baris JSON pertama membuktikan pipeline observabilitas Anda hidup.


Bacaan lanjutan

Komentar dinonaktifkan untuk artikel ini.