From 8174239c738c01b984b9733e1549dbb26b7286a4 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 25 Mar 2025 14:26:39 +0000 Subject: [PATCH 1/2] support configuring metrics --- CHANGELOG.md | 1 + Cargo.toml | 2 +- src/bridges/tracing.rs | 23 +- src/config.rs | 62 +- src/exporters.rs | 19 +- src/internal/exporters/console.rs | 30 +- src/internal/exporters/remove_pending.rs | 13 +- src/lib.rs | 1747 ++------------------- src/test_utils.rs | 139 ++ tests/test_basic_exports.rs | 1772 ++++++++++++++++++++++ 10 files changed, 2154 insertions(+), 1654 deletions(-) create mode 100644 src/test_utils.rs create mode 100644 tests/test_basic_exports.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6160dfa..2c70688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Add `span_exporter()` helper to build a span exporter directly in [#20](https://github.com/pydantic/logfire-rust/pull/20) - Fix `set_resource` not being called on span processors added with `with_additional_span_processor()` in [#20](https://github.com/pydantic/logfire-rust/pull/20) +- Add `MetricsOptions` for configuring additional metrics exporters in [#21](https://github.com/pydantic/logfire-rust/pull/21) ## [v0.1.0] (2025-03-13) diff --git a/Cargo.toml b/Cargo.toml index b6bd773..252f997 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ chrono = "0.4.39" [dev-dependencies] insta = "1.42.1" -opentelemetry_sdk = { version = "0.28", default-features = false, features = ["testing"] } +opentelemetry_sdk = { version = "0.28", default-features = false, features = ["testing", "internal-logs"] } regex = "1.11.1" ulid = "1.2.0" diff --git a/src/bridges/tracing.rs b/src/bridges/tracing.rs index fbb7b01..83a968e 100644 --- a/src/bridges/tracing.rs +++ b/src/bridges/tracing.rs @@ -141,14 +141,14 @@ mod tests { use crate::{ config::{AdvancedOptions, ConsoleOptions, Target}, set_local_logfire, - tests::{DeterministicExporter, DeterministicIdGenerator}, + test_utils::{DeterministicExporter, DeterministicIdGenerator}, }; #[test] fn test_tracing_bridge() { let exporter = InMemorySpanExporterBuilder::new().build(); - let config = crate::configure() + let handler = crate::configure() .send_to_logfire(false) .with_additional_span_processor(SimpleSpanProcessor::new(Box::new( DeterministicExporter::new(exporter.clone(), file!(), line!()), @@ -157,11 +157,13 @@ mod tests { .with_default_level_filter(LevelFilter::TRACE) .with_advanced_options( AdvancedOptions::default().with_id_generator(DeterministicIdGenerator::new()), - ); + ) + .finish() + .unwrap(); - let guard = set_local_logfire(config).unwrap(); + let guard = set_local_logfire(handler); - tracing::subscriber::with_default(guard.subscriber.clone(), || { + tracing::subscriber::with_default(guard.subscriber().clone(), || { let root = tracing::span!(Level::INFO, "root span").entered(); let _ = tracing::span!(Level::INFO, "hello world span").entered(); let _ = tracing::span!(Level::DEBUG, "debug span"); @@ -1186,15 +1188,18 @@ mod tests { ..ConsoleOptions::default() }; - let config = crate::configure() + let handler = crate::configure() + .local() .send_to_logfire(false) .console_options(console_options.clone()) .install_panic_handler() - .with_default_level_filter(LevelFilter::TRACE); + .with_default_level_filter(LevelFilter::TRACE) + .finish() + .unwrap(); - let guard = set_local_logfire(config).unwrap(); + let guard = crate::set_local_logfire(handler); - tracing::subscriber::with_default(guard.subscriber.clone(), || { + tracing::subscriber::with_default(guard.subscriber().clone(), || { let root = tracing::span!(Level::INFO, "root span").entered(); let _ = tracing::span!(Level::INFO, "hello world span").entered(); let _ = tracing::span!(Level::DEBUG, "debug span"); diff --git a/src/config.rs b/src/config.rs index 7d0656b..421c5ee 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,7 +8,10 @@ use std::{ sync::{Arc, Mutex}, }; -use opentelemetry_sdk::trace::{IdGenerator, SpanProcessor}; +use opentelemetry_sdk::{ + metrics::reader::MetricReader, + trace::{IdGenerator, SpanProcessor}, +}; use tracing::Level; use crate::ConfigureError; @@ -199,6 +202,25 @@ impl AdvancedOptions { } } +/// Configuration of metrics. +/// +/// This only has one option for now, but it's a place to add more related options in the future. +#[derive(Default)] +pub struct MetricsOptions { + /// Sequence of metric readers to be used in addition to the default which exports metrics to Logfire's API. + pub(crate) additional_readers: Vec, +} + +impl MetricsOptions { + /// Add a metric reader to the list of additional readers. + #[must_use] + pub fn with_additional_reader(mut self, reader: T) -> Self { + self.additional_readers + .push(BoxedMetricReader::new(Box::new(reader))); + self + } +} + /// Wrapper around a `SpanProcessor` to use in `additional_span_processors`. #[derive(Debug)] pub(crate) struct BoxedSpanProcessor(Box); @@ -251,6 +273,44 @@ impl IdGenerator for BoxedIdGenerator { } } +/// Wrapper around a `MetricReader` to use in `additional_readers`. +#[derive(Debug)] +pub(crate) struct BoxedMetricReader(Box); + +impl BoxedMetricReader { + pub fn new(reader: Box) -> Self { + BoxedMetricReader(reader) + } +} + +impl MetricReader for BoxedMetricReader { + fn register_pipeline(&self, pipeline: std::sync::Weak) { + self.0.register_pipeline(pipeline); + } + + fn collect( + &self, + rm: &mut opentelemetry_sdk::metrics::data::ResourceMetrics, + ) -> opentelemetry_sdk::metrics::MetricResult<()> { + self.0.collect(rm) + } + + fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult { + self.0.force_flush() + } + + fn shutdown(&self) -> opentelemetry_sdk::error::OTelSdkResult { + self.0.shutdown() + } + + fn temporality( + &self, + kind: opentelemetry_sdk::metrics::InstrumentKind, + ) -> opentelemetry_sdk::metrics::Temporality { + self.0.temporality(kind) + } +} + #[cfg(test)] mod tests { use crate::config::SendToLogfire; diff --git a/src/exporters.rs b/src/exporters.rs index 5a16fde..e2952d6 100644 --- a/src/exporters.rs +++ b/src/exporters.rs @@ -27,7 +27,7 @@ macro_rules! feature_required { }}; } -/// Build a [`SpanExporter`][opentelemetry::trace::SpanExporter] for passing to +/// Build a [`SpanExporter`][opentelemetry_sdk::trace::SpanExporter] for passing to /// [`with_additional_span_processor()`][crate::LogfireConfigBuilder::with_additional_span_processor]. /// /// This uses `OTEL_EXPORTER_OTLP_PROTOCOL` and `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` environment @@ -98,8 +98,21 @@ pub fn span_exporter( Ok(RemovePendingSpansExporter::new(span_exporter)) } -// TODO: make this public? -pub(crate) fn metric_exporter( +/// Build a [`PushMetricExporter`][opentelemetry_sdk::metrics::exporter::PushMetricExporter] for passing to +/// [`with_metrics_options()`][crate::LogfireConfigBuilder::with_metrics_options]. +/// +/// This uses `OTEL_EXPORTER_OTLP_PROTOCOL` and `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` environment +/// variables to determine the protocol to use (or otherwise defaults to [`Protocol::HttpBinary`]). +/// +/// # Errors +/// +/// Returns an error if the protocol specified by the env var is not supported or if the required feature is not enabled for +/// the given protocol. +/// +/// Returns an error if the endpoint is not a valid URI. +/// +/// Returns an error if any headers are not valid HTTP headers. +pub fn metric_exporter( endpoint: &str, headers: Option>, ) -> Result, ConfigureError> { diff --git a/src/internal/exporters/console.rs b/src/internal/exporters/console.rs index c0a62f4..7a03027 100644 --- a/src/internal/exporters/console.rs +++ b/src/internal/exporters/console.rs @@ -196,7 +196,7 @@ impl ConsoleWriter { let mut visitor = FieldsVisitor { message: None, // TODO: support formatting the fields? Maybe according to `ConsoleOptions`. - // fields: Vec::new(), + fields: Vec::new(), }; event.record(&mut visitor); @@ -219,6 +219,15 @@ impl ConsoleWriter { write!(w, " {}", BOLD.paint(msg))?; + if !visitor.fields.is_empty() { + for (idx, (key, value)) in visitor.fields.iter().enumerate() { + write!(w, " {}={value}", ITALIC.paint(*key))?; + if idx < visitor.fields.len() - 1 { + write!(w, ",")?; + } + } + } + writeln!(w) } } @@ -226,7 +235,7 @@ impl ConsoleWriter { /// Internal helper to `visit` a `tracing::Event` and collect relevant fields. struct FieldsVisitor { message: Option, - // fields: Vec<(&'static str, String)>, + fields: Vec<(&'static str, String)>, } impl Visit for FieldsVisitor { @@ -234,7 +243,7 @@ impl Visit for FieldsVisitor { if field.name() == "message" { self.message = Some(value.to_string()); } else { - // self.fields.push((field.name(), value.to_string())); + self.fields.push((field.name(), value.to_string())); } } @@ -242,7 +251,7 @@ impl Visit for FieldsVisitor { if field.name() == "message" { self.message = Some(format!("{value:?}")); } else { - // self.fields.push((field.name(), format!("{value:?}"))); + self.fields.push((field.name(), format!("{value:?}"))); } } } @@ -259,7 +268,7 @@ mod tests { config::{ConsoleOptions, Target}, internal::exporters::console::{ConsoleWriter, SimpleConsoleSpanExporter}, set_local_logfire, - tests::DeterministicExporter, + test_utils::DeterministicExporter, }; #[test] @@ -271,7 +280,8 @@ mod tests { ..ConsoleOptions::default() }; - let config = crate::configure() + let handler = crate::configure() + .local() .send_to_logfire(false) .with_additional_span_processor(SimpleSpanProcessor::new(Box::new( DeterministicExporter::new( @@ -281,12 +291,14 @@ mod tests { ), ))) .install_panic_handler() - .with_default_level_filter(LevelFilter::TRACE); + .with_default_level_filter(LevelFilter::TRACE) + .finish() + .unwrap(); - let guard = set_local_logfire(config).unwrap(); + let guard = set_local_logfire(handler); std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - tracing::subscriber::with_default(guard.subscriber.clone(), || { + tracing::subscriber::with_default(guard.subscriber().clone(), || { let root = crate::span!("root span").entered(); let _ = crate::span!("hello world span").entered(); let _ = crate::span!(level: Level::DEBUG, "debug span"); diff --git a/src/internal/exporters/remove_pending.rs b/src/internal/exporters/remove_pending.rs index df420da..680c96b 100644 --- a/src/internal/exporters/remove_pending.rs +++ b/src/internal/exporters/remove_pending.rs @@ -66,8 +66,7 @@ mod tests { use crate::config::AdvancedOptions; use crate::set_local_logfire; - use crate::tests::DeterministicExporter; - use crate::tests::DeterministicIdGenerator; + use crate::test_utils::{DeterministicExporter, DeterministicIdGenerator}; use super::*; @@ -81,7 +80,7 @@ mod tests { fn test_remove_pending_spans() { let exporter = InMemorySpanExporterBuilder::new().build(); - let config = crate::configure() + let guard = crate::configure() .send_to_logfire(false) .install_panic_handler() .with_additional_span_processor( @@ -102,11 +101,13 @@ mod tests { .with_default_level_filter(LevelFilter::TRACE) .with_advanced_options( AdvancedOptions::default().with_id_generator(DeterministicIdGenerator::new()), - ); + ) + .finish() + .unwrap(); - let guard = set_local_logfire(config).unwrap(); + let guard = set_local_logfire(guard); - tracing::subscriber::with_default(guard.subscriber.clone(), || { + tracing::subscriber::with_default(guard.subscriber(), || { let _root = crate::span!("root span").entered(); let _hello = crate::span!("hello world span").entered(); let _debug = crate::span!(level: Level::DEBUG, "debug span").entered(); diff --git a/src/lib.rs b/src/lib.rs index 9e3908d..b0ff836 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,9 +102,6 @@ use std::panic::PanicHookInfo; use std::sync::{Arc, Once}; use std::{backtrace::Backtrace, env::VarError, str::FromStr, sync::OnceLock, time::Duration}; -use bridges::tracing::LogfireTracingLayer; -use config::{AdvancedOptions, BoxedSpanProcessor, ConsoleOptions, SendToLogfire}; -use internal::exporters::console::{ConsoleWriter, SimpleConsoleSpanExporter}; use opentelemetry::trace::TracerProvider; use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider}; use opentelemetry_sdk::trace::{ @@ -116,6 +113,12 @@ use tracing::Subscriber; use tracing::level_filters::LevelFilter; use tracing_subscriber::layer::SubscriberExt; +use crate::bridges::tracing::LogfireTracingLayer; +use crate::config::{ + AdvancedOptions, BoxedSpanProcessor, ConsoleOptions, MetricsOptions, SendToLogfire, +}; +use crate::internal::exporters::console::{ConsoleWriter, SimpleConsoleSpanExporter}; + mod bridges; pub mod config; pub mod exporters; @@ -234,11 +237,14 @@ impl FromStr for ConsoleMode { #[must_use = "call `.finish()` to complete logfire configuration."] pub fn configure() -> LogfireConfigBuilder { LogfireConfigBuilder { + local: false, send_to_logfire: None, token: None, console_options: None, additional_span_processors: Vec::new(), advanced: None, + metrics: None, + enable_metrics: true, install_panic_handler: false, default_level_filter: None, } @@ -246,8 +252,7 @@ pub fn configure() -> LogfireConfigBuilder { /// Builder for logfire configuration, returned from [`logfire::configure()`][configure]. pub struct LogfireConfigBuilder { - // TODO: support all options supported by the Python SDK - // local: bool, + local: bool, send_to_logfire: Option, token: Option, // service_name: Option, @@ -263,13 +268,14 @@ pub struct LogfireConfigBuilder { // tracer_provider: Option, // TODO: advanced Python options not yet supported by the Rust SDK - // metrics: MetricsOptions | Literal[False] | None = None, // scrubbing: ScrubbingOptions | Literal[False] | None = None, // inspect_arguments: bool | None = None, // sampling: SamplingOptions | None = None, // code_source: CodeSource | None = None, // distributed_tracing: bool | None = None, advanced: Option, + metrics: Option, + enable_metrics: bool, // Rust specific options install_panic_handler: bool, @@ -277,6 +283,16 @@ pub struct LogfireConfigBuilder { } impl LogfireConfigBuilder { + /// Call to configure Logfire for local use only. + /// + /// This prevents the configured `Logfire` from setting global `tracing`, `log` and `opentelemetry` state. + #[doc(hidden)] + #[must_use] + pub fn local(mut self) -> Self { + self.local = true; + self + } + /// Call to install a hook to log panics. /// /// Any existing panic hook will be preserved and called after the logfire panic hook. @@ -355,6 +371,22 @@ impl LogfireConfigBuilder { self } + /// Configure [metrics options](crate::config::MetricsOptions). + #[must_use] + pub fn with_metrics_options(mut self, metrics: MetricsOptions) -> Self { + self.metrics = Some(metrics); + self + } + + /// Whether to enable metrics. + /// + /// If set to false, this will override [`with_metrics_options`][Self::with_metrics_options]. + #[must_use] + pub fn enable_metrics(mut self, enable: bool) -> Self { + self.enable_metrics = enable; + self + } + /// Finish configuring Logfire. /// /// Because this configures global state for the opentelemetry SDK, this can typically only ever be called once per program. @@ -364,52 +396,43 @@ impl LogfireConfigBuilder { /// See [`ConfigureError`] for possible errors. pub fn finish(self) -> Result { let LogfireParts { + local, tracer, subscriber, tracer_provider, - base_url, - http_headers, - send_to_logfire, + meter_provider, + .. } = self.build_parts(None)?; - tracing::subscriber::set_global_default(subscriber)?; - let logger = bridges::log::LogfireLogger::init(tracer.inner.clone()); - log::set_logger(logger)?; - log::set_max_level(logger.max_level()); + if !local { + tracing::subscriber::set_global_default(subscriber.clone())?; + let logger = bridges::log::LogfireLogger::init(tracer.inner.clone()); + log::set_logger(logger)?; + log::set_max_level(logger.max_level()); - GLOBAL_TRACER - .set(tracer) - .map_err(|_| ConfigureError::AlreadyConfigured)?; + GLOBAL_TRACER + .set(tracer.clone()) + .map_err(|_| ConfigureError::AlreadyConfigured)?; - let propagator = opentelemetry::propagation::TextMapCompositePropagator::new(vec![ - Box::new(opentelemetry_sdk::propagation::TraceContextPropagator::new()), - Box::new(opentelemetry_sdk::propagation::BaggagePropagator::new()), - ]); - opentelemetry::global::set_text_map_propagator(propagator); - - // setup metrics only if sending to logfire - let meter_provider = if send_to_logfire { - let metric_reader = - PeriodicReader::builder(exporters::metric_exporter(&base_url, http_headers)?) - .build(); - - let meter_provider = SdkMeterProvider::builder() - .with_reader(metric_reader) - .build(); + let propagator = opentelemetry::propagation::TextMapCompositePropagator::new(vec![ + Box::new(opentelemetry_sdk::propagation::TraceContextPropagator::new()), + Box::new(opentelemetry_sdk::propagation::BaggagePropagator::new()), + ]); + opentelemetry::global::set_text_map_propagator(propagator); + println!("setting meter provider"); opentelemetry::global::set_meter_provider(meter_provider.clone()); - - Some(meter_provider) - } else { - None - }; + } Ok(ShutdownHandler { tracer_provider, + tracer, + subscriber, meter_provider, }) } + #[expect(clippy::too_many_lines)] fn build_parts( self, env: Option<&HashMap>, @@ -512,11 +535,32 @@ impl LogfireConfigBuilder { ) .with(LogfireTracingLayer(tracer.clone())); + let mut meter_provider_builder = SdkMeterProvider::builder(); + + if send_to_logfire && self.enable_metrics { + let metric_reader = PeriodicReader::builder(exporters::metric_exporter( + &advanced_options.base_url, + http_headers, + )?) + .build(); + + meter_provider_builder = meter_provider_builder.with_reader(metric_reader); + }; + + if let Some(metrics) = self.metrics.filter(|_| self.enable_metrics) { + for reader in metrics.additional_readers { + meter_provider_builder = meter_provider_builder.with_reader(reader); + } + } + + let meter_provider = meter_provider_builder.build(); + if self.install_panic_handler { install_panic_handler(); } Ok(LogfireParts { + local: self.local, tracer: LogfireTracer { inner: tracer, handle_panics: self.install_panic_handler, @@ -524,8 +568,8 @@ impl LogfireConfigBuilder { }, subscriber: Arc::new(subscriber), tracer_provider, - base_url: advanced_options.base_url, - http_headers, + meter_provider, + #[cfg(test)] send_to_logfire, }) } @@ -535,11 +579,13 @@ impl LogfireConfigBuilder { /// /// Calling `.shutdown()` will flush the logfire exporters and make further /// logfire calls into no-ops. -#[derive(Debug, Clone)] +#[derive(Clone)] #[must_use = "this should be kept alive until logging should be stopped"] pub struct ShutdownHandler { tracer_provider: SdkTracerProvider, - meter_provider: Option, + tracer: LogfireTracer, + subscriber: Arc, + meter_provider: SdkMeterProvider, } impl ShutdownHandler { @@ -555,21 +601,20 @@ impl ShutdownHandler { self.tracer_provider .shutdown() .map_err(|e| ConfigureError::Other(e.into()))?; - if let Some(meter_provider) = &self.meter_provider { - meter_provider - .shutdown() - .map_err(|e| ConfigureError::Other(e.into()))?; - } + self.meter_provider + .shutdown() + .map_err(|e| ConfigureError::Other(e.into()))?; Ok(()) } } struct LogfireParts { + local: bool, tracer: LogfireTracer, subscriber: Arc, tracer_provider: SdkTracerProvider, - base_url: String, - http_headers: Option>, + meter_provider: SdkMeterProvider, + #[cfg(test)] send_to_logfire: bool, } @@ -626,6 +671,8 @@ fn get_optional_env( } } } + +#[derive(Clone)] struct LogfireTracer { inner: Tracer, handle_panics: bool, @@ -661,17 +708,28 @@ fn try_with_logfire_tracer(f: impl FnOnce(&LogfireTracer) -> R) -> Option /// Helper for installing a logfire guard locally to a thread. /// /// This is a bit of a mess, it's only implemented far enough to make tests pass... -#[cfg(test)] -struct LocalLogfireGuard { +#[doc(hidden)] +pub struct LocalLogfireGuard { prior: Option, - /// Tracing subscriber which can be used to subscribe tracing - subscriber: Arc, /// Shutdown handler #[allow(dead_code)] shutdown_handler: ShutdownHandler, } -#[cfg(test)] +impl LocalLogfireGuard { + /// Get the current tracer. + #[must_use] + pub fn subscriber(&self) -> Arc { + self.shutdown_handler.subscriber.clone() + } + + /// Ge the current meter provider + #[must_use] + pub fn meter_provider(&self) -> &SdkMeterProvider { + &self.shutdown_handler.meter_provider + } +} + impl Drop for LocalLogfireGuard { fn drop(&mut self) { // FIXME: if drop order is not consistent with creation order, does this create strange @@ -682,1588 +740,27 @@ impl Drop for LocalLogfireGuard { } } -#[cfg(test)] -#[expect(clippy::needless_pass_by_value)] // might consume in the future, leave it for now -fn set_local_logfire(config: LogfireConfigBuilder) -> Result { - let LogfireParts { - tracer, - subscriber, - tracer_provider, - .. - } = config.build_parts(None)?; - - let prior = LOCAL_TRACER.with_borrow_mut(|local_logfire| local_logfire.replace(tracer)); +#[doc(hidden)] // used in tests +#[must_use] +pub fn set_local_logfire(shutdown_handler: ShutdownHandler) -> LocalLogfireGuard { + let prior = LOCAL_TRACER + .with_borrow_mut(|local_logfire| local_logfire.replace(shutdown_handler.tracer.clone())); // TODO: logs?? // TODO: metrics?? - Ok(LocalLogfireGuard { + LocalLogfireGuard { prior, - subscriber, - shutdown_handler: ShutdownHandler { - tracer_provider, - meter_provider: None, - }, - }) + shutdown_handler, + } } #[cfg(test)] -mod tests { - use std::{ - collections::{HashMap, hash_map::Entry}, - future::Future, - pin::Pin, - sync::atomic::{AtomicU64, Ordering}, - time::SystemTime, - }; - - use insta::assert_debug_snapshot; - use opentelemetry::{ - Value, - trace::{SpanId, TraceId}, - }; - use opentelemetry_sdk::{ - error::OTelSdkResult, - trace::{IdGenerator, InMemorySpanExporterBuilder, SpanData, SpanExporter}, - }; - use tracing::Level; - - use super::*; - - #[derive(Debug)] - pub struct DeterministicIdGenerator { - next_trace_id: AtomicU64, - next_span_id: AtomicU64, - } - - impl IdGenerator for DeterministicIdGenerator { - fn new_trace_id(&self) -> opentelemetry::trace::TraceId { - TraceId::from_u128(self.next_trace_id.fetch_add(1, Ordering::Relaxed).into()) - } - - fn new_span_id(&self) -> opentelemetry::trace::SpanId { - SpanId::from_u64(self.next_span_id.fetch_add(1, Ordering::Relaxed)) - } - } +mod test_utils; - impl DeterministicIdGenerator { - pub fn new() -> Self { - // start at OxF0 because 0 is reserved for invalid IDs, - // and if we have a couple of bytes used, it's a more interesting check of - // the hex formatting - Self { - next_trace_id: 0xF0.into(), - next_span_id: 0xF0.into(), - } - } - } - - #[derive(Debug)] - pub struct DeterministicExporter { - exporter: Inner, - next_timestamp: u64, - timestamp_remap: HashMap, - // Information used to adjust line number to help minimise test churn - file: &'static str, - line_offset: u32, - } - - impl SpanExporter for DeterministicExporter { - fn export( - &mut self, - mut batch: Vec, - ) -> Pin + Send>> { - for span in &mut batch { - // By remapping timestamps to deterministic values, we should find that - // - pending spans have the same start time as their real span - // - pending spans also have the same end as start - span.start_time = self.remap_timestamp(span.start_time); - span.end_time = self.remap_timestamp(span.end_time); - - let mut remap_line = false; - - for attr in &mut span.attributes { - // thread info is not deterministic - // nor are timings - if attr.key.as_str() == "thread.id" - || attr.key.as_str() == "busy_ns" - || attr.key.as_str() == "idle_ns" - { - attr.value = 0.into(); - } - - // to minimize churn on tests, remap line numbers in the test to be relative to - // the test function - if attr.key.as_str() == "code.filepath" && attr.value.as_str() == self.file { - remap_line = true; - } - } - - if remap_line { - for attr in &mut span.attributes { - if attr.key.as_str() == "code.lineno" { - if let Value::I64(line) = &mut attr.value { - *line -= i64::from(self.line_offset); - } - } - - // panic location - if attr.key.as_str() == "location" { - let string_value = attr.value.as_str(); - let mut parts = string_value.splitn(3, ':'); - let file = parts.next().unwrap(); - let line = parts.next().unwrap().parse::().unwrap() - - i64::from(self.line_offset); - let column = parts.next().unwrap(); - attr.value = format!("{file}:{line}:{column}").into(); - } - } - } - - for event in &mut span.events.events { - event.timestamp = self.remap_timestamp(event.timestamp); - } - } - self.exporter.export(batch) - } - } - - impl DeterministicExporter { - /// Create deterministic exporter, feeding it current file and line. - pub fn new(exporter: Inner, file: &'static str, line_offset: u32) -> Self { - Self { - exporter, - next_timestamp: 0, - timestamp_remap: HashMap::new(), - file, - line_offset, - } - } - - fn remap_timestamp(&mut self, from: SystemTime) -> SystemTime { - match self.timestamp_remap.entry(from) { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => { - let new_timestamp = SystemTime::UNIX_EPOCH - + std::time::Duration::from_secs(self.next_timestamp); - self.next_timestamp += 1; - *entry.insert(new_timestamp) - } - } - } - } - - #[expect(clippy::too_many_lines)] - #[test] - fn test_basic_span() { - let exporter = InMemorySpanExporterBuilder::new().build(); - - let config = crate::configure() - .send_to_logfire(false) - .with_additional_span_processor(SimpleSpanProcessor::new(Box::new( - DeterministicExporter::new(exporter.clone(), file!(), line!()), - ))) - .install_panic_handler() - .with_default_level_filter(LevelFilter::TRACE) - .with_advanced_options( - AdvancedOptions::default().with_id_generator(DeterministicIdGenerator::new()), - ); - - let guard = set_local_logfire(config).unwrap(); - - std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - tracing::subscriber::with_default(guard.subscriber.clone(), || { - let root = span!("root span").entered(); - let _ = span!("hello world span").entered(); - let _ = span!(level: Level::DEBUG, "debug span"); - let _ = - span!(parent: &root, level: Level::DEBUG, "debug span with explicit parent"); - info!("hello world log"); - panic!("oh no!"); - }); - })) - .unwrap_err(); - - let spans = exporter.get_finished_spans().unwrap(); - assert_debug_snapshot!(spans, @r#" - [ - SpanData { - span_context: SpanContext { - trace_id: 000000000000000000000000000000f0, - span_id: 00000000000000f1, - trace_flags: TraceFlags( - 1, - ), - is_remote: false, - trace_state: TraceState( - None, - ), - }, - parent_span_id: 00000000000000f0, - span_kind: Internal, - name: "root span", - start_time: SystemTime { - tv_sec: 0, - tv_nsec: 0, - }, - end_time: SystemTime { - tv_sec: 0, - tv_nsec: 0, - }, - attributes: [ - KeyValue { - key: Static( - "code.filepath", - ), - value: String( - Static( - "src/lib.rs", - ), - ), - }, - KeyValue { - key: Static( - "code.namespace", - ), - value: String( - Static( - "logfire::tests", - ), - ), - }, - KeyValue { - key: Static( - "code.lineno", - ), - value: I64( - 12, - ), - }, - KeyValue { - key: Static( - "thread.id", - ), - value: I64( - 0, - ), - }, - KeyValue { - key: Static( - "thread.name", - ), - value: String( - Owned( - "tests::test_basic_span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.msg", - ), - value: String( - Owned( - "root span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.json_schema", - ), - value: String( - Owned( - "{\"type\":\"object\",\"properties\":{}}", - ), - ), - }, - KeyValue { - key: Static( - "logfire.level_num", - ), - value: I64( - 9, - ), - }, - KeyValue { - key: Static( - "logfire.span_type", - ), - value: String( - Static( - "pending_span", - ), - ), - }, - ], - dropped_attributes_count: 0, - events: SpanEvents { - events: [], - dropped_count: 0, - }, - links: SpanLinks { - links: [], - dropped_count: 0, - }, - status: Unset, - instrumentation_scope: InstrumentationScope { - name: "logfire", - version: None, - schema_url: None, - attributes: [], - }, - }, - SpanData { - span_context: SpanContext { - trace_id: 000000000000000000000000000000f0, - span_id: 00000000000000f3, - trace_flags: TraceFlags( - 1, - ), - is_remote: false, - trace_state: TraceState( - None, - ), - }, - parent_span_id: 00000000000000f2, - span_kind: Internal, - name: "hello world span", - start_time: SystemTime { - tv_sec: 1, - tv_nsec: 0, - }, - end_time: SystemTime { - tv_sec: 1, - tv_nsec: 0, - }, - attributes: [ - KeyValue { - key: Static( - "code.filepath", - ), - value: String( - Static( - "src/lib.rs", - ), - ), - }, - KeyValue { - key: Static( - "code.namespace", - ), - value: String( - Static( - "logfire::tests", - ), - ), - }, - KeyValue { - key: Static( - "code.lineno", - ), - value: I64( - 13, - ), - }, - KeyValue { - key: Static( - "thread.id", - ), - value: I64( - 0, - ), - }, - KeyValue { - key: Static( - "thread.name", - ), - value: String( - Owned( - "tests::test_basic_span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.msg", - ), - value: String( - Owned( - "hello world span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.json_schema", - ), - value: String( - Owned( - "{\"type\":\"object\",\"properties\":{}}", - ), - ), - }, - KeyValue { - key: Static( - "logfire.level_num", - ), - value: I64( - 9, - ), - }, - KeyValue { - key: Static( - "logfire.span_type", - ), - value: String( - Static( - "pending_span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.pending_parent_id", - ), - value: String( - Owned( - "00000000000000f0", - ), - ), - }, - ], - dropped_attributes_count: 0, - events: SpanEvents { - events: [], - dropped_count: 0, - }, - links: SpanLinks { - links: [], - dropped_count: 0, - }, - status: Unset, - instrumentation_scope: InstrumentationScope { - name: "logfire", - version: None, - schema_url: None, - attributes: [], - }, - }, - SpanData { - span_context: SpanContext { - trace_id: 000000000000000000000000000000f0, - span_id: 00000000000000f2, - trace_flags: TraceFlags( - 1, - ), - is_remote: false, - trace_state: TraceState( - None, - ), - }, - parent_span_id: 00000000000000f0, - span_kind: Internal, - name: "hello world span", - start_time: SystemTime { - tv_sec: 1, - tv_nsec: 0, - }, - end_time: SystemTime { - tv_sec: 2, - tv_nsec: 0, - }, - attributes: [ - KeyValue { - key: Static( - "code.filepath", - ), - value: String( - Static( - "src/lib.rs", - ), - ), - }, - KeyValue { - key: Static( - "code.namespace", - ), - value: String( - Static( - "logfire::tests", - ), - ), - }, - KeyValue { - key: Static( - "code.lineno", - ), - value: I64( - 13, - ), - }, - KeyValue { - key: Static( - "thread.id", - ), - value: I64( - 0, - ), - }, - KeyValue { - key: Static( - "thread.name", - ), - value: String( - Owned( - "tests::test_basic_span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.msg", - ), - value: String( - Owned( - "hello world span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.json_schema", - ), - value: String( - Owned( - "{\"type\":\"object\",\"properties\":{}}", - ), - ), - }, - KeyValue { - key: Static( - "logfire.level_num", - ), - value: I64( - 9, - ), - }, - KeyValue { - key: Static( - "logfire.span_type", - ), - value: String( - Static( - "span", - ), - ), - }, - KeyValue { - key: Static( - "busy_ns", - ), - value: I64( - 0, - ), - }, - KeyValue { - key: Static( - "idle_ns", - ), - value: I64( - 0, - ), - }, - ], - dropped_attributes_count: 0, - events: SpanEvents { - events: [], - dropped_count: 0, - }, - links: SpanLinks { - links: [], - dropped_count: 0, - }, - status: Unset, - instrumentation_scope: InstrumentationScope { - name: "logfire", - version: None, - schema_url: None, - attributes: [], - }, - }, - SpanData { - span_context: SpanContext { - trace_id: 000000000000000000000000000000f0, - span_id: 00000000000000f5, - trace_flags: TraceFlags( - 1, - ), - is_remote: false, - trace_state: TraceState( - None, - ), - }, - parent_span_id: 00000000000000f4, - span_kind: Internal, - name: "debug span", - start_time: SystemTime { - tv_sec: 3, - tv_nsec: 0, - }, - end_time: SystemTime { - tv_sec: 3, - tv_nsec: 0, - }, - attributes: [ - KeyValue { - key: Static( - "code.filepath", - ), - value: String( - Static( - "src/lib.rs", - ), - ), - }, - KeyValue { - key: Static( - "code.namespace", - ), - value: String( - Static( - "logfire::tests", - ), - ), - }, - KeyValue { - key: Static( - "code.lineno", - ), - value: I64( - 14, - ), - }, - KeyValue { - key: Static( - "thread.id", - ), - value: I64( - 0, - ), - }, - KeyValue { - key: Static( - "thread.name", - ), - value: String( - Owned( - "tests::test_basic_span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.msg", - ), - value: String( - Owned( - "debug span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.json_schema", - ), - value: String( - Owned( - "{\"type\":\"object\",\"properties\":{}}", - ), - ), - }, - KeyValue { - key: Static( - "logfire.level_num", - ), - value: I64( - 5, - ), - }, - KeyValue { - key: Static( - "logfire.span_type", - ), - value: String( - Static( - "pending_span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.pending_parent_id", - ), - value: String( - Owned( - "00000000000000f0", - ), - ), - }, - ], - dropped_attributes_count: 0, - events: SpanEvents { - events: [], - dropped_count: 0, - }, - links: SpanLinks { - links: [], - dropped_count: 0, - }, - status: Unset, - instrumentation_scope: InstrumentationScope { - name: "logfire", - version: None, - schema_url: None, - attributes: [], - }, - }, - SpanData { - span_context: SpanContext { - trace_id: 000000000000000000000000000000f0, - span_id: 00000000000000f4, - trace_flags: TraceFlags( - 1, - ), - is_remote: false, - trace_state: TraceState( - None, - ), - }, - parent_span_id: 00000000000000f0, - span_kind: Internal, - name: "debug span", - start_time: SystemTime { - tv_sec: 3, - tv_nsec: 0, - }, - end_time: SystemTime { - tv_sec: 4, - tv_nsec: 0, - }, - attributes: [ - KeyValue { - key: Static( - "code.filepath", - ), - value: String( - Static( - "src/lib.rs", - ), - ), - }, - KeyValue { - key: Static( - "code.namespace", - ), - value: String( - Static( - "logfire::tests", - ), - ), - }, - KeyValue { - key: Static( - "code.lineno", - ), - value: I64( - 14, - ), - }, - KeyValue { - key: Static( - "thread.id", - ), - value: I64( - 0, - ), - }, - KeyValue { - key: Static( - "thread.name", - ), - value: String( - Owned( - "tests::test_basic_span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.msg", - ), - value: String( - Owned( - "debug span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.json_schema", - ), - value: String( - Owned( - "{\"type\":\"object\",\"properties\":{}}", - ), - ), - }, - KeyValue { - key: Static( - "logfire.level_num", - ), - value: I64( - 5, - ), - }, - KeyValue { - key: Static( - "logfire.span_type", - ), - value: String( - Static( - "span", - ), - ), - }, - KeyValue { - key: Static( - "busy_ns", - ), - value: I64( - 0, - ), - }, - KeyValue { - key: Static( - "idle_ns", - ), - value: I64( - 0, - ), - }, - ], - dropped_attributes_count: 0, - events: SpanEvents { - events: [], - dropped_count: 0, - }, - links: SpanLinks { - links: [], - dropped_count: 0, - }, - status: Unset, - instrumentation_scope: InstrumentationScope { - name: "logfire", - version: None, - schema_url: None, - attributes: [], - }, - }, - SpanData { - span_context: SpanContext { - trace_id: 000000000000000000000000000000f0, - span_id: 00000000000000f7, - trace_flags: TraceFlags( - 1, - ), - is_remote: false, - trace_state: TraceState( - None, - ), - }, - parent_span_id: 00000000000000f6, - span_kind: Internal, - name: "debug span with explicit parent", - start_time: SystemTime { - tv_sec: 5, - tv_nsec: 0, - }, - end_time: SystemTime { - tv_sec: 5, - tv_nsec: 0, - }, - attributes: [ - KeyValue { - key: Static( - "code.filepath", - ), - value: String( - Static( - "src/lib.rs", - ), - ), - }, - KeyValue { - key: Static( - "code.namespace", - ), - value: String( - Static( - "logfire::tests", - ), - ), - }, - KeyValue { - key: Static( - "code.lineno", - ), - value: I64( - 16, - ), - }, - KeyValue { - key: Static( - "thread.id", - ), - value: I64( - 0, - ), - }, - KeyValue { - key: Static( - "thread.name", - ), - value: String( - Owned( - "tests::test_basic_span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.msg", - ), - value: String( - Owned( - "debug span with explicit parent", - ), - ), - }, - KeyValue { - key: Static( - "logfire.json_schema", - ), - value: String( - Owned( - "{\"type\":\"object\",\"properties\":{}}", - ), - ), - }, - KeyValue { - key: Static( - "logfire.level_num", - ), - value: I64( - 5, - ), - }, - KeyValue { - key: Static( - "logfire.span_type", - ), - value: String( - Static( - "pending_span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.pending_parent_id", - ), - value: String( - Owned( - "00000000000000f0", - ), - ), - }, - ], - dropped_attributes_count: 0, - events: SpanEvents { - events: [], - dropped_count: 0, - }, - links: SpanLinks { - links: [], - dropped_count: 0, - }, - status: Unset, - instrumentation_scope: InstrumentationScope { - name: "logfire", - version: None, - schema_url: None, - attributes: [], - }, - }, - SpanData { - span_context: SpanContext { - trace_id: 000000000000000000000000000000f0, - span_id: 00000000000000f6, - trace_flags: TraceFlags( - 1, - ), - is_remote: false, - trace_state: TraceState( - None, - ), - }, - parent_span_id: 00000000000000f0, - span_kind: Internal, - name: "debug span with explicit parent", - start_time: SystemTime { - tv_sec: 5, - tv_nsec: 0, - }, - end_time: SystemTime { - tv_sec: 6, - tv_nsec: 0, - }, - attributes: [ - KeyValue { - key: Static( - "code.filepath", - ), - value: String( - Static( - "src/lib.rs", - ), - ), - }, - KeyValue { - key: Static( - "code.namespace", - ), - value: String( - Static( - "logfire::tests", - ), - ), - }, - KeyValue { - key: Static( - "code.lineno", - ), - value: I64( - 16, - ), - }, - KeyValue { - key: Static( - "thread.id", - ), - value: I64( - 0, - ), - }, - KeyValue { - key: Static( - "thread.name", - ), - value: String( - Owned( - "tests::test_basic_span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.msg", - ), - value: String( - Owned( - "debug span with explicit parent", - ), - ), - }, - KeyValue { - key: Static( - "logfire.json_schema", - ), - value: String( - Owned( - "{\"type\":\"object\",\"properties\":{}}", - ), - ), - }, - KeyValue { - key: Static( - "logfire.level_num", - ), - value: I64( - 5, - ), - }, - KeyValue { - key: Static( - "logfire.span_type", - ), - value: String( - Static( - "span", - ), - ), - }, - KeyValue { - key: Static( - "busy_ns", - ), - value: I64( - 0, - ), - }, - KeyValue { - key: Static( - "idle_ns", - ), - value: I64( - 0, - ), - }, - ], - dropped_attributes_count: 0, - events: SpanEvents { - events: [], - dropped_count: 0, - }, - links: SpanLinks { - links: [], - dropped_count: 0, - }, - status: Unset, - instrumentation_scope: InstrumentationScope { - name: "logfire", - version: None, - schema_url: None, - attributes: [], - }, - }, - SpanData { - span_context: SpanContext { - trace_id: 000000000000000000000000000000f0, - span_id: 00000000000000f8, - trace_flags: TraceFlags( - 1, - ), - is_remote: false, - trace_state: TraceState( - None, - ), - }, - parent_span_id: 00000000000000f0, - span_kind: Internal, - name: "hello world log", - start_time: SystemTime { - tv_sec: 7, - tv_nsec: 0, - }, - end_time: SystemTime { - tv_sec: 7, - tv_nsec: 0, - }, - attributes: [ - KeyValue { - key: Static( - "logfire.msg", - ), - value: String( - Owned( - "hello world log", - ), - ), - }, - KeyValue { - key: Static( - "logfire.level_num", - ), - value: I64( - 9, - ), - }, - KeyValue { - key: Static( - "logfire.span_type", - ), - value: String( - Static( - "log", - ), - ), - }, - KeyValue { - key: Static( - "logfire.json_schema", - ), - value: String( - Static( - "{\"type\":\"object\",\"properties\":{}}", - ), - ), - }, - KeyValue { - key: Static( - "code.filepath", - ), - value: String( - Static( - "src/lib.rs", - ), - ), - }, - KeyValue { - key: Static( - "code.lineno", - ), - value: I64( - 17, - ), - }, - KeyValue { - key: Static( - "code.namespace", - ), - value: String( - Static( - "logfire::tests", - ), - ), - }, - KeyValue { - key: Static( - "thread.id", - ), - value: I64( - 0, - ), - }, - KeyValue { - key: Static( - "thread.name", - ), - value: String( - Owned( - "tests::test_basic_span", - ), - ), - }, - ], - dropped_attributes_count: 0, - events: SpanEvents { - events: [], - dropped_count: 0, - }, - links: SpanLinks { - links: [], - dropped_count: 0, - }, - status: Unset, - instrumentation_scope: InstrumentationScope { - name: "logfire", - version: None, - schema_url: None, - attributes: [], - }, - }, - SpanData { - span_context: SpanContext { - trace_id: 000000000000000000000000000000f0, - span_id: 00000000000000f9, - trace_flags: TraceFlags( - 1, - ), - is_remote: false, - trace_state: TraceState( - None, - ), - }, - parent_span_id: 00000000000000f0, - span_kind: Internal, - name: "panic: {message}", - start_time: SystemTime { - tv_sec: 8, - tv_nsec: 0, - }, - end_time: SystemTime { - tv_sec: 8, - tv_nsec: 0, - }, - attributes: [ - KeyValue { - key: Static( - "location", - ), - value: String( - Owned( - "src/lib.rs:18:17", - ), - ), - }, - KeyValue { - key: Static( - "backtrace", - ), - value: String( - Owned( - "disabled backtrace", - ), - ), - }, - KeyValue { - key: Static( - "logfire.msg", - ), - value: String( - Owned( - "panic: oh no!", - ), - ), - }, - KeyValue { - key: Static( - "logfire.level_num", - ), - value: I64( - 17, - ), - }, - KeyValue { - key: Static( - "logfire.span_type", - ), - value: String( - Static( - "log", - ), - ), - }, - KeyValue { - key: Static( - "logfire.json_schema", - ), - value: String( - Static( - "{\"type\":\"object\",\"properties\":{\"location\":{},\"backtrace\":{}}}", - ), - ), - }, - KeyValue { - key: Static( - "code.filepath", - ), - value: String( - Static( - "src/lib.rs", - ), - ), - }, - KeyValue { - key: Static( - "code.lineno", - ), - value: I64( - -271, - ), - }, - KeyValue { - key: Static( - "code.namespace", - ), - value: String( - Static( - "logfire", - ), - ), - }, - KeyValue { - key: Static( - "thread.id", - ), - value: I64( - 0, - ), - }, - KeyValue { - key: Static( - "thread.name", - ), - value: String( - Owned( - "tests::test_basic_span", - ), - ), - }, - ], - dropped_attributes_count: 0, - events: SpanEvents { - events: [], - dropped_count: 0, - }, - links: SpanLinks { - links: [], - dropped_count: 0, - }, - status: Unset, - instrumentation_scope: InstrumentationScope { - name: "logfire", - version: None, - schema_url: None, - attributes: [], - }, - }, - SpanData { - span_context: SpanContext { - trace_id: 000000000000000000000000000000f0, - span_id: 00000000000000f0, - trace_flags: TraceFlags( - 1, - ), - is_remote: false, - trace_state: TraceState( - None, - ), - }, - parent_span_id: 0000000000000000, - span_kind: Internal, - name: "root span", - start_time: SystemTime { - tv_sec: 0, - tv_nsec: 0, - }, - end_time: SystemTime { - tv_sec: 9, - tv_nsec: 0, - }, - attributes: [ - KeyValue { - key: Static( - "code.filepath", - ), - value: String( - Static( - "src/lib.rs", - ), - ), - }, - KeyValue { - key: Static( - "code.namespace", - ), - value: String( - Static( - "logfire::tests", - ), - ), - }, - KeyValue { - key: Static( - "code.lineno", - ), - value: I64( - 12, - ), - }, - KeyValue { - key: Static( - "thread.id", - ), - value: I64( - 0, - ), - }, - KeyValue { - key: Static( - "thread.name", - ), - value: String( - Owned( - "tests::test_basic_span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.msg", - ), - value: String( - Owned( - "root span", - ), - ), - }, - KeyValue { - key: Static( - "logfire.json_schema", - ), - value: String( - Owned( - "{\"type\":\"object\",\"properties\":{}}", - ), - ), - }, - KeyValue { - key: Static( - "logfire.level_num", - ), - value: I64( - 9, - ), - }, - KeyValue { - key: Static( - "logfire.span_type", - ), - value: String( - Static( - "span", - ), - ), - }, - KeyValue { - key: Static( - "busy_ns", - ), - value: I64( - 0, - ), - }, - KeyValue { - key: Static( - "idle_ns", - ), - value: I64( - 0, - ), - }, - ], - dropped_attributes_count: 0, - events: SpanEvents { - events: [], - dropped_count: 0, - }, - links: SpanLinks { - links: [], - dropped_count: 0, - }, - status: Unset, - instrumentation_scope: InstrumentationScope { - name: "logfire", - version: None, - schema_url: None, - attributes: [], - }, - }, - ] - "#); - } +#[cfg(test)] +mod tests { + use crate::{ConfigureError, config::SendToLogfire}; #[test] fn test_send_to_logfire() { diff --git a/src/test_utils.rs b/src/test_utils.rs new file mode 100644 index 0000000..3070af4 --- /dev/null +++ b/src/test_utils.rs @@ -0,0 +1,139 @@ +use std::{ + collections::{HashMap, hash_map::Entry}, + future::Future, + pin::Pin, + sync::atomic::{AtomicU64, Ordering}, + time::SystemTime, +}; + +use opentelemetry::{ + Value, + trace::{SpanId, TraceId}, +}; +use opentelemetry_sdk::{ + error::OTelSdkResult, + trace::{IdGenerator, SpanData, SpanExporter}, +}; + +#[derive(Debug)] +pub struct DeterministicIdGenerator { + next_trace_id: AtomicU64, + next_span_id: AtomicU64, +} + +impl IdGenerator for DeterministicIdGenerator { + fn new_trace_id(&self) -> opentelemetry::trace::TraceId { + TraceId::from_u128(self.next_trace_id.fetch_add(1, Ordering::Relaxed).into()) + } + + fn new_span_id(&self) -> opentelemetry::trace::SpanId { + SpanId::from_u64(self.next_span_id.fetch_add(1, Ordering::Relaxed)) + } +} + +impl DeterministicIdGenerator { + pub fn new() -> Self { + // start at OxF0 because 0 is reserved for invalid IDs, + // and if we have a couple of bytes used, it's a more interesting check of + // the hex formatting + Self { + next_trace_id: 0xF0.into(), + next_span_id: 0xF0.into(), + } + } +} + +#[derive(Debug)] +pub struct DeterministicExporter { + exporter: Inner, + next_timestamp: u64, + timestamp_remap: HashMap, + // Information used to adjust line number to help minimise test churn + file: &'static str, + line_offset: u32, +} + +impl SpanExporter for DeterministicExporter { + fn export( + &mut self, + mut batch: Vec, + ) -> Pin + Send>> { + for span in &mut batch { + // By remapping timestamps to deterministic values, we should find that + // - pending spans have the same start time as their real span + // - pending spans also have the same end as start + span.start_time = self.remap_timestamp(span.start_time); + span.end_time = self.remap_timestamp(span.end_time); + + let mut remap_line = false; + + for attr in &mut span.attributes { + // thread info is not deterministic + // nor are timings + if attr.key.as_str() == "thread.id" + || attr.key.as_str() == "busy_ns" + || attr.key.as_str() == "idle_ns" + { + attr.value = 0.into(); + } + + // to minimize churn on tests, remap line numbers in the test to be relative to + // the test function + if attr.key.as_str() == "code.filepath" && attr.value.as_str() == self.file { + remap_line = true; + } + } + + if remap_line { + for attr in &mut span.attributes { + if attr.key.as_str() == "code.lineno" { + if let Value::I64(line) = &mut attr.value { + *line -= i64::from(self.line_offset); + } + } + + // panic location + if attr.key.as_str() == "location" { + let string_value = attr.value.as_str(); + let mut parts = string_value.splitn(3, ':'); + let file = parts.next().unwrap(); + let line = parts.next().unwrap().parse::().unwrap() + - i64::from(self.line_offset); + let column = parts.next().unwrap(); + attr.value = format!("{file}:{line}:{column}").into(); + } + } + } + + for event in &mut span.events.events { + event.timestamp = self.remap_timestamp(event.timestamp); + } + } + self.exporter.export(batch) + } +} + +impl DeterministicExporter { + /// Create deterministic exporter, feeding it current file and line. + pub fn new(exporter: Inner, file: &'static str, line_offset: u32) -> Self { + Self { + exporter, + next_timestamp: 0, + timestamp_remap: HashMap::new(), + file, + line_offset, + } + } + + fn remap_timestamp(&mut self, from: SystemTime) -> SystemTime { + match self.timestamp_remap.entry(from) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let new_timestamp = + SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(self.next_timestamp); + self.next_timestamp += 1; + *entry.insert(new_timestamp) + } + } + } +} diff --git a/tests/test_basic_exports.rs b/tests/test_basic_exports.rs new file mode 100644 index 0000000..3f4f8da --- /dev/null +++ b/tests/test_basic_exports.rs @@ -0,0 +1,1772 @@ +//! Basic snapshot tests for data produced by the logfire APIs. + +use std::time::Duration; + +use insta::assert_debug_snapshot; +use opentelemetry_sdk::{ + metrics::{InMemoryMetricExporterBuilder, PeriodicReader}, + trace::{InMemorySpanExporterBuilder, SimpleSpanProcessor}, +}; +use tracing::{Level, level_filters::LevelFilter}; + +use logfire::{ + config::{AdvancedOptions, MetricsOptions}, + info, span, +}; + +#[path = "../src/test_utils.rs"] +mod test_utils; + +use test_utils::{DeterministicExporter, DeterministicIdGenerator}; + +#[expect(clippy::too_many_lines)] +#[test] +fn test_basic_span() { + let exporter = InMemorySpanExporterBuilder::new().build(); + + let handler = logfire::configure() + .local() + .send_to_logfire(false) + .with_additional_span_processor(SimpleSpanProcessor::new(Box::new( + DeterministicExporter::new(exporter.clone(), file!(), line!()), + ))) + .install_panic_handler() + .with_default_level_filter(LevelFilter::TRACE) + .with_advanced_options( + AdvancedOptions::default().with_id_generator(DeterministicIdGenerator::new()), + ) + .finish() + .unwrap(); + + let guard = logfire::set_local_logfire(handler); + + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + tracing::subscriber::with_default(guard.subscriber(), || { + let root = span!("root span").entered(); + let _ = span!("hello world span").entered(); + let _ = span!(level: Level::DEBUG, "debug span"); + let _ = span!(parent: &root, level: Level::DEBUG, "debug span with explicit parent"); + info!("hello world log"); + panic!("oh no!"); + }); + })) + .unwrap_err(); + + let spans = exporter.get_finished_spans().unwrap(); + assert_debug_snapshot!(spans, @r#" + [ + SpanData { + span_context: SpanContext { + trace_id: 000000000000000000000000000000f0, + span_id: 00000000000000f1, + trace_flags: TraceFlags( + 1, + ), + is_remote: false, + trace_state: TraceState( + None, + ), + }, + parent_span_id: 00000000000000f0, + span_kind: Internal, + name: "root span", + start_time: SystemTime { + tv_sec: 0, + tv_nsec: 0, + }, + end_time: SystemTime { + tv_sec: 0, + tv_nsec: 0, + }, + attributes: [ + KeyValue { + key: Static( + "code.filepath", + ), + value: String( + Static( + "tests/test_basic_exports.rs", + ), + ), + }, + KeyValue { + key: Static( + "code.namespace", + ), + value: String( + Static( + "test_basic_exports", + ), + ), + }, + KeyValue { + key: Static( + "code.lineno", + ), + value: I64( + 14, + ), + }, + KeyValue { + key: Static( + "thread.id", + ), + value: I64( + 0, + ), + }, + KeyValue { + key: Static( + "thread.name", + ), + value: String( + Owned( + "test_basic_span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.msg", + ), + value: String( + Owned( + "root span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.json_schema", + ), + value: String( + Owned( + "{\"type\":\"object\",\"properties\":{}}", + ), + ), + }, + KeyValue { + key: Static( + "logfire.level_num", + ), + value: I64( + 9, + ), + }, + KeyValue { + key: Static( + "logfire.span_type", + ), + value: String( + Static( + "pending_span", + ), + ), + }, + ], + dropped_attributes_count: 0, + events: SpanEvents { + events: [], + dropped_count: 0, + }, + links: SpanLinks { + links: [], + dropped_count: 0, + }, + status: Unset, + instrumentation_scope: InstrumentationScope { + name: "logfire", + version: None, + schema_url: None, + attributes: [], + }, + }, + SpanData { + span_context: SpanContext { + trace_id: 000000000000000000000000000000f0, + span_id: 00000000000000f3, + trace_flags: TraceFlags( + 1, + ), + is_remote: false, + trace_state: TraceState( + None, + ), + }, + parent_span_id: 00000000000000f2, + span_kind: Internal, + name: "hello world span", + start_time: SystemTime { + tv_sec: 1, + tv_nsec: 0, + }, + end_time: SystemTime { + tv_sec: 1, + tv_nsec: 0, + }, + attributes: [ + KeyValue { + key: Static( + "code.filepath", + ), + value: String( + Static( + "tests/test_basic_exports.rs", + ), + ), + }, + KeyValue { + key: Static( + "code.namespace", + ), + value: String( + Static( + "test_basic_exports", + ), + ), + }, + KeyValue { + key: Static( + "code.lineno", + ), + value: I64( + 15, + ), + }, + KeyValue { + key: Static( + "thread.id", + ), + value: I64( + 0, + ), + }, + KeyValue { + key: Static( + "thread.name", + ), + value: String( + Owned( + "test_basic_span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.msg", + ), + value: String( + Owned( + "hello world span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.json_schema", + ), + value: String( + Owned( + "{\"type\":\"object\",\"properties\":{}}", + ), + ), + }, + KeyValue { + key: Static( + "logfire.level_num", + ), + value: I64( + 9, + ), + }, + KeyValue { + key: Static( + "logfire.span_type", + ), + value: String( + Static( + "pending_span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.pending_parent_id", + ), + value: String( + Owned( + "00000000000000f0", + ), + ), + }, + ], + dropped_attributes_count: 0, + events: SpanEvents { + events: [], + dropped_count: 0, + }, + links: SpanLinks { + links: [], + dropped_count: 0, + }, + status: Unset, + instrumentation_scope: InstrumentationScope { + name: "logfire", + version: None, + schema_url: None, + attributes: [], + }, + }, + SpanData { + span_context: SpanContext { + trace_id: 000000000000000000000000000000f0, + span_id: 00000000000000f2, + trace_flags: TraceFlags( + 1, + ), + is_remote: false, + trace_state: TraceState( + None, + ), + }, + parent_span_id: 00000000000000f0, + span_kind: Internal, + name: "hello world span", + start_time: SystemTime { + tv_sec: 1, + tv_nsec: 0, + }, + end_time: SystemTime { + tv_sec: 2, + tv_nsec: 0, + }, + attributes: [ + KeyValue { + key: Static( + "code.filepath", + ), + value: String( + Static( + "tests/test_basic_exports.rs", + ), + ), + }, + KeyValue { + key: Static( + "code.namespace", + ), + value: String( + Static( + "test_basic_exports", + ), + ), + }, + KeyValue { + key: Static( + "code.lineno", + ), + value: I64( + 15, + ), + }, + KeyValue { + key: Static( + "thread.id", + ), + value: I64( + 0, + ), + }, + KeyValue { + key: Static( + "thread.name", + ), + value: String( + Owned( + "test_basic_span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.msg", + ), + value: String( + Owned( + "hello world span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.json_schema", + ), + value: String( + Owned( + "{\"type\":\"object\",\"properties\":{}}", + ), + ), + }, + KeyValue { + key: Static( + "logfire.level_num", + ), + value: I64( + 9, + ), + }, + KeyValue { + key: Static( + "logfire.span_type", + ), + value: String( + Static( + "span", + ), + ), + }, + KeyValue { + key: Static( + "busy_ns", + ), + value: I64( + 0, + ), + }, + KeyValue { + key: Static( + "idle_ns", + ), + value: I64( + 0, + ), + }, + ], + dropped_attributes_count: 0, + events: SpanEvents { + events: [], + dropped_count: 0, + }, + links: SpanLinks { + links: [], + dropped_count: 0, + }, + status: Unset, + instrumentation_scope: InstrumentationScope { + name: "logfire", + version: None, + schema_url: None, + attributes: [], + }, + }, + SpanData { + span_context: SpanContext { + trace_id: 000000000000000000000000000000f0, + span_id: 00000000000000f5, + trace_flags: TraceFlags( + 1, + ), + is_remote: false, + trace_state: TraceState( + None, + ), + }, + parent_span_id: 00000000000000f4, + span_kind: Internal, + name: "debug span", + start_time: SystemTime { + tv_sec: 3, + tv_nsec: 0, + }, + end_time: SystemTime { + tv_sec: 3, + tv_nsec: 0, + }, + attributes: [ + KeyValue { + key: Static( + "code.filepath", + ), + value: String( + Static( + "tests/test_basic_exports.rs", + ), + ), + }, + KeyValue { + key: Static( + "code.namespace", + ), + value: String( + Static( + "test_basic_exports", + ), + ), + }, + KeyValue { + key: Static( + "code.lineno", + ), + value: I64( + 16, + ), + }, + KeyValue { + key: Static( + "thread.id", + ), + value: I64( + 0, + ), + }, + KeyValue { + key: Static( + "thread.name", + ), + value: String( + Owned( + "test_basic_span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.msg", + ), + value: String( + Owned( + "debug span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.json_schema", + ), + value: String( + Owned( + "{\"type\":\"object\",\"properties\":{}}", + ), + ), + }, + KeyValue { + key: Static( + "logfire.level_num", + ), + value: I64( + 5, + ), + }, + KeyValue { + key: Static( + "logfire.span_type", + ), + value: String( + Static( + "pending_span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.pending_parent_id", + ), + value: String( + Owned( + "00000000000000f0", + ), + ), + }, + ], + dropped_attributes_count: 0, + events: SpanEvents { + events: [], + dropped_count: 0, + }, + links: SpanLinks { + links: [], + dropped_count: 0, + }, + status: Unset, + instrumentation_scope: InstrumentationScope { + name: "logfire", + version: None, + schema_url: None, + attributes: [], + }, + }, + SpanData { + span_context: SpanContext { + trace_id: 000000000000000000000000000000f0, + span_id: 00000000000000f4, + trace_flags: TraceFlags( + 1, + ), + is_remote: false, + trace_state: TraceState( + None, + ), + }, + parent_span_id: 00000000000000f0, + span_kind: Internal, + name: "debug span", + start_time: SystemTime { + tv_sec: 3, + tv_nsec: 0, + }, + end_time: SystemTime { + tv_sec: 4, + tv_nsec: 0, + }, + attributes: [ + KeyValue { + key: Static( + "code.filepath", + ), + value: String( + Static( + "tests/test_basic_exports.rs", + ), + ), + }, + KeyValue { + key: Static( + "code.namespace", + ), + value: String( + Static( + "test_basic_exports", + ), + ), + }, + KeyValue { + key: Static( + "code.lineno", + ), + value: I64( + 16, + ), + }, + KeyValue { + key: Static( + "thread.id", + ), + value: I64( + 0, + ), + }, + KeyValue { + key: Static( + "thread.name", + ), + value: String( + Owned( + "test_basic_span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.msg", + ), + value: String( + Owned( + "debug span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.json_schema", + ), + value: String( + Owned( + "{\"type\":\"object\",\"properties\":{}}", + ), + ), + }, + KeyValue { + key: Static( + "logfire.level_num", + ), + value: I64( + 5, + ), + }, + KeyValue { + key: Static( + "logfire.span_type", + ), + value: String( + Static( + "span", + ), + ), + }, + KeyValue { + key: Static( + "busy_ns", + ), + value: I64( + 0, + ), + }, + KeyValue { + key: Static( + "idle_ns", + ), + value: I64( + 0, + ), + }, + ], + dropped_attributes_count: 0, + events: SpanEvents { + events: [], + dropped_count: 0, + }, + links: SpanLinks { + links: [], + dropped_count: 0, + }, + status: Unset, + instrumentation_scope: InstrumentationScope { + name: "logfire", + version: None, + schema_url: None, + attributes: [], + }, + }, + SpanData { + span_context: SpanContext { + trace_id: 000000000000000000000000000000f0, + span_id: 00000000000000f7, + trace_flags: TraceFlags( + 1, + ), + is_remote: false, + trace_state: TraceState( + None, + ), + }, + parent_span_id: 00000000000000f6, + span_kind: Internal, + name: "debug span with explicit parent", + start_time: SystemTime { + tv_sec: 5, + tv_nsec: 0, + }, + end_time: SystemTime { + tv_sec: 5, + tv_nsec: 0, + }, + attributes: [ + KeyValue { + key: Static( + "code.filepath", + ), + value: String( + Static( + "tests/test_basic_exports.rs", + ), + ), + }, + KeyValue { + key: Static( + "code.namespace", + ), + value: String( + Static( + "test_basic_exports", + ), + ), + }, + KeyValue { + key: Static( + "code.lineno", + ), + value: I64( + 17, + ), + }, + KeyValue { + key: Static( + "thread.id", + ), + value: I64( + 0, + ), + }, + KeyValue { + key: Static( + "thread.name", + ), + value: String( + Owned( + "test_basic_span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.msg", + ), + value: String( + Owned( + "debug span with explicit parent", + ), + ), + }, + KeyValue { + key: Static( + "logfire.json_schema", + ), + value: String( + Owned( + "{\"type\":\"object\",\"properties\":{}}", + ), + ), + }, + KeyValue { + key: Static( + "logfire.level_num", + ), + value: I64( + 5, + ), + }, + KeyValue { + key: Static( + "logfire.span_type", + ), + value: String( + Static( + "pending_span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.pending_parent_id", + ), + value: String( + Owned( + "00000000000000f0", + ), + ), + }, + ], + dropped_attributes_count: 0, + events: SpanEvents { + events: [], + dropped_count: 0, + }, + links: SpanLinks { + links: [], + dropped_count: 0, + }, + status: Unset, + instrumentation_scope: InstrumentationScope { + name: "logfire", + version: None, + schema_url: None, + attributes: [], + }, + }, + SpanData { + span_context: SpanContext { + trace_id: 000000000000000000000000000000f0, + span_id: 00000000000000f6, + trace_flags: TraceFlags( + 1, + ), + is_remote: false, + trace_state: TraceState( + None, + ), + }, + parent_span_id: 00000000000000f0, + span_kind: Internal, + name: "debug span with explicit parent", + start_time: SystemTime { + tv_sec: 5, + tv_nsec: 0, + }, + end_time: SystemTime { + tv_sec: 6, + tv_nsec: 0, + }, + attributes: [ + KeyValue { + key: Static( + "code.filepath", + ), + value: String( + Static( + "tests/test_basic_exports.rs", + ), + ), + }, + KeyValue { + key: Static( + "code.namespace", + ), + value: String( + Static( + "test_basic_exports", + ), + ), + }, + KeyValue { + key: Static( + "code.lineno", + ), + value: I64( + 17, + ), + }, + KeyValue { + key: Static( + "thread.id", + ), + value: I64( + 0, + ), + }, + KeyValue { + key: Static( + "thread.name", + ), + value: String( + Owned( + "test_basic_span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.msg", + ), + value: String( + Owned( + "debug span with explicit parent", + ), + ), + }, + KeyValue { + key: Static( + "logfire.json_schema", + ), + value: String( + Owned( + "{\"type\":\"object\",\"properties\":{}}", + ), + ), + }, + KeyValue { + key: Static( + "logfire.level_num", + ), + value: I64( + 5, + ), + }, + KeyValue { + key: Static( + "logfire.span_type", + ), + value: String( + Static( + "span", + ), + ), + }, + KeyValue { + key: Static( + "busy_ns", + ), + value: I64( + 0, + ), + }, + KeyValue { + key: Static( + "idle_ns", + ), + value: I64( + 0, + ), + }, + ], + dropped_attributes_count: 0, + events: SpanEvents { + events: [], + dropped_count: 0, + }, + links: SpanLinks { + links: [], + dropped_count: 0, + }, + status: Unset, + instrumentation_scope: InstrumentationScope { + name: "logfire", + version: None, + schema_url: None, + attributes: [], + }, + }, + SpanData { + span_context: SpanContext { + trace_id: 000000000000000000000000000000f0, + span_id: 00000000000000f8, + trace_flags: TraceFlags( + 1, + ), + is_remote: false, + trace_state: TraceState( + None, + ), + }, + parent_span_id: 00000000000000f0, + span_kind: Internal, + name: "hello world log", + start_time: SystemTime { + tv_sec: 7, + tv_nsec: 0, + }, + end_time: SystemTime { + tv_sec: 7, + tv_nsec: 0, + }, + attributes: [ + KeyValue { + key: Static( + "logfire.msg", + ), + value: String( + Owned( + "hello world log", + ), + ), + }, + KeyValue { + key: Static( + "logfire.level_num", + ), + value: I64( + 9, + ), + }, + KeyValue { + key: Static( + "logfire.span_type", + ), + value: String( + Static( + "log", + ), + ), + }, + KeyValue { + key: Static( + "logfire.json_schema", + ), + value: String( + Static( + "{\"type\":\"object\",\"properties\":{}}", + ), + ), + }, + KeyValue { + key: Static( + "code.filepath", + ), + value: String( + Static( + "tests/test_basic_exports.rs", + ), + ), + }, + KeyValue { + key: Static( + "code.lineno", + ), + value: I64( + 18, + ), + }, + KeyValue { + key: Static( + "code.namespace", + ), + value: String( + Static( + "test_basic_exports", + ), + ), + }, + KeyValue { + key: Static( + "thread.id", + ), + value: I64( + 0, + ), + }, + KeyValue { + key: Static( + "thread.name", + ), + value: String( + Owned( + "test_basic_span", + ), + ), + }, + ], + dropped_attributes_count: 0, + events: SpanEvents { + events: [], + dropped_count: 0, + }, + links: SpanLinks { + links: [], + dropped_count: 0, + }, + status: Unset, + instrumentation_scope: InstrumentationScope { + name: "logfire", + version: None, + schema_url: None, + attributes: [], + }, + }, + SpanData { + span_context: SpanContext { + trace_id: 000000000000000000000000000000f0, + span_id: 00000000000000f9, + trace_flags: TraceFlags( + 1, + ), + is_remote: false, + trace_state: TraceState( + None, + ), + }, + parent_span_id: 00000000000000f0, + span_kind: Internal, + name: "panic: {message}", + start_time: SystemTime { + tv_sec: 8, + tv_nsec: 0, + }, + end_time: SystemTime { + tv_sec: 8, + tv_nsec: 0, + }, + attributes: [ + KeyValue { + key: Static( + "location", + ), + value: String( + Owned( + "tests/test_basic_exports.rs:50:13", + ), + ), + }, + KeyValue { + key: Static( + "backtrace", + ), + value: String( + Owned( + "disabled backtrace", + ), + ), + }, + KeyValue { + key: Static( + "logfire.msg", + ), + value: String( + Owned( + "panic: oh no!", + ), + ), + }, + KeyValue { + key: Static( + "logfire.level_num", + ), + value: I64( + 17, + ), + }, + KeyValue { + key: Static( + "logfire.span_type", + ), + value: String( + Static( + "log", + ), + ), + }, + KeyValue { + key: Static( + "logfire.json_schema", + ), + value: String( + Static( + "{\"type\":\"object\",\"properties\":{\"location\":{},\"backtrace\":{}}}", + ), + ), + }, + KeyValue { + key: Static( + "code.filepath", + ), + value: String( + Static( + "src/lib.rs", + ), + ), + }, + KeyValue { + key: Static( + "code.lineno", + ), + value: I64( + 637, + ), + }, + KeyValue { + key: Static( + "code.namespace", + ), + value: String( + Static( + "logfire", + ), + ), + }, + KeyValue { + key: Static( + "thread.id", + ), + value: I64( + 0, + ), + }, + KeyValue { + key: Static( + "thread.name", + ), + value: String( + Owned( + "test_basic_span", + ), + ), + }, + ], + dropped_attributes_count: 0, + events: SpanEvents { + events: [], + dropped_count: 0, + }, + links: SpanLinks { + links: [], + dropped_count: 0, + }, + status: Unset, + instrumentation_scope: InstrumentationScope { + name: "logfire", + version: None, + schema_url: None, + attributes: [], + }, + }, + SpanData { + span_context: SpanContext { + trace_id: 000000000000000000000000000000f0, + span_id: 00000000000000f0, + trace_flags: TraceFlags( + 1, + ), + is_remote: false, + trace_state: TraceState( + None, + ), + }, + parent_span_id: 0000000000000000, + span_kind: Internal, + name: "root span", + start_time: SystemTime { + tv_sec: 0, + tv_nsec: 0, + }, + end_time: SystemTime { + tv_sec: 9, + tv_nsec: 0, + }, + attributes: [ + KeyValue { + key: Static( + "code.filepath", + ), + value: String( + Static( + "tests/test_basic_exports.rs", + ), + ), + }, + KeyValue { + key: Static( + "code.namespace", + ), + value: String( + Static( + "test_basic_exports", + ), + ), + }, + KeyValue { + key: Static( + "code.lineno", + ), + value: I64( + 14, + ), + }, + KeyValue { + key: Static( + "thread.id", + ), + value: I64( + 0, + ), + }, + KeyValue { + key: Static( + "thread.name", + ), + value: String( + Owned( + "test_basic_span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.msg", + ), + value: String( + Owned( + "root span", + ), + ), + }, + KeyValue { + key: Static( + "logfire.json_schema", + ), + value: String( + Owned( + "{\"type\":\"object\",\"properties\":{}}", + ), + ), + }, + KeyValue { + key: Static( + "logfire.level_num", + ), + value: I64( + 9, + ), + }, + KeyValue { + key: Static( + "logfire.span_type", + ), + value: String( + Static( + "span", + ), + ), + }, + KeyValue { + key: Static( + "busy_ns", + ), + value: I64( + 0, + ), + }, + KeyValue { + key: Static( + "idle_ns", + ), + value: I64( + 0, + ), + }, + ], + dropped_attributes_count: 0, + events: SpanEvents { + events: [], + dropped_count: 0, + }, + links: SpanLinks { + links: [], + dropped_count: 0, + }, + status: Unset, + instrumentation_scope: InstrumentationScope { + name: "logfire", + version: None, + schema_url: None, + attributes: [], + }, + }, + ] + "#); +} + +#[test] +fn test_basic_metrics() { + let exporter = InMemoryMetricExporterBuilder::new().build(); + + let interval = Duration::from_millis(500); + + let handler = logfire::configure() + .send_to_logfire(false) + .with_metrics_options( + MetricsOptions::default().with_additional_reader( + PeriodicReader::builder(exporter.clone()) + .with_interval(interval) + .build(), + ), + ) + .finish() + .unwrap(); + + let guard = logfire::set_local_logfire(handler.clone()); + + use opentelemetry::metrics::MeterProvider; + + let counter = guard + .meter_provider() + .meter("logfire") + .u64_counter("basic_counter") + .build(); + + counter.add(1, &[]); + + std::thread::sleep(interval * 2); + + counter.add(2, &[]); + + std::thread::sleep(interval * 2); + + handler.shutdown().unwrap(); + + let metrics = exporter.get_finished_metrics().unwrap(); + + assert_debug_snapshot!(metrics, @r#" + [ + ResourceMetrics { + resource: Resource { + inner: ResourceInner { + attrs: { + Static( + "telemetry.sdk.name", + ): String( + Static( + "opentelemetry", + ), + ), + Static( + "telemetry.sdk.language", + ): String( + Static( + "rust", + ), + ), + Static( + "telemetry.sdk.version", + ): String( + Static( + "0.28.0", + ), + ), + Static( + "service.name", + ): String( + Static( + "unknown_service", + ), + ), + }, + schema_url: None, + }, + }, + scope_metrics: [ + ScopeMetrics { + scope: InstrumentationScope { + name: "logfire", + version: None, + schema_url: None, + attributes: [], + }, + metrics: [ + Metric { + name: "basic_counter", + description: "", + unit: "", + data: Sum { + data_points: [ + SumDataPoint { + attributes: [], + value: 1, + exemplars: [], + }, + ], + start_time: SystemTime { + tv_sec: 1742912513, + tv_nsec: 696872000, + }, + time: SystemTime { + tv_sec: 1742912514, + tv_nsec: 195601000, + }, + temporality: Cumulative, + is_monotonic: true, + }, + }, + ], + }, + ], + }, + ResourceMetrics { + resource: Resource { + inner: ResourceInner { + attrs: { + Static( + "telemetry.sdk.name", + ): String( + Static( + "opentelemetry", + ), + ), + Static( + "telemetry.sdk.language", + ): String( + Static( + "rust", + ), + ), + Static( + "telemetry.sdk.version", + ): String( + Static( + "0.28.0", + ), + ), + Static( + "service.name", + ): String( + Static( + "unknown_service", + ), + ), + }, + schema_url: None, + }, + }, + scope_metrics: [ + ScopeMetrics { + scope: InstrumentationScope { + name: "logfire", + version: None, + schema_url: None, + attributes: [], + }, + metrics: [ + Metric { + name: "basic_counter", + description: "", + unit: "", + data: Sum { + data_points: [ + SumDataPoint { + attributes: [], + value: 3, + exemplars: [], + }, + ], + start_time: SystemTime { + tv_sec: 1742912513, + tv_nsec: 696872000, + }, + time: SystemTime { + tv_sec: 1742912514, + tv_nsec: 698014000, + }, + temporality: Cumulative, + is_monotonic: true, + }, + }, + ], + }, + ], + }, + ResourceMetrics { + resource: Resource { + inner: ResourceInner { + attrs: { + Static( + "telemetry.sdk.name", + ): String( + Static( + "opentelemetry", + ), + ), + Static( + "telemetry.sdk.language", + ): String( + Static( + "rust", + ), + ), + Static( + "telemetry.sdk.version", + ): String( + Static( + "0.28.0", + ), + ), + Static( + "service.name", + ): String( + Static( + "unknown_service", + ), + ), + }, + schema_url: None, + }, + }, + scope_metrics: [ + ScopeMetrics { + scope: InstrumentationScope { + name: "logfire", + version: None, + schema_url: None, + attributes: [], + }, + metrics: [ + Metric { + name: "basic_counter", + description: "", + unit: "", + data: Sum { + data_points: [ + SumDataPoint { + attributes: [], + value: 3, + exemplars: [], + }, + ], + start_time: SystemTime { + tv_sec: 1742912513, + tv_nsec: 696872000, + }, + time: SystemTime { + tv_sec: 1742912515, + tv_nsec: 202834000, + }, + temporality: Cumulative, + is_monotonic: true, + }, + }, + ], + }, + ], + }, + ResourceMetrics { + resource: Resource { + inner: ResourceInner { + attrs: { + Static( + "telemetry.sdk.name", + ): String( + Static( + "opentelemetry", + ), + ), + Static( + "telemetry.sdk.language", + ): String( + Static( + "rust", + ), + ), + Static( + "telemetry.sdk.version", + ): String( + Static( + "0.28.0", + ), + ), + Static( + "service.name", + ): String( + Static( + "unknown_service", + ), + ), + }, + schema_url: None, + }, + }, + scope_metrics: [ + ScopeMetrics { + scope: InstrumentationScope { + name: "logfire", + version: None, + schema_url: None, + attributes: [], + }, + metrics: [ + Metric { + name: "basic_counter", + description: "", + unit: "", + data: Sum { + data_points: [ + SumDataPoint { + attributes: [], + value: 3, + exemplars: [], + }, + ], + start_time: SystemTime { + tv_sec: 1742912513, + tv_nsec: 696872000, + }, + time: SystemTime { + tv_sec: 1742912515, + tv_nsec: 705693000, + }, + temporality: Cumulative, + is_monotonic: true, + }, + }, + ], + }, + ], + }, + ] + "#) +} From 0d0d2fd5a7c9fc88d6c57b33e7274b51c95b127c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 25 Mar 2025 16:06:31 +0000 Subject: [PATCH 2/2] make tests stable --- Cargo.toml | 4 +- src/bridges/tracing.rs | 19 +- src/config.rs | 10 + src/internal/exporters/console.rs | 2 +- src/internal/exporters/remove_pending.rs | 2 +- src/lib.rs | 8 + src/test_utils.rs | 81 +++++- tests/test_basic_exports.rs | 314 +++++++---------------- 8 files changed, 203 insertions(+), 237 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 252f997..09e0985 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,9 +35,11 @@ nu-ansi-term = "0.50.1" chrono = "0.4.39" [dev-dependencies] +async-trait = "0.1.88" insta = "1.42.1" -opentelemetry_sdk = { version = "0.28", default-features = false, features = ["testing", "internal-logs"] } +opentelemetry_sdk = { version = "0.28", default-features = false, features = ["testing"] } regex = "1.11.1" +tokio = {version = "1.44.1", features = ["test-util"] } ulid = "1.2.0" [features] diff --git a/src/bridges/tracing.rs b/src/bridges/tracing.rs index 83a968e..e4d2dec 100644 --- a/src/bridges/tracing.rs +++ b/src/bridges/tracing.rs @@ -149,6 +149,7 @@ mod tests { let exporter = InMemorySpanExporterBuilder::new().build(); let handler = crate::configure() + .local() .send_to_logfire(false) .with_additional_span_processor(SimpleSpanProcessor::new(Box::new( DeterministicExporter::new(exporter.clone(), file!(), line!()), @@ -223,7 +224,7 @@ mod tests { "code.lineno", ), value: I64( - 11, + 13, ), }, KeyValue { @@ -329,7 +330,7 @@ mod tests { "code.lineno", ), value: I64( - 12, + 14, ), }, KeyValue { @@ -445,7 +446,7 @@ mod tests { "code.lineno", ), value: I64( - 12, + 14, ), }, KeyValue { @@ -567,7 +568,7 @@ mod tests { "code.lineno", ), value: I64( - 13, + 15, ), }, KeyValue { @@ -683,7 +684,7 @@ mod tests { "code.lineno", ), value: I64( - 13, + 15, ), }, KeyValue { @@ -805,7 +806,7 @@ mod tests { "code.lineno", ), value: I64( - 14, + 16, ), }, KeyValue { @@ -921,7 +922,7 @@ mod tests { "code.lineno", ), value: I64( - 14, + 16, ), }, KeyValue { @@ -1043,7 +1044,7 @@ mod tests { "code.lineno", ), value: I64( - 11, + 13, ), }, KeyValue { @@ -1154,7 +1155,7 @@ mod tests { "code.lineno", ), value: I64( - 169, + 172, ), }, ], diff --git a/src/config.rs b/src/config.rs index 421c5ee..695746f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -163,6 +163,8 @@ pub struct AdvancedOptions { pub(crate) base_url: String, /// Generator for trace and span IDs. pub(crate) id_generator: Option, + /// Resource to override default resource detection. + pub(crate) resource: Option, // // // TODO: arguments below supported by Python @@ -179,6 +181,7 @@ impl Default for AdvancedOptions { AdvancedOptions { base_url: "https://logfire-api.pydantic.dev".to_string(), id_generator: None, + resource: None, } } } @@ -200,6 +203,13 @@ impl AdvancedOptions { self.id_generator = Some(BoxedIdGenerator::new(Box::new(generator))); self } + + /// Set the resource; overrides default resource detection. + #[must_use] + pub fn with_resource(mut self, resource: opentelemetry_sdk::Resource) -> Self { + self.resource = Some(resource); + self + } } /// Configuration of metrics. diff --git a/src/internal/exporters/console.rs b/src/internal/exporters/console.rs index 7a03027..c800bdb 100644 --- a/src/internal/exporters/console.rs +++ b/src/internal/exporters/console.rs @@ -320,7 +320,7 @@ mod tests { 1970-01-01T00:00:03.000000Z DEBUG logfire::internal::exporters::console::tests debug span 1970-01-01T00:00:05.000000Z DEBUG logfire::internal::exporters::console::tests debug span with explicit parent 1970-01-01T00:00:07.000000Z INFO logfire::internal::exporters::console::tests hello world log - 1970-01-01T00:00:08.000000Z ERROR logfire panic: oh no! location=src/internal/exporters/console.rs:295:17, backtrace=disabled backtrace + 1970-01-01T00:00:08.000000Z ERROR logfire panic: oh no! location=src/internal/exporters/console.rs:307:17, backtrace=disabled backtrace "#); } } diff --git a/src/internal/exporters/remove_pending.rs b/src/internal/exporters/remove_pending.rs index 680c96b..df06705 100644 --- a/src/internal/exporters/remove_pending.rs +++ b/src/internal/exporters/remove_pending.rs @@ -81,8 +81,8 @@ mod tests { let exporter = InMemorySpanExporterBuilder::new().build(); let guard = crate::configure() + .local() .send_to_logfire(false) - .install_panic_handler() .with_additional_span_processor( BatchSpanProcessor::builder(DeterministicExporter::new( RemovePendingSpansExporter(exporter.clone()), diff --git a/src/lib.rs b/src/lib.rs index b0ff836..d2de002 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -467,6 +467,10 @@ impl LogfireConfigBuilder { tracer_provider_builder.with_id_generator(UlidIdGenerator::new()); }; + if let Some(resource) = advanced_options.resource.clone() { + tracer_provider_builder = tracer_provider_builder.with_resource(resource); + } + let mut http_headers: Option> = None; if send_to_logfire { @@ -553,6 +557,10 @@ impl LogfireConfigBuilder { } } + if let Some(resource) = advanced_options.resource { + meter_provider_builder = meter_provider_builder.with_resource(resource); + } + let meter_provider = meter_provider_builder.build(); if self.install_panic_handler { diff --git a/src/test_utils.rs b/src/test_utils.rs index 3070af4..aba5e31 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1,17 +1,28 @@ +#![allow(dead_code)] // used by lib and test suites individually + use std::{ collections::{HashMap, hash_map::Entry}, future::Future, pin::Pin, - sync::atomic::{AtomicU64, Ordering}, + sync::{ + Arc, Mutex, + atomic::{AtomicU64, Ordering}, + }, time::SystemTime, }; +use async_trait::async_trait; use opentelemetry::{ Value, trace::{SpanId, TraceId}, }; use opentelemetry_sdk::{ error::OTelSdkResult, + metrics::{ + Temporality, + data::{ResourceMetrics, Sum}, + exporter::PushMetricExporter, + }, trace::{IdGenerator, SpanData, SpanExporter}, }; @@ -46,13 +57,18 @@ impl DeterministicIdGenerator { #[derive(Debug)] pub struct DeterministicExporter { exporter: Inner, - next_timestamp: u64, - timestamp_remap: HashMap, + timestamp_remap: Arc>, // Information used to adjust line number to help minimise test churn file: &'static str, line_offset: u32, } +impl DeterministicExporter { + pub fn inner(&self) -> &Inner { + &self.exporter + } +} + impl SpanExporter for DeterministicExporter { fn export( &mut self, @@ -113,18 +129,73 @@ impl SpanExporter for DeterministicExporter { } } +#[async_trait] +impl PushMetricExporter for DeterministicExporter { + async fn export(&self, metrics: &mut ResourceMetrics) -> OTelSdkResult { + let timestamp_remap = self.timestamp_remap.clone(); + for scope in &mut metrics.scope_metrics { + for metric in &mut scope.metrics { + if let Some(sum) = (*metric.data).as_mut().downcast_mut::>() { + sum.start_time = timestamp_remap + .lock() + .unwrap() + .remap_timestamp(sum.start_time); + sum.time = timestamp_remap.lock().unwrap().remap_timestamp(sum.time); + + for data_point in &mut sum.data_points { + data_point + .attributes + .sort_by_cached_key(|kv| kv.key.to_string()); + } + } + } + } + self.exporter.export(metrics).await + } + + async fn force_flush(&self) -> OTelSdkResult { + self.exporter.force_flush().await + } + + fn shutdown(&self) -> OTelSdkResult { + self.exporter.shutdown() + } + + fn temporality(&self) -> Temporality { + self.exporter.temporality() + } +} + impl DeterministicExporter { /// Create deterministic exporter, feeding it current file and line. pub fn new(exporter: Inner, file: &'static str, line_offset: u32) -> Self { Self { exporter, - next_timestamp: 0, - timestamp_remap: HashMap::new(), + timestamp_remap: Arc::new(Mutex::new(TimestampRemapper::new())), file, line_offset, } } + fn remap_timestamp(&mut self, from: SystemTime) -> SystemTime { + self.timestamp_remap.lock().unwrap().remap_timestamp(from) + } +} + +#[derive(Debug)] +struct TimestampRemapper { + next_timestamp: u64, + timestamp_remap: HashMap, +} + +impl TimestampRemapper { + fn new() -> Self { + Self { + next_timestamp: 0, + timestamp_remap: HashMap::new(), + } + } + fn remap_timestamp(&mut self, from: SystemTime) -> SystemTime { match self.timestamp_remap.entry(from) { Entry::Occupied(entry) => *entry.get(), diff --git a/tests/test_basic_exports.rs b/tests/test_basic_exports.rs index 3f4f8da..0c7132f 100644 --- a/tests/test_basic_exports.rs +++ b/tests/test_basic_exports.rs @@ -1,10 +1,16 @@ //! Basic snapshot tests for data produced by the logfire APIs. -use std::time::Duration; +use std::sync::Arc; use insta::assert_debug_snapshot; use opentelemetry_sdk::{ - metrics::{InMemoryMetricExporterBuilder, PeriodicReader}, + Resource, + metrics::{ + InMemoryMetricExporterBuilder, ManualReader, + data::ResourceMetrics, + exporter::{self}, + reader::MetricReader, + }, trace::{InMemorySpanExporterBuilder, SimpleSpanProcessor}, }; use tracing::{Level, level_filters::LevelFilter}; @@ -1171,7 +1177,7 @@ fn test_basic_span() { ), value: String( Owned( - "tests/test_basic_exports.rs:50:13", + "tests/test_basic_exports.rs:56:13", ), ), }, @@ -1238,7 +1244,7 @@ fn test_basic_span() { "code.lineno", ), value: I64( - 637, + 646, ), }, KeyValue { @@ -1433,20 +1439,78 @@ fn test_basic_span() { "#); } -#[test] -fn test_basic_metrics() { - let exporter = InMemoryMetricExporterBuilder::new().build(); +#[derive(Clone, Debug)] +struct SharedManualReader { + reader: Arc, +} + +impl SharedManualReader { + fn new(reader: ManualReader) -> Self { + Self { + reader: Arc::new(reader), + } + } + + async fn export(&self, exporter: &mut dyn exporter::PushMetricExporter) { + let mut metrics = ResourceMetrics { + resource: Resource::builder_empty().build(), + scope_metrics: Vec::new(), + }; + dbg!(&metrics); + self.reader.collect(&mut metrics).unwrap(); + dbg!(&metrics); + exporter.export(&mut metrics).await.unwrap(); + } +} + +impl MetricReader for SharedManualReader { + fn register_pipeline(&self, pipeline: std::sync::Weak) { + self.reader.register_pipeline(pipeline); + } + + fn collect( + &self, + rm: &mut opentelemetry_sdk::metrics::data::ResourceMetrics, + ) -> opentelemetry_sdk::metrics::MetricResult<()> { + self.reader.collect(rm) + } + + fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult { + self.reader.force_flush() + } + + fn shutdown(&self) -> opentelemetry_sdk::error::OTelSdkResult { + self.reader.shutdown() + } + + fn temporality( + &self, + kind: opentelemetry_sdk::metrics::InstrumentKind, + ) -> opentelemetry_sdk::metrics::Temporality { + self.reader.temporality(kind) + } +} - let interval = Duration::from_millis(500); +#[tokio::test] +async fn test_basic_metrics() { + let mut exporter = DeterministicExporter::new( + InMemoryMetricExporterBuilder::new().build(), + file!(), + line!(), + ); + + let reader = SharedManualReader::new( + ManualReader::builder() + .with_temporality(opentelemetry_sdk::metrics::Temporality::Delta) + .build(), + ); let handler = logfire::configure() .send_to_logfire(false) - .with_metrics_options( - MetricsOptions::default().with_additional_reader( - PeriodicReader::builder(exporter.clone()) - .with_interval(interval) - .build(), - ), + .with_metrics_options(MetricsOptions::default().with_additional_reader(reader.clone())) + .with_advanced_options( + AdvancedOptions::default() + .with_resource(Resource::builder_empty().with_service_name("test").build()), ) .finish() .unwrap(); @@ -1462,16 +1526,14 @@ fn test_basic_metrics() { .build(); counter.add(1, &[]); - - std::thread::sleep(interval * 2); + reader.export(&mut exporter).await; counter.add(2, &[]); - - std::thread::sleep(interval * 2); + reader.export(&mut exporter).await; handler.shutdown().unwrap(); - let metrics = exporter.get_finished_metrics().unwrap(); + let metrics = exporter.inner().get_finished_metrics().unwrap(); assert_debug_snapshot!(metrics, @r#" [ @@ -1479,32 +1541,11 @@ fn test_basic_metrics() { resource: Resource { inner: ResourceInner { attrs: { - Static( - "telemetry.sdk.name", - ): String( - Static( - "opentelemetry", - ), - ), - Static( - "telemetry.sdk.language", - ): String( - Static( - "rust", - ), - ), - Static( - "telemetry.sdk.version", - ): String( - Static( - "0.28.0", - ), - ), Static( "service.name", ): String( Static( - "unknown_service", + "test", ), ), }, @@ -1533,14 +1574,14 @@ fn test_basic_metrics() { }, ], start_time: SystemTime { - tv_sec: 1742912513, - tv_nsec: 696872000, + tv_sec: 0, + tv_nsec: 0, }, time: SystemTime { - tv_sec: 1742912514, - tv_nsec: 195601000, + tv_sec: 1, + tv_nsec: 0, }, - temporality: Cumulative, + temporality: Delta, is_monotonic: true, }, }, @@ -1552,178 +1593,11 @@ fn test_basic_metrics() { resource: Resource { inner: ResourceInner { attrs: { - Static( - "telemetry.sdk.name", - ): String( - Static( - "opentelemetry", - ), - ), - Static( - "telemetry.sdk.language", - ): String( - Static( - "rust", - ), - ), - Static( - "telemetry.sdk.version", - ): String( - Static( - "0.28.0", - ), - ), - Static( - "service.name", - ): String( - Static( - "unknown_service", - ), - ), - }, - schema_url: None, - }, - }, - scope_metrics: [ - ScopeMetrics { - scope: InstrumentationScope { - name: "logfire", - version: None, - schema_url: None, - attributes: [], - }, - metrics: [ - Metric { - name: "basic_counter", - description: "", - unit: "", - data: Sum { - data_points: [ - SumDataPoint { - attributes: [], - value: 3, - exemplars: [], - }, - ], - start_time: SystemTime { - tv_sec: 1742912513, - tv_nsec: 696872000, - }, - time: SystemTime { - tv_sec: 1742912514, - tv_nsec: 698014000, - }, - temporality: Cumulative, - is_monotonic: true, - }, - }, - ], - }, - ], - }, - ResourceMetrics { - resource: Resource { - inner: ResourceInner { - attrs: { - Static( - "telemetry.sdk.name", - ): String( - Static( - "opentelemetry", - ), - ), - Static( - "telemetry.sdk.language", - ): String( - Static( - "rust", - ), - ), - Static( - "telemetry.sdk.version", - ): String( - Static( - "0.28.0", - ), - ), - Static( - "service.name", - ): String( - Static( - "unknown_service", - ), - ), - }, - schema_url: None, - }, - }, - scope_metrics: [ - ScopeMetrics { - scope: InstrumentationScope { - name: "logfire", - version: None, - schema_url: None, - attributes: [], - }, - metrics: [ - Metric { - name: "basic_counter", - description: "", - unit: "", - data: Sum { - data_points: [ - SumDataPoint { - attributes: [], - value: 3, - exemplars: [], - }, - ], - start_time: SystemTime { - tv_sec: 1742912513, - tv_nsec: 696872000, - }, - time: SystemTime { - tv_sec: 1742912515, - tv_nsec: 202834000, - }, - temporality: Cumulative, - is_monotonic: true, - }, - }, - ], - }, - ], - }, - ResourceMetrics { - resource: Resource { - inner: ResourceInner { - attrs: { - Static( - "telemetry.sdk.name", - ): String( - Static( - "opentelemetry", - ), - ), - Static( - "telemetry.sdk.language", - ): String( - Static( - "rust", - ), - ), - Static( - "telemetry.sdk.version", - ): String( - Static( - "0.28.0", - ), - ), Static( "service.name", ): String( Static( - "unknown_service", + "test", ), ), }, @@ -1747,19 +1621,19 @@ fn test_basic_metrics() { data_points: [ SumDataPoint { attributes: [], - value: 3, + value: 2, exemplars: [], }, ], start_time: SystemTime { - tv_sec: 1742912513, - tv_nsec: 696872000, + tv_sec: 1, + tv_nsec: 0, }, time: SystemTime { - tv_sec: 1742912515, - tv_nsec: 705693000, + tv_sec: 2, + tv_nsec: 0, }, - temporality: Cumulative, + temporality: Delta, is_monotonic: true, }, },