# Instrument: any language over OTLP

LoopOps ingest is standard OTLP/HTTP — JSON or protobuf, plain or gzip/deflate-compressed — on all three pillars:

- `POST /v1/traces` — spans (`ExportTraceServiceRequest`)
- `POST /v1/logs` — logs and events (`ExportLogsServiceRequest`)
- `POST /v1/metrics` — metrics (`ExportMetricsServiceRequest`)

Any official OpenTelemetry SDK works: configure its OTLP/HTTP exporter with
the LoopOps endpoint and your producer key.

**Python example:**

```python
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

OTLPSpanExporter(
    endpoint="https://loopops.dev/v1/traces",
    headers={"authorization": f"Bearer {os.environ['LOOPOPS_INGEST_KEY']}"},
)
```

**Or raw HTTP from anywhere:**

```http
POST /v1/traces
Authorization: Bearer <producer_key>
Content-Type: application/json

{ "resourceSpans": [{
  "resource": { "attributes": [
    { "key": "service.name", "value": { "stringValue": "checkout-api" } },
    { "key": "service.version", "value": { "stringValue": "1.4.2" } }
  ]},
  "scopeSpans": [{ "spans": [{
    "traceId": "<32 hex>", "spanId": "<16 hex>",
    "name": "charge_card",
    "startTimeUnixNano": "1781856000000000000",
    "status": { "code": 2, "message": "card declined" }
  }] }]
}] }
```

## Cloudflare Workers

Start with Cloudflare's automatic tracing (open beta since 2025-11) and a
Workers Observability OTLP destination — created in the Cloudflare dashboard
under Workers Observability → Destinations, type Traces (there is no API for
this step). Put `authorization: Bearer <producer_key>` in the destination's
custom headers, point it at `https://ingest.loopops.dev/v1/traces`, and merge its
name into Wrangler. If the human approves application-log capture, create a
second destination of type Logs at `https://ingest.loopops.dev/v1/logs` with the
same header and merge that name too:

```jsonc
{
  "observability": {
    "traces": {
      "enabled": true,
      "destinations": ["<existing-destination>", "loopops-<project_id>-traces"]
    },
    "logs": {
      "enabled": true,
      "destinations": ["<existing-log-destination>", "loopops-<project_id>-logs"]
    }
  }
}
```

Cloudflare emits handler, outbound fetch, and binding spans (KV, R2, D1,
Durable Objects, Queues, AI) without an app-code wrapper — binding spans carry
`cloudflare.binding.type`/`cloudflare.binding.name`, which the system map
reads as dependency edges. Durable Object and service-binding subrequests
arrive as one unified trace (since 2026-05). Span and attribute names are
beta and may shift. Use the direct OTLP JSON example above only when the
destination is unavailable or for narrow business/model/tool semantics the
platform cannot infer. Logs remain opt-in because they include `console.*`,
exceptions, and system-generated logs. Never point a Worker's destination at
an OTLP receiver hosted by that same Worker; the export invocations become new
telemetry and recurse. LoopOps itself terminates OTLP on a dedicated receiver
Worker with no export destinations. Append/de-duplicate each approved destination and
preserve existing `head_sampling_rate` and `persist` settings; delivery can
take a few minutes.

**Pitfall (verified 2026-07):** if the Worker builds with
`@cloudflare/vite-plugin` (React Router, Remix, Astro), the plugin
regenerates the deploy config and flattens `observability` to
`{ enabled }` — silently dropping the `traces` block, so tracing never
activates. Re-apply the full `observability` object onto the BUILT
`wrangler.json` in a post-build step, and verify the deployed config
actually carries `traces.destinations` before trusting the wiring.

## Metrics — the third pillar

`POST /v1/metrics` accepts OTLP/HTTP metrics (JSON or protobuf) with the same
key and the same gates as traces: gauges, sums, histograms, exponential
histograms, and summaries. Every data point becomes a telemetry record
(`signal_type: metric.<name>`) with the numeric payload projected flat —
value, count, sum, min/max, buckets, quantiles — and exemplars kept, so a slow
histogram tail can be drilled to the trace that fell in it. Metrics are the
aggregate lane that survives trace sampling: token, latency, and error-rate
baselines land here when spans are sampled.

## What matters on the wire

- **`service.name`**, **`service.version`**, **`deployment.environment.name`**
  on the resource — workload identity, verify/regression anchoring, environment
  scoping.
- **Exceptions** via span events (`recordException` semantics) are extracted —
  type, message, stacktrace.
- **Span events and links** are stored (bounded) — in-span timelines and
  cross-trace causality (retry chains, fan-out provenance) survive ingest.
- **Replayable captures**: carry `gen_ai.*` semconv (or `loopops.input` /
  `loopops.output` / `loopops.unit`) and failures become replayable evidence.
- Distributed trace context (W3C `traceparent`) is preserved — browser spans,
  service spans, and model calls correlate into one journey by `trace_id`.

## Not supported (say it plainly)

gRPC OTLP is not accepted; use OTLP/HTTP (JSON or protobuf) on all three
pillars — `/v1/traces`, `/v1/logs`, `/v1/metrics`.

**Next:** [Limits, caps & ingest health →](/docs/limits-and-health)