captchaforge 0.2.36

[DO NOT USE — UNDER ACTIVE DEVELOPMENT, NOT PRODUCTION-READY] Captcha solver scaffolding for chromiumoxide-driven browsers. The architecture is in place (vendor solvers, retry-loop iframe walking, VLM provider abstraction, real-WAF bench harness) but the live-vendor success rate is still 0% — Cloudflare Turnstile / hCaptcha / reCAPTCHA detect us at a TLS / CDP fingerprint layer that no flag-based stealth has cleared. Watch the repo; do not depend on this for any real workload.
Documentation
# Audit-log exporters (Datadog / Sentry / Honeycomb / Loki / …)

captchaforge ships three built-in `SolverTelemetry` sinks:

| Sink | Wire shape | When to use |
|------|------------|-------------|
| `NoopTelemetry` | drops every event | default; no observability |
| `JsonTelemetry` | `tracing::event!` at target `captchaforge.solve` | when you have a `tracing-subscriber` JSON layer pointed at your log pipeline |
| `MetricsTelemetry` | Prometheus exposition + JSON snapshot | when you want in-process counters / histograms; served by `captchaforge serve` at `/metrics` |

For ANY other backend (Datadog / Sentry / Honeycomb / OpenTelemetry /
Splunk / Loki / your in-house store) the integration pattern is:

1. Implement `SolverTelemetry` against the backend's SDK.
2. Compose it with the built-ins via `FanoutTelemetry`.

The library deliberately does NOT vendor every backend's SDK —
those are large, churn fast, and would inflate captchaforge's
dependency graph. Each operator wires the one they actually use.

## Pattern: fan out to multiple sinks

```rust
use std::sync::Arc;
use captchaforge::telemetry::{FanoutTelemetry, JsonTelemetry, MetricsTelemetry, SolverTelemetry};

let metrics = Arc::new(MetricsTelemetry::new());
let fanout: Arc<dyn SolverTelemetry> = Arc::new(
    FanoutTelemetry::new()
        .with_sink(metrics.clone())
        .with_sink(Arc::new(JsonTelemetry))
        .with_sink(Arc::new(YourCustomExporter::new("https://api.datadoghq.com")))
);

let chain = captchaforge::solver::CaptchaSolverChain::default_chain()
    .with_telemetry(fanout);
```

## Recipe: Datadog

```rust
use captchaforge::telemetry::{SolveEvent, SolverTelemetry};

pub struct DatadogExporter {
    client: reqwest::blocking::Client,
    api_key: String,
}

impl SolverTelemetry for DatadogExporter {
    fn record(&self, evt: &SolveEvent<'_>) {
        let payload = serde_json::json!({
            "series": [{
                "metric": "captchaforge.solve.attempts",
                "type": "count",
                "points": [[chrono::Utc::now().timestamp(), 1]],
                "tags": [
                    format!("solver:{}", evt.solver),
                    format!("domain:{}", evt.domain),
                    format!("outcome:{:?}", evt.outcome),
                ],
            }]
        });
        let _ = self.client
            .post("https://api.datadoghq.com/api/v1/series")
            .header("DD-API-KEY", &self.api_key)
            .json(&payload)
            .send();
    }
}
```

> Always wrap network exporters in a non-blocking buffer
> (`tokio::sync::mpsc` + background flush) — `record()` runs in the
> solver hot path, and a blocking HTTP call here adds the network
> round-trip to every solve's latency.

## Recipe: Sentry breadcrumbs

```rust
impl SolverTelemetry for SentryBreadcrumbExporter {
    fn record(&self, evt: &SolveEvent<'_>) {
        sentry::add_breadcrumb(sentry::Breadcrumb {
            ty: "captchaforge.solve".into(),
            category: Some(evt.solver.to_string()),
            level: match evt.outcome {
                captchaforge::telemetry::SolveOutcome::Success => sentry::Level::Info,
                captchaforge::telemetry::SolveOutcome::Failure => sentry::Level::Warning,
                captchaforge::telemetry::SolveOutcome::Error => sentry::Level::Error,
                captchaforge::telemetry::SolveOutcome::Timeout => sentry::Level::Warning,
            },
            message: Some(format!("captchaforge {:?} -> {:?}", evt.kind, evt.outcome)),
            ..Default::default()
        });
    }
}
```

## Recipe: Honeycomb / OpenTelemetry

Most operators wire OTLP via `tracing-subscriber` → `opentelemetry`
crate stack, which means `JsonTelemetry` already covers it — the
events emitted by `tracing::event!(target: "captchaforge.solve", ...)`
flow straight into the OTLP exporter when the global subscriber
is configured.

## Hot-path advice

- Buffer + flush in a background task. Never block the solve path.
- Sample for high-volume vendors (e.g. 10% of `success` events) if
  ingestion cost matters.
- Use `MetricsTelemetry` for aggregates and your exporter for
  per-event detail — most dashboards only need the aggregates.