@@ -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
373378impl < 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
408417impl < 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 ( ) {
0 commit comments