Skip to content

fix(telemetry): opt-in preservation of trace-context headers on subgraph requests - #10014

Draft
OriginLeon wants to merge 8 commits into
devfrom
marcelo/ROUTER-2060
Draft

fix(telemetry): opt-in preservation of trace-context headers on subgraph requests#10014
OriginLeon wants to merge 8 commits into
devfrom
marcelo/ROUTER-2060

Conversation

@OriginLeon

@OriginLeon OriginLeon commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Status: draft, pending engineering sign-off on strategy

Implementation is complete and has been validated end-to-end (live, in a real cloud deployment, across multiple scenarios — see below). This is still marked draft because the underlying strategy (introducing a new opt-in config option) has not yet been signed off by the team — see the three questions posted on ROUTER-2060:

  1. Is this considered a bug?
  2. Is the proposed opt-in-config strategy approved, or is there a preferred alternative?
  3. If both are yes: OK to proceed given it adds a new config option?

Please don't merge this until that sign-off lands, even if CI is green.

What changed

Under the OTLP exporter, HttpClientService::call (apollo-router/src/services/http/service.rs) unconditionally re-injects the router's own span context into outgoing traceparent/tracestate (and the custom trace-ID header, if propagation.request.header_name is configured) on every subgraph HTTP call — discarding any value already present, whether it came from a coprocessor rewrite, header propagation, or a Rhai script. Under the Datadog-native exporter this doesn't happen, but only because DatadogPropagator never touches those headers — incidental, not a designed safeguard.

This silently breaks trace correlation for customers who deliberately set these headers before the subgraph call (RUM tracing use cases). Confirmed independently across two customers and reproduced in a controlled test environment — see the repro table in ROUTER-2060.

Adds telemetry.exporters.tracing.propagation.preserve_trace_context_on_subgraph_requests (default false). When enabled, the router snapshots whichever trace-context header is already present immediately before the propagator injection and restores it afterward — the injection call itself is never conditioned or skipped, so no other propagator (baggage, jaeger, zipkin, datadog, x-ray) is affected. Only one trace-ID-carrying header is preserved per call: the custom header (if configured and non-empty) takes priority, falling back to traceparent, mirroring the same precedence already used for extraction (#9984). tracestate has no custom-header equivalent and is always preserved independently when present. Scoped to Router→subgraph calls only — Router→coprocessor and Router→connector calls are unaffected.

Validation

  • Manually validated live against a real cloud deployment: config disabled (baseline unaffected), config enabled with a coprocessor rewriting traceparent's span-id (rewritten value correctly preserved end-to-end), config enabled with no coprocessor rewrite (original client-sent value correctly passed through), and a fan-out scenario across multiple subgraphs.
  • Along the way, found and fixed a real bug in the initial implementation: the telemetry plugin was being looked up by the wrong registry key ("telemetry" instead of its actual registered "apollo.telemetry"), which silently made the config always evaluate as disabled regardless of router.yaml.
  • Unit tests added in services/http/service.rs covering: preserve disabled (baseline unaffected), preserve enabled restores traceparent+tracestate, custom trace-ID header takes priority, and falls back to traceparent when the custom header is present but empty.
  • Integration tests added in tests/integration/coprocessor.rs (+ two new fixtures) exercising the coprocessor-rewrite scenario end-to-end with the config on and off.

Checklist

  • PR description explains the motivation for the change and relevant context for reviewing
  • PR description links appropriate GitHub/Jira tickets (creating when necessary)
  • Changeset is included for user-facing changes
  • Changes are compatible
  • Documentation completed
  • Performance impact assessed and acceptable
  • Metrics and logs are added and documented
  • Tests added and passing
    • Unit tests
    • Integration tests
    • Manual tests (live cloud deployment, see Validation above)

Exceptions

Pending items above (compatibility review, docs) are deliberately left for after the ROUTER-2060 sign-off, since the config name/shape could still change based on that discussion. Test execution in my own dev sandbox was constrained by local environment issues unrelated to this change (DNS resolver config, missing test Apollo Studio credentials for GraphOS-gated integration tests) — tests are written and compile cleanly; CI has what it needs to actually run them.

…ubgraph requests

Under OTLP, HttpClientService::call unconditionally re-injects the router's
own span context into outgoing traceparent/tracestate (and the custom trace
ID header, if configured) on every subgraph fetch, discarding any value
already present — whether from a coprocessor rewrite, header propagation, or
a Rhai script. This silently breaks RUM trace correlation for customers who
deliberately set these headers before the subgraph call (RH-1411, TSH-23612,
TSH-24441).

Adds telemetry.exporters.tracing.propagation.preserve_subgraph_trace_context
(default false): when enabled, the router snapshots whichever trace-context
header is already present before injection and restores it afterward,
without conditioning or skipping the injection call itself, so no other
propagator (baggage, jaeger, zipkin, datadog, x-ray) is affected.

Implementation only, to validate the approach locally — tests, changeset,
and usage metric are pending sign-off from engineering on ROUTER-2060.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@OriginLeon
OriginLeon requested a review from a team as a code owner August 17, 2026 18:55
@apollo-librarian

apollo-librarian Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

✅ Docs preview has no changes

The preview was not built because there were no changes.

Build ID: 1206e637f973d2be4c6397b4
Build Logs: View logs


✅ AI Style Review — No Changes Detected

No MDX files were changed in this pull request.

Review Log: View detailed log

This review is AI-generated. Please use common sense when accepting these suggestions, as they may not always be accurate or appropriate for your specific context.

@github-actions

This comment has been minimized.

OriginLeon and others added 5 commits August 17, 2026 15:55
CI's fmt_check/lint jobs failed on an import ordering issue introduced by
the ROUTER-2060 changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Renamed preserve_subgraph_trace_context to
  preserve_trace_context_on_subgraph_requests for clearer directionality.
- Fixed router_factory.rs looking up the telemetry plugin by "telemetry"
  instead of its actual registered key "apollo.telemetry" (group.name,
  per PluginFactory::new_private) — this silently made the config always
  read as disabled regardless of what was set in router.yaml.
- Added temporary tracing::warn! debug logs around the config resolution
  and the snapshot/inject/restore steps in HttpClientService::call, to
  validate the fix end-to-end. Marked TEMP DEBUG ROUTER-2060 for removal
  once validated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…text preservation

- Remove the temporary TEMP DEBUG logging used to validate the fix live.
- Add unit tests in services/http/service.rs covering: preserve disabled
  (baseline overwrite unaffected), preserve enabled restores traceparent +
  tracestate, custom trace-ID header takes priority, and falls back to
  traceparent when the custom header is empty.
- Add integration tests in tests/integration/coprocessor.rs (+ two new
  fixtures) exercising the coprocessor-rewrite scenario end-to-end with the
  config on and off.
- Add the required apollo.router.config.telemetry usage-metric attribute for
  the new preserve_trace_context_on_subgraph_requests option.
- Add the changeset for this fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@OriginLeon
OriginLeon marked this pull request as draft August 19, 2026 12:43
@OriginLeon OriginLeon changed the title WIP — not ready for review. fix(telemetry): opt-in preservation of trace-context headers on subgraph requests Aug 19, 2026
OriginLeon and others added 2 commits August 19, 2026 10:32
otel::layer() defaults to a NoopTracer, which never produces a valid OTel
SpanContext. TraceContextPropagator::inject_context silently declines to
write anything when the span context is invalid, so the "disabled" and
"custom header priority" tests were only exercising the restore side of
snapshot/restore, not real injection -- CI caught this (both failed with
`left == right`, i.e. no overwrite happened even with preservation off).

- Add a local setup_tracing_with_real_span_context() that wires a real
  SdkTracerProvider via otel::layer().with_tracer(...), following the same
  pattern already used in crate::tracer::test.
- Drop test_preserve_enabled_custom_header_takes_priority_over_traceparent:
  its assertion specifically needed real injection to be meaningful, and a
  dedicated regression test for that exact behavior is redundant with the
  integration test's coverage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant