Repository navigation
Conversation
The Logfire backend attaches an `x-logfire-warning` header to API responses to signal things it wants the user to see (e.g. a deprecated endpoint) without relying on the response body, as added to the Python SDK in pydantic/logfire#1906. Responses to OTLP exports over HTTP and to `logfire-client` queries are now inspected, and any warning is printed to stderr (deduplicated, so a chatty server only warns once per process). `AdvancedOptions::with_server_response_hook` replaces that default behaviour, mirroring the Python SDK's `server_response_hook`; the hook can call `ServerResponse::default_hook` to keep it, or do nothing to opt out. To see response headers the OTLP exporters now use an HTTP client built by this SDK rather than the one `opentelemetry-otlp` builds internally; there is no equivalent for gRPC exports, which is documented. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
| ) -> Result<crate::server_response::LogfireHttpClient, ConfigureError> { | ||
| let mut timeout = DEFAULT_EXPORT_TIMEOUT; | ||
| for var_name in [signal_timeout_var, "OTEL_EXPORTER_OTLP_TIMEOUT"] { | ||
| if let Some(value) = get_optional_env(var_name, None)? |
There was a problem hiding this comment.
🟠 High src/exporters.rs:387
An inherited non-UTF-8 OTEL_EXPORTER_OTLP_*_TIMEOUT value makes every HTTP exporter construction return an error and prevents Logfire initialization. get_optional_env(var_name, None)? propagates the decoding error instead of treating that value like an invalid timeout and continuing to the next variable or the default; ignore the environment lookup error before parsing the value.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @logfire/src/exporters.rs around line 387:
An inherited non-UTF-8 `OTEL_EXPORTER_OTLP_*_TIMEOUT` value makes every HTTP exporter construction return an error and prevents Logfire initialization. `get_optional_env(var_name, None)?` propagates the decoding error instead of treating that value like an invalid timeout and continuing to the next variable or the default; ignore the environment lookup error before parsing the value.
| /// process (matching the deduplication which Python's `warnings` module does for the Python SDK). | ||
| pub fn emit_warning(message: &str) { | ||
| if is_first_occurrence(message) { | ||
| eprintln!("Logfire server warning: {message}"); |
There was a problem hiding this comment.
🟠 High src/server_response.rs:21
emit_warning panics when writing the warning to stderr fails, so a valid warning header can crash the query/export path when stderr is closed, full, or non-blocking. Replace eprintln! with fallible stderr I/O and ignore or otherwise handle the write error.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @logfire-core/src/server_response.rs around line 21:
`emit_warning` panics when writing the warning to `stderr` fails, so a valid warning header can crash the query/export path when `stderr` is closed, full, or non-blocking. Replace `eprintln!` with fallible stderr I/O and ignore or otherwise handle the write error.
There was a problem hiding this comment.
Can we use warn! instead?
There was a problem hiding this comment.
logfire-core has no logging facade dependency, and warn! would be a no-op without a configured logger, changing the promised stderr behavior. A fallible writeln!(std::io::stderr(), ...) while ignoring the error preserves it without panicking.
| SEEN.get_or_init(Mutex::default) | ||
| .lock() | ||
| .unwrap_or_else(PoisonError::into_inner) | ||
| .insert(message.to_owned()) |
There was a problem hiding this comment.
🟠 High src/server_response.rs:31
is_first_occurrence retains every distinct server-supplied warning in the process-global HashSet, so unique x-logfire-warning values accumulate for the entire process lifetime and can cause unbounded memory growth or OOM. Bound the deduplication state or add eviction so changing backend warnings cannot permanently consume memory.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @logfire-core/src/server_response.rs around line 31:
`is_first_occurrence` retains every distinct server-supplied warning in the process-global `HashSet`, so unique `x-logfire-warning` values accumulate for the entire process lifetime and can cause unbounded memory growth or OOM. Bound the deduplication state or add eviction so changing backend warnings cannot permanently consume memory.
There was a problem hiding this comment.
Agreed, this is not acceptable. It's unlikely warnings are high cardinality, I wonder if a fixed size ordered list wouldn't be better.
There was a problem hiding this comment.
Agreed. Would you like me to prepare that bounded deduplication change?
| /// process (matching the deduplication which Python's `warnings` module does for the Python SDK). | ||
| pub fn emit_warning(message: &str) { | ||
| if is_first_occurrence(message) { | ||
| eprintln!("Logfire server warning: {message}"); |
There was a problem hiding this comment.
Can we use warn! instead?
| SEEN.get_or_init(Mutex::default) | ||
| .lock() | ||
| .unwrap_or_else(PoisonError::into_inner) | ||
| .insert(message.to_owned()) |
There was a problem hiding this comment.
Agreed, this is not acceptable. It's unlikely warnings are high cardinality, I wonder if a fixed size ordered list wouldn't be better.
|
|
||
| #[cfg(any(feature = "export-http-protobuf", feature = "export-http-json"))] | ||
| #[async_trait::async_trait] | ||
| impl opentelemetry_http::HttpClient for LogfireHttpClient { |
There was a problem hiding this comment.
I guess this duplication / vendoring is unavoidable if we want to support this.
| #[cfg_attr( | ||
| not(any(feature = "export-http-protobuf", feature = "export-http-json")), | ||
| allow(dead_code, reason = "only used by the HTTP exporters") | ||
| )] |
There was a problem hiding this comment.
Presumably we could have a version of this that works for gRPC as well? Not sure if there's a blocker there or we avoided it for complexity reasons.
| /// `opentelemetry-otlp` would build one itself, but logfire needs its own so that responses can | ||
| /// be passed to the [server response hook][crate::config::AdvancedOptions::with_server_response_hook]. | ||
| /// That means resolving the export timeout here too, the same way `opentelemetry-otlp` does. | ||
| #[cfg(any(feature = "export-http-protobuf", feature = "export-http-json"))] |
There was a problem hiding this comment.
This seems like a lot of new code touching unrelated things (export timeouts, etc.). I guess this is also something we'd have to vendor?
|
Overall seems like too much faff & risk to take no right now w/ no one asking for this. I'm going to prioritize stability and not introducing bugs over supporting this feature. |
Rust support for pydantic/logfire#1906: the backend attaches an
x-logfire-warningheader to API responses to signal something it wants the user to see (e.g. a deprecated endpoint) without relying on the response body.What this adds
logfire-core::server_response— the shared header name andemit_warning(), which printsLogfire server warning: <message>to stderr, deduplicated so a chatty server only warns once per process (the stand-in for the dedupe Python gets fromwarnings).logfire-clientquery responses are now inspected, and surface any warning by default.AdvancedOptions::with_server_response_hookmirrors the Python SDK'sserver_response_hook: it replaces the default behaviour, and a hook can callServerResponse::default_hook()to keep it or do nothing to opt out entirely.Notes for review
opentelemetry-otlpbuilds internally. That client is otherwise identical, but it does mean the export timeout (OTEL_EXPORTER_OTLP_<SIGNAL>_TIMEOUT/OTEL_EXPORTER_OTLP_TIMEOUT, default 10s) is now resolved inexporters::http_clientrather than upstream — a small duplication ofopentelemetry-otlp's own resolution.error_for_status()(unlike the upstreamHttpClient for reqwest::Clientimpl), so a warning sent alongside an error response is not lost.WithTonicConfig::with_channeltakes a concretetonic::transport::Channeland tonic interceptors only see requests, so there is nowhere to inspect responses. This is documented on the module and on the option. HTTP is the default protocol, so the default path is covered.CHANGELOG.mdentry, since entries there are written with PR links at release prep.Tests
Unit tests for the dedupe, header parsing and hook dispatch, plus an integration test asserting the hook fires for both HTTP protocols including on a
500.cargo test(all-features, no-default-features and doctests), clippy across each feature combination, fmt and the docs build are all clean.🤖 Generated with Claude Code