De-risk the migration off the raw OTel SDK onto tracing + tracing-opentelemetry by proving, on a throwaway branch, that OTel context propagates correctly across .awaitand into tokio::spawn-ed tasks — the exact case that produced orphaned llm.request spans on the streaming chat path (fixed manually in PR #129 / f850c53).
This is a spike: small, bounded, throwaway. The goal is a go/no-go + a validated init_tracing shape, not a full migration.
Why
Today we use log (via opentelemetry-appender-log) + the raw OTel SDK for spans: every span is tracer.start_with_context(...), context is threaded by hand (cx: &Context first param on DAO/LLM methods, Context::current_with_span(span) in every handler), and there's no #[instrument]. The raw SDK's Context::current() is a thread-local that only .attach() guards set, and nothing in async actix/tokio propagates it across .await/spawn — that's the root of the orphan-span bug class. tracing + tracing-opentelemetry is the standard fix: tracing propagates the current span (and its OTel context) across .await automatically, and #[instrument] captures the parent span by value at call time, so it survives tokio::spawn.
Version pin (verified)
tracing-opentelemetry = "0.32" depends on opentelemetry 0.31.0 + opentelemetry_sdk 0.31.0 (confirmed from its Cargo.toml) — exact match for the current stack, no otel bump.
Rewire otel::init_tracing() (currently src/otel.rs:20) to build a tracing_subscriber::Registry with tracing_opentelemetry::OpenTelemetryLayer::new(tracer)plus the existing stdout/env-filter layers. Confirm the existing OTLP gRPC exporter (OTLP_OTLS_ENDPOINT) still receives spans.
Instrument one end-to-end path that contains a tokio::spawn — the streaming chat path: chat_stream_handler → chat_turn_stream → chat_turn_stream_inner (the tokio::spawn) → run_streaming_turn → run_bootstrap_streaming → describe_image / run_streaming_agentic_loop.
Add #[tracing::instrument] to these fns. Leave the llm.request spans as raw SDK for now (the point is parent/child nesting, not converting everything) — or instrument exactly one LLM call to see it nest.
Graft the incoming traceparent onto the request span via tracing_opentelemetry::OpenTelemetrySpanExt::set_parent(...). Also test whether opentelemetry-instrumentation-actix-web::RequestTracing does this for us (check its otel-0.31 compatibility).
Success criteria (go / no-go)
In Jaeger/Tempo, the describe_imagellm.request span from the spawned streaming task nests under the HTTP request span, all sharing one trace ID.
No orphaned root spans on that path.
Existing OTLP export is unaffected (span names/attributes/status intact).
Deliverable
A short writeup posted back on this issue: what worked, what didn't, the exact validated init_tracing shape, gotchas (e.g. does the parent span need to be active at call time for #[instrument]; does the batch exporter need different provider wiring; does the actix middleware expose context downstream), and a clear proceed / adjust / abandon recommendation.
Out of scope
Converting DAO/LLM methods or dropping cx: &Context params.
Migrating all log:: calls to tracing macros.
Converting the llm.request / trace_db_call helpers in otel.rs.
Unblocks
The full tracing migration issue (created alongside this one).
## Goal
De-risk the migration off the raw OTel SDK onto `tracing` + `tracing-opentelemetry` by proving, on a throwaway branch, that OTel context propagates correctly across `.await` **and into `tokio::spawn`-ed tasks** — the exact case that produced orphaned `llm.request` spans on the streaming chat path (fixed manually in PR #129 / `f850c53`).
This is a **spike**: small, bounded, throwaway. The goal is a go/no-go + a validated `init_tracing` shape, not a full migration.
## Why
Today we use `log` (via `opentelemetry-appender-log`) + the **raw OTel SDK** for spans: every span is `tracer.start_with_context(...)`, context is threaded by hand (`cx: &Context` first param on DAO/LLM methods, `Context::current_with_span(span)` in every handler), and there's no `#[instrument]`. The raw SDK's `Context::current()` is a thread-local that only `.attach()` guards set, and nothing in async actix/tokio propagates it across `.await`/`spawn` — that's the root of the orphan-span bug class. `tracing` + `tracing-opentelemetry` is the standard fix: `tracing` propagates the current span (and its OTel context) across `.await` automatically, and `#[instrument]` captures the parent span **by value at call time**, so it survives `tokio::spawn`.
## Version pin (verified)
- **`tracing-opentelemetry = "0.32"`** depends on **`opentelemetry 0.31.0`** + `opentelemetry_sdk 0.31.0` (confirmed from its `Cargo.toml`) — exact match for the current stack, no otel bump.
- Add `tracing`, `tracing-subscriber` (with `registry`), and `tracing-log` (so existing `log::` calls keep working).
## Spike scope (one branch, minimal)
1. Add the deps above.
2. Rewire `otel::init_tracing()` (currently `src/otel.rs:20`) to build a `tracing_subscriber::Registry` with `tracing_opentelemetry::OpenTelemetryLayer::new(tracer)` **plus** the existing stdout/env-filter layers. Confirm the existing OTLP gRPC exporter (`OTLP_OTLS_ENDPOINT`) still receives spans.
3. Instrument **one** end-to-end path that contains a `tokio::spawn` — the streaming chat path:
`chat_stream_handler` → `chat_turn_stream` → `chat_turn_stream_inner` (the `tokio::spawn`) → `run_streaming_turn` → `run_bootstrap_streaming` → `describe_image` / `run_streaming_agentic_loop`.
Add `#[tracing::instrument]` to these fns. Leave the `llm.request` spans as raw SDK for now (the point is parent/child nesting, not converting everything) — or instrument exactly one LLM call to see it nest.
4. Graft the incoming `traceparent` onto the request span via `tracing_opentelemetry::OpenTelemetrySpanExt::set_parent(...)`. Also test whether `opentelemetry-instrumentation-actix-web::RequestTracing` does this for us (check its otel-0.31 compatibility).
## Success criteria (go / no-go)
- In Jaeger/Tempo, the `describe_image` `llm.request` span from the **spawned** streaming task nests under the HTTP request span, all sharing **one trace ID**.
- No orphaned root spans on that path.
- Existing OTLP export is unaffected (span names/attributes/status intact).
## Deliverable
A short writeup posted back on this issue: what worked, what didn't, the exact validated `init_tracing` shape, gotchas (e.g. does the parent span need to be active at call time for `#[instrument]`; does the batch exporter need different provider wiring; does the actix middleware expose context downstream), and a clear **proceed / adjust / abandon** recommendation.
## Out of scope
- Converting DAO/LLM methods or dropping `cx: &Context` params.
- Migrating all `log::` calls to `tracing` macros.
- Converting the `llm.request` / `trace_db_call` helpers in `otel.rs`.
## Unblocks
The full tracing migration issue (created alongside this one).
Verdict: ADJUST — the tracing + tracing-opentelemetry migration is viable and the wiring is sound, but a bare #[instrument] is NOT sufficient to fix the streaming tokio::spawn orphaning. Every spawn boundary must carry the parent span explicitly. This is more precise than the spike's original premise assumed, and it directly shapes #134.
The decisive question
Does tracing + tracing-opentelemetry 0.32 propagate OTel context across .awaitand into a tokio::spawn-ed task via #[instrument]?
Answer: across .await, yes. Across tokio::spawn, no — not with a bare #[instrument]. The span of an instrumented async fn is created at first poll, which for a spawned task is always on a worker thread where the parent span was never entered. Tracing span context is thread-local, so the parent link is lost → the child exports as an orphan (fresh trace_id, parent_span_id = 0000000000000000).
Evidence (final passing run — tests/otel_propagation.rs)
S5 (same-task .await) propagates → the OpenTelemetryLayer + exporter wiring is sound; the failure mode is specifically the spawn/thread boundary.
S2 is the production shape (chat_turn_stream_inner's tokio::spawn(async move { run_streaming_turn(...).await })) → orphaned. S4 (direct-call spawn) also orphans.
S6 / S7 (the fixes) both propagate → the migration works once the span is carried.
Root cause (verified via cargo expand on tracing-attributes)
For an async#[instrument] fn, the generated code creates the span inside the future body (at first poll), not at call time. A tokio::spawn-ed future first-polls on a runtime worker thread, so the parent is looked up on a thread where it was never entered. Same-thread (S1) and same-task (S5) cases propagate because the parent is active on the polling thread.
The validated fix (S6 and S7 both pass)
Capture the active span in the parent's scope, then carry it into the spawned future:
// FIX S6 — instrument the future
letparent=tracing::Span::current();tokio::spawn(asyncmove{run_streaming_turn(...).await}.instrument(parent));// FIX S7 — enter a guard for the block
letparent=tracing::Span::current();tokio::spawn(asyncmove{let_g=parent.enter();run_streaming_turn(...).await});
.instrument() re-enters the span on every poll (including the first, on the worker), so an inner #[instrument] child nests correctly.
Gotchas
set_default() is thread-local. Spans from worker threads are silently dropped. Install the tracing subscriber process-globally (try_init() / init() → dispatcher::set_global_default). The app's existing init_tracing uses global::set_tracer_provider (global), so the provider side is fine — just don't use set_default() for the new subscriber.
#[instrument] alone is not enough at spawn sites — it must be combined with the explicit carry above.
opentelemetry_sdk needs the testing feature for InMemorySpanExporter (only for the regression test).
Version resolved clean:tracing-opentelemetry 0.32.1 against opentelemetry 0.31 — no otel bump. Deps added: tracing, tracing-subscriber, tracing-log, tracing-opentelemetry = "0.32".
Existing log:: calls keep working via tracing-log (bridges log records into tracing), so no forced migration of the ~hundreds of log:: sites.
Recommended init_tracing shape (validated wiring; NOT yet applied to the app)
What was NOT done (timed out at 45 min on a cold build)
The mechanism — the actual go/no-go — is complete and passing. What didn't finish is the secondary real-app rewire: init_tracing() was not yet rewired and the streaming path was not yet #[instrument]-ed (the run was reading the target fns when the clock expired). That is now #134's job, with the validated pattern above. No src/ files were changed by the spike.
Proceed, but record a hard requirement the original plan under-specified: every tokio::spawn boundary must carry the parent span (.instrument(parent) or a span.enter() guard). The streaming path (insight_chat::chat_turn_stream_inner) is the critical spawn site. Keep tests/otel_propagation.rs as the regression test (it pins S2/S4-orphan and S6/S7-fix). Stage the migration (subscriber + one module, verify in Jaeger/Tempo, then roll out) rather than one mega-commit.
Artifacts
Patch (deps + both test files): /tmp/pi-subagents-uid-1000/async-subagent-runs/159c9378-f32e-4f08-bba0-8ba8900e64e8/worktree-diffs/step-0/task-0-worker.patch
The worktree/branch were auto-cleaned after the timeout. Reapply the patch (or copy tests/otel_propagation.rs) to reuse the regression test in #134.
# Spike #133 — Cliff Notes
**Verdict: ADJUST** — the `tracing` + `tracing-opentelemetry` migration is viable and the wiring is sound, but a **bare `#[instrument]` is NOT sufficient** to fix the streaming `tokio::spawn` orphaning. Every spawn boundary must **carry the parent span explicitly**. This is more precise than the spike's original premise assumed, and it directly shapes #134.
## The decisive question
Does `tracing` + `tracing-opentelemetry 0.32` propagate OTel context across `.await` **and** into a `tokio::spawn`-ed task via `#[instrument]`?
**Answer:** across `.await`, yes. Across `tokio::spawn`, **no** — not with a bare `#[instrument]`. The span of an instrumented `async fn` is created at **first poll**, which for a spawned task is always on a worker thread where the parent span was never entered. Tracing span context is thread-local, so the parent link is lost → the child exports as an orphan (fresh `trace_id`, `parent_span_id = 0000000000000000`).
## Evidence (final passing run — `tests/otel_propagation.rs`)
```
S1 current-thread, in-block call propagated=true child_trace=ecb87b552ecfd1416f28cc0d1397056c parent_trace=ecb87b552ecfd1416f28cc0d1397056c child.parent_span_id=73bc4103efc992c1 parent.span_id=73bc4103efc992c1
S2 multi-thread, in-block call (PRODUCTION) propagated=false child_trace=9a86457cf6facd3f60ca617aae336cfb parent_trace=ad92eb863832fa1005942d79929eb033 child.parent_span_id=0000000000000000 parent.span_id=1e024efef5b81efa
S4 multi-thread, direct-call spawn propagated=false child_trace=dbdebb741e0f2851b8b6ea2baae431fb parent_trace=5009e3c648e852a1c3afef659971a204 child.parent_span_id=0000000000000000 parent.span_id=6a3c8b9f9b11b510
S5 multi-thread, same-task await (control) propagated=true child_trace=47375c2822473f90a57d4692c2794cf0 parent_trace=47375c2822473f90a57d4692c2794cf0 child.parent_span_id=50b9ec58c4fbb5d6 parent.span_id=50b9ec58c4fbb5d6
S6 multi-thread, spawn + .instrument() (FIX) propagated=true child_trace=4df597a00740a995aa4cfa1694c02100 parent_trace=4df597a00740a995aa4cfa1694c02100 child.parent_span_id=a1792e5fc458e27d parent.span_id=a1792e5fc458e27d
S7 multi-thread, spawn + span.enter() (FIX) propagated=true child_trace=5337a3340f851f122594b7d75580505e parent_trace=5337a3340f851f122594b7d75580505e child.parent_span_id=837c447d5ed34a7c parent.span_id=837c447d5ed34a7c
test result: ok. 1 passed; 0 failed; 0 ignored
```
- **S5** (same-task `.await`) propagates → the `OpenTelemetryLayer` + exporter wiring is sound; the failure mode is specifically the spawn/thread boundary.
- **S2** is the **production shape** (`chat_turn_stream_inner`'s `tokio::spawn(async move { run_streaming_turn(...).await })`) → orphaned. **S4** (direct-call spawn) also orphans.
- **S6 / S7** (the fixes) both propagate → the migration works once the span is carried.
## Root cause (verified via `cargo expand` on tracing-attributes)
For an **async** `#[instrument]` fn, the generated code creates the span **inside the future body** (at first poll), not at call time. A `tokio::spawn`-ed future first-polls on a runtime worker thread, so the parent is looked up on a thread where it was never entered. Same-thread (S1) and same-task (S5) cases propagate because the parent is active on the polling thread.
## The validated fix (S6 and S7 both pass)
Capture the active span in the parent's scope, then carry it into the spawned future:
```rust
// FIX S6 — instrument the future
let parent = tracing::Span::current();
tokio::spawn(async move { run_streaming_turn(...).await }.instrument(parent));
// FIX S7 — enter a guard for the block
let parent = tracing::Span::current();
tokio::spawn(async move {
let _g = parent.enter();
run_streaming_turn(...).await
});
```
`.instrument()` re-enters the span on every poll (including the first, on the worker), so an inner `#[instrument]` child nests correctly.
## Gotchas
- **`set_default()` is thread-local.** Spans from worker threads are silently dropped. Install the tracing subscriber **process-globally** (`try_init()` / `init()` → `dispatcher::set_global_default`). The app's existing `init_tracing` uses `global::set_tracer_provider` (global), so the provider side is fine — just don't use `set_default()` for the new subscriber.
- **`#[instrument]` alone is not enough at spawn sites** — it must be combined with the explicit carry above.
- **`opentelemetry_sdk` needs the `testing` feature** for `InMemorySpanExporter` (only for the regression test).
- **Version resolved clean:** `tracing-opentelemetry 0.32.1` against `opentelemetry 0.31` — **no otel bump**. Deps added: `tracing`, `tracing-subscriber`, `tracing-log`, `tracing-opentelemetry = "0.32"`.
- Existing `log::` calls keep working via **`tracing-log`** (bridges `log` records into `tracing`), so no forced migration of the ~hundreds of `log::` sites.
## Recommended `init_tracing` shape (validated wiring; NOT yet applied to the app)
```rust
pub fn init_tracing() {
// ... existing SdkTracerProvider (batch OTLP exporter + resource), unchanged ...
global::set_tracer_provider(tracer_provider);
// NEW: bridge tracing spans -> OTel, installed process-globally
let tracer = opentelemetry::global::tracer("image-server");
let _ = tracing_subscriber::Registry::default()
.with(tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")))
.with(tracing_opentelemetry::OpenTelemetryLayer::new(tracer))
.try_init(); // global, NOT set_default()
}
```
## What was NOT done (timed out at 45 min on a cold build)
The **mechanism — the actual go/no-go — is complete and passing.** What didn't finish is the secondary real-app rewire: `init_tracing()` was not yet rewired and the streaming path was not yet `#[instrument]`-ed (the run was reading the target fns when the clock expired). That is now #134's job, with the validated pattern above. No `src/` files were changed by the spike.
## Recommendation for #134
**Proceed**, but record a hard requirement the original plan under-specified: **every `tokio::spawn` boundary must carry the parent span** (`.instrument(parent)` or a `span.enter()` guard). The streaming path (`insight_chat::chat_turn_stream_inner`) is the critical spawn site. Keep `tests/otel_propagation.rs` as the regression test (it pins S2/S4-orphan and S6/S7-fix). Stage the migration (subscriber + one module, verify in Jaeger/Tempo, then roll out) rather than one mega-commit.
## Artifacts
- Patch (deps + both test files): `/tmp/pi-subagents-uid-1000/async-subagent-runs/159c9378-f32e-4f08-bba0-8ba8900e64e8/worktree-diffs/step-0/task-0-worker.patch`
- The worktree/branch were auto-cleaned after the timeout. Reapply the patch (or copy `tests/otel_propagation.rs`) to reuse the regression test in #134.
Closing: the ADJUST verdict was adopted by the migration (map #135, merged as PR #148). Every spawn carries its parent via .instrument, and try_init() sets the subscriber process-wide. The spike's scenarios live on as tests/otel_propagation.rs, and tests/otel_llm_spans.rs / otel_background.rs extend them to the production shapes.
Closing: the ADJUST verdict was adopted by the migration (map #135, merged as PR #148). Every spawn carries its parent via `.instrument`, and `try_init()` sets the subscriber process-wide. The spike's scenarios live on as `tests/otel_propagation.rs`, and `tests/otel_llm_spans.rs` / `otel_background.rs` extend them to the production shapes.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Goal
De-risk the migration off the raw OTel SDK onto
tracing+tracing-opentelemetryby proving, on a throwaway branch, that OTel context propagates correctly across.awaitand intotokio::spawn-ed tasks — the exact case that produced orphanedllm.requestspans on the streaming chat path (fixed manually in PR #129 /f850c53).This is a spike: small, bounded, throwaway. The goal is a go/no-go + a validated
init_tracingshape, not a full migration.Why
Today we use
log(viaopentelemetry-appender-log) + the raw OTel SDK for spans: every span istracer.start_with_context(...), context is threaded by hand (cx: &Contextfirst param on DAO/LLM methods,Context::current_with_span(span)in every handler), and there's no#[instrument]. The raw SDK'sContext::current()is a thread-local that only.attach()guards set, and nothing in async actix/tokio propagates it across.await/spawn— that's the root of the orphan-span bug class.tracing+tracing-opentelemetryis the standard fix:tracingpropagates the current span (and its OTel context) across.awaitautomatically, and#[instrument]captures the parent span by value at call time, so it survivestokio::spawn.Version pin (verified)
tracing-opentelemetry = "0.32"depends onopentelemetry 0.31.0+opentelemetry_sdk 0.31.0(confirmed from itsCargo.toml) — exact match for the current stack, no otel bump.tracing,tracing-subscriber(withregistry), andtracing-log(so existinglog::calls keep working).Spike scope (one branch, minimal)
otel::init_tracing()(currentlysrc/otel.rs:20) to build atracing_subscriber::Registrywithtracing_opentelemetry::OpenTelemetryLayer::new(tracer)plus the existing stdout/env-filter layers. Confirm the existing OTLP gRPC exporter (OTLP_OTLS_ENDPOINT) still receives spans.tokio::spawn— the streaming chat path:chat_stream_handler→chat_turn_stream→chat_turn_stream_inner(thetokio::spawn) →run_streaming_turn→run_bootstrap_streaming→describe_image/run_streaming_agentic_loop.Add
#[tracing::instrument]to these fns. Leave thellm.requestspans as raw SDK for now (the point is parent/child nesting, not converting everything) — or instrument exactly one LLM call to see it nest.traceparentonto the request span viatracing_opentelemetry::OpenTelemetrySpanExt::set_parent(...). Also test whetheropentelemetry-instrumentation-actix-web::RequestTracingdoes this for us (check its otel-0.31 compatibility).Success criteria (go / no-go)
describe_imagellm.requestspan from the spawned streaming task nests under the HTTP request span, all sharing one trace ID.Deliverable
A short writeup posted back on this issue: what worked, what didn't, the exact validated
init_tracingshape, gotchas (e.g. does the parent span need to be active at call time for#[instrument]; does the batch exporter need different provider wiring; does the actix middleware expose context downstream), and a clear proceed / adjust / abandon recommendation.Out of scope
cx: &Contextparams.log::calls totracingmacros.llm.request/trace_db_callhelpers inotel.rs.Unblocks
The full tracing migration issue (created alongside this one).
Spike #133 — Cliff Notes
Verdict: ADJUST — the
tracing+tracing-opentelemetrymigration is viable and the wiring is sound, but a bare#[instrument]is NOT sufficient to fix the streamingtokio::spawnorphaning. Every spawn boundary must carry the parent span explicitly. This is more precise than the spike's original premise assumed, and it directly shapes #134.The decisive question
Does
tracing+tracing-opentelemetry 0.32propagate OTel context across.awaitand into atokio::spawn-ed task via#[instrument]?Answer: across
.await, yes. Acrosstokio::spawn, no — not with a bare#[instrument]. The span of an instrumentedasync fnis created at first poll, which for a spawned task is always on a worker thread where the parent span was never entered. Tracing span context is thread-local, so the parent link is lost → the child exports as an orphan (freshtrace_id,parent_span_id = 0000000000000000).Evidence (final passing run —
tests/otel_propagation.rs).await) propagates → theOpenTelemetryLayer+ exporter wiring is sound; the failure mode is specifically the spawn/thread boundary.chat_turn_stream_inner'stokio::spawn(async move { run_streaming_turn(...).await })) → orphaned. S4 (direct-call spawn) also orphans.Root cause (verified via
cargo expandon tracing-attributes)For an async
#[instrument]fn, the generated code creates the span inside the future body (at first poll), not at call time. Atokio::spawn-ed future first-polls on a runtime worker thread, so the parent is looked up on a thread where it was never entered. Same-thread (S1) and same-task (S5) cases propagate because the parent is active on the polling thread.The validated fix (S6 and S7 both pass)
Capture the active span in the parent's scope, then carry it into the spawned future:
.instrument()re-enters the span on every poll (including the first, on the worker), so an inner#[instrument]child nests correctly.Gotchas
set_default()is thread-local. Spans from worker threads are silently dropped. Install the tracing subscriber process-globally (try_init()/init()→dispatcher::set_global_default). The app's existinginit_tracingusesglobal::set_tracer_provider(global), so the provider side is fine — just don't useset_default()for the new subscriber.#[instrument]alone is not enough at spawn sites — it must be combined with the explicit carry above.opentelemetry_sdkneeds thetestingfeature forInMemorySpanExporter(only for the regression test).tracing-opentelemetry 0.32.1againstopentelemetry 0.31— no otel bump. Deps added:tracing,tracing-subscriber,tracing-log,tracing-opentelemetry = "0.32".log::calls keep working viatracing-log(bridgeslogrecords intotracing), so no forced migration of the ~hundreds oflog::sites.Recommended
init_tracingshape (validated wiring; NOT yet applied to the app)What was NOT done (timed out at 45 min on a cold build)
The mechanism — the actual go/no-go — is complete and passing. What didn't finish is the secondary real-app rewire:
init_tracing()was not yet rewired and the streaming path was not yet#[instrument]-ed (the run was reading the target fns when the clock expired). That is now #134's job, with the validated pattern above. Nosrc/files were changed by the spike.Recommendation for #134
Proceed, but record a hard requirement the original plan under-specified: every
tokio::spawnboundary must carry the parent span (.instrument(parent)or aspan.enter()guard). The streaming path (insight_chat::chat_turn_stream_inner) is the critical spawn site. Keeptests/otel_propagation.rsas the regression test (it pins S2/S4-orphan and S6/S7-fix). Stage the migration (subscriber + one module, verify in Jaeger/Tempo, then roll out) rather than one mega-commit.Artifacts
/tmp/pi-subagents-uid-1000/async-subagent-runs/159c9378-f32e-4f08-bba0-8ba8900e64e8/worktree-diffs/step-0/task-0-worker.patchtests/otel_propagation.rs) to reuse the regression test in #134.Closing: the ADJUST verdict was adopted by the migration (map #135, merged as PR #148). Every spawn carries its parent via
.instrument, andtry_init()sets the subscriber process-wide. The spike's scenarios live on astests/otel_propagation.rs, andtests/otel_llm_spans.rs/otel_background.rsextend them to the production shapes.