DevOps & Scaling

OpenTelemetry Tracing untuk Pipeline Solve CAPTCHA

Trace Anda menunjukkan satu solve reCAPTCHA v2 makan waktu 24 detik — tapi dari mana asalnya? Network ke in.php, antrean penyelesaian di sisi CaptchaAI, atau interval polling yang Anda set sendiri di res.php? Tanpa tracing, jawabannya cuma tebakan. OpenTelemetry (OTel) menjawabnya dengan span per fase: instrumentasikan pipeline solve CAPTCHA Anda satu kali, lalu ekspor jejaknya ke Jaeger, Zipkin, Datadog, atau backend apa pun yang kompatibel dengan OTel — vendor mana pun yang dipakai tim Anda sekarang atau nanti.

Kenapa Pipeline Solve CAPTCHA Butuh Distributed Tracing

Log biasa memberi tahu kapan sebuah task disubmit dan kapan token diterima — tapi tidak memberi tahu kenapa jeda di antaranya kadang 8 detik, kadang 45 detik. Untuk tim automation yang menjalankan worker di kawasan seperti AWS ap-southeast-1 (Singapura) atau GCP asia-southeast2 (Jakarta), pertanyaan ini penting: apakah solve time yang naik itu network latency ke endpoint CaptchaAI, interval polling yang terlalu lambat, atau memang CAPTCHA-nya sendiri butuh waktu lebih lama? Distributed tracing memecah satu solve CAPTCHA jadi span per fase — submit, tiap percobaan polling, dan penerapan token — sehingga jawabannya langsung terlihat di dashboard, bukan hasil menebak dari membaca log satu per satu.

Tiga gejala yang biasa memicu tim automation akhirnya memasang tracing:

  • Solve time rata-rata "kelihatan" normal di dashboard billing, tapi keluhan timeout dari QA tetap masuk
  • Tidak jelas apakah lambat itu network ke ocr.captchaai.com, antrean di sisi Anda, atau memang CAPTCHA-nya berat
  • Log submit dan log hasil solve ada di file terpisah, jadi menyusun urutan kejadian per task makan waktu lebih lama daripada menyelesaikan masalahnya sendiri

Jawabannya bukan menambah lebih banyak print() atau log baris demi baris — tapi satu parent span per solve, dengan child span untuk tiap fase, supaya urutan dan durasinya kelihatan langsung di satu tampilan.

Bentuk Trace: dari Scrape sampai Token Diterapkan

Sebelum masuk ke kode, begini bentuk trace yang akan Anda dapat: satu parent span Solve CAPTCHA membungkus tiga child span — submit task, polling (dengan span anak per percobaan), dan penerapan token ke form.

[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

Setelah instrumentasi jalan, berikut atribut span yang bisa Anda cari dan filter di dashboard tracing — bacaan yang bagus untuk dipegang sebelum masuk ke kode implementasinya:

Atribut Span Nilai Wawasan
captcha.type recaptcha_v2 Jenis CAPTCHA mana yang paling sering jadi bottleneck
captcha.solve_time_s 24.5 Latensi solve yang sebenarnya, bukan estimasi
captcha.poll.count 5 Berapa kali polling dibutuhkan sampai token siap
captcha.error ERROR_WRONG_CAPTCHA_ID Rincian jenis error, langsung dari respons API
captcha.id 73519... Telusuri satu upaya solve tertentu sampai ke akar masalahnya

captcha.poll.count yang tinggi biasanya pertanda interval polling Anda terlalu rapat. Turunkan frekuensinya alih-alih menambah thread — CaptchaAI menagih per thread konkuren yang Anda pakai, bukan per request polling, jadi polling yang boros hanya membuang siklus tanpa mempercepat solve.

Instrumentasi OpenTelemetry di Python

Instalasi dependency

Instal SDK OTel plus exporter OTLP dan auto-instrumentation untuk requests:

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

Kode instrumentasi lengkap

Fungsi solve_captcha di bawah membungkus tiga fase — submit, polling, dan status akhir — masing-masing sebagai span terpisah, dan mengisi atribut-atribut di atas lewat set_attribute:

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

Instrumentasi OpenTelemetry di Node.js

Instalasi dependency

Paket Node.js-nya setara dengan versi Python — SDK, exporter OTLP, dan instrumentation HTTP otomatis:

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

Kode instrumentasi lengkap

Polanya sama seperti Python: solveCaptchaWithTracing membungkus submit dan polling sebagai span anak, lalu menutup tiap span di blok finally supaya trace tidak pernah terfragmentasi meski terjadi error:

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

Konfigurasi OTel Collector

Kalau Anda mengekspor ke Jaeger atau backend self-hosted lain, OTel Collector jadi perantara antara aplikasi dan backend observability-nya. Contoh konfigurasi minimal:

# 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]

Debug Cepat: Masalah Umum saat Tracing

Sebelum menelusuri tabelnya, cek tiga hal ini dulu:

  • Apakah OTel Collector-nya benar-benar sedang berjalan di endpoint yang Anda konfigurasi?
  • Apakah setiap span punya pasangan .end(), termasuk di jalur error?
  • Apakah context span diteruskan lewat startActiveSpan, bukan dibuat lepas satu-satu?

Empat masalah yang paling sering muncul saat pertama kali memasang tracing di pipeline solve CAPTCHA:

Masalah Penyebab Solusi
Tidak ada jejak yang muncul OTel Collector tidak berjalan Periksa docker ps; verifikasi URL endpoint-nya
Child span tidak ada Span tidak diakhiri dengan benar Selalu panggil span.end() di blok finally
Trace terfragmentasi Context tidak ikut disebarkan Pakai startActiveSpan supaya context otomatis menyebar
Peringatan kardinalitas tinggi Terlalu banyak nilai atribut unik Jangan pakai captcha.id sebagai tag di metrics

Pertanyaan Seputar Tracing Pipeline CAPTCHA

Apakah tracing menambah latensi ke proses solve CAPTCHA?

Praktis tidak. OTel mengekspor span secara batch dan asinkron, jadi overhead-nya cuma mikrodetik per span. Untuk task solve CAPTCHA yang makan waktu 5–120 detik, tambahan itu tidak akan pernah terasa atau terukur di trace Anda sendiri.

Perlukah saya melacak setiap penyelesaian CAPTCHA, atau cukup sebagian?

Di development, lacak semuanya — Anda sedang mencari pola, jadi butuh data lengkap. Di produksi, sampling (misalnya 10% dari total trace) sudah cukup untuk visibilitas statistik sambil menekan biaya storage backend observability. Satu pengecualian: lacak setiap error di 100%, karena itu justru data yang paling Anda butuhkan saat troubleshooting.

Backend observability apa yang cocok untuk tim kecil — self-host Jaeger atau layanan terkelola seperti Datadog?

Tergantung kapasitas ops tim Anda. Jaeger self-hosted gratis dari sisi lisensi, tapi Anda menanggung uptime dan storage-nya sendiri. Layanan terkelola seperti Datadog atau New Relic mengurangi beban operasional tapi menambah biaya bulanan. OTel netral terhadap keduanya — mulai dari Jaeger lokal, lalu pindah backend kapan saja tanpa mengubah kode instrumentasi.

Bagaimana cara menghubungkan trace ini dengan kode error CaptchaAI seperti ERROR_WRONG_CAPTCHA_ID?

Atribut captcha.error pada span captcha.submit atau captcha.poll.attempt menyimpan kode error mentah dari respons CaptchaAI — sama seperti nilai kolom request pada res.php. Cari atribut ini di dashboard tracing untuk melihat sebaran jenis error tanpa grep log satu per satu.

Apakah CaptchaAI menyediakan dashboard tracing bawaan?

Tidak. CaptchaAI adalah REST API murni — endpoint in.php untuk submit dan res.php untuk polling — tanpa dashboard observability bawaan di sisinya. Instrumentasi OTel seperti di panduan ini yang membuat trace-nya ada; CaptchaAI hanya perlu menerima request dan mengembalikan respons seperti biasa.

Langkah Selanjutnya

Pasang tracing di pipeline solve CAPTCHA Anda sendiri — dapatkan API key CaptchaAI dan tambahkan instrumentasi OpenTelemetry di atas kode yang sudah berjalan hari ini.

Panduan terkait:

Komentar dinonaktifkan untuk artikel ini.