Skip to content

Throwaway OTel retroactive-span diagnostics spike#4685

Closed
NaluTripician wants to merge 5 commits into
Azure:mainfrom
NaluTripician:nalutripician/cosmos-otel-span-spike
Closed

Throwaway OTel retroactive-span diagnostics spike#4685
NaluTripician wants to merge 5 commits into
Azure:mainfrom
NaluTripician:nalutripician/cosmos-otel-span-spike

Conversation

@NaluTripician

Copy link
Copy Markdown
Contributor

Throwaway OpenTelemetry retroactive-span feasibility spike

Companion to the diagnostics contract doc PR (#4684). This is the isolated, throwaway spike that backs up §7–§8 of DIAGNOSTICS-CONTRACT.md — kept on its own branch so the contract doc stands alone.

What it proves

A completed DiagnosticsContext can be reconstructed into a backdated OpenTelemetry span tree using the raw opentelemetry SpanBuilder::with_start_time/with_end_time — timestamps the azure_core::tracing abstraction does not expose (it builds spans at "now"). The recorded attempts are laid on an explicit, clearly-backdated (2020) timeline with injected durations, and the operation → attempt parent/child relationships are reconstructed and asserted against an in-memory exporter.

Decoupled from the deferred #4619 capture engine

  • In-crate #[cfg(all(test, feature = "otel_spans_spike"))] module so it can use the pub(crate) DiagnosticsContextBuilder.
  • Builds the sample context from main types only — crate::diagnostics / driver::routing / options / models — with no dependency on the diagnostics::capture prototype.
  • Off by default; pulls opentelemetry + opentelemetry_sdk (in-memory exporter) only behind the otel_spans_spike feature.

Verification

  • cargo fmt ✅ · cargo clippy --all-features --all-targets -- -D warnings ✅ clean · the spike test ✅ passes.
  • Throwaway/feasibility only; no public API change; no CHANGELOG. Draft (design review).

4 files, +231: the in-crate test module, the otel_spans_spike feature + optional deps, and the module declaration.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

In-crate, off-by-default (otel_spans_spike feature) test that reconstructs a
completed DiagnosticsContext into a BACKDATED OpenTelemetry span tree via
opentelemetry SpanBuilder::with_start_time/with_end_time. Proves the OTel
mapping in DIAGNOSTICS-CONTRACT.md is feasible.

Decoupled from any capture-engine prototype: it builds the sample context
from the pub(crate) DiagnosticsContextBuilder using only crate::diagnostics
/ driver::routing / options / models types available on main - no dependency
on the deferred Azure#4619 capture module. The azure_core::tracing abstraction
builds spans at "now" and has no backdating hook, so the spike talks to the
raw opentelemetry crate + an in-memory exporter.

Throwaway/feasibility only, gated OFF by default; no public API change; no
CHANGELOG.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@NaluTripician

Copy link
Copy Markdown
Contributor Author

Nice throwaway — it cleanly proves the backdated SpanBuilder::with_start_time / with_end_time path works without touching the capture engine, and the in-memory-exporter assertions on parent/child + backdated timestamps are convincing. Two things to keep in mind so this doesn't get carried into real wiring as-is:

Latent coupling to #4683's request_count() change

The test builds one span per element of ctx.requests() (the retained list) but asserts spans.len() == ctx.request_count() + 1. That holds only because compaction isn't on this branch; once #4683 lands, request_count() returns the pre-compaction total while requests() is the retained (bounded) list, so this assertion breaks. If any of this becomes production span emission, count/iterate off the same list.

Contract §7.3 gap

Same "one span per attempt" assumption — under compaction a run collapses to first+last, so a faithful emitter can't produce request_count() spans. Fine for the spike; flagging so it's not baked into the eventual emitter.

Feature / dep scoping

otel_spans_spike = ["dep:opentelemetry", "dep:opentelemetry_sdk"] enables the deps unconditionally while the only consumer is #[cfg(all(test, feature = "otel_spans_spike"))]. A non-test --all-features build pulls opentelemetry + opentelemetry_sdk (with the testing feature) into the graph with no user. Harmless for a throwaway, but worth a note if it ever graduates.

Address self-review on PR Azure#4685:

- Count/iterate attempt spans off ctx.requests() (the retained list the
  spans are actually built from) instead of ctx.request_count(). On main the
  two are equal, but once PR Azure#4683's retry-storm compaction lands
  request_count() reports the true pre-compaction total while requests() is
  the bounded first+last-per-run list, which would break the assertions. A
  faithful emitter must count off the list it emits from.
- Note (DIAGNOSTICS-CONTRACT.md 7.3) that under compaction a run collapses to
  a single span + count, so an emitter cannot produce request_count() spans.
- Document the otel_spans_spike feature dep-scoping caveat: it enables
  opentelemetry(+_sdk) unconditionally though the only consumer is the
  test-gated module, so a non-test --all-features build pulls them in unused.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@NaluTripician

Copy link
Copy Markdown
Contributor Author

Addressed in 77db3bb:

  • request_count() coupling — span count/iteration is now anchored on ctx.requests() (the retained list the spans are built from); the two span-count assertions use
    equests.len() instead of ctx.request_count(), so they stay correct once Bound Cosmos diagnostics under retry storms #4683's compaction makes
    equest_count() the pre-compaction total. Added an explanatory comment at the emit site.
  • Contract §7.3 gap — added a note that under compaction a run collapses to first+last (one span + a repeat count), so a faithful emitter cannot produce
    equest_count() spans.
  • Feature/dep scoping — extended the otel_spans_spike comment in Cargo.toml to call out that the deps are enabled unconditionally while the only consumer is the test-gated module (a non-test --all-features build pulls them in unused), with a pointer to move them behind a non-test consumer / dev-dependency if this ever graduates.

cargo fmt + cargo clippy --features otel_spans_spike --all-targets clean; the spike test still passes.

NaluTripician and others added 2 commits July 6, 2026 11:17
The main merge pulled in the opentelemetry 0.32 dependency bump (Azure#4691),
which removed SpanBuilder::with_end_time. Drop the two redundant
.with_end_time(..) builder calls; the backdated end time is already
applied via end_with_timestamp(..) when each span ends, so the exported
spans and all timeline assertions are unchanged. Update the module doc
link accordingly to avoid a broken intra-doc reference.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@analogrelay

Copy link
Copy Markdown
Member

Shall we throw this away now, @NaluTripician ?

@NaluTripician

Copy link
Copy Markdown
Contributor Author

Superseded. This throwaway spike proved backdated OTel spans are feasible from a completed DiagnosticsContext; that technique is now productionized in two places: #4784 adds the additive start_span_at/end_at backdating methods to the core azure_core/typespec_client_core tracing traits, and #4789 (WS4) implements the tail-sampled, backdated Cosmos tracing handler (via the raw opentelemetry SpanBuilder path). Closing the spike.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Cosmos The azure_cosmos crate Do Not Merge

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants