DevOps & Scaling

CAPTCHA Çözme İşlem Hatları için OpenTelemetry İzleme

CAPTCHA çözümü ortalama yirmi saniye sürdüğünde asıl soru şudur: bu süre tam olarak nerede geçiyor? OpenTelemetry (OTel), iş hattınızı span'lere bölerek görev gönderimini, sonuç sorgulamasını ve ağ gecikmesini ayrı ayrı ölçmenizi sağlar — böylece tahmin etmeyi bırakıp gecikmeyi grafik üzerinde görürsünüz. Enstrümantasyonu bir kez kurarsınız; izleri Jaeger, Zipkin, Datadog veya OTel uyumlu herhangi bir arka uca, satıcıya kilitlenmeden aktarırsınız.

Diyelim ki İstanbul'daki bir ekip için ödeme akışını test eden bir otomasyon geliştiricisisiniz ve çözüm süresi ara sıra fırlıyor. İz olmadan sorunun CaptchaAI gönderiminde mi, proxy'nizde mi yoksa hedef sayfanın yüklenmesinde mi olduğunu bilemezsiniz. Aşağıdaki enstrümantasyon tam olarak bu ayrımı verir.

İz yapısı: bir çözüm hangi span'lere ayrılır

  • captcha.solve — tüm çözümü kapsayan üst span (parent span).
  • captcha.submit — görevi in.php'ye POST eden ve dönen kimliği kaydeden alt span.
  • captcha.poll — sonucu res.php'den periyodik sorgulayan alt span; her deneme kendi alt span'ini açar.
  • Token uygulaması — çözülen token'ı forma yerleştiren son adım.
[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

Python ile OpenTelemetry enstrümantasyonu

Önce OTel API'sini, SDK'yı, OTLP dışa aktarıcısını ve requests kütüphanesi için otomatik enstrümantasyon paketini kurun:

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

Ardından TracerProviderservice.name ile yapılandırın, izleri OTLP üzerinden bir OTel Collector'a (ya da doğrudan Jaeger/Zipkin'e) gönderin ve requests çağrılarını otomatik enstrümante edin. Çözüm fonksiyonunu üç span katmanına sarın: captcha.solve (üst span), captcha.submit ve captcha.poll. Her aşamaya captcha.type, captcha.id ve captcha.solve_time_s gibi öznitelikler ekleyin — bunlar sonradan izlerde filtreleyeceğiniz alanlardır. Böylece tek bir çözümün hangi aşamasında ne kadar beklediğini uçtan uca görürsünüz.

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

Node.js ile OpenTelemetry enstrümantasyonu

Node.js tarafında SDK'yı, OTLP gRPC dışa aktarıcısını ve HTTP enstrümantasyonunu kurun:

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

Ardından NodeSDK'yı başlatın ve aynı span hiyerarşisini startActiveSpan ile kurun. startActiveSpan bağlamı otomatik yaydığı için alt span'ler üst span'e doğru şekilde bağlanır. Her span'i finally bloğunda end() ile kapatmayı unutmayın; aksi hâlde izler eksik ya da parçalanmış görünür.

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

OTel Collector yapılandırması

İzleri uygulamadan doğrudan arka uca göndermek yerine araya bir OTel Collector koymak üretimde çok daha esnektir: uygulamanız yalnızca Collector'ı tanır, hedefi (Jaeger, Datadog, New Relic…) tek bir yapılandırma dosyasından değiştirirsiniz. Aşağıdaki yapılandırma OTLP'yi gRPC üzerinden 4317 portunda dinler, span'leri toplu işler ve Jaeger'a aktarır:

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

İzlerde hangi metrikleri göreceksiniz

İzler geldikten sonra her span'in öznitelikleri üzerinden filtreleme ve gruplama yapabilirsiniz. En çok işinize yarayacak alanlar şunlardır:

  • captcha.type (recaptcha_v2) — hangi CAPTCHA türünün en uzun sürdüğünü kırılımla gösterir.
  • captcha.solve_time_s (24.5) — gönderimden çözüme kadar geçen gerçek süre.
  • captcha.poll.count (5) — sonuç hazır olana kadar kaç sorgulama yapıldığı.
  • captcha.error (ERROR_WRONG_CAPTCHA_ID) — hataların türe göre dağılımı.
  • captcha.id (73519...) — tek bir çözüm girişimini uçtan uca izleme.

Sorun giderme

Sorun Sebep Düzeltme
İz hiç görünmüyor OTel Collector çalışmıyor docker ps ile kontrol edin; uç nokta URL'sini doğrulayın
Alt span'ler eksik Span düzgün kapatılmadı finally bloğunda her zaman span.end() çağırın
İzler parçalanmış Bağlam yayılmadı Bağlamı otomatik yaymak için startActiveSpan kullanın
Yüksek kardinalite uyarısı Çok fazla benzersiz öznitelik değeri Metriklerde captcha.id'yi etiket olarak kullanmayın

İzleri kapasite planlaması için kullanmak

İzler yalnızca hata ayıklamaya yaramaz; kapasite planlaması için de değerlidir. captcha.poll.count ve captcha.solve_time_s değerlerinin dağılımı, aynı anda kaç çözümün kuyrukta beklediğini gösterir. CaptchaAI planları thread tabanlıdır — yani eşzamanlı in-flight CAPTCHA sayısına göre fiyatlanır, çözüm başına değil. BASIC ($15/ay, 5 thread) ile başlayıp izlerde thread doygunluğu görürseniz STANDARD ($30/ay, 15 thread) veya ADVANCE ($90/ay, 50 thread) planına geçmek, TL'nin dalgalı olduğu bir ortamda öngörülebilir aylık USD maliyetini korur. Aynı izler, yoğun saatlerde (Europe/Istanbul saat dilimine göre akşam trafiği gibi) çözüm süresinin nasıl değiştiğini de ortaya koyar; kapasiteyi tahminle değil, ölçülen thread kullanımıyla ayarlarsınız.

Sık sorulan sorular

Çözüm süresinin çoğu ağ gecikmesinde mi yoksa sorgulama beklemesinde mi geçiyor, nasıl anlarım?

captcha.submit span'inin süresi ağa ve gönderime, captcha.poll span'inin süresi ise çözümün hazır olmasını beklemeye karşılık gelir. İkisini yan yana koyduğunuzda darboğazın hangi aşamada olduğunu doğrudan görürsünüz; süre çoğunlukla sorgulama beklemesinde geçer, ağ gecikmesinde değil.

captcha.id gibi yüksek kardinaliteli alanları nasıl yönetmeliyim?

captcha.id'yi span özniteliği olarak tutmak sorun değildir; tekil çözümleri izlemek için gereklidir. Ama onu metrik etiketi (label) olarak kullanmayın — her benzersiz değer yeni bir zaman serisi yaratır ve metrik arka ucunuzu şişirir. Etiketleri captcha.type gibi düşük kardinaliteli alanlarla sınırlayın.

Üretimde her çözümü mü izlemeliyim?

Geliştirmede evet. Üretimde depolama maliyetini düşürmek için örnekleme yapın — örneğin izlerin %10'u yeterli istatistiksel görünürlük sağlar. Hataları ise örnekleme dışında tutun, hepsini izleyin; asıl ilgilendiğiniz olaylar onlardır.

Aynı ize scrape ve proxy adımlarını da dahil edebilir miyim?

Evet. captcha.solve span'ini sayfayı çeken üst span'in altına yerleştirdiğinizde sayfa yüklemesi, proxy isteği ve CAPTCHA çözümü tek bir uçtan uca izde birleşir. Böylece gecikmenin CaptchaAI'de mi yoksa iş hattınızın başka bir adımında mı olduğunu net biçimde görürsünüz.

Sonraki adımlar

Enstrümantasyon dakikalar sürer, karşılığında hattınızın her aşamasını görürsünüz. CaptchaAI API anahtarınızı alın, yukarıdaki solver'ı OpenTelemetry ile sarın ve ilk izinizi arka ucunuzda inceleyin.

İlgili kılavuzlar:

Bu makale için yorumlar devre dışı bırakılmıştır.