Any language (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:
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:
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:
{
"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 (
recordExceptionsemantics) 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 (orloopops.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.