diff --git a/Cargo.toml b/Cargo.toml index 431348b..2b4fef1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = ["logfire", "logfire-client", "logfire-core"] [workspace.package] edition = "2024" license = "MIT" -rust-version = "1.88" +rust-version = "1.91" repository = "https://github.com/pydantic/logfire-rust" [workspace.dependencies] diff --git a/logfire/Cargo.toml b/logfire/Cargo.toml index a062805..83271f7 100644 --- a/logfire/Cargo.toml +++ b/logfire/Cargo.toml @@ -13,19 +13,19 @@ repository.workspace = true logfire-core.workspace = true log = "0.4" -env_filter = "0.1" +env_filter = "2" # deps for grpc export http = { version = "1.2", optional = true } tonic = { version = "0.14", optional = true } -rand = "0.9.0" +rand = "0.10" -opentelemetry = { version = "0.31", default-features = false, features = [ +opentelemetry = { version = "0.32", default-features = false, features = [ "trace", "logs", ] } -opentelemetry_sdk = { version = "0.31", default-features = false, features = [ +opentelemetry_sdk = { version = "0.32", default-features = false, features = [ "trace", "experimental_metrics_custom_reader", "experimental_trace_batch_span_processor_with_async_runtime", @@ -35,7 +35,7 @@ opentelemetry_sdk = { version = "0.31", default-features = false, features = [ "logs", "spec_unstable_metrics_views" ] } -opentelemetry-otlp = { version = "0.31", default-features = false, features = [ +opentelemetry-otlp = { version = "0.32", default-features = false, features = [ "trace", "metrics", "logs", @@ -47,7 +47,7 @@ tokio = { version = "1.44.1", default-features = false, features = [ ] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tracing-opentelemetry = "0.32" +tracing-opentelemetry = "0.33" thiserror.workspace = true @@ -60,7 +60,7 @@ chrono = "0.4.39" async-trait = "0.1.88" futures = { version = "0.3.31", features = ["futures-executor"] } insta = "1.42.1" -opentelemetry_sdk = { version = "0.31", default-features = false, features = [ +opentelemetry_sdk = { version = "0.32", default-features = false, features = [ "testing", ] } regex = "1.11.1" @@ -70,11 +70,11 @@ tokio = { version = "1.44.1", features = [ "rt-multi-thread", ] } ulid = "1.2.0" -wiremock = "=0.6.4" +wiremock = "0.6.4" tonic-build = "0.14" tonic = { version = "0.14", features = ["transport"] } prost = "0.14" -opentelemetry-proto = { version = "0.31", features = [ +opentelemetry-proto = { version = "0.32", features = [ "tonic", "gen-tonic-messages", ] } @@ -82,12 +82,12 @@ tokio-stream = { version = "0.1", features = ["net"] } # Dependencies for examples axum = { version = "0.8", features = ["macros"] } -axum-tracing-opentelemetry = { version = "0.29", features = [ +axum-tracing-opentelemetry = { version = "0.38", features = [ "tracing_level_info", ] } -axum-otel-metrics = "0.12.0" +axum-otel-metrics = "0.13.0" actix-web = "4.0" -opentelemetry-instrumentation-actix-web = { version = "0.22", features = [ +opentelemetry-instrumentation-actix-web = { version = "0.24", features = [ "metrics", ] } serde = { version = "1.0", features = ["derive"] } diff --git a/logfire/src/bridges/tracing.rs b/logfire/src/bridges/tracing.rs index b97e1c2..f2f5ac1 100644 --- a/logfire/src/bridges/tracing.rs +++ b/logfire/src/bridges/tracing.rs @@ -495,8 +495,11 @@ mod tests { tracing::info!(name: "hello world log with value", field_value = 1); } - guard.shutdown().unwrap(); let spans = exporter.get_finished_spans().unwrap(); + let logs = log_exporter.get_emitted_logs().unwrap(); + + guard.shutdown().unwrap(); + assert_debug_snapshot!(spans, @r#" [ SpanData { @@ -1412,7 +1415,6 @@ mod tests { ] "#); - let logs = log_exporter.get_emitted_logs().unwrap(); let logs = make_deterministic_logs(logs, TEST_FILE, TEST_LINE); assert_debug_snapshot!(logs, @r#" [ @@ -2275,7 +2277,7 @@ mod tests { scope: InstrumentationScope { name: "tracing/tracing-opentelemetry", version: Some( - "0.32.1", + "0.33.0", ), schema_url: None, attributes: [], @@ -2351,7 +2353,7 @@ mod tests { scope: InstrumentationScope { name: "tracing/tracing-opentelemetry", version: Some( - "0.32.1", + "0.33.0", ), schema_url: None, attributes: [], diff --git a/logfire/src/exporters.rs b/logfire/src/exporters.rs index ed0fd74..7471311 100644 --- a/logfire/src/exporters.rs +++ b/logfire/src/exporters.rs @@ -52,7 +52,7 @@ pub fn span_exporter( endpoint: &str, headers: Option>, ) -> Result, ConfigureError> { - let (source, protocol) = protocol_from_env("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL")?; + let protocol = protocol_from_env("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL")?; // FIXME: it would be nice to let `opentelemetry-rust` handle this; ideally we could detect if // OTEL_EXPORTER_OTLP_PROTOCOL or OTEL_EXPORTER_OTLP_TRACES_PROTOCOL is set and let the SDK @@ -60,46 +60,44 @@ pub fn span_exporter( // // But at the moment otel-rust ignores these env vars; see // https://github.com/open-telemetry/opentelemetry-rust/issues/1983 - let span_exporter = - match protocol { - Protocol::Grpc => { - feature_required!("export-grpc", source, { - use opentelemetry_otlp::WithTonicConfig; - opentelemetry_otlp::SpanExporter::builder() - .with_tonic() - .with_channel( - tonic::transport::Channel::builder(endpoint.try_into().map_err( - |e: http::uri::InvalidUri| ConfigureError::Other(e.into()), - )?) - .connect_lazy(), - ) - .with_metadata(build_metadata_from_headers(headers.as_ref())?) - .build()? - }) - } - Protocol::HttpBinary => { - feature_required!("export-http-protobuf", source, { - use opentelemetry_otlp::{WithExportConfig, WithHttpConfig}; - opentelemetry_otlp::SpanExporter::builder() - .with_http() - .with_protocol(Protocol::HttpBinary) - .with_headers(headers.unwrap_or_default()) - .with_endpoint(format!("{endpoint}/v1/traces")) - .build()? - }) - } - Protocol::HttpJson => { - feature_required!("export-http-json", source, { - use opentelemetry_otlp::{WithExportConfig, WithHttpConfig}; - opentelemetry_otlp::SpanExporter::builder() - .with_http() - .with_protocol(Protocol::HttpJson) - .with_headers(headers.unwrap_or_default()) - .with_endpoint(format!("{endpoint}/v1/traces")) - .build()? - }) - } - }; + let span_exporter = match protocol { + #[cfg(feature = "export-grpc")] + Protocol::Grpc => { + use opentelemetry_otlp::WithTonicConfig; + opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_channel( + tonic::transport::Channel::builder( + endpoint + .try_into() + .map_err(|e: http::uri::InvalidUri| ConfigureError::Other(e.into()))?, + ) + .connect_lazy(), + ) + .with_metadata(build_metadata_from_headers(headers.as_ref())?) + .build()? + } + #[cfg(feature = "export-http-protobuf")] + Protocol::HttpBinary => { + use opentelemetry_otlp::{WithExportConfig, WithHttpConfig}; + opentelemetry_otlp::SpanExporter::builder() + .with_http() + .with_protocol(Protocol::HttpBinary) + .with_headers(headers.unwrap_or_default()) + .with_endpoint(format!("{endpoint}/v1/traces")) + .build()? + } + #[cfg(feature = "export-http-json")] + Protocol::HttpJson => { + use opentelemetry_otlp::{WithExportConfig, WithHttpConfig}; + opentelemetry_otlp::SpanExporter::builder() + .with_http() + .with_protocol(Protocol::HttpJson) + .with_headers(headers.unwrap_or_default()) + .with_endpoint(format!("{endpoint}/v1/traces")) + .build()? + } + }; #[cfg(not(any( feature = "export-grpc", @@ -142,7 +140,7 @@ pub fn metric_exporter( endpoint: &str, headers: Option>, ) -> Result, ConfigureError> { - let (source, protocol) = protocol_from_env("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL")?; + let protocol = protocol_from_env("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL")?; // FIXME: it would be nice to let `opentelemetry-rust` handle this; ideally we could detect if // OTEL_EXPORTER_OTLP_PROTOCOL or OTEL_EXPORTER_OTLP_METRICS_PROTOCOL is set and let the SDK @@ -151,47 +149,44 @@ pub fn metric_exporter( // But at the moment otel-rust ignores these env vars; see // https://github.com/open-telemetry/opentelemetry-rust/issues/1983 match protocol { + #[cfg(feature = "export-grpc")] Protocol::Grpc => { - feature_required!("export-grpc", source, { - use opentelemetry_otlp::WithTonicConfig; - Ok(opentelemetry_otlp::MetricExporter::builder() - .with_temporality(opentelemetry_sdk::metrics::Temporality::Delta) - .with_tonic() - .with_channel( - tonic::transport::Channel::builder( - endpoint.try_into().map_err(|e: http::uri::InvalidUri| { - ConfigureError::Other(e.into()) - })?, - ) - .connect_lazy(), + use opentelemetry_otlp::WithTonicConfig; + Ok(opentelemetry_otlp::MetricExporter::builder() + .with_temporality(opentelemetry_sdk::metrics::Temporality::Delta) + .with_tonic() + .with_channel( + tonic::transport::Channel::builder( + endpoint + .try_into() + .map_err(|e: http::uri::InvalidUri| ConfigureError::Other(e.into()))?, ) - .with_metadata(build_metadata_from_headers(headers.as_ref())?) - .build()?) - }) + .connect_lazy(), + ) + .with_metadata(build_metadata_from_headers(headers.as_ref())?) + .build()?) } + #[cfg(feature = "export-http-protobuf")] Protocol::HttpBinary => { - feature_required!("export-http-protobuf", source, { - use opentelemetry_otlp::{WithExportConfig, WithHttpConfig}; - Ok(opentelemetry_otlp::MetricExporter::builder() - .with_temporality(opentelemetry_sdk::metrics::Temporality::Delta) - .with_http() - .with_protocol(Protocol::HttpBinary) - .with_headers(headers.unwrap_or_default()) - .with_endpoint(format!("{endpoint}/v1/metrics")) - .build()?) - }) + use opentelemetry_otlp::{WithExportConfig, WithHttpConfig}; + Ok(opentelemetry_otlp::MetricExporter::builder() + .with_temporality(opentelemetry_sdk::metrics::Temporality::Delta) + .with_http() + .with_protocol(Protocol::HttpBinary) + .with_headers(headers.unwrap_or_default()) + .with_endpoint(format!("{endpoint}/v1/metrics")) + .build()?) } + #[cfg(feature = "export-http-json")] Protocol::HttpJson => { - feature_required!("export-http-json", source, { - use opentelemetry_otlp::{WithExportConfig, WithHttpConfig}; - Ok(opentelemetry_otlp::MetricExporter::builder() - .with_temporality(opentelemetry_sdk::metrics::Temporality::Delta) - .with_http() - .with_protocol(Protocol::HttpJson) - .with_headers(headers.unwrap_or_default()) - .with_endpoint(format!("{endpoint}/v1/metrics")) - .build()?) - }) + use opentelemetry_otlp::{WithExportConfig, WithHttpConfig}; + Ok(opentelemetry_otlp::MetricExporter::builder() + .with_temporality(opentelemetry_sdk::metrics::Temporality::Delta) + .with_http() + .with_protocol(Protocol::HttpJson) + .with_headers(headers.unwrap_or_default()) + .with_endpoint(format!("{endpoint}/v1/metrics")) + .build()?) } } @@ -205,7 +200,6 @@ pub fn metric_exporter( // suppress unused var warnings let _ = endpoint; let _ = headers; - let _ = source; let _ = protocol; Ok(UnreachableExporter) } @@ -228,7 +222,7 @@ pub fn log_exporter( endpoint: &str, headers: Option>, ) -> Result, ConfigureError> { - let (source, protocol) = protocol_from_env("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL")?; + let protocol = protocol_from_env("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL")?; // FIXME: it would be nice to let `opentelemetry-rust` handle this; ideally we could detect if // OTEL_EXPORTER_OTLP_PROTOCOL or OTEL_EXPORTER_OTLP_LOGS_PROTOCOL is set and let the SDK @@ -237,44 +231,41 @@ pub fn log_exporter( // But at the moment otel-rust ignores these env vars; see // https://github.com/open-telemetry/opentelemetry-rust/issues/1983 match protocol { + #[cfg(feature = "export-grpc")] Protocol::Grpc => { - feature_required!("export-grpc", source, { - use opentelemetry_otlp::WithTonicConfig; - Ok(opentelemetry_otlp::LogExporter::builder() - .with_tonic() - .with_channel( - tonic::transport::Channel::builder( - endpoint.try_into().map_err(|e: http::uri::InvalidUri| { - ConfigureError::Other(e.into()) - })?, - ) - .connect_lazy(), + use opentelemetry_otlp::WithTonicConfig; + Ok(opentelemetry_otlp::LogExporter::builder() + .with_tonic() + .with_channel( + tonic::transport::Channel::builder( + endpoint + .try_into() + .map_err(|e: http::uri::InvalidUri| ConfigureError::Other(e.into()))?, ) - .with_metadata(build_metadata_from_headers(headers.as_ref())?) - .build()?) - }) + .connect_lazy(), + ) + .with_metadata(build_metadata_from_headers(headers.as_ref())?) + .build()?) } + #[cfg(feature = "export-http-protobuf")] Protocol::HttpBinary => { - feature_required!("export-http-protobuf", source, { - use opentelemetry_otlp::{WithExportConfig, WithHttpConfig}; - Ok(opentelemetry_otlp::LogExporter::builder() - .with_http() - .with_protocol(Protocol::HttpBinary) - .with_headers(headers.unwrap_or_default()) - .with_endpoint(format!("{endpoint}/v1/logs")) - .build()?) - }) + use opentelemetry_otlp::{WithExportConfig, WithHttpConfig}; + Ok(opentelemetry_otlp::LogExporter::builder() + .with_http() + .with_protocol(Protocol::HttpBinary) + .with_headers(headers.unwrap_or_default()) + .with_endpoint(format!("{endpoint}/v1/logs")) + .build()?) } + #[cfg(feature = "export-http-json")] Protocol::HttpJson => { - feature_required!("export-http-json", source, { - use opentelemetry_otlp::{WithExportConfig, WithHttpConfig}; - Ok(opentelemetry_otlp::LogExporter::builder() - .with_http() - .with_protocol(Protocol::HttpJson) - .with_headers(headers.unwrap_or_default()) - .with_endpoint(format!("{endpoint}/v1/logs")) - .build()?) - }) + use opentelemetry_otlp::{WithExportConfig, WithHttpConfig}; + Ok(opentelemetry_otlp::LogExporter::builder() + .with_http() + .with_protocol(Protocol::HttpJson) + .with_headers(headers.unwrap_or_default()) + .with_endpoint(format!("{endpoint}/v1/logs")) + .build()?) } } @@ -288,7 +279,6 @@ pub fn log_exporter( // suppress unused var warnings let _ = endpoint; let _ = headers; - let _ = source; let _ = protocol; Ok(UnreachableExporter) } @@ -313,30 +303,17 @@ fn build_metadata_from_headers( } // current default logfire protocol is to export over HTTP in binary format -const DEFAULT_LOGFIRE_PROTOCOL: Protocol = Protocol::HttpBinary; +const DEFAULT_LOGFIRE_PROTOCOL: &str = OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF; // standard OTLP protocol values in configuration const OTEL_EXPORTER_OTLP_PROTOCOL_GRPC: &str = "grpc"; const OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF: &str = "http/protobuf"; const OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON: &str = "http/json"; -/// Temporary workaround for lack of -fn protocol_from_str(value: &str) -> Result { - match value { - OTEL_EXPORTER_OTLP_PROTOCOL_GRPC => Ok(Protocol::Grpc), - OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF => Ok(Protocol::HttpBinary), - OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON => Ok(Protocol::HttpJson), - _ => Err(ConfigureError::Other( - format!("unsupported protocol: {value}").into(), - )), - } -} - -/// Get a protocol from the environment (or default value), returning a string describing the source -/// plus the parsed protocol. -fn protocol_from_env(data_env_var: &str) -> Result<(String, Protocol), ConfigureError> { +/// Get a protocol from the environment (or default value). +fn protocol_from_env(data_env_var: &str) -> Result { // try both data-specific env var and general protocol - [data_env_var, "OTEL_EXPORTER_OTLP_PROTOCOL"] + let (source, value) = [data_env_var, "OTEL_EXPORTER_OTLP_PROTOCOL"] .into_iter() .find_map(|var_name| match get_optional_env(var_name, None) { Ok(Some(value)) => Some(Ok((var_name, value))), @@ -344,15 +321,28 @@ fn protocol_from_env(data_env_var: &str) -> Result<(String, Protocol), Configure Err(e) => Some(Err(e)), }) .transpose()? - .map_or_else( - || { - Ok(( - "the default logfire export protocol".to_string(), - DEFAULT_LOGFIRE_PROTOCOL, - )) - }, - |(var_name, value)| Ok((format!("`{var_name}={value}`"), protocol_from_str(&value)?)), - ) + .map_or( + ( + "the default logfire export protocol".to_string(), + DEFAULT_LOGFIRE_PROTOCOL.to_string(), + ), + |(var_name, value)| (format!("`{var_name}={value}`"), value), + ); + + match value.as_str() { + OTEL_EXPORTER_OTLP_PROTOCOL_GRPC => { + feature_required!("export-grpc", source, Ok(Protocol::Grpc)) + } + OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF => { + feature_required!("export-http-protobuf", source, Ok(Protocol::HttpBinary)) + } + OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON => { + feature_required!("export-http-json", source, Ok(Protocol::HttpJson)) + } + _ => Err(ConfigureError::Other( + format!("unsupported protocol: {value}").into(), + )), + } } /// Internal type used when no export features are available to allow for type inference diff --git a/logfire/src/internal/exporters/remove_pending.rs b/logfire/src/internal/exporters/remove_pending.rs index 3f3343b..3c129c8 100644 --- a/logfire/src/internal/exporters/remove_pending.rs +++ b/logfire/src/internal/exporters/remove_pending.rs @@ -57,11 +57,11 @@ impl SpanExporter for RemovePendingSpansExporter { self.0.export(spans).await } - fn shutdown(&mut self) -> OTelSdkResult { + fn shutdown(&self) -> OTelSdkResult { self.0.shutdown() } - fn force_flush(&mut self) -> OTelSdkResult { + fn force_flush(&self) -> OTelSdkResult { self.0.force_flush() } @@ -123,8 +123,9 @@ mod tests { let _debug = crate::span!(level: Level::DEBUG, "debug span").entered(); } - guard.shutdown().unwrap(); + guard.force_flush().unwrap(); let mut spans = exporter.get_finished_spans().unwrap(); + guard.shutdown().unwrap(); spans.sort_unstable_by(|a, b| a.name.cmp(&b.name)); let spans = spans diff --git a/logfire/src/lib.rs b/logfire/src/lib.rs index d5810fe..25c9240 100644 --- a/logfire/src/lib.rs +++ b/logfire/src/lib.rs @@ -121,10 +121,6 @@ pub enum ConfigureError { #[error("Error configuring the global logger: {0}")] Logging(#[from] log::SetLoggerError), - /// Error configuring the OpenTelemetry tracer. - #[error("Error configuring the OpenTelemetry tracer: {0}")] - Trace(#[from] opentelemetry_sdk::trace::TraceError), - /// OpenTelemetry exporter failed to build #[error("Error building the OpenTelemetry exporter: {0}")] ExporterBuildError(#[from] opentelemetry_otlp::ExporterBuildError), diff --git a/logfire/src/logfire.rs b/logfire/src/logfire.rs index 43553bd..478c436 100644 --- a/logfire/src/logfire.rs +++ b/logfire/src/logfire.rs @@ -656,6 +656,11 @@ impl LocalLogfireGuard { &self.logfire.meter_provider } + /// Convenience function to force flush the current data captured by Logfire. + pub fn force_flush(&self) -> Result<(), opentelemetry_sdk::error::OTelSdkError> { + self.logfire.force_flush() + } + /// Convenience function to release this guard and shutdown Logfire. pub fn shutdown(self) -> Result<(), ShutdownError> { let logfire = self.logfire.clone(); diff --git a/logfire/src/macros/mod.rs b/logfire/src/macros/mod.rs index edbba9e..0ccf717 100644 --- a/logfire/src/macros/mod.rs +++ b/logfire/src/macros/mod.rs @@ -533,9 +533,8 @@ mod tests { crate::info!("optional integer: {value:?}", value = Some(12)); crate::info!("optional float: {value:?}", value = Some(5.6)); - guard.shutdown().unwrap(); - let logs = log_exporter.get_emitted_logs().unwrap(); + guard.shutdown().unwrap(); let messages_and_value = logs .iter() diff --git a/logfire/src/metrics.rs b/logfire/src/metrics.rs index c599afb..8a061d0 100644 --- a/logfire/src/metrics.rs +++ b/logfire/src/metrics.rs @@ -447,7 +447,6 @@ impl<'a> LogfireMetrics<'a> { macro_rules! logfire_metrics_method { ($method:ident -> $return_ty:ty) => { #[doc = concat!("See [`", stringify!($method), "`][crate::", stringify!($method), "].")] - #[must_use] pub fn $method(&self, name: impl Into>) -> $return_ty { self.meter.$method(name) } diff --git a/logfire/src/test_utils.rs b/logfire/src/test_utils.rs index 84e1897..1d8f0cc 100644 --- a/logfire/src/test_utils.rs +++ b/logfire/src/test_utils.rs @@ -130,6 +130,18 @@ impl SpanExporter for DeterministicExporter { } self.exporter.export(batch) } + + fn shutdown(&self) -> OTelSdkResult { + self.exporter.shutdown() + } + + fn shutdown_with_timeout(&self, timeout: time::Duration) -> OTelSdkResult { + self.exporter.shutdown_with_timeout(timeout) + } + + fn force_flush(&self) -> OTelSdkResult { + self.exporter.force_flush() + } } impl DeterministicExporter { @@ -195,19 +207,91 @@ pub fn remap_timestamps_in_console_output(output: &str) -> Cow<'_, str> { }) } +/// Abstraction over opentelemetry KeyValue and opentelemetry_proto's KeyValue to enable +/// making deterministic resource attributes for both types. +trait CommonKeyValue { + fn key_str(&self) -> Cow<'_, str>; + fn value_str(&self) -> Cow<'_, str>; + fn set_value(&mut self, value: impl Into>); +} + +impl CommonKeyValue for KeyValue { + fn key_str(&self) -> Cow<'_, str> { + self.key.as_str().into() + } + + fn value_str(&self) -> Cow<'_, str> { + self.value.as_str().into() + } + + fn set_value(&mut self, value: impl Into>) { + self.value = value.into().into(); + } +} + +impl CommonKeyValue for opentelemetry_proto::tonic::common::v1::KeyValue { + fn key_str(&self) -> Cow<'_, str> { + self.key.as_str().into() + } + + fn value_str(&self) -> Cow<'_, str> { + match &self.value { + Some(v) => match &v.value { + Some(opentelemetry_proto::tonic::common::v1::any_value::Value::StringValue(s)) => { + s.as_str().into() + } + _ => "".into(), + }, + None => "".into(), + } + } + + fn set_value(&mut self, value: impl Into>) { + self.value = Some(opentelemetry_proto::tonic::common::v1::AnyValue { + value: Some( + opentelemetry_proto::tonic::common::v1::any_value::Value::StringValue( + value.into().into_owned(), + ), + ), + }); + } +} + +fn make_deterministic_resource_attrs(attrs: &mut Vec) { + attrs.sort_by_key(|kv| kv.key_str().into_owned()); + for attr in attrs.iter_mut() { + // don't care about opentelemetry sdk version for tests + if attr.key_str() == "telemetry.sdk.version" { + attr.set_value("0.0.0"); + } + } + + // service.name has the excutable name suffixed, which includes the metadata hash + // in tests, so we need to trim that off + // + // see https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/#service-name + if let Some(service_name_attr) = attrs.iter_mut().find(|kv| kv.key_str() == "service.name") { + let service_name = service_name_attr.value_str(); + if service_name.starts_with("unknown_service:") { + let current_exe = std::env::current_exe().unwrap(); + let process_name = current_exe.file_name().unwrap(); + assert_eq!( + service_name, + format!("unknown_service:{}", process_name.display()) + ); + + service_name_attr.set_value("unknown_service:"); + } + } +} + /// `Resource` contains a hashmap, so deterministic tests need to convert to an ordered container. pub fn make_deterministic_resource(resource: &Resource) -> Vec { let mut attrs: Vec<_> = resource .iter() .map(|(k, v)| KeyValue::new(k.clone(), v.clone())) .collect(); - attrs.sort_by_key(|kv| kv.key.clone()); - for attr in &mut attrs { - // don't care about opentelemetry sdk version for tests - if attr.key.as_str() == "telemetry.sdk.version" { - attr.value = "0.0.0".into(); - } - } + make_deterministic_resource_attrs(&mut attrs); attrs } @@ -431,8 +515,7 @@ pub fn make_trace_request_deterministic(req: &mut ExportTraceServiceRequest) { for resource_span in &mut req.resource_spans { if let Some(resource) = &mut resource_span.resource { - // Sort attributes by key - resource.attributes.sort_by_key(|attr| attr.key.clone()); + make_deterministic_resource_attrs(&mut resource.attributes); } for scope_span in &mut resource_span.scope_spans { @@ -479,7 +562,7 @@ pub fn make_log_request_deterministic(req: &mut ExportLogsServiceRequest) { for resource_log in &mut req.resource_logs { if let Some(resource) = &mut resource_log.resource { - resource.attributes.sort_by_key(|attr| attr.key.clone()); + make_deterministic_resource_attrs(&mut resource.attributes); } for scope_log in &mut resource_log.scope_logs { diff --git a/logfire/src/ulid_id_generator.rs b/logfire/src/ulid_id_generator.rs index 728b64e..c71e3b8 100644 --- a/logfire/src/ulid_id_generator.rs +++ b/logfire/src/ulid_id_generator.rs @@ -1,6 +1,6 @@ use std::cell::RefCell; -use rand::{Rng, RngCore, SeedableRng, rngs}; +use rand::{Rng, RngExt, SeedableRng, rngs}; use opentelemetry::trace::{SpanId, TraceId}; use opentelemetry_sdk::trace::IdGenerator; diff --git a/logfire/tests/test_basic_exports.rs b/logfire/tests/test_basic_exports.rs index 78f0741..755272c 100644 --- a/logfire/tests/test_basic_exports.rs +++ b/logfire/tests/test_basic_exports.rs @@ -73,9 +73,10 @@ fn test_basic_span() { })) .unwrap_err(); + let spans = exporter.get_finished_spans().unwrap(); + let logs = log_exporter.get_emitted_logs().unwrap(); guard.shutdown().unwrap(); - let spans = exporter.get_finished_spans().unwrap(); assert_debug_snapshot!(spans, @r#" [ SpanData { @@ -633,7 +634,6 @@ fn test_basic_span() { ] "#); - let logs = log_exporter.get_emitted_logs().unwrap(); let logs = make_deterministic_logs(logs, TEST_FILE, TEST_LINE); assert_debug_snapshot!(logs, @r#" [ diff --git a/logfire/tests/test_grpc_sink.rs b/logfire/tests/test_grpc_sink.rs index 2ccb736..657c943 100644 --- a/logfire/tests/test_grpc_sink.rs +++ b/logfire/tests/test_grpc_sink.rs @@ -218,11 +218,12 @@ async fn test_grpc_protobuf_export() { AnyValue { value: Some( StringValue( - "unknown_service", + "unknown_service:", ), ), }, ), + key_strindex: 0, }, KeyValue { key: "telemetry.sdk.language", @@ -235,6 +236,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "telemetry.sdk.name", @@ -247,6 +249,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "telemetry.sdk.version", @@ -254,11 +257,12 @@ async fn test_grpc_protobuf_export() { AnyValue { value: Some( StringValue( - "0.31.0", + "0.0.0", ), ), }, ), + key_strindex: 0, }, ], dropped_attributes_count: 0, @@ -324,6 +328,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "code.file.path", @@ -336,6 +341,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "code.line.number", @@ -348,6 +354,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "code.module.name", @@ -360,6 +367,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "idle_ns", @@ -372,6 +380,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "logfire.json_schema", @@ -384,6 +393,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "logfire.level_num", @@ -396,6 +406,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "logfire.msg", @@ -408,6 +419,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "thread.id", @@ -420,6 +432,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "thread.name", @@ -432,6 +445,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, ], dropped_attributes_count: 0, @@ -475,11 +489,12 @@ async fn test_grpc_protobuf_export() { AnyValue { value: Some( StringValue( - "unknown_service", + "unknown_service:", ), ), }, ), + key_strindex: 0, }, KeyValue { key: "telemetry.sdk.language", @@ -492,6 +507,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "telemetry.sdk.name", @@ -504,6 +520,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "telemetry.sdk.version", @@ -511,11 +528,12 @@ async fn test_grpc_protobuf_export() { AnyValue { value: Some( StringValue( - "0.31.0", + "0.0.0", ), ), }, ), + key_strindex: 0, }, ], dropped_attributes_count: 0, @@ -559,6 +577,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "code.line.number", @@ -571,6 +590,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "code.module.name", @@ -583,6 +603,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "logfire.json_schema", @@ -595,6 +616,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "thread.id", @@ -607,6 +629,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "thread.name", @@ -619,6 +642,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, ], dropped_attributes_count: 0, @@ -679,6 +703,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "code.line.number", @@ -691,6 +716,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "code.module.name", @@ -703,6 +729,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "logfire.json_schema", @@ -715,6 +742,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "thread.id", @@ -727,6 +755,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "thread.name", @@ -739,6 +768,7 @@ async fn test_grpc_protobuf_export() { ), }, ), + key_strindex: 0, }, ], dropped_attributes_count: 0, diff --git a/logfire/tests/test_http_sink.rs b/logfire/tests/test_http_sink.rs index 0100338..984e028 100644 --- a/logfire/tests/test_http_sink.rs +++ b/logfire/tests/test_http_sink.rs @@ -78,11 +78,12 @@ async fn test_http_protobuf_export() { AnyValue { value: Some( StringValue( - "unknown_service", + "unknown_service:", ), ), }, ), + key_strindex: 0, }, KeyValue { key: "telemetry.sdk.language", @@ -95,6 +96,7 @@ async fn test_http_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "telemetry.sdk.name", @@ -107,6 +109,7 @@ async fn test_http_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "telemetry.sdk.version", @@ -114,11 +117,12 @@ async fn test_http_protobuf_export() { AnyValue { value: Some( StringValue( - "0.31.0", + "0.0.0", ), ), }, ), + key_strindex: 0, }, ], dropped_attributes_count: 0, @@ -184,6 +188,7 @@ async fn test_http_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "code.file.path", @@ -196,6 +201,7 @@ async fn test_http_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "code.line.number", @@ -208,6 +214,7 @@ async fn test_http_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "code.module.name", @@ -220,6 +227,7 @@ async fn test_http_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "idle_ns", @@ -232,6 +240,7 @@ async fn test_http_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "logfire.json_schema", @@ -244,6 +253,7 @@ async fn test_http_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "logfire.level_num", @@ -256,6 +266,7 @@ async fn test_http_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "logfire.msg", @@ -268,6 +279,7 @@ async fn test_http_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "thread.id", @@ -280,6 +292,7 @@ async fn test_http_protobuf_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "thread.name", @@ -292,6 +305,7 @@ async fn test_http_protobuf_export() { ), }, ), + key_strindex: 0, }, ], dropped_attributes_count: 0, @@ -382,11 +396,12 @@ async fn test_http_json_export() { AnyValue { value: Some( StringValue( - "unknown_service", + "unknown_service:", ), ), }, ), + key_strindex: 0, }, KeyValue { key: "telemetry.sdk.language", @@ -399,6 +414,7 @@ async fn test_http_json_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "telemetry.sdk.name", @@ -411,6 +427,7 @@ async fn test_http_json_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "telemetry.sdk.version", @@ -418,11 +435,12 @@ async fn test_http_json_export() { AnyValue { value: Some( StringValue( - "0.31.0", + "0.0.0", ), ), }, ), + key_strindex: 0, }, ], dropped_attributes_count: 0, @@ -488,6 +506,7 @@ async fn test_http_json_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "code.file.path", @@ -500,6 +519,7 @@ async fn test_http_json_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "code.line.number", @@ -507,11 +527,12 @@ async fn test_http_json_export() { AnyValue { value: Some( IntValue( - 358, + 372, ), ), }, ), + key_strindex: 0, }, KeyValue { key: "code.module.name", @@ -524,6 +545,7 @@ async fn test_http_json_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "idle_ns", @@ -536,6 +558,7 @@ async fn test_http_json_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "logfire.json_schema", @@ -548,6 +571,7 @@ async fn test_http_json_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "logfire.level_num", @@ -560,6 +584,7 @@ async fn test_http_json_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "logfire.msg", @@ -572,6 +597,7 @@ async fn test_http_json_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "thread.id", @@ -584,6 +610,7 @@ async fn test_http_json_export() { ), }, ), + key_strindex: 0, }, KeyValue { key: "thread.name", @@ -596,6 +623,7 @@ async fn test_http_json_export() { ), }, ), + key_strindex: 0, }, ], dropped_attributes_count: 0, diff --git a/logfire/tests/test_log_attributes.rs b/logfire/tests/test_log_attributes.rs index cfa45cf..eb7a9ef 100644 --- a/logfire/tests/test_log_attributes.rs +++ b/logfire/tests/test_log_attributes.rs @@ -36,8 +36,8 @@ fn test_log_macro_attributes() { d.e = 3 ); - guard.shutdown().unwrap(); let logs = log_exporter.get_emitted_logs().unwrap(); + guard.shutdown().unwrap(); // String attribute let log = find_log(&logs, "string_attr_log"); @@ -119,9 +119,8 @@ fn test_log_macro_shorthand_ident() { multi.d_e ); - guard.shutdown().unwrap(); - let logs = log_exporter.get_emitted_logs().unwrap(); + guard.shutdown().unwrap(); // Dotted key attribute let log = find_log(&logs, "dotted_attr_log"); diff --git a/logfire/tests/test_resource_attributes.rs b/logfire/tests/test_resource_attributes.rs index 99c47b3..e7b7f6f 100644 --- a/logfire/tests/test_resource_attributes.rs +++ b/logfire/tests/test_resource_attributes.rs @@ -45,9 +45,8 @@ fn try_get_resource_attrs(config: LogfireConfigBuilder, env: &[(&str, &str)]) -> logfire::info!("test span"); - guard.shutdown().expect("shutdown should succeed"); - let mut logs = exporter.get_emitted_logs().unwrap(); + guard.shutdown().expect("shutdown should succeed"); assert_eq!(logs.len(), 1); let log = logs.pop().unwrap(); @@ -67,49 +66,49 @@ fn test_no_service_resource_attributes() { let attrs = try_get_resource_attrs(configure(), &[]); assert_debug_snapshot!(attrs, @r#" - [ - KeyValue { - key: Static( - "service.name", - ), - value: String( - Static( - "unknown_service", - ), - ), - }, - KeyValue { - key: Static( - "telemetry.sdk.language", - ), - value: String( - Static( - "rust", - ), - ), - }, - KeyValue { - key: Static( - "telemetry.sdk.name", + [ + KeyValue { + key: Static( + "service.name", + ), + value: String( + Static( + "unknown_service:", ), - value: String( - Static( - "opentelemetry", - ), + ), + }, + KeyValue { + key: Static( + "telemetry.sdk.language", + ), + value: String( + Static( + "rust", ), - }, - KeyValue { - key: Static( - "telemetry.sdk.version", + ), + }, + KeyValue { + key: Static( + "telemetry.sdk.name", + ), + value: String( + Static( + "opentelemetry", ), - value: String( - Static( - "0.0.0", - ), + ), + }, + KeyValue { + key: Static( + "telemetry.sdk.version", + ), + value: String( + Static( + "0.0.0", ), - }, - ] - "#); + ), + }, + ] + "#); } #[test]