From a1936d24c122b3ddef1a609c32cc9f0b0cae2838 Mon Sep 17 00:00:00 2001 From: Richard Janis Goldschmidt Date: Tue, 21 Apr 2026 17:00:10 +0200 Subject: [PATCH 1/5] feat: propagate span name to logs changelog --- opentelemetry-appender-tracing/CHANGELOG.md | 2 + opentelemetry-appender-tracing/src/layer.rs | 97 ++++++++++++++++++--- 2 files changed, 89 insertions(+), 10 deletions(-) diff --git a/opentelemetry-appender-tracing/CHANGELOG.md b/opentelemetry-appender-tracing/CHANGELOG.md index fe4e67acd7..0123a4bcfe 100644 --- a/opentelemetry-appender-tracing/CHANGELOG.md +++ b/opentelemetry-appender-tracing/CHANGELOG.md @@ -2,6 +2,8 @@ ## vNext +- Propagate tracing span names to log records. + - New *experimental* feature to enrich log records with attributes from active tracing spans (`experimental_span_attributes`). Use `OpenTelemetryTracingBridge::builder()` with `with_span_attribute_allowlist` diff --git a/opentelemetry-appender-tracing/src/layer.rs b/opentelemetry-appender-tracing/src/layer.rs index ac3084a61b..d35c19d8dc 100644 --- a/opentelemetry-appender-tracing/src/layer.rs +++ b/opentelemetry-appender-tracing/src/layer.rs @@ -303,6 +303,7 @@ impl tracing::field::Visit for SpanFieldVisitor<'_> { #[cfg(feature = "experimental_span_attributes")] #[derive(Debug)] struct StoredSpanAttributes { + span_name: &'static str, attributes: Vec<(Key, AnyValue)>, } @@ -414,15 +415,21 @@ where { // 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 + let mut current_span_name: Option<&'static str> = None; for span_ref in scope.from_root() { // Access extensions inline - each span has its own extension lock let extensions = span_ref.extensions(); if let Some(stored) = extensions.get::() { + current_span_name = Some(stored.span_name); for (key, value) in stored.attributes.iter() { log_record.add_attribute(key.clone(), value.clone()); } } } + if let Some(name) = current_span_name { + log_record.add_attribute(Key::new("span.name"), AnyValue::from(name)); + } } } @@ -443,6 +450,7 @@ where ctx: tracing_subscriber::layer::Context<'_, S>, ) { let span = ctx.span(id).expect("Span not found; this is a bug"); + let span_name = attrs.metadata().name(); let mut fields = Vec::with_capacity(attrs.fields().len()); let mut visitor = SpanFieldVisitor { attributes: &mut fields, @@ -450,13 +458,13 @@ where }; attrs.record(&mut visitor); - // Only store if we actually found attributes to avoid empty allocations - if !fields.is_empty() { - let stored = StoredSpanAttributes { attributes: fields }; + let stored = StoredSpanAttributes { + span_name, + attributes: fields, + }; - let mut extensions = span.extensions_mut(); - extensions.insert(stored); - } + let mut extensions = span.extensions_mut(); + extensions.insert(stored); } #[cfg(feature = "experimental_span_attributes")] @@ -478,16 +486,18 @@ where values.record(&mut visitor); } else { // No existing attributes, create new storage + let span_name = span.metadata().name(); let mut fields = Vec::with_capacity(values.len()); let mut visitor = SpanFieldVisitor { attributes: &mut fields, allowlist: self.span_attribute_allowlist.as_ref(), }; values.record(&mut visitor); - if !fields.is_empty() { - let stored = StoredSpanAttributes { attributes: fields }; - extensions.insert(stored); - } + let stored = StoredSpanAttributes { + span_name, + attributes: fields, + }; + extensions.insert(stored); } } } @@ -1520,4 +1530,71 @@ mod tests { .attributes_iter() .any(|(k, _)| k == &Key::new("ignored"))); } + + #[test] + #[cfg(feature = "experimental_span_attributes")] + fn tracing_appender_propagates_span_name() { + let exporter = InMemoryLogExporter::default(); + let provider = SdkLoggerProvider::builder() + .with_simple_exporter(exporter.clone()) + .build(); + + let layer = layer::OpenTelemetryTracingBridge::new(&provider).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] + #[cfg(feature = "experimental_span_attributes")] + 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::new(&provider).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]; + + // The span.name should be the current (leaf/innermost) span + assert!(attributes_contains( + &log.record, + &Key::new("span.name"), + &AnyValue::String("inner_span".into()) + )); + } } From 0f7f1af0bb93eebda9c05f5f5a626ceb64dbf149 Mon Sep 17 00:00:00 2001 From: Richard Janis Goldschmidt Date: Mon, 4 May 2026 11:49:00 +0200 Subject: [PATCH 2/5] users toggle span name injection by providing a span name --- opentelemetry-appender-tracing/CHANGELOG.md | 5 +- opentelemetry-appender-tracing/src/layer.rs | 112 ++++++++++++++++++-- 2 files changed, 106 insertions(+), 11 deletions(-) diff --git a/opentelemetry-appender-tracing/CHANGELOG.md b/opentelemetry-appender-tracing/CHANGELOG.md index 0123a4bcfe..e834a17314 100644 --- a/opentelemetry-appender-tracing/CHANGELOG.md +++ b/opentelemetry-appender-tracing/CHANGELOG.md @@ -2,7 +2,10 @@ ## vNext -- Propagate tracing span names to log records. +- Propagate tracing span names to log records under a user-configured + attribute key. Use `OpenTelemetryTracingBridge::builder()` with + `with_span_name()` to enable propagation and choose the attribute key + (e.g., `"span.name"`); when not set, the span name is not propagated. - New *experimental* feature to enrich log records with attributes from active tracing spans (`experimental_span_attributes`). Use diff --git a/opentelemetry-appender-tracing/src/layer.rs b/opentelemetry-appender-tracing/src/layer.rs index d35c19d8dc..614d592528 100644 --- a/opentelemetry-appender-tracing/src/layer.rs +++ b/opentelemetry-appender-tracing/src/layer.rs @@ -316,6 +316,8 @@ where _phantom: std::marker::PhantomData

, // P is not used. #[cfg(feature = "experimental_span_attributes")] span_attribute_allowlist: Option>>, + #[cfg(feature = "experimental_span_attributes")] + span_name_key: Option, } impl OpenTelemetryTracingBridge @@ -338,6 +340,8 @@ where _phantom: Default::default(), #[cfg(feature = "experimental_span_attributes")] span_attribute_allowlist: None, + #[cfg(feature = "experimental_span_attributes")] + span_name_key: None, } } } @@ -351,6 +355,8 @@ where _phantom: std::marker::PhantomData

, #[cfg(feature = "experimental_span_attributes")] span_attribute_allowlist: Option>>, + #[cfg(feature = "experimental_span_attributes")] + span_name_key: Option, } impl OpenTelemetryTracingBridgeBuilder @@ -369,6 +375,15 @@ 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. + #[cfg(feature = "experimental_span_attributes")] + pub fn with_span_name(mut self, key: impl Into) -> Self { + self.span_name_key = Some(key.into()); + self + } + pub fn build(self) -> OpenTelemetryTracingBridge { OpenTelemetryTracingBridge { logger: self.logger, @@ -376,6 +391,8 @@ where #[cfg(feature = "experimental_span_attributes")] // Treat empty allowlist as not set - disable the feature flag instead. span_attribute_allowlist: self.span_attribute_allowlist.filter(|s| !s.is_empty()), + #[cfg(feature = "experimental_span_attributes")] + span_name_key: self.span_name_key, } } } @@ -416,6 +433,9 @@ where // 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() { // Access extensions inline - each span has its own extension lock @@ -427,8 +447,8 @@ where } } } - if let Some(name) = current_span_name { - log_record.add_attribute(Key::new("span.name"), AnyValue::from(name)); + if let (Some(key), Some(name)) = (self.span_name_key.as_ref(), current_span_name) { + log_record.add_attribute(key.clone(), AnyValue::from(name)); } } } @@ -1539,11 +1559,12 @@ mod tests { .with_simple_exporter(exporter.clone()) .build(); - let layer = layer::OpenTelemetryTracingBridge::new(&provider).with_filter( - tracing_subscriber::filter::filter_fn(|meta| { + let layer = layer::OpenTelemetryTracingBridge::builder(&provider) + .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); @@ -1571,11 +1592,12 @@ mod tests { .with_simple_exporter(exporter.clone()) .build(); - let layer = layer::OpenTelemetryTracingBridge::new(&provider).with_filter( - tracing_subscriber::filter::filter_fn(|meta| { + let layer = layer::OpenTelemetryTracingBridge::builder(&provider) + .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); @@ -1597,4 +1619,74 @@ mod tests { &AnyValue::String("inner_span".into()) )); } + + #[test] + #[cfg(feature = "experimental_span_attributes")] + 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_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] + #[cfg(feature = "experimental_span_attributes")] + 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::new(&provider).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"))); + } } From 85941a7f866c67c0c32afe440e0fac765b6baa88 Mon Sep 17 00:00:00 2001 From: Richard Janis Goldschmidt Date: Mon, 4 May 2026 11:57:40 +0200 Subject: [PATCH 3/5] treat the span name as an attribute --- opentelemetry-appender-tracing/src/layer.rs | 26 +++++++++------------ 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/opentelemetry-appender-tracing/src/layer.rs b/opentelemetry-appender-tracing/src/layer.rs index 614d592528..972c23e4e6 100644 --- a/opentelemetry-appender-tracing/src/layer.rs +++ b/opentelemetry-appender-tracing/src/layer.rs @@ -303,7 +303,6 @@ impl tracing::field::Visit for SpanFieldVisitor<'_> { #[cfg(feature = "experimental_span_attributes")] #[derive(Debug)] struct StoredSpanAttributes { - span_name: &'static str, attributes: Vec<(Key, AnyValue)>, } @@ -438,10 +437,10 @@ where // 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::() { - current_span_name = Some(stored.span_name); for (key, value) in stored.attributes.iter() { log_record.add_attribute(key.clone(), value.clone()); } @@ -470,7 +469,6 @@ where ctx: tracing_subscriber::layer::Context<'_, S>, ) { let span = ctx.span(id).expect("Span not found; this is a bug"); - let span_name = attrs.metadata().name(); let mut fields = Vec::with_capacity(attrs.fields().len()); let mut visitor = SpanFieldVisitor { attributes: &mut fields, @@ -478,13 +476,13 @@ where }; attrs.record(&mut visitor); - let stored = StoredSpanAttributes { - span_name, - attributes: fields, - }; + // Only store if we actually found attributes to avoid empty allocations + if !fields.is_empty() { + let stored = StoredSpanAttributes { attributes: fields }; - let mut extensions = span.extensions_mut(); - extensions.insert(stored); + let mut extensions = span.extensions_mut(); + extensions.insert(stored); + } } #[cfg(feature = "experimental_span_attributes")] @@ -506,18 +504,16 @@ where values.record(&mut visitor); } else { // No existing attributes, create new storage - let span_name = span.metadata().name(); let mut fields = Vec::with_capacity(values.len()); let mut visitor = SpanFieldVisitor { attributes: &mut fields, allowlist: self.span_attribute_allowlist.as_ref(), }; values.record(&mut visitor); - let stored = StoredSpanAttributes { - span_name, - attributes: fields, - }; - extensions.insert(stored); + if !fields.is_empty() { + let stored = StoredSpanAttributes { attributes: fields }; + extensions.insert(stored); + } } } } From 9129343da3b8305c736a965d82942c0b8cbce7c4 Mon Sep 17 00:00:00 2001 From: Richard Janis Goldschmidt Date: Thu, 7 May 2026 15:42:20 +0200 Subject: [PATCH 4/5] Only use &'static str when setting span names --- opentelemetry-appender-tracing/src/layer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-appender-tracing/src/layer.rs b/opentelemetry-appender-tracing/src/layer.rs index 972c23e4e6..6970f40c82 100644 --- a/opentelemetry-appender-tracing/src/layer.rs +++ b/opentelemetry-appender-tracing/src/layer.rs @@ -378,8 +378,8 @@ where /// attribute key. Setting this enables the propagation; leaving it unset /// (the default) disables it. #[cfg(feature = "experimental_span_attributes")] - pub fn with_span_name(mut self, key: impl Into) -> Self { - self.span_name_key = Some(key.into()); + pub fn with_span_name(mut self, name: &'static str) -> Self { + self.span_name_key = Some(Key::from_static_str(name)); self } From b1253218e65f74d6e189537e9e3faeaf0f1022fc Mon Sep 17 00:00:00 2001 From: Richard Janis Goldschmidt Date: Thu, 7 May 2026 16:07:11 +0200 Subject: [PATCH 5/5] enable span attribute enrichment --- opentelemetry-appender-tracing/src/layer.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/opentelemetry-appender-tracing/src/layer.rs b/opentelemetry-appender-tracing/src/layer.rs index 5f758f680b..0e0eaa111d 100644 --- a/opentelemetry-appender-tracing/src/layer.rs +++ b/opentelemetry-appender-tracing/src/layer.rs @@ -424,6 +424,8 @@ where /// 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. pub fn with_span_name(mut self, name: &'static str) -> Self { self.span_name = Some(Key::from_static_str(name)); self @@ -1653,6 +1655,7 @@ mod tests { .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| { @@ -1685,6 +1688,7 @@ mod tests { .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| { @@ -1704,6 +1708,8 @@ mod tests { assert_eq!(logs.len(), 1); let log = &logs[0]; + println!("{:?}", log.record); + // The span.name should be the current (leaf/innermost) span assert!(attributes_contains( &log.record, @@ -1720,6 +1726,7 @@ mod tests { .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| { @@ -1756,11 +1763,12 @@ mod tests { .with_simple_exporter(exporter.clone()) .build(); - let layer = layer::OpenTelemetryTracingBridge::new(&provider).with_filter( - tracing_subscriber::filter::filter_fn(|meta| { + 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);