From bddef292336d8fb8aefe5ad4e408f39fcfd3d257 Mon Sep 17 00:00:00 2001 From: Rudi Floren Date: Fri, 29 Sep 2023 20:19:54 +0200 Subject: [PATCH] Add support for instrumented functions which return Result (#28) ## Motivation Currently, the tracing instrumentation macro emits a single event after the function call, but in the current span, with just a field named error set. For example: ```rust #[instrument(err)] fn test() -> Result<(), ()> { ...body } ``` gets roughly expanded to ```rust fn test() -> Result<(), ()> { let span = span!("test") fn inner() -> Result<(), ()> { ...body } match inner() { Ok(x) => Ok(x), Err(err) => { error!(error=%err) Err(err) } } ``` In the error case of the result, the macro will emit an error level event with just an `error` field set to the display (or debug) value of the returned error. While there exists support for the Error primitive in tracing, the primitive only supports 'static Errors. See https://github.com/tokio-rs/tracing/issues/1308 ## Solution This PR adds support to use this event to fill the span status error description with the content of the error field of this event. Additionally, this ass support to emit these events (or manually created ones that follow the same format) as OTel events following the exception convention. The operation is optional and can be configured using the `ErrorFieldConfig`. This seems like another hack similar to `otel.*` fields, but should reduce some boilerplate in existing codebases. I propose to keep this until `tracing` improves support for Error fields. --- examples/opentelemetry-error.rs | 186 ++++++++++++++++++ src/layer.rs | 332 +++++++++++++++++++++++++++++--- tests/errors.rs | 122 ++++++++++++ 3 files changed, 611 insertions(+), 29 deletions(-) create mode 100644 examples/opentelemetry-error.rs create mode 100644 tests/errors.rs diff --git a/examples/opentelemetry-error.rs b/examples/opentelemetry-error.rs new file mode 100644 index 0000000..fe93cf6 --- /dev/null +++ b/examples/opentelemetry-error.rs @@ -0,0 +1,186 @@ +use std::{ + borrow::Cow, + error::Error as StdError, + fmt::{Debug, Display}, + io::Write, + thread, + time::{Duration, SystemTime}, +}; + +use opentelemetry::{ + global, + sdk::{ + self, + export::trace::{ExportResult, SpanExporter}, + }, + trace::TracerProvider, +}; +use tracing::{error, instrument, span, trace, warn}; +use tracing_subscriber::prelude::*; + +#[derive(Debug)] +enum Error { + ErrorQueryPassed, +} + +impl StdError for Error {} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ErrorQueryPassed => write!(f, "Encountered the error flag in the query"), + } + } +} + +#[instrument(err)] +fn failable_work(fail: bool) -> Result<&'static str, Error> { + span!(tracing::Level::INFO, "expensive_step_1") + .in_scope(|| thread::sleep(Duration::from_millis(25))); + span!(tracing::Level::INFO, "expensive_step_2") + .in_scope(|| thread::sleep(Duration::from_millis(25))); + + if fail { + return Err(Error::ErrorQueryPassed); + } + Ok("success") +} + +#[instrument(err)] +fn double_failable_work(fail: bool) -> Result<&'static str, Error> { + span!(tracing::Level::INFO, "expensive_step_1") + .in_scope(|| thread::sleep(Duration::from_millis(25))); + span!(tracing::Level::INFO, "expensive_step_2") + .in_scope(|| thread::sleep(Duration::from_millis(25))); + error!(error = "test", "hello"); + if fail { + return Err(Error::ErrorQueryPassed); + } + Ok("success") +} + +fn main() -> Result<(), Box> { + let builder = sdk::trace::TracerProvider::builder().with_simple_exporter(WriterExporter); + let provider = builder.build(); + let tracer = provider.versioned_tracer( + "opentelemetry-write-exporter", + None::>, + None::>, + None, + ); + global::set_tracer_provider(provider); + + let opentelemetry = tracing_opentelemetry::layer().with_tracer(tracer); + tracing_subscriber::registry() + .with(opentelemetry) + .try_init()?; + + { + let root = span!(tracing::Level::INFO, "app_start", work_units = 2); + let _enter = root.enter(); + + let work_result = failable_work(false); + + trace!("status: {}", work_result.unwrap()); + let work_result = failable_work(true); + + trace!("status: {}", work_result.err().unwrap()); + warn!("About to exit!"); + + let _ = double_failable_work(true); + } // Once this scope is closed, all spans inside are closed as well + + // Shut down the current tracer provider. This will invoke the shutdown + // method on all span processors. span processors should export remaining + // spans before return. + global::shutdown_tracer_provider(); + + Ok(()) +} + +#[derive(Debug)] +struct WriterExporter; + +impl SpanExporter for WriterExporter { + fn export( + &mut self, + batch: Vec, + ) -> futures_util::future::BoxFuture<'static, opentelemetry::sdk::export::trace::ExportResult> + { + let mut writer = std::io::stdout(); + for span in batch { + writeln!(writer, "{}", SpanData(span)).unwrap(); + } + writeln!(writer).unwrap(); + + Box::pin(async move { ExportResult::Ok(()) }) + } +} + +struct SpanData(opentelemetry::sdk::export::trace::SpanData); +impl Display for SpanData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Span: \"{}\"", self.0.name)?; + match &self.0.status { + opentelemetry::trace::Status::Unset => {} + opentelemetry::trace::Status::Error { description } => { + writeln!(f, "- Status: Error")?; + writeln!(f, "- Error: {description}")? + } + opentelemetry::trace::Status::Ok => writeln!(f, "- Status: Ok")?, + } + writeln!( + f, + "- Start: {}", + self.0 + .start_time + .duration_since(SystemTime::UNIX_EPOCH) + .expect("start time is before the unix epoch") + .as_secs() + )?; + writeln!( + f, + "- End: {}", + self.0 + .end_time + .duration_since(SystemTime::UNIX_EPOCH) + .expect("end time is before the unix epoch") + .as_secs() + )?; + writeln!(f, "- Resource:")?; + for (k, v) in self.0.resource.iter() { + writeln!(f, " - {}: {}", k, v)?; + } + writeln!(f, "- Attributes:")?; + for (k, v) in self.0.attributes.iter() { + writeln!(f, " - {}: {}", k, v)?; + } + + writeln!(f, "- Events:")?; + for event in self.0.events.iter() { + if let Some(error) = + event + .attributes + .iter() + .fold(Option::::None, |mut acc, d| { + if let Some(mut acc) = acc.take() { + use std::fmt::Write; + let _ = write!(acc, ", {}={}", d.key, d.value); + Some(acc) + } else { + Some(format!("{} = {}", d.key, d.value)) + } + }) + { + writeln!(f, " - \"{}\" {{{error}}}", event.name)?; + } else { + writeln!(f, " - \"{}\"", event.name)?; + } + } + writeln!(f, "- Links:")?; + for link in self.0.links.iter() { + writeln!(f, " - {:?}", link)?; + } + Ok(()) + } +} diff --git a/src/layer.rs b/src/layer.rs index 8bb0069..c61346e 100644 --- a/src/layer.rs +++ b/src/layer.rs @@ -25,6 +25,7 @@ const SPAN_KIND_FIELD: &str = "otel.kind"; const SPAN_STATUS_CODE_FIELD: &str = "otel.status_code"; const SPAN_STATUS_MESSAGE_FIELD: &str = "otel.status_message"; +const EVENT_EXCEPTION_NAME: &str = "exception"; const FIELD_EXCEPTION_MESSAGE: &str = "exception.message"; const FIELD_EXCEPTION_STACKTRACE: &str = "exception.stacktrace"; @@ -38,7 +39,7 @@ pub struct OpenTelemetryLayer { location: bool, tracked_inactivity: bool, with_threads: bool, - exception_config: ExceptionFieldConfig, + sem_conv_config: SemConvConfig, get_context: WithContext, _registry: marker::PhantomData, } @@ -119,7 +120,7 @@ fn str_to_status(s: &str) -> otel::Status { struct SpanEventVisitor<'a, 'b> { event_builder: &'a mut otel::Event, span_builder: Option<&'b mut otel::SpanBuilder>, - exception_config: ExceptionFieldConfig, + sem_conv_config: SemConvConfig, } impl<'a, 'b> field::Visit for SpanEventVisitor<'a, 'b> { @@ -180,6 +181,27 @@ impl<'a, 'b> field::Visit for SpanEventVisitor<'a, 'b> { fn record_str(&mut self, field: &field::Field, value: &str) { match field.name() { "message" => self.event_builder.name = value.to_string().into(), + // While tracing supports the error primitive, the instrumentation macro does not + // use the primitive and instead uses the debug or display primitive. + // In both cases, an event with an empty name and with an error attribute is created. + "error" if self.event_builder.name.is_empty() => { + if self.sem_conv_config.error_events_to_status { + if let Some(span) = &mut self.span_builder { + span.status = otel::Status::error(format!("{:?}", value)); + } + } + if self.sem_conv_config.error_events_to_exceptions { + self.event_builder.name = EVENT_EXCEPTION_NAME.into(); + self.event_builder.attributes.push(KeyValue::new( + FIELD_EXCEPTION_MESSAGE, + format!("{:?}", value), + )); + } else { + self.event_builder + .attributes + .push(KeyValue::new("error", format!("{:?}", value))); + } + } // Skip fields that are actually log metadata that have already been handled #[cfg(feature = "tracing-log")] name if name.starts_with("log.") => (), @@ -198,6 +220,27 @@ impl<'a, 'b> field::Visit for SpanEventVisitor<'a, 'b> { fn record_debug(&mut self, field: &field::Field, value: &dyn fmt::Debug) { match field.name() { "message" => self.event_builder.name = format!("{:?}", value).into(), + // While tracing supports the error primitive, the instrumentation macro does not + // use the primitive and instead uses the debug or display primitive. + // In both cases, an event with an empty name and with an error attribute is created. + "error" if self.event_builder.name.is_empty() => { + if self.sem_conv_config.error_events_to_status { + if let Some(span) = &mut self.span_builder { + span.status = otel::Status::error(format!("{:?}", value)); + } + } + if self.sem_conv_config.error_events_to_exceptions { + self.event_builder.name = EVENT_EXCEPTION_NAME.into(); + self.event_builder.attributes.push(KeyValue::new( + FIELD_EXCEPTION_MESSAGE, + format!("{:?}", value), + )); + } else { + self.event_builder + .attributes + .push(KeyValue::new("error", format!("{:?}", value))); + } + } // Skip fields that are actually log metadata that have already been handled #[cfg(feature = "tracing-log")] name if name.starts_with("log.") => (), @@ -228,7 +271,7 @@ impl<'a, 'b> field::Visit for SpanEventVisitor<'a, 'b> { let error_msg = value.to_string(); - if self.exception_config.record { + if self.sem_conv_config.error_fields_to_exceptions { self.event_builder .attributes .push(Key::new(FIELD_EXCEPTION_MESSAGE).string(error_msg.clone())); @@ -244,7 +287,7 @@ impl<'a, 'b> field::Visit for SpanEventVisitor<'a, 'b> { .push(Key::new(FIELD_EXCEPTION_STACKTRACE).array(chain.clone())); } - if self.exception_config.propagate { + if self.sem_conv_config.error_records_to_exceptions { if let Some(span) = &mut self.span_builder { if let Some(attrs) = span.attributes.as_mut() { attrs.insert( @@ -275,21 +318,44 @@ impl<'a, 'b> field::Visit for SpanEventVisitor<'a, 'b> { } } -/// Control over opentelemetry conventional exception fields +/// Control over the mapping between tracing fields/events and OpenTelemetry conventional status/exception fields #[derive(Clone, Copy)] -struct ExceptionFieldConfig { +struct SemConvConfig { /// If an error value is recorded on an event/span, should the otel fields /// be added - record: bool, + /// + /// Note that this uses tracings `record_error` which is only implemented for `(dyn Error + 'static)`. + error_fields_to_exceptions: bool, /// If an error value is recorded on an event, should the otel fields be /// added to the corresponding span - propagate: bool, + /// + /// Note that this uses tracings `record_error` which is only implemented for `(dyn Error + 'static)`. + error_records_to_exceptions: bool, + + /// If a function is instrumented and returns a `Result`, should the error + /// value be propagated to the span status. + /// + /// Without this enabled, the span status will be "Error" with an empty description + /// when at least one error event is recorded in the span. + /// + /// Note: the instrument macro will emit an error event if the function returns the `Err` variant. + /// This is not affected by this setting. Disabling this will only affect the span status. + error_events_to_status: bool, + + /// If an event with an empty name and a field named `error` is recorded, + /// should the event be rewritten to have the name `exception` and the field `exception.message` + /// + /// Follows the semantic conventions for exceptions. + /// + /// Note: the instrument macro will emit an error event if the function returns the `Err` variant. + /// This is not affected by this setting. Disabling this will only affect the created fields on the OTel span. + error_events_to_exceptions: bool, } struct SpanAttributeVisitor<'a> { span_builder: &'a mut otel::SpanBuilder, - exception_config: ExceptionFieldConfig, + sem_conv_config: SemConvConfig, } impl<'a> SpanAttributeVisitor<'a> { @@ -377,7 +443,7 @@ impl<'a> field::Visit for SpanAttributeVisitor<'a> { let error_msg = value.to_string(); - if self.exception_config.record { + if self.sem_conv_config.error_fields_to_exceptions { self.record(Key::new(FIELD_EXCEPTION_MESSAGE).string(error_msg.clone())); // NOTE: This is actually not the stacktrace of the exception. This is @@ -432,10 +498,13 @@ where location: true, tracked_inactivity: true, with_threads: true, - exception_config: ExceptionFieldConfig { - record: false, - propagate: false, + sem_conv_config: SemConvConfig { + error_fields_to_exceptions: true, + error_records_to_exceptions: true, + error_events_to_exceptions: true, + error_events_to_status: true, }, + get_context: WithContext(Self::get_context), _registry: marker::PhantomData, } @@ -476,7 +545,7 @@ where location: self.location, tracked_inactivity: self.tracked_inactivity, with_threads: self.with_threads, - exception_config: self.exception_config, + sem_conv_config: self.sem_conv_config, get_context: WithContext(OpenTelemetryLayer::::get_context), _registry: self._registry, } @@ -491,14 +560,81 @@ where /// These attributes follow the [OpenTelemetry semantic conventions for /// exceptions][conv]. /// - /// By default, these attributes are not recorded. + /// By default, these attributes are recorded. + /// Note that this only works for `(dyn Error + 'static)`. + /// See [Implementations on Foreign Types of tracing::Value][impls] or [`OpenTelemetryLayer::with_error_events_to_exceptions`] /// /// [conv]: https://github.com/open-telemetry/semantic-conventions/tree/main/docs/exceptions/ + /// [impls]: https://docs.rs/tracing/0.1.37/tracing/trait.Value.html#foreign-impls + #[deprecated( + since = "0.21.0", + note = "renamed to `OpenTelemetryLayer::with_error_fields_to_exceptions`" + )] pub fn with_exception_fields(self, exception_fields: bool) -> Self { Self { - exception_config: ExceptionFieldConfig { - record: exception_fields, - ..self.exception_config + sem_conv_config: SemConvConfig { + error_fields_to_exceptions: exception_fields, + ..self.sem_conv_config + }, + ..self + } + } + + /// Sets whether or not span and event metadata should include OpenTelemetry + /// exception fields such as `exception.message` and `exception.backtrace` + /// when an `Error` value is recorded. If multiple error values are recorded + /// on the same span/event, only the most recently recorded error value will + /// show up under these fields. + /// + /// These attributes follow the [OpenTelemetry semantic conventions for + /// exceptions][conv]. + /// + /// By default, these attributes are recorded. + /// Note that this only works for `(dyn Error + 'static)`. + /// See [Implementations on Foreign Types of tracing::Value][impls] or [`OpenTelemetryLayer::with_error_events_to_exceptions`] + /// + /// [conv]: https://github.com/open-telemetry/semantic-conventions/tree/main/docs/exceptions/ + /// [impls]: https://docs.rs/tracing/0.1.37/tracing/trait.Value.html#foreign-impls + pub fn with_error_fields_to_exceptions(self, error_fields_to_exceptions: bool) -> Self { + Self { + sem_conv_config: SemConvConfig { + error_fields_to_exceptions, + ..self.sem_conv_config + }, + ..self + } + } + + /// Sets whether or not an event considered for exception mapping (see [`OpenTelemetryLayer::with_error_recording`]) + /// should be propagated to the span status error description. + /// + /// + /// By default, these events do set the span status error description. + pub fn with_error_events_to_status(self, error_events_to_status: bool) -> Self { + Self { + sem_conv_config: SemConvConfig { + error_events_to_status, + ..self.sem_conv_config + }, + ..self + } + } + + /// Sets whether or not a subset of events following the described schema are mapped to + /// events following the [OpenTelemetry semantic conventions for + /// exceptions][conv]. + /// + /// * Only events without a message field (unnamed events) and at least one field with the name error + /// are considered for mapping. + /// + /// By default, these events are mapped. + /// + /// [conv]: https://github.com/open-telemetry/semantic-conventions/tree/main/docs/exceptions/ + pub fn with_error_events_to_exceptions(self, error_events_to_exceptions: bool) -> Self { + Self { + sem_conv_config: SemConvConfig { + error_events_to_exceptions, + ..self.sem_conv_config }, ..self } @@ -514,14 +650,45 @@ where /// These attributes follow the [OpenTelemetry semantic conventions for /// exceptions][conv]. /// - /// By default, these attributes are not propagated to the span. + /// By default, these attributes are propagated to the span. Note that this only works for `(dyn Error + 'static)`. + /// See [Implementations on Foreign Types of tracing::Value][impls] or [`OpenTelemetryLayer::with_error_events_to_exceptions`] /// /// [conv]: https://github.com/open-telemetry/semantic-conventions/tree/main/docs/exceptions/ + /// [impls]: https://docs.rs/tracing/0.1.37/tracing/trait.Value.html#foreign-impls + #[deprecated( + since = "0.21.0", + note = "renamed to `OpenTelemetryLayer::with_error_records_to_exceptions`" + )] pub fn with_exception_field_propagation(self, exception_field_propagation: bool) -> Self { Self { - exception_config: ExceptionFieldConfig { - propagate: exception_field_propagation, - ..self.exception_config + sem_conv_config: SemConvConfig { + error_records_to_exceptions: exception_field_propagation, + ..self.sem_conv_config + }, + ..self + } + } + + /// Sets whether or not reporting an `Error` value on an event will + /// propagate the OpenTelemetry exception fields such as `exception.message` + /// and `exception.backtrace` to the corresponding span. You do not need to + /// enable `with_exception_fields` in order to enable this. If multiple + /// error values are recorded on the same span/event, only the most recently + /// recorded error value will show up under these fields. + /// + /// These attributes follow the [OpenTelemetry semantic conventions for + /// exceptions][conv]. + /// + /// By default, these attributes are propagated to the span. Note that this only works for `(dyn Error + 'static)`. + /// See [Implementations on Foreign Types of tracing::Value][impls] or [`OpenTelemetryLayer::with_error_events_to_exceptions`] + /// + /// [conv]: https://github.com/open-telemetry/semantic-conventions/tree/main/docs/exceptions/ + /// [impls]: https://docs.rs/tracing/0.1.37/tracing/trait.Value.html#foreign-impls + pub fn with_error_records_to_exceptions(self, error_records_to_exceptions: bool) -> Self { + Self { + sem_conv_config: SemConvConfig { + error_records_to_exceptions, + ..self.sem_conv_config }, ..self } @@ -725,7 +892,7 @@ where attrs.record(&mut SpanAttributeVisitor { span_builder: &mut builder, - exception_config: self.exception_config, + sem_conv_config: self.sem_conv_config, }); extensions.insert(OtelData { builder, parent_cx }); } @@ -769,7 +936,7 @@ where if let Some(data) = extensions.get_mut::() { values.record(&mut SpanAttributeVisitor { span_builder: &mut data.builder, - exception_config: self.exception_config, + sem_conv_config: self.sem_conv_config, }); } } @@ -845,10 +1012,11 @@ where vec![Key::new("level").string(meta.level().as_str()), target], 0, ); + event.record(&mut SpanEventVisitor { event_builder: &mut otel_event, span_builder, - exception_config: self.exception_config, + sem_conv_config: self.sem_conv_config, }); if let Some(mut otel_data) = otel_data { @@ -1209,11 +1377,69 @@ mod tests { #[test] fn records_error_fields() { + let tracer = TestTracer(Arc::new(Mutex::new(None))); + let subscriber = tracing_subscriber::registry().with(layer().with_tracer(tracer.clone())); + + let err = TestDynError::new("base error") + .with_parent("intermediate error") + .with_parent("user error"); + + tracing::subscriber::with_default(subscriber, || { + tracing::debug_span!( + "request", + error = &err as &(dyn std::error::Error + 'static) + ); + }); + + let attributes = tracer + .0 + .lock() + .unwrap() + .as_ref() + .unwrap() + .builder + .attributes + .as_ref() + .unwrap() + .clone(); + + let key_values = attributes + .into_iter() + .map(|(key, value)| (key.as_str().to_owned(), value)) + .collect::>(); + + assert_eq!(key_values["error"].as_str(), "user error"); + assert_eq!( + key_values["error.chain"], + Value::Array( + vec![ + StringValue::from("intermediate error"), + StringValue::from("base error") + ] + .into() + ) + ); + + assert_eq!(key_values[FIELD_EXCEPTION_MESSAGE].as_str(), "user error"); + assert_eq!( + key_values[FIELD_EXCEPTION_STACKTRACE], + Value::Array( + vec![ + StringValue::from("intermediate error"), + StringValue::from("base error") + ] + .into() + ) + ); + } + + #[test] + fn records_no_error_fields() { let tracer = TestTracer(Arc::new(Mutex::new(None))); let subscriber = tracing_subscriber::registry().with( layer() - .with_tracer(tracer.clone()) - .with_exception_fields(true), + .with_error_records_to_exceptions(false) + .with_tracer(tracer.clone()), ); let err = TestDynError::new("base error") @@ -1355,11 +1581,59 @@ mod tests { #[test] fn propagates_error_fields_from_event_to_span() { + let tracer = TestTracer(Arc::new(Mutex::new(None))); + let subscriber = tracing_subscriber::registry().with(layer().with_tracer(tracer.clone())); + + let err = TestDynError::new("base error") + .with_parent("intermediate error") + .with_parent("user error"); + + tracing::subscriber::with_default(subscriber, || { + let _guard = tracing::debug_span!("request",).entered(); + + tracing::error!( + error = &err as &(dyn std::error::Error + 'static), + "request error!" + ) + }); + + let attributes = tracer + .0 + .lock() + .unwrap() + .as_ref() + .unwrap() + .builder + .attributes + .as_ref() + .unwrap() + .clone(); + + let key_values = attributes + .into_iter() + .map(|(key, value)| (key.as_str().to_owned(), value)) + .collect::>(); + + assert_eq!(key_values[FIELD_EXCEPTION_MESSAGE].as_str(), "user error"); + assert_eq!( + key_values[FIELD_EXCEPTION_STACKTRACE], + Value::Array( + vec![ + StringValue::from("intermediate error"), + StringValue::from("base error") + ] + .into() + ) + ); + } + + #[test] + fn propagates_no_error_fields_from_event_to_span() { let tracer = TestTracer(Arc::new(Mutex::new(None))); let subscriber = tracing_subscriber::registry().with( layer() - .with_tracer(tracer.clone()) - .with_exception_field_propagation(true), + .with_error_fields_to_exceptions(false) + .with_tracer(tracer.clone()), ); let err = TestDynError::new("base error") diff --git a/tests/errors.rs b/tests/errors.rs new file mode 100644 index 0000000..d1e92e1 --- /dev/null +++ b/tests/errors.rs @@ -0,0 +1,122 @@ +use futures_util::future::BoxFuture; +use opentelemetry::{ + sdk::{ + export::trace::{ExportResult, SpanData, SpanExporter}, + trace::{Tracer, TracerProvider}, + }, + trace::TracerProvider as _, +}; +use std::sync::{Arc, Mutex}; +use tracing::{instrument, Subscriber}; +use tracing_opentelemetry::layer; +use tracing_subscriber::prelude::*; + +#[test] +fn map_error_event_to_status_description() { + let (_tracer, provider, exporter, subscriber) = test_tracer(Some(false), None); + + #[instrument(err)] + fn test_fn() -> Result<(), &'static str> { + Err("test error") + } + + tracing::subscriber::with_default(subscriber, || { + let _ = test_fn(); + }); + + drop(provider); // flush all spans + + // Ensure the error event is mapped to the status description + let spans = exporter.0.lock().unwrap(); + let span = spans.iter().find(|s| s.name == "test_fn").unwrap(); + assert!(span.status == opentelemetry::trace::Status::error("test error")); +} + +#[test] +fn error_mapping_disabled() { + let (_tracer, provider, exporter, subscriber) = test_tracer(Some(false), Some(false)); + + #[instrument(err)] + fn test_fn() -> Result<(), &'static str> { + Err("test error") + } + + tracing::subscriber::with_default(subscriber, || { + let _ = test_fn(); + }); + + drop(provider); // flush all spans + + // Ensure the error event is not mapped to the status description + let spans = exporter.0.lock().unwrap(); + let span = spans.iter().find(|s| s.name == "test_fn").unwrap(); + assert!(span.status == opentelemetry::trace::Status::error("")); + + let exception_event = span.events.iter().any(|e| e.name == "exception"); + assert!(!exception_event); +} + +#[test] +fn transform_error_event_to_exception_event() { + let (_tracer, provider, exporter, subscriber) = test_tracer(None, Some(false)); + + #[instrument(err)] + fn test_fn() -> Result<(), &'static str> { + Err("test error") + } + + tracing::subscriber::with_default(subscriber, || { + let _ = test_fn(); + }); + + drop(provider); // flush all spans + + // Ensure that there is an exception event created and it contains our error. + let spans = exporter.0.lock().unwrap(); + let span = spans.iter().find(|s| s.name == "test_fn").unwrap(); + let exception_event = span.events.iter().find(|e| e.name == "exception").unwrap(); + let exception_attribute = exception_event + .attributes + .iter() + .find(|a| a.key.as_str() == "exception.message") + .unwrap(); + assert!(exception_attribute.value.as_str() == "test error"); +} + +fn test_tracer( + // Uses options to capture changes of the default behavior + error_event_exceptions: Option, + error_event_status: Option, +) -> (Tracer, TracerProvider, TestExporter, impl Subscriber) { + let exporter = TestExporter::default(); + let provider = TracerProvider::builder() + .with_simple_exporter(exporter.clone()) + .build(); + let tracer = provider.tracer("test"); + + let mut layer = layer().with_tracer(tracer.clone()); + if let Some(error_event_exceptions) = error_event_exceptions { + layer = layer.with_error_events_to_exceptions(error_event_exceptions) + } + if let Some(error_event_status) = error_event_status { + layer = layer.with_error_events_to_status(error_event_status) + } + let subscriber = tracing_subscriber::registry().with(layer); + + (tracer, provider, exporter, subscriber) +} + +#[derive(Clone, Default, Debug)] +struct TestExporter(Arc>>); + +impl SpanExporter for TestExporter { + fn export(&mut self, mut batch: Vec) -> BoxFuture<'static, ExportResult> { + let spans = self.0.clone(); + Box::pin(async move { + if let Ok(mut inner) = spans.lock() { + inner.append(&mut batch); + } + Ok(()) + }) + } +}