Skip to content

Commit 2073c8b

Browse files
committed
feat(appende-tracing): propagate span name to logs
1 parent 1b3846c commit 2073c8b

3 files changed

Lines changed: 255 additions & 16 deletions

File tree

opentelemetry-appender-tracing/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## vNext
44

5+
- Add optional tracing span name enrichment under the
6+
`experimental_span_attributes` feature. Enable with
7+
`OpenTelemetryTracingBridge::builder().with_tracing_span_name(<attribute-key>)`
8+
by setting a user-chosen attribute key for the span name.
9+
510
## 0.32.0
611

712
Released 2026-May-08

opentelemetry-appender-tracing/src/layer.rs

Lines changed: 226 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,11 @@ where
368368
// - `Some(Allowlist(set))` => copy only attributes whose keys are in `set`.
369369
#[cfg(feature = "experimental_span_attributes")]
370370
span_attributes: Option<TracingSpanAttributesInner>,
371+
// Tracing-span name enrichment configuration:
372+
// - `None` => disabled (default). No scope walk unless span attributes are enabled.
373+
// - `Some(key)` => copy the current tracing span name using `key`.
374+
#[cfg(feature = "experimental_span_attributes")]
375+
span_name: Option<Key>,
371376
}
372377

373378
impl<P, L> OpenTelemetryTracingBridge<P, L>
@@ -390,6 +395,8 @@ where
390395
_phantom: Default::default(),
391396
#[cfg(feature = "experimental_span_attributes")]
392397
span_attributes: None,
398+
#[cfg(feature = "experimental_span_attributes")]
399+
span_name: None,
393400
}
394401
}
395402
}
@@ -403,6 +410,8 @@ where
403410
_phantom: std::marker::PhantomData<P>,
404411
#[cfg(feature = "experimental_span_attributes")]
405412
span_attributes: Option<TracingSpanAttributesInner>,
413+
#[cfg(feature = "experimental_span_attributes")]
414+
span_name: Option<Key>,
406415
}
407416

408417
impl<P, L> OpenTelemetryTracingBridgeBuilder<P, L>
@@ -432,12 +441,37 @@ where
432441
self
433442
}
434443

444+
/// Store the current tracing span's name on each emitted log record using
445+
/// the given log attribute key.
446+
///
447+
/// "Current" means the leaf/innermost [`tracing::span!`] active when the
448+
/// event is emitted. `span_name` is the log attribute key where that
449+
/// tracing span name will be stored, with the span name being the attribute
450+
/// value.
451+
///
452+
/// This option is independent from [`Self::with_tracing_span_attributes`]:
453+
/// callers can enable tracing-span attributes only, tracing-span name only,
454+
/// or both. By default, span-name enrichment is disabled and no span-name
455+
/// attribute is added.
456+
///
457+
/// Calling this method multiple times replaces any prior span-name key -
458+
/// the last call wins.
459+
///
460+
/// [`tracing::span!`]: https://docs.rs/tracing/latest/tracing/macro.span.html
461+
#[cfg(feature = "experimental_span_attributes")]
462+
pub fn with_tracing_span_name(mut self, span_name: &'static str) -> Self {
463+
self.span_name = Some(Key::from_static_str(span_name));
464+
self
465+
}
466+
435467
pub fn build(self) -> OpenTelemetryTracingBridge<P, L> {
436468
OpenTelemetryTracingBridge {
437469
logger: self.logger,
438470
_phantom: self._phantom,
439471
#[cfg(feature = "experimental_span_attributes")]
440472
span_attributes: self.span_attributes,
473+
#[cfg(feature = "experimental_span_attributes")]
474+
span_name: self.span_name,
441475
}
442476
}
443477
}
@@ -476,20 +510,31 @@ where
476510
log_record.set_severity_number(severity);
477511
log_record.set_severity_text(metadata.level().as_str());
478512

479-
// Extract tracing-span attributes if enrichment is enabled.
513+
// Extract tracing-span enrichment if enabled.
480514
#[cfg(feature = "experimental_span_attributes")]
481-
if self.span_attributes.is_some() {
515+
if self.span_attributes.is_some() || self.span_name.is_some() {
482516
// Collect attributes from all parent spans (root to leaf), including current span
483517
if let Some(scope) = ctx.event_scope(event) {
518+
let mut current_span_name = None;
484519
for span_ref in scope.from_root() {
485-
// Access extensions inline - each span has its own extension lock
486-
let extensions = span_ref.extensions();
487-
if let Some(stored) = extensions.get::<StoredSpanAttributes>() {
488-
for (key, value) in stored.attributes.iter() {
489-
log_record.add_attribute(key.clone(), value.clone());
520+
if self.span_name.is_some() {
521+
current_span_name = Some(span_ref.metadata().name());
522+
}
523+
524+
if self.span_attributes.is_some() {
525+
// Access extensions inline - each span has its own extension lock
526+
let extensions = span_ref.extensions();
527+
if let Some(stored) = extensions.get::<StoredSpanAttributes>() {
528+
for (key, value) in stored.attributes.iter() {
529+
log_record.add_attribute(key.clone(), value.clone());
530+
}
490531
}
491532
}
492533
}
534+
535+
if let (Some(key), Some(value)) = (self.span_name.as_ref(), current_span_name) {
536+
log_record.add_attribute(key.clone(), AnyValue::from(value));
537+
}
493538
}
494539
}
495540

@@ -1199,6 +1244,180 @@ mod tests {
11991244
.any(|(k, _)| k == &Key::new("endpoint")));
12001245
}
12011246

1247+
#[test]
1248+
#[cfg(feature = "experimental_span_attributes")]
1249+
fn tracing_appender_span_name_disabled_by_default() {
1250+
let exporter = InMemoryLogExporter::default();
1251+
let provider = SdkLoggerProvider::builder()
1252+
.with_simple_exporter(exporter.clone())
1253+
.build();
1254+
1255+
let layer = layer::OpenTelemetryTracingBridge::builder(&provider)
1256+
.with_tracing_span_attributes(TracingSpanAttributes::all())
1257+
.build()
1258+
.with_filter(tracing_subscriber::filter::filter_fn(|meta| {
1259+
meta.is_span() || *meta.level() <= tracing::Level::ERROR
1260+
}));
1261+
let subscriber = tracing_subscriber::registry().with(layer);
1262+
let _guard = tracing::subscriber::set_default(subscriber);
1263+
1264+
let span = tracing::info_span!("test_span", user_id = 123);
1265+
let _enter = span.enter();
1266+
tracing::error!("test message");
1267+
1268+
provider.force_flush().unwrap();
1269+
let logs = exporter.get_emitted_logs().unwrap();
1270+
assert_eq!(logs.len(), 1);
1271+
let log = &logs[0];
1272+
1273+
assert!(attributes_contains(
1274+
&log.record,
1275+
&Key::new("user_id"),
1276+
&AnyValue::Int(123)
1277+
));
1278+
assert!(!log
1279+
.record
1280+
.attributes_iter()
1281+
.any(|(k, _)| k == &Key::new("span.name")));
1282+
}
1283+
1284+
#[test]
1285+
#[cfg(feature = "experimental_span_attributes")]
1286+
fn tracing_appender_span_name_enrichment_without_span_attributes() {
1287+
let exporter = InMemoryLogExporter::default();
1288+
let provider = SdkLoggerProvider::builder()
1289+
.with_simple_exporter(exporter.clone())
1290+
.build();
1291+
1292+
let layer = layer::OpenTelemetryTracingBridge::builder(&provider)
1293+
.with_tracing_span_name("span.name")
1294+
.build()
1295+
.with_filter(tracing_subscriber::filter::filter_fn(|meta| {
1296+
meta.is_span() || *meta.level() <= tracing::Level::ERROR
1297+
}));
1298+
let subscriber = tracing_subscriber::registry().with(layer);
1299+
let _guard = tracing::subscriber::set_default(subscriber);
1300+
1301+
let span = tracing::info_span!("test_span", user_id = 123);
1302+
let _enter = span.enter();
1303+
tracing::error!(status = 200, "test message");
1304+
1305+
provider.force_flush().unwrap();
1306+
let logs = exporter.get_emitted_logs().unwrap();
1307+
assert_eq!(logs.len(), 1);
1308+
let log = &logs[0];
1309+
1310+
assert!(attributes_contains(
1311+
&log.record,
1312+
&Key::new("span.name"),
1313+
&AnyValue::String("test_span".into())
1314+
));
1315+
assert!(attributes_contains(
1316+
&log.record,
1317+
&Key::new("status"),
1318+
&AnyValue::Int(200)
1319+
));
1320+
assert!(!log
1321+
.record
1322+
.attributes_iter()
1323+
.any(|(k, _)| k == &Key::new("user_id")));
1324+
}
1325+
1326+
#[test]
1327+
#[cfg(feature = "experimental_span_attributes")]
1328+
fn tracing_appender_span_name_uses_current_span_and_configured_key() {
1329+
let exporter = InMemoryLogExporter::default();
1330+
let provider = SdkLoggerProvider::builder()
1331+
.with_simple_exporter(exporter.clone())
1332+
.build();
1333+
1334+
let layer = layer::OpenTelemetryTracingBridge::builder(&provider)
1335+
.with_tracing_span_name("custom.span_name")
1336+
.build()
1337+
.with_filter(tracing_subscriber::filter::filter_fn(|meta| {
1338+
meta.is_span() || *meta.level() <= tracing::Level::ERROR
1339+
}));
1340+
let subscriber = tracing_subscriber::registry().with(layer);
1341+
let _guard = tracing::subscriber::set_default(subscriber);
1342+
1343+
let outer = tracing::info_span!("outer_span");
1344+
let _outer_guard = outer.enter();
1345+
let inner = tracing::info_span!("inner_span");
1346+
let _inner_guard = inner.enter();
1347+
tracing::error!("test message");
1348+
1349+
provider.force_flush().unwrap();
1350+
let logs = exporter.get_emitted_logs().unwrap();
1351+
assert_eq!(logs.len(), 1);
1352+
let log = &logs[0];
1353+
1354+
assert!(attributes_contains(
1355+
&log.record,
1356+
&Key::new("custom.span_name"),
1357+
&AnyValue::String("inner_span".into())
1358+
));
1359+
assert!(!log
1360+
.record
1361+
.attributes_iter()
1362+
.any(|(k, _)| k == &Key::new("span.name")));
1363+
assert!(!attributes_contains(
1364+
&log.record,
1365+
&Key::new("custom.span_name"),
1366+
&AnyValue::String("outer_span".into())
1367+
));
1368+
}
1369+
1370+
#[test]
1371+
#[cfg(feature = "experimental_span_attributes")]
1372+
fn tracing_appender_span_name_and_attributes_can_both_be_enabled() {
1373+
let exporter = InMemoryLogExporter::default();
1374+
let provider = SdkLoggerProvider::builder()
1375+
.with_simple_exporter(exporter.clone())
1376+
.build();
1377+
1378+
let layer = layer::OpenTelemetryTracingBridge::builder(&provider)
1379+
.with_tracing_span_attributes(TracingSpanAttributes::all())
1380+
.with_tracing_span_name("span.name")
1381+
.build()
1382+
.with_filter(tracing_subscriber::filter::filter_fn(|meta| {
1383+
meta.is_span() || *meta.level() <= tracing::Level::ERROR
1384+
}));
1385+
let subscriber = tracing_subscriber::registry().with(layer);
1386+
let _guard = tracing::subscriber::set_default(subscriber);
1387+
1388+
let outer = tracing::info_span!("outer_span", request_id = "req-123");
1389+
let _outer_guard = outer.enter();
1390+
let inner = tracing::info_span!("inner_span", user_id = 123);
1391+
let _inner_guard = inner.enter();
1392+
tracing::error!(status = 200, "test message");
1393+
1394+
provider.force_flush().unwrap();
1395+
let logs = exporter.get_emitted_logs().unwrap();
1396+
assert_eq!(logs.len(), 1);
1397+
let log = &logs[0];
1398+
1399+
assert!(attributes_contains(
1400+
&log.record,
1401+
&Key::new("span.name"),
1402+
&AnyValue::String("inner_span".into())
1403+
));
1404+
assert!(attributes_contains(
1405+
&log.record,
1406+
&Key::new("request_id"),
1407+
&AnyValue::String("req-123".into())
1408+
));
1409+
assert!(attributes_contains(
1410+
&log.record,
1411+
&Key::new("user_id"),
1412+
&AnyValue::Int(123)
1413+
));
1414+
assert!(attributes_contains(
1415+
&log.record,
1416+
&Key::new("status"),
1417+
&AnyValue::Int(200)
1418+
));
1419+
}
1420+
12021421
#[test]
12031422
#[cfg(feature = "experimental_span_attributes")]
12041423
fn tracing_appender_span_context_enrichment_enabled() {

opentelemetry-appender-tracing/src/lib.rs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,12 @@
122122
//!
123123
//! In future, additional types may be supported.
124124
//!
125-
//! ## Tracing Span Attribute Enrichment
125+
//! ## Tracing Span Enrichment
126126
//!
127127
//! By default, only the fields on the `tracing` event itself are captured. Optionally,
128128
//! attributes from active [`tracing::span!`](https://docs.rs/tracing/latest/tracing/macro.span.html)
129-
//! scopes can be copied onto each emitted log record. **"Span" here refers to a `tracing` span,
130-
//! not an `opentelemetry::trace::Span`.**
129+
//! scopes and/or the current tracing span name can be copied onto each emitted log record.
130+
//! **"Span" here refers to a `tracing` span, not an `opentelemetry::trace::Span`.**
131131
//!
132132
//! Gated behind the `experimental_span_attributes` cargo feature.
133133
//!
@@ -146,20 +146,35 @@
146146
//! let layer = OpenTelemetryTracingBridge::builder(&provider)
147147
//! .with_tracing_span_attributes(TracingSpanAttributes::allowlist(["session.id"]))
148148
//! .build();
149+
//!
150+
//! // Copy the current tracing span name onto log records under a user-chosen
151+
//! // log attribute key:
152+
//! let layer = OpenTelemetryTracingBridge::builder(&provider)
153+
//! .with_tracing_span_name("span.name")
154+
//! .build();
155+
//!
156+
//! // Span attributes and span name can also be enabled together:
157+
//! let layer = OpenTelemetryTracingBridge::builder(&provider)
158+
//! .with_tracing_span_attributes(TracingSpanAttributes::all())
159+
//! .with_tracing_span_name("span.name")
160+
//! .build();
149161
//! ```
150162
//!
151-
//! When enrichment is enabled, attributes from all ancestor spans (root to leaf)
152-
//! are collected and added to the log record before the event's own fields.
163+
//! When span-attribute enrichment is enabled, attributes from all ancestor spans
164+
//! (root to leaf) are collected and added to the log record before the event's
165+
//! own fields. When span-name enrichment is enabled, the current (leaf) tracing
166+
//! span name is stored under the provided log attribute key. The argument to
167+
//! `with_tracing_span_name` is the log attribute key, not the span name itself.
153168
//!
154169
//! > **Note:** This crate does not convert `tracing` spans into OpenTelemetry spans.
155170
//! > Use [`tracing-opentelemetry`](https://docs.rs/tracing-opentelemetry/latest/tracing_opentelemetry/)
156-
//! > for that. The span enrichment feature here only *reads* tracing-span fields to
157-
//! > copy them onto log records — it does not create or manage OpenTelemetry spans.
171+
//! > for that. The span enrichment feature here only *reads* tracing-span fields and
172+
//! > metadata to copy them onto log records — it does not create or manage OpenTelemetry spans.
158173
//!
159174
//! ## Feature Flags
160175
//!
161-
//! - `experimental_span_attributes`: Enables tracing-span attribute enrichment
162-
//! (`TracingSpanAttributes`, `with_tracing_span_attributes`).
176+
//! - `experimental_span_attributes`: Enables tracing-span enrichment
177+
//! (`TracingSpanAttributes`, `with_tracing_span_attributes`, `with_tracing_span_name`).
163178
//! - `experimental_metadata_attributes`: Adds source code metadata (`code.filepath`,
164179
//! `code.filename`, `code.namespace`, `code.lineno`) as log record attributes.
165180
//!

0 commit comments

Comments
 (0)