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: