Skip to content

Commit

Permalink
fixup! opentelemetry: add semconv exception fields
Browse files Browse the repository at this point in the history
  • Loading branch information
lilymara-onesignal committed Jun 9, 2022
1 parent e3e0b1e commit ebae7af
Showing 1 changed file with 207 additions and 48 deletions.
255 changes: 207 additions & 48 deletions tracing-opentelemetry/src/layer.rs
Expand Up @@ -30,6 +30,7 @@ pub struct OpenTelemetryLayer<S, T> {
tracer: T,
event_location: bool,
tracked_inactivity: bool,
exception_config: ExceptionFieldConfig,
get_context: WithContext,
_registry: marker::PhantomData<S>,
}
Expand Down Expand Up @@ -107,9 +108,13 @@ fn str_to_status_code(s: &str) -> Option<otel::StatusCode> {
}
}

struct SpanEventVisitor<'a>(&'a mut otel::Event);
struct SpanEventVisitor<'a, 'b>(
&'a mut otel::Event,
Option<&'b mut otel::SpanBuilder>,
ExceptionFieldConfig,
);

impl<'a> field::Visit for SpanEventVisitor<'a> {
impl<'a, 'b> field::Visit for SpanEventVisitor<'a, 'b> {
/// Record events on the underlying OpenTelemetry [`Span`] from `bool` values.
///
/// [`Span`]: opentelemetry::trace::Span
Expand Down Expand Up @@ -189,9 +194,72 @@ impl<'a> field::Visit for SpanEventVisitor<'a> {
}
}
}

/// Set attributes on the underlying OpenTelemetry [`Span`] using a [`std::error::Error`]'s
/// [`std::fmt::Display`] implementation. Also adds the `source` chain as an extra field
///
/// [`Span`]: opentelemetry::trace::Span
fn record_error(
&mut self,
field: &tracing_core::Field,
value: &(dyn std::error::Error + 'static),
) {
let mut chain = Vec::new();
let mut next_err = value.source();

while let Some(err) = next_err {
chain.push(Cow::Owned(err.to_string()));
next_err = err.source();
}

self.0
.attributes
.push(Key::new(field.name()).string(value.to_string()));
self.0
.attributes
.push(Key::new(format!("{}.chain", field.name())).array(chain.clone()));

if self.2.record {
self.0
.attributes
.push(Key::new("exception.message").string(value.to_string()));

// NOTE: This is actually not the stacktrace of the exception. This is
// the "source chain". It represents the heirarchy of errors from the
// app level to the lowest level such as IO. It does not represent all
// of the callsites in the code that led to the error happening.
// `std::error::Error::backtrace` is a nightly-only API and cannot be
// used here until the feature is stabilized.
self.0
.attributes
.push(Key::new("exception.stacktrace").array(chain.clone()));
}

if self.2.propagate {
if let Some(span) = &mut self.1 {
if let Some(attrs) = span.attributes.as_mut() {
attrs.push(Key::new("exception.message").string(value.to_string()));

// NOTE: This is actually not the stacktrace of the exception. This is
// the "source chain". It represents the heirarchy of errors from the
// app level to the lowest level such as IO. It does not represent all
// of the callsites in the code that led to the error happening.
// `std::error::Error::backtrace` is a nightly-only API and cannot be
// used here until the feature is stabilized.
attrs.push(Key::new("exception.stacktrace").array(chain));
}
}
}
}
}

struct SpanAttributeVisitor<'a>(&'a mut otel::SpanBuilder);
#[derive(Clone, Copy)]
struct ExceptionFieldConfig {
record: bool,
propagate: bool,
}

struct SpanAttributeVisitor<'a>(&'a mut otel::SpanBuilder, ExceptionFieldConfig);

impl<'a> SpanAttributeVisitor<'a> {
fn record(&mut self, attribute: KeyValue) {
Expand Down Expand Up @@ -275,15 +343,17 @@ impl<'a> field::Visit for SpanAttributeVisitor<'a> {
self.record(Key::new(field.name()).string(value.to_string()));
self.record(Key::new(format!("{}.chain", field.name())).array(chain.clone()));

self.record(Key::new("exception.message").string(value.to_string()));
if self.1.record {
self.record(Key::new("exception.message").string(value.to_string()));

// NOTE: This is actually not the stacktrace of the exception. This is
// the "source chain". It represents the heirarchy of errors from the
// app level to the lowest level such as IO. It does not represent all
// of the callsites in the code that led to the error happening.
// `std::error::Error::backtrace` is a nightly-only API and cannot be
// used here until the feature is stabilized.
self.record(Key::new("exception.stacktrace").array(chain));
// NOTE: This is actually not the stacktrace of the exception. This is
// the "source chain". It represents the heirarchy of errors from the
// app level to the lowest level such as IO. It does not represent all
// of the callsites in the code that led to the error happening.
// `std::error::Error::backtrace` is a nightly-only API and cannot be
// used here until the feature is stabilized.
self.record(Key::new("exception.stacktrace").array(chain));
}
}
}

Expand Down Expand Up @@ -324,6 +394,10 @@ where
tracer,
event_location: true,
tracked_inactivity: true,
exception_config: ExceptionFieldConfig {
record: true,
propagate: true,
},
get_context: WithContext(Self::get_context),
_registry: marker::PhantomData,
}
Expand Down Expand Up @@ -363,11 +437,32 @@ where
tracer,
event_location: self.event_location,
tracked_inactivity: self.tracked_inactivity,
exception_config: self.exception_config,
get_context: WithContext(OpenTelemetryLayer::<S, Tracer>::get_context),
_registry: self._registry,
}
}

pub fn with_exception_fields(self, exception_fields: bool) -> Self {
Self {
exception_config: ExceptionFieldConfig {
record: exception_fields,
..self.exception_config
},
..self
}
}

pub fn with_exception_field_propagation(self, exception_field_propagation: bool) -> Self {
Self {
exception_config: ExceptionFieldConfig {
propagate: exception_field_propagation,
..self.exception_config
},
..self
}
}

/// Sets whether or not event span's metadata should include detailed location
/// information, such as the file, module and line number.
///
Expand Down Expand Up @@ -491,7 +586,10 @@ where
builder_attrs.push(KeyValue::new("code.lineno", line as i64));
}

attrs.record(&mut SpanAttributeVisitor(&mut builder));
attrs.record(&mut SpanAttributeVisitor(
&mut builder,
self.exception_config,
));
extensions.insert(OtelData { builder, parent_cx });
}

Expand Down Expand Up @@ -532,7 +630,10 @@ where
let span = ctx.span(id).expect("Span not found, this is a bug");
let mut extensions = span.extensions_mut();
if let Some(data) = extensions.get_mut::<OtelData>() {
values.record(&mut SpanAttributeVisitor(&mut data.builder));
values.record(&mut SpanAttributeVisitor(
&mut data.builder,
self.exception_config,
));
}
}

Expand Down Expand Up @@ -597,15 +698,23 @@ where
#[cfg(not(feature = "tracing-log"))]
let target = target.string(meta.target());

let mut extensions = span.extensions_mut();
let span_builder = extensions
.get_mut::<OtelData>()
.map(|data| &mut data.builder);

let mut otel_event = otel::Event::new(
String::new(),
SystemTime::now(),
vec![Key::new("level").string(meta.level().as_str()), target],
0,
);
event.record(&mut SpanEventVisitor(&mut otel_event));
event.record(&mut SpanEventVisitor(
&mut otel_event,
span_builder,
self.exception_config,
));

let mut extensions = span.extensions_mut();
if let Some(OtelData { builder, .. }) = extensions.get_mut::<OtelData>() {
if builder.status_code.is_none() && *meta.level() == tracing_core::Level::ERROR {
builder.status_code = Some(otel::StatusCode::Error);
Expand Down Expand Up @@ -717,6 +826,8 @@ mod tests {
use opentelemetry::trace::{noop, SpanKind, TraceFlags};
use std::borrow::Cow;
use std::collections::HashMap;
use std::error::Error;
use std::fmt::Display;
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use tracing_subscriber::prelude::*;
Expand Down Expand Up @@ -784,6 +895,36 @@ mod tests {
fn end_with_timestamp(&mut self, _timestamp: SystemTime) {}
}

#[derive(Debug)]
struct TestDynError {
msg: &'static str,
source: Option<Box<TestDynError>>,
}
impl Display for TestDynError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.msg)
}
}
impl Error for TestDynError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match &self.source {
Some(source) => Some(source),
None => None,
}
}
}
impl TestDynError {
fn new(msg: &'static str) -> Self {
Self { msg, source: None }
}
fn with_parent(self, parent_msg: &'static str) -> Self {
Self {
msg: parent_msg,
source: Some(Box::new(self)),
}
}
}

#[test]
fn dynamic_span_names() {
let dynamic_name = "GET http://example.com".to_string();
Expand Down Expand Up @@ -935,39 +1076,9 @@ mod tests {
let tracer = TestTracer(Arc::new(Mutex::new(None)));
let subscriber = tracing_subscriber::registry().with(layer().with_tracer(tracer.clone()));

use std::error::Error;
use std::fmt::Display;

#[derive(Debug)]
struct DynError {
msg: &'static str,
source: Option<Box<DynError>>,
}

impl Display for DynError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.msg)
}
}
impl Error for DynError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match &self.source {
Some(source) => Some(source),
None => None,
}
}
}

let err = DynError {
msg: "user error",
source: Some(Box::new(DynError {
msg: "intermediate error",
source: Some(Box::new(DynError {
msg: "base error",
source: None,
})),
})),
};
let err = TestDynError::new("base error")
.with_parent("intermediate error")
.with_parent("user error");

tracing::subscriber::with_default(subscriber, || {
tracing::debug_span!(
Expand Down Expand Up @@ -1017,4 +1128,52 @@ 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(|attr| (attr.key.as_str().to_owned(), attr.value))
.collect::<HashMap<_, _>>();

assert_eq!(key_values["exception.message"].as_str(), "user error");
assert_eq!(
key_values["exception.stacktrace"],
Value::Array(
vec![
Cow::Borrowed("intermediate error"),
Cow::Borrowed("base error")
]
.into()
)
);
}
}

0 comments on commit ebae7af

Please sign in to comment.