pydantic / pydantic/logfire-rust
Metrics export runs on the application's tokio runtime, leaking internal telemetry (and hanging shutdown)
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 77
- Forks
- 10
- Avg merge
- 8h 26m
- Merged PRs (30d)
- 5
Description
Two symptoms, one root cause. Both need metrics export to Logfire enabled (.with_metrics(Some(MetricsOptions::default())) with send_to_logfire), and both require logfire::configure() to be called from inside a tokio runtime — i.e. any #[tokio::main] application.
Root cause
spawn_runtime_and_exporters() creates a dedicated, telemetry-suppressed export runtime and builds the PeriodicReader under handle.enter(), exactly as it does for the span/log batch processors. But unlike those processors, the async-runtime PeriodicReader does not spawn its worker task in build() — it stashes a closure and spawns it lazily from MetricReader::register_pipeline():
opentelemetry_sdk0.32.1,src/metrics/periodic_reader_with_async_runtime.rs:build()storesProducerOrWorker::Worker(...);register_pipeline()callsworker(self)→runtime.spawn(...)→tokio::spawnon whatever runtime is ambient at that moment.
register_pipeline() runs when the meter provider is built — meter_provider_builder.build() in Logfire::configure() (logfire/src/logfire.rs), on the caller's thread, outside the export runtime's handle.enter(). So the metrics worker task ends up on the application's runtime.
Symptom 1: internal HTTP-client telemetry is exported to Logfire
Because the worker isn't on the suppressed export runtime, the metrics export's own HTTP traffic generates telemetry that the SDK captures and ships.
Measured with a mock server, global (non-.local()) logfire, one real info! call and one counter increment: 15 of 17 exported log records were hyper-util / reqwest / mio internals — "checkout waiting for idle connection", "starting new connection", "http1 handshake complete, spawning background dispatcher task", "registering event source with poller", and so on. Every one of them carried thread.name = "tokio-runtime-worker" (the app runtime) rather than logfire-export-runtime. With metrics disabled, the same test exports exactly 1 record.
So users with metrics enabled are paying for, and wading through, a stream of connection-pool noise from the SDK's own exporter.
Symptom 2: shutdown() hangs
PeriodicReader::shutdown_with_timeout() sends a Message::Shutdown to that worker task and then futures_executor::block_ons the reply. If the worker is on the application's current_thread runtime and shutdown() is called from that runtime, the thread is blocked and the message can never be processed — deadlock:
Logfire::shutdown
SdkMeterProvider::shutdown → Pipelines::shutdown → PeriodicReader::shutdown_with_timeout [blocked forever]
Reproduces under a plain #[tokio::test]; does not reproduce under flavor = "multi_thread" with spawn_blocking. logfire/tests/test_http_sink.rs::test_http_metrics_export currently works around it with spawn_blocking.
Candidate fix
Enter the export runtime's handle around meter_provider_builder.build() so register_pipeline spawns the worker on logfire's runtime. Locally this dropped the leaked records to zero. Open questions: how it should interact with user-supplied additional_readers (registered by the same build() call), and whether it fully resolves the hang for ShutdownGuard dropped at the end of an async main. Under investigation; a PR may follow.
Possibly also worth an upstream report — spawning in register_pipeline rather than build() makes the reader's runtime depend on an unrelated later call site.
🤖 Generated with Claude Code
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in spawn_runtime_and_exporters() and logfire/src/logfire.rs, focusing on meter_provider_builder.build() and the export runtime's handle.enter(). Run logfire/tests/test_http_sink.rs::test_http_metrics_export, then compare metrics-enabled export records and shutdown behavior under the tokio test configurations described. Done means metrics export no longer leaks internal HTTP-client telemetry and shutdown does not hang, including consideration of additional_readers.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- observability-sre
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100