feat(#2862): Level 2 distributed tracing — OTLP export to a backend - #3903
Conversation
Adds go.opentelemetry.io/otel, otel/sdk, otel/trace, and the otlptracehttp exporter for ADR 0050 Level 2 trace export. Only the HTTP/protobuf exporter is imported — no gRPC exporter; grpc appears in the module graph transitively via the OTLP proto definitions. Signed-off-by: Dharit Shah <dhshah@redhat.com>
ADR 0050 and the tracing guide name gen_ai.operation.name and gen_ai.agent.name on run spans; Level 1 never emitted them. Add them at the source — the recorder's root span_start — so the local file and the Level 2 export stay two views of one truth. The bare "agent" key stays for existing consumers of the Level 1 schema. Signed-off-by: Dharit Shah <dhshah@redhat.com>
New internal/telemetry/otlp package implementing ADR 0050 Level 2 as a replay of the Level 1 artifacts: run-telemetry.jsonl and run-summary.json are parsed back into span snapshots (identical ids, nanosecond-exact timestamps) and sent through the OTel SDK's OTLP/HTTP exporter in one bounded export at run close. Export is therefore, by construction, the same trace the local files record — and a future 'fullsend telemetry push' can replay any captured run directory. Gating and safety: - Inert unless OTEL_EXPORTER_OTLP_(TRACES_)ENDPOINT is set; honors OTEL_SDK_DISABLED and OTEL_TRACES_EXPORTER=none. - Endpoint values are pre-validated: the SDK silently falls back to localhost:4318 on malformed input, fullsend refuses instead. - Only http/protobuf; a grpc protocol setting is refused loudly. - Hard 5s wall-clock budget over construction, send (capped retries), and shutdown, derived from context.Background() so traces of failed or cancelled runs still flush. Fail-open: errors are returned for a single warning line and never affect the run. - An inbound-unsampled trace (W3C flags -00) is not exported; Level 1 files are always written. - Header/TLS/timeout/compression config is delegated entirely to the exporter's standard env handling, matching the published guide. tracetest.SpanStub is the SDK's only public ReadOnlySpan constructor (the interface has an unexported method); the replay design needs exact id/timestamp control, which a live TracerProvider cannot guarantee. Signed-off-by: Dharit Shah <dhshah@redhat.com>
…nto the runner Export runs inside the existing Finalize defer, after the Level 1 artifacts are complete on disk. Agent iteration spans gain the ADR-named gen_ai.operation.name/gen_ai.agent.name at span start via the agentSpanStartAttrs helper; sandbox_create carries create_agent. Signed-off-by: Dharit Shah <dhshah@redhat.com>
Replays a captured run directory through the production Level 2 export path against any OTLP backend — the validation workhorse for fullsend-ai#2862 and the prototype for a future 'fullsend telemetry push'. Signed-off-by: Dharit Shah <dhshah@redhat.com>
The span-structure section described a hierarchy that was never emitted; it now matches the real spans (run, sandbox_create, agent) and the attribute tables match what Level 1 records. Adds the Level 2 operational contract (bounded export at run close, sampled-flag behavior, http/protobuf only, endpoint validation, kill switches, private CAs), an MLflow >= 3.6 example incl. the experiment-id header and Basic-auth percent-encoding, and marks Level 3 content capture as planned rather than available. The env-var contract section is unchanged — the implementation conforms to it. Signed-off-by: Dharit Shah <dhshah@redhat.com>
…kflow Orgs consume the reusable workflows, whose job env they cannot set, so there was no way to reach the fullsend process with the standard OTEL export configuration. Pass it through from the caller's context: the endpoint and resource attributes as Actions variables, the headers as an optional secret (they may carry backend auth). Unset values leave the exporter inert, per the Level 2 gate. Triage only for now — the remaining reusable workflows follow once the pattern is validated in the rehearsal org. Signed-off-by: Dharit Shah <dhshah@redhat.com>
E2E tests did not runE2E tests run automatically for org/repo members and collaborators on pull requests. For other contributors, a maintainer must add the See E2E testing guide for details. |
1 similar comment
E2E tests did not runE2E tests run automatically for org/repo members and collaborators on pull requests. For other contributors, a maintainer must add the See E2E testing guide for details. |
PR Summary by QodoAdd Level 2 tracing: replay L1 run artifacts via OTLP/HTTP export
AI Description
Diagram
High-Level Assessment
Files changed (13)
|
Code Review by Qodo
Context used✅ Tickets:
🎫 feat: implement Level 2 distributed tracing — OTLP export to a backend 🎫 Explore the OpenTelemetry Go SDK for Level 2 trace export✅ Compliance rules (platform):
54 rules✅ Skills:
writing-user-docs, writing-adrs 1.
|
…rror, docs) - otlp: reject authority-less endpoints (e.g. http://) up front to match the "refuse malformed endpoint" guard, with a clearer error; add an http:// case to the fail-open test - otlp: name OTEL_EXPORTER_OTLP_(TRACES_)PROTOCOL in the unsupported-protocol error (protocolFromEnv prefers the traces-specific variable) - docs(tracing): add a Prerequisites section; convert Local development to numbered steps (writing-user-docs convention) Signed-off-by: Dharit Shah <dhshah@redhat.com>
Signed-off-by: Dharit Shah <dhshah@redhat.com> # Conflicts: # go.mod # go.sum
Site previewPreview: https://55991576-site.fullsend-ai.workers.dev Commit: |
|
/fs-review |
|
🤖 Finished Review · ✅ Success · Started 5:36 PM UTC · Completed 5:48 PM UTC |
ReviewVerdict: approve — well-engineered Level 2 OTLP export implementation. No medium or higher findings. SummaryThis PR implements ADR 0050 Level 2 distributed tracing: opt-in OTLP/HTTP export of the metadata spans already recorded at Level 1. The design is a replay of finalized L1 artifacts ( Correctness: The export flow is well-structured — env-var gating, URL validation (preventing the SDK's silent localhost fallback), kill switches ( Security: Content sandboxing is intact — the data path from Intent & coherence: The change traces cleanly to issue #2862 and ADR 0050. The Test adequacy: The test suite is thorough — ~930 lines covering gating (endpoint presence, kill switches, malformed endpoints, unsupported protocols, unsampled traces), fidelity (span identity byte-equality, attribute type preservation, parent resolution, SpanKind, status mapping, work-item-id propagation, resource attributes), fail-open (TCP black hole, hanging HTTP, connection refused, DNS Cross-repo contracts: All changes are backward-compatible. The new reusable workflow secret is Documentation: The extensive updates to Low-severity notes
Positive signals
|
…insensitively The OTel spec recommends case-insensitive comparison of env var values; the adjacent OTEL_SDK_DISABLED check already uses strings.EqualFold. Signed-off-by: Dharit Shah <dhshah@redhat.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Summary-without-telemetry-file, blank lines, a line beyond the scanner cap (the one readRun error), a bad end-side timestamp, and a number too large for float64. Lifts the otlp package to 99.3% coverage; the one remaining block is the summarySampled ParseUint fallback, unreachable because ParseTraceParent already validates flags as two lowercase hex chars. Signed-off-by: Dharit Shah <dhshah@redhat.com>
rh-hemartin
left a comment
There was a problem hiding this comment.
LGTM waiting for @ascerra review
ascerra
left a comment
There was a problem hiding this comment.
Review
Exporter looks solid — rehearsal artifact 5a7818ccd9dd4a24bdc557b77e58f61c (run 28968403182) matches a real MLflow trace in experiment 11, so L1→OTLP→backend works. Not MLflow-locked (generic otlptracehttp + standard OTEL env).
The gap is enablement: the reusable workflow declares/consumes the headers secret, but callers never pass it, so the “set the org secret” path doesn’t work after merge.
Findings not attachable to this diff
- [high][api-contract] Thread
OTEL_EXPORTER_OTLP_TRACES_HEADERSthrough:.github/workflows/reusable-dispatch.yml(triage jobsecrets:)internal/scaffold/fullsend-repo/.github/workflows/triage.yml- Existing
.fullsendcallers until scaffold sync
GHA secrets do not auto-inherit. Endpoint viavars.*works; headers silently empty → 401 on authenticated backends.
- [medium][protected-path]
.github/workflows/reusable-triage.yml— human approval still required.
Ask before merge
- Thread the headers secret through dispatch + scaffold (see inline on the consumer).
- Align the GHA docs example with the real vars/secrets names; note triage-only + crash-no-export.
GHA reusable workflows do not inherit secrets, so the secret consumed by reusable-triage.yml must be forwarded at every hop: the per-repo chain (shim -> reusable-dispatch -> reusable-triage) and the per-org thin caller. TestOTELHeadersSecretThreading guards all four links — the alignment test only enforces required secrets, and this optional one arrives silently empty when a forward is missing. Signed-off-by: Dharit Shah <dhshah@redhat.com>
|
🥳 thanks for this @dhshah13 !! |
|
🤖 Finished Retro · ✅ Success · Started 6:38 PM UTC · Completed 6:47 PM UTC |
|
PR #3903 implemented Level 2 OTLP trace export (1796 additions, 17 files). The fullsend review agent approved with only 1 low finding, but human reviewer ascerra subsequently found 1 high-severity API contract issue (GHA secrets not threaded through reusable workflow callers), 2 medium-severity documentation accuracy issues, and 3 low-severity documentation completeness issues. The most impactful miss was the secret threading gap: Proposals filed
|
Closes #2862. Implements Level 2 of ADR 0050 — OTLP export of the metadata spans fullsend already records at Level 1. The export-path approach it takes relates to #2780 (below).
What this does
When
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT(orOTEL_EXPORTER_OTLP_ENDPOINT) is set, every run exports its Level 1 span tree via OTLP/HTTP to the configured backend, in addition to the local L1 files. Unset → inert, byte-for-byte the L1 path. Still metadata-only (no prompt/completion content — that's L3).Approach — export path (relates to #2780)
Adopt the OpenTelemetry Go SDK +
otlptracehttprather than hand-rolled OTLP. Export is a replay of the finalized L1 artifacts at run close (otlp.ExportRunDir), not a liveTracerProvider: L1 span IDs/timestamps are reproduced byte-for-byte viatracetest.SpanStub, so the wire spans are identical to the L1 file. Fail-open by construction — a bounded 5s flush fromcontext.Background(), capped retries, and the export runs in a post-Finalizedefer whose error only warns and never touches the run's exit code.Commits
build(deps): OTel SDK + otlptracehttp (http/protobuf only)feat(#2862):gen_ai.*identity attrs at the L1 sourcefeat(#2862):internal/telemetry/otlpexporter packagefeat(#2862): runner wiring (export in the Finalize defer)chore(hack):telemetry-replaydev tool (replay a captured run to any OTLP backend)docs(tracing): operator guideci(triage): OTEL passthrough so orgs can enable L2 via the reusable workflowValidation
go test ./internal/telemetry/otlp/...— real in-process OTLP/HTTP protobuf sink; span-identity fidelity (byte-equal ids, ns-exact times); fail-open matrix (TCP black-hole, hanging server, refused, DNS.invalid, 4xx, 503→retry→delivered); gating + kill switches (OTEL_SDK_DISABLED,OTEL_TRACES_EXPORTER=none, malformed-endpoint refusal, grpc refusal). otlp pkg ~95%./v1/traceswire (Bearer + experiment-scoped) → trace ingested with state OK, fullrun → sandbox_create, agenttree,gen_ai.usage.*, tool_calls, model. Backend is an adopter decision per ADR 0050 — proven against MLflow ≥3.6 (local) and the managed instance.Deliberate deviations (disclosed)
OTEL_EXPORTER_OTLP_TRACES_ENDPOINTalongside the ADR-named generic var (OTel-spec precedence; documented).gen_ai.systemkey; semconv ≥1.30 renamed it togen_ai.provider.name→ tracked as a follow-up.Enabling export (next steps — env-var contract)
L2 is opt-in per environment. Merging ships the capability; a run captures only when its environment is configured:
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT— e.g.https://<backend>/v1/tracesOTEL_EXPORTER_OTLP_TRACES_HEADERS— backend auth + routing (for MLflow:Authorization=Bearer%20<token>,x-mlflow-experiment-id=<id>)OTEL_EXPORTER_OTLP_CERTIFICATE— optional PEM for a private CA (e.g. an internal MLflow)Kill switches honored:
OTEL_SDK_DISABLED=true,OTEL_TRACES_EXPORTER=none.v0is a fixed tag, so orgs pick up the reusable-workflow passthrough only after a release moves it (post-merge).Out of scope (deliberate)
gen_ai.*attributes MLflow already renders (tokens, cost, model, tool_calls); richer trace-level columns need an L1 run-context capture plus a vendor-coupling decision, so they're out of scope here — a separate feature request will be opened after this PR lands.Follow-up:
gen_ai.system→gen_ai.provider.namesemconv rename.