Pipeline giải CAPTCHA chạy êm ở staging nhưng lên production lại chậm bất thường vào một số khung giờ, hoặc lỗi rải rác không theo quy luật — câu hỏi khó nhất lúc đó là "lỗi nằm ở đâu: gửi task, polling, hay thuần túy độ trễ mạng?" OpenTelemetry (OTel) trả lời câu hỏi đó bằng distributed tracing trung lập nhà cung cấp: instrument một lần, xuất trace sang Jaeger, Zipkin, Datadog hoặc backend nào tương thích OTel, rồi thấy chính xác thời gian trôi ở đâu trong luồng gửi API → polling → áp token.
Tình huống thường gặp: team QA của một công ty outsourcing tại TP.HCM vận hành pipeline theo dõi giá trên các sàn thương mại điện tử, xử lý vài nghìn CAPTCHA mỗi ngày qua CaptchaAI. Khi tỷ lệ lỗi tăng đột biến vào một khung giờ nhất định, log text thuần không đủ để biết lỗi rơi vào bước nào. Đây là lúc trace theo span phát huy tác dụng: mở trace của một lần giải lỗi, thấy ngay span nào set status ERROR và giá trị captcha.error tương ứng — thay vì đoán mò qua hàng nghìn dòng log.
Cấu trúc trace: span cha bọc span con
Mỗi lần giải một CAPTCHA tạo ra một cây span: span captcha.solve là cha, bọc span captcha.submit, span captcha.poll (với span con captcha.poll.attempt cho từng lượt polling) và bước áp token vào 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
Instrument Python với OpenTelemetry
Cài đặt
Cài các package sau trước khi viết code instrument:
pip install opentelemetry-api opentelemetry-sdk \
opentelemetry-exporter-otlp \
opentelemetry-instrumentation-requests
Triển khai
Đoạn code dưới đây bọc toàn bộ luồng gửi task, polling và trả kết quả trong span, gắn attribute để sau này lọc theo captcha.type, captcha.id hoặc thời gian giải:
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"}
Instrument JavaScript/Node.js với OpenTelemetry
Cài đặt
npm install @opentelemetry/api @opentelemetry/sdk-node \
@opentelemetry/sdk-trace-node \
@opentelemetry/exporter-trace-otlp-grpc \
@opentelemetry/instrumentation-http
Triển khai
Bản Node.js dùng đúng cấu trúc span cha/con như bản Python, chỉ khác cú pháp async/await:
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 };
Cấu hình OTel Collector
Không muốn export thẳng từ ứng dụng? Chạy OTel Collector làm lớp trung gian: nhận trace qua gRPC rồi đẩy sang Jaeger, Datadog, New Relic — đổi backend chỉ cần sửa cấu hình, không đụng code:
# 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]
Thông tin bạn đọc được từ trace
| Thuộc tính span | Giá trị mẫu | Ý nghĩa |
|---|---|---|
captcha.type |
recaptcha_v2 |
Loại CAPTCHA nào tốn thời gian giải nhất |
captcha.solve_time_s |
24.5 |
Độ trễ giải thực tế, tính bằng giây |
captcha.poll.count |
5 |
Cần bao nhiêu vòng polling mới có kết quả |
captcha.error |
ERROR_WRONG_CAPTCHA_ID |
Phân loại lỗi để biết nên sửa ở đâu |
captcha.id |
73519... |
Truy lại chính xác một lần giải khi debug |
Xử lý sự cố thường gặp
| Vấn đề | Nguyên nhân | Cách xử lý |
|---|---|---|
| Không thấy trace nào | OTel Collector chưa chạy | Kiểm tra docker ps; xác nhận lại endpoint URL |
| Thiếu span con | Span không được .end() đúng cách |
Luôn gọi span.end() trong khối finally |
| Trace bị đứt đoạn | Context không được truyền tiếp | Dùng startActiveSpan để tự động propagate context |
| Cảnh báo cardinality cao | Quá nhiều giá trị attribute duy nhất | Không dùng captcha.id làm tag trong metrics |
Câu hỏi thường gặp
OpenTelemetry hay SDK riêng của Datadog/New Relic — chọn cái nào?
Nếu bạn chỉ dùng một backend quan sát và không có ý định đổi, SDK riêng của nhà cung cấp vẫn ổn. Chọn OpenTelemetry khi có khả năng đổi platform trong tương lai, hoặc khi dùng nhiều backend cùng lúc (Jaeger cho trace, Prometheus cho metric) — instrument một lần, export đi đâu cũng được.
Trace có giúp tìm ra vì sao một số task giải CAPTCHA mất hơn 60 giây không?
Có. Mở trace của task đó và xem span captcha.poll: nếu captcha.poll.count cao bất thường, vấn đề nằm ở tốc độ giải phía CaptchaAI hoặc mạng, không phải code của bạn. Nếu span captcha.submit mất nhiều thời gian, kiểm tra kết nối tới in.php trước.
Chạy OTel Collector có cần server riêng không?
Không bắt buộc. Với pipeline nhỏ, export thẳng từ ứng dụng sang Jaeger hoặc Datadog qua OTLP là đủ. Chỉ cần Collector riêng khi muốn batch, lọc bớt attribute nhạy cảm, hoặc fan-out trace tới nhiều backend cùng lúc.
Sampling 10% trace có bỏ sót lỗi hiếm gặp không?
Không, nếu tách rule sampling theo status. Lấy mẫu 10% trace thành công để tiết kiệm chi phí lưu trữ, nhưng luôn giữ 100% trace lỗi — cách áp dụng phổ biến trong production.
Bước tiếp theo
Muốn có trace đầy đủ cho từng lần giải CAPTCHA trong pipeline của bạn? Lấy API key CaptchaAI rồi gắn đoạn code OpenTelemetry ở trên vào luồng gửi/polling hiện có.
Hướng dẫn liên quan: