Skip to content

fix(core): keep Tokio runtime alive for OTel batch exporter - #982

Closed
Ladas wants to merge 3 commits into
praxis-proxy:mainfrom
Ladas:issue-tokio-otel-runtime
Closed

fix(core): keep Tokio runtime alive for OTel batch exporter#982
Ladas wants to merge 3 commits into
praxis-proxy:mainfrom
Ladas:issue-tokio-otel-runtime

Conversation

@Ladas

@Ladas Ladas commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Problem

The default BatchSpanProcessor in opentelemetry_sdk 0.32 drives export
calls via futures_executor::block_on, which cannot poll Tokio I/O
primitives. The tonic gRPC transport requires a Tokio reactor for its
Hyper-based HTTP/2 connections. Without a live Tokio runtime, traces are
silently dropped — the exporter builds successfully but every batch
export fails at the network layer.

Fix

Create a persistent multi-thread Tokio runtime (1 worker thread) stored
in TracingGuard that stays alive for both the initial tonic channel
build and all subsequent batch export calls. The runtime is dropped on
graceful shutdown alongside the tracer provider.

Testing

Verified end-to-end on KIND with OTel Collector + Tempo:

  • Before fix: 0 traces in Tempo, collector receives nothing
  • After fix: traces flow correctly, visible in Grafana

Related: praxis-proxy/forge#2 (OTel benchmark demo)

@Ladas
Ladas force-pushed the issue-tokio-otel-runtime branch 2 times, most recently from 225596d to a7c634b Compare August 14, 2026 16:23
@Ladas
Ladas marked this pull request as ready for review August 14, 2026 18:08
@Ladas
Ladas requested a review from a team August 14, 2026 18:08
@Ladas
Ladas requested a review from shaneutt as a code owner August 14, 2026 18:08
Comment thread core/src/logging.rs
@shaneutt shaneutt moved this from Next to Review in AI Gateway - Model Serving Aug 14, 2026
@shaneutt shaneutt added this to the v0.5.3 milestone Aug 14, 2026
@Ladas
Ladas force-pushed the issue-tokio-otel-runtime branch from a7c634b to 634c41b Compare August 14, 2026 20:58
@Ladas
Ladas requested a review from a team August 14, 2026 20:58
Ladas added 3 commits August 14, 2026 23:37
The default BatchSpanProcessor drives export calls via
futures_executor::block_on, which cannot poll Tokio I/O. Keep a
persistent multi-thread runtime in TracingGuard for tonic's transport.

Signed-off-by: Ladislav Smola <lsmola@redhat.com>
Adds an integration test that starts a real gRPC collector (using
opentelemetry-proto TraceService), configures the proxy to export
spans to it, and asserts that spans arrive. This exercises the
full export pipeline: TracingGuard keeps the Tokio runtime alive,
BatchSpanProcessor flushes, tonic gRPC transport delivers.

Signed-off-by: Ladislav Smola <lsmola@redhat.com>
Mirrors the experimental-features matrix from tests.yaml but for
integration test crates. Runs cargo-hack --each-feature on
praxis-tests-integration, covering otel, cpex-policy-engine, and
basic-auth-filter feature gates that were previously untested in CI.

Excludes no-mac-cert-rotation-tests (platform flag, not a feature).

Signed-off-by: Ladislav Smola <lsmola@redhat.com>
@Ladas
Ladas force-pushed the issue-tokio-otel-runtime branch from 634c41b to e65a9a6 Compare August 14, 2026 21:38

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: fix(core): keep Tokio runtime alive for OTel batch exporter

Fixes silent span loss caused by the default BatchSpanProcessor using futures_executor::block_on, which cannot drive Tokio I/O primitives needed by tonic's gRPC transport. The fix creates a dedicated multi-thread Tokio runtime (1 worker) that stays alive in TracingGuard for both the initial channel build and all subsequent batch exports.

Overall Assessment

The core fix is sound: creating the exporter inside runtime.block_on() binds the tonic channel to that runtime's I/O driver, and storing the runtime in TracingGuard keeps it alive. Drop ordering is correct -- the explicit Drop impl shuts down the provider (flushing spans) while the runtime is still alive, then field destructors clean up. The new integration test with a fake gRPC collector is a valuable addition.

Four items to address, all medium severity.

Findings

# Severity File Finding
1 Medium tests/integration/Cargo.toml opentelemetry-proto should be a workspace dependency
2 Medium core/src/logging.rs Unit test should also verify the runtime element is None
3 Medium tests/integration/tests/suite/examples/tracing_otlp.rs Inline comment in test body violates convention
4 Medium tests/integration/tests/suite/examples/tracing_otlp.rs Fixed sleep for flush wait; poll loop would be more robust

tokio-rustls = { workspace = true }
tokio-stream = { workspace = true }
tokio-tungstenite = { workspace = true }
opentelemetry-proto = { version = "0.32.0", default-features = false, features = ["gen-tonic", "trace"] }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Project convention: "Prefer workspace dependencies to keep versions consistent across crates." All other opentelemetry* crates are declared in the root [workspace.dependencies]. Add opentelemetry-proto there and reference it here as { workspace = true, default-features = false, features = ["gen-tonic", "trace"] }.

Comment thread core/src/logging.rs
let provider = build_otel_provider(&config).expect("should succeed with no endpoint");
assert!(
provider.is_none(),
provider.0.is_none(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] build_otel_provider now returns a tuple. This assertion verifies only the provider (.0) is None, but should also verify the runtime (.1) is None when no endpoint is configured:

assert!(
    provider.0.is_none() && provider.1.is_none(),
    "both provider and runtime should be None when no endpoint configured"
);

let (status, _) = http_get(proxy.addr(), "/", None);
assert_eq!(status, 200);

// Wait for the batch exporter to flush (interval=1s, add margin).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Convention: "Never use inline comments inside test function bodies. All explanatory text must be either an assertion message or a tracing::info! / debug! / trace! call." Remove this comment and move the intent into the assertion message on line 117 if needed.

assert_eq!(status, 200);

// Wait for the batch exporter to flush (interval=1s, add margin).
std::thread::sleep(std::time::Duration::from_secs(3));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] A fixed 3-second sleep can be fragile on slow CI runners. Consider a poll loop with a timeout instead:

let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
while span_count.load(Ordering::Relaxed) == 0 {
    assert!(
        std::time::Instant::now() < deadline,
        "timed out waiting for spans to arrive at fake collector"
    );
    std::thread::sleep(std::time::Duration::from_millis(100));
}

This is both faster in the happy path (returns as soon as spans arrive) and more tolerant of slow environments.

@shaneutt shaneutt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs some conflict and comment resolution and then I think we're pretty good to go.

@shaneutt

Copy link
Copy Markdown
Member

Thank you for your contribution! This has been merged into main as part of a bulk merge, see: 6fcfaca

@shaneutt shaneutt closed this Aug 25, 2026
@github-project-automation github-project-automation Bot moved this from Review to Done in AI Gateway - Model Serving Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants