Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions opentelemetry-appender-tracing/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## vNext

- Propagate tracing span names to log records under a user-configured
attribute key. Use `OpenTelemetryTracingBridge::builder()` with
`with_span_name(<key>)` to enable propagation and choose the attribute key
(e.g., `"span.name"`); when not set, the span name is not propagated.

- **Add tracing span attribute enrichment.** When enabled, attributes attached
to active [`tracing`] spans are copied onto each emitted log record. "Span"
here refers to a [`tracing::span!`][tracing-span] from the [`tracing`] crate
Expand Down
164 changes: 164 additions & 0 deletions opentelemetry-appender-tracing/src/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ where
// - `Some(All)` => copy all tracing-span attributes onto log records.
// - `Some(Allowlist(set))` => copy only attributes whose keys are in `set`.
span_attributes: Option<TracingSpanAttributesInner>,
span_name: Option<Key>,
}

impl<P, L> OpenTelemetryTracingBridge<P, L>
Expand All @@ -378,6 +379,7 @@ where
logger: provider.logger(""),
_phantom: Default::default(),
span_attributes: None,
span_name: None,
}
}
}
Expand All @@ -390,6 +392,7 @@ where
logger: L,
_phantom: std::marker::PhantomData<P>,
span_attributes: Option<TracingSpanAttributesInner>,
span_name: Option<Key>,
}

impl<P, L> OpenTelemetryTracingBridgeBuilder<P, L>
Expand Down Expand Up @@ -418,11 +421,22 @@ where
self
}

/// Propagate the current (leaf) span's name to log records using the given
/// attribute key. Setting this enables the propagation; leaving it unset
/// (the default) disables it.
///
/// This does nothing if `Self::with_tracing_span_attributes` is not set.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interaction with TracingSpanAttributes is odd now.

Should we scrap with_span_name in favor of extending TracingSpanAttributes (kinda like OpenOptions)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SuperFluffy This requires some discussion to make sure we do a holistic approach. Given we are doing a release today, I reverted and kept the attribute enrichment gated behind feature flag (#3505).

I'll proceed with the release today, and after that, will get back to this. (Sorry we'll miss span-name enrichment in this release, but we can do ad-hoc release of this crate soon, so we don't have to wait for next full release).

pub fn with_span_name(mut self, name: &'static str) -> Self {
self.span_name = Some(Key::from_static_str(name));
self
}

pub fn build(self) -> OpenTelemetryTracingBridge<P, L> {
OpenTelemetryTracingBridge {
logger: self.logger,
_phantom: self._phantom,
span_attributes: self.span_attributes,
span_name: self.span_name,
}
}
}
Expand Down Expand Up @@ -460,7 +474,13 @@ where
if self.span_attributes.is_some() {
// Collect attributes from all parent spans (root to leaf), including current span
if let Some(scope) = ctx.event_scope(event) {
// Track the current (leaf) span's name to propagate it
// TODO: consider alternative (user configurable?) strategies
// to only tracking the leaf span. For example, providing a list
// of all branches/parent spans down to the child.
let mut current_span_name: Option<&'static str> = None;
for span_ref in scope.from_root() {
current_span_name = Some(span_ref.metadata().name());
// Access extensions inline - each span has its own extension lock
let extensions = span_ref.extensions();
if let Some(stored) = extensions.get::<StoredSpanAttributes>() {
Expand All @@ -469,6 +489,9 @@ where
}
}
}
if let (Some(key), Some(val)) = (self.span_name.as_ref(), current_span_name) {
log_record.add_attribute(key.clone(), AnyValue::from(val));
}
}
}

Expand Down Expand Up @@ -1624,6 +1647,147 @@ mod tests {
.any(|(k, _)| k == &Key::new("ignored")));
}

#[test]
fn tracing_appender_propagates_span_name() {
let exporter = InMemoryLogExporter::default();
let provider = SdkLoggerProvider::builder()
.with_simple_exporter(exporter.clone())
.build();

let layer = layer::OpenTelemetryTracingBridge::builder(&provider)
.with_tracing_span_attributes(TracingSpanAttributes::all())
.with_span_name("span.name")
.build()
.with_filter(tracing_subscriber::filter::filter_fn(|meta| {
meta.is_span() || *meta.level() <= tracing::Level::ERROR
}));
let subscriber = tracing_subscriber::registry().with(layer);
let _guard = tracing::subscriber::set_default(subscriber);

let span = tracing::info_span!("my_span_name", some_field = "value");
let _enter = span.enter();
tracing::error!("test message");

provider.force_flush().unwrap();
let logs = exporter.get_emitted_logs().unwrap();
assert_eq!(logs.len(), 1);
let log = &logs[0];

assert!(attributes_contains(
&log.record,
&Key::new("span.name"),
&AnyValue::String("my_span_name".into())
));
}

#[test]
fn tracing_appender_propagates_current_span_name_from_nested_spans() {
let exporter = InMemoryLogExporter::default();
let provider = SdkLoggerProvider::builder()
.with_simple_exporter(exporter.clone())
.build();

let layer = layer::OpenTelemetryTracingBridge::builder(&provider)
.with_tracing_span_attributes(TracingSpanAttributes::all())
.with_span_name("span.name")
.build()
.with_filter(tracing_subscriber::filter::filter_fn(|meta| {
meta.is_span() || *meta.level() <= tracing::Level::ERROR
}));
let subscriber = tracing_subscriber::registry().with(layer);
let _guard = tracing::subscriber::set_default(subscriber);

let outer = tracing::info_span!("outer_span");
let _outer_guard = outer.enter();
let inner = tracing::info_span!("inner_span");
let _inner_guard = inner.enter();
tracing::error!("test message");

provider.force_flush().unwrap();
let logs = exporter.get_emitted_logs().unwrap();
assert_eq!(logs.len(), 1);
let log = &logs[0];

println!("{:?}", log.record);

// The span.name should be the current (leaf/innermost) span
Comment thread
cijothomas marked this conversation as resolved.
assert!(attributes_contains(
&log.record,
&Key::new("span.name"),
&AnyValue::String("inner_span".into())
));
}

#[test]
fn tracing_appender_span_name_uses_configured_key() {
let exporter = InMemoryLogExporter::default();
let provider = SdkLoggerProvider::builder()
.with_simple_exporter(exporter.clone())
.build();

let layer = layer::OpenTelemetryTracingBridge::builder(&provider)
.with_tracing_span_attributes(TracingSpanAttributes::all())
.with_span_name("custom.span_name")
.build()
.with_filter(tracing_subscriber::filter::filter_fn(|meta| {
meta.is_span() || *meta.level() <= tracing::Level::ERROR
}));
let subscriber = tracing_subscriber::registry().with(layer);
let _guard = tracing::subscriber::set_default(subscriber);

let span = tracing::info_span!("my_span_name");
let _enter = span.enter();
tracing::error!("test message");

provider.force_flush().unwrap();
let logs = exporter.get_emitted_logs().unwrap();
assert_eq!(logs.len(), 1);
let log = &logs[0];

assert!(attributes_contains(
&log.record,
&Key::new("custom.span_name"),
&AnyValue::String("my_span_name".into())
));
// The default `span.name` key should not be present.
assert!(!log
.record
.attributes_iter()
.any(|(k, _)| k == &Key::new("span.name")));
}

#[test]
fn tracing_appender_does_not_propagate_span_name_by_default() {
let exporter = InMemoryLogExporter::default();
let provider = SdkLoggerProvider::builder()
.with_simple_exporter(exporter.clone())
.build();

let layer = layer::OpenTelemetryTracingBridge::builder(&provider)
.with_tracing_span_attributes(TracingSpanAttributes::all())
.build()
.with_filter(tracing_subscriber::filter::filter_fn(|meta| {
meta.is_span() || *meta.level() <= tracing::Level::ERROR
}));
let subscriber = tracing_subscriber::registry().with(layer);
let _guard = tracing::subscriber::set_default(subscriber);

let span = tracing::info_span!("my_span_name");
let _enter = span.enter();
tracing::error!("test message");

provider.force_flush().unwrap();
let logs = exporter.get_emitted_logs().unwrap();
assert_eq!(logs.len(), 1);
let log = &logs[0];

// No span name attribute should be present without explicit opt-in.
assert!(!log
.record
.attributes_iter()
.any(|(k, _)| k == &Key::new("span.name")));
}

#[test]
fn tracing_appender_empty_allowlist_copies_nothing() {
// An empty allowlist means "allow nothing" — enrichment is enabled
Expand Down
Loading