@@ -571,6 +571,7 @@ mod tests {
571571 use datafusion:: execution:: SessionStateBuilder ;
572572 use datafusion:: prelude:: SessionContext ;
573573 use futures:: StreamExt ;
574+ use std:: collections:: HashMap ;
574575 use std:: sync:: atomic:: AtomicBool ;
575576 use tracing:: field:: { Field , Visit } ;
576577 use tracing:: { Id , Subscriber } ;
@@ -584,6 +585,7 @@ mod tests {
584585 // -----------------------------------------------------------------------
585586
586587 struct CapturedName ( String ) ;
588+ struct CapturedFields ( HashMap < String , String > ) ;
587589
588590 #[ derive( Clone , Default ) ]
589591 struct SpanCapture ( Arc < Mutex < Vec < SpanEvent > > > ) ;
@@ -592,16 +594,20 @@ mod tests {
592594 struct SpanEvent {
593595 kind : & ' static str ,
594596 name : String ,
597+ fields : HashMap < String , String > ,
595598 }
596599
597- struct NameVisitor ( Option < String > ) ;
598- impl Visit for NameVisitor {
600+ #[ derive( Default ) ]
601+ struct FieldVisitor ( HashMap < String , String > ) ;
602+
603+ impl Visit for FieldVisitor {
599604 fn record_str ( & mut self , field : & Field , value : & str ) {
600- if field. name ( ) == "otel.name" {
601- self . 0 = Some ( value. to_owned ( ) ) ;
602- }
605+ self . 0 . insert ( field. name ( ) . to_owned ( ) , value. to_owned ( ) ) ;
606+ }
607+
608+ fn record_debug ( & mut self , field : & Field , value : & dyn Debug ) {
609+ self . 0 . insert ( field. name ( ) . to_owned ( ) , format ! ( "{value:?}" ) ) ;
603610 }
604- fn record_debug ( & mut self , _: & Field , _: & dyn Debug ) { }
605611 }
606612
607613 impl SpanCapture {
@@ -622,6 +628,16 @@ mod tests {
622628 . filter ( |e| e. kind == "close" && e. name == name)
623629 . count ( )
624630 }
631+
632+ fn closed_field_values ( & self , name : & str , field : & str ) -> Vec < String > {
633+ self . 0
634+ . lock ( )
635+ . unwrap ( )
636+ . iter ( )
637+ . filter ( |e| e. kind == "close" && e. name == name)
638+ . filter_map ( |e| e. fields . get ( field) . cloned ( ) )
639+ . collect ( )
640+ }
625641 }
626642
627643 impl < S : Subscriber + for < ' s > LookupSpan < ' s > > Layer < S > for SpanCapture {
@@ -631,26 +647,60 @@ mod tests {
631647 id : & Id ,
632648 ctx : Context < ' _ , S > ,
633649 ) {
634- let mut v = NameVisitor ( None ) ;
635- attrs. record ( & mut v) ;
636- let name = v. 0 . unwrap_or_else ( || attrs. metadata ( ) . name ( ) . to_owned ( ) ) ;
650+ let mut visitor = FieldVisitor :: default ( ) ;
651+ attrs. record ( & mut visitor) ;
652+ let name = visitor
653+ . 0
654+ . get ( "otel.name" )
655+ . cloned ( )
656+ . unwrap_or_else ( || attrs. metadata ( ) . name ( ) . to_owned ( ) ) ;
637657 if let Some ( span) = ctx. span ( id) {
638- span. extensions_mut ( ) . insert ( CapturedName ( name. clone ( ) ) ) ;
658+ let mut extensions = span. extensions_mut ( ) ;
659+ extensions. insert ( CapturedName ( name. clone ( ) ) ) ;
660+ extensions. insert ( CapturedFields ( visitor. 0 . clone ( ) ) ) ;
661+ }
662+ self . 0 . lock ( ) . unwrap ( ) . push ( SpanEvent {
663+ kind : "open" ,
664+ name,
665+ fields : visitor. 0 ,
666+ } ) ;
667+ }
668+
669+ fn on_record (
670+ & self ,
671+ id : & Id ,
672+ values : & tracing:: span:: Record < ' _ > ,
673+ ctx : Context < ' _ , S > ,
674+ ) {
675+ let mut visitor = FieldVisitor :: default ( ) ;
676+ values. record ( & mut visitor) ;
677+ if let Some ( span) = ctx. span ( id)
678+ && let Some ( fields) = span. extensions_mut ( ) . get_mut :: < CapturedFields > ( )
679+ {
680+ fields. 0 . extend ( visitor. 0 ) ;
639681 }
640- self . 0
641- . lock ( )
642- . unwrap ( )
643- . push ( SpanEvent { kind : "open" , name } ) ;
644682 }
645683
646684 fn on_close ( & self , id : Id , ctx : Context < ' _ , S > ) {
647- let name = ctx
685+ let ( name, fields ) = ctx
648686 . span ( & id)
649- . and_then ( |s| s. extensions ( ) . get :: < CapturedName > ( ) . map ( |n| n. 0 . clone ( ) ) )
687+ . map ( |span| {
688+ let extensions = span. extensions ( ) ;
689+ let name = extensions
690+ . get :: < CapturedName > ( )
691+ . map ( |n| n. 0 . clone ( ) )
692+ . unwrap_or_default ( ) ;
693+ let fields = extensions
694+ . get :: < CapturedFields > ( )
695+ . map ( |fields| fields. 0 . clone ( ) )
696+ . unwrap_or_default ( ) ;
697+ ( name, fields)
698+ } )
650699 . unwrap_or_default ( ) ;
651700 self . 0 . lock ( ) . unwrap ( ) . push ( SpanEvent {
652701 kind : "close" ,
653702 name,
703+ fields,
654704 } ) ;
655705 }
656706 }
@@ -660,7 +710,11 @@ mod tests {
660710 // -----------------------------------------------------------------------
661711
662712 async fn make_ctx ( ) -> SessionContext {
663- let rule = crate :: instrument_with_info_spans!( options: InstrumentationOptions :: default ( ) ) ;
713+ make_ctx_with_options ( InstrumentationOptions :: default ( ) ) . await
714+ }
715+
716+ async fn make_ctx_with_options ( options : InstrumentationOptions ) -> SessionContext {
717+ let rule = crate :: instrument_with_info_spans!( options: options) ;
664718 let state = SessionStateBuilder :: new ( )
665719 . with_default_features ( )
666720 . with_physical_optimizer_rule ( rule)
@@ -792,10 +846,22 @@ mod tests {
792846 }
793847
794848 fn two_partition_plan ( ) -> InstrumentedExec {
849+ two_partition_plan_with_options ( & InstrumentationOptions :: default ( ) )
850+ }
851+
852+ fn two_partition_plan_with_options (
853+ options : & InstrumentationOptions ,
854+ ) -> InstrumentedExec {
795855 InstrumentedExec :: new (
796856 two_partition_inner ( ) ,
797- Arc :: new ( || tracing:: info_span!( "InstrumentedExec" ) ) ,
798- & InstrumentationOptions :: default ( ) ,
857+ Arc :: new ( || {
858+ tracing:: info_span!(
859+ "InstrumentedExec" ,
860+ datafusion. preview = field:: Empty ,
861+ datafusion. metrics. output_rows = field:: Empty ,
862+ )
863+ } ) ,
864+ options,
799865 )
800866 }
801867
@@ -1083,4 +1149,94 @@ mod tests {
10831149 "each overlapping duplicate stream should close its own span"
10841150 ) ;
10851151 }
1152+
1153+ /// Preview recorders are owned by a recorder group. Independent groups must
1154+ /// record independent previews instead of sharing preview state.
1155+ #[ tokio:: test]
1156+ async fn previews_do_not_mix_between_independent_recorder_groups ( ) {
1157+ let capture = SpanCapture :: default ( ) ;
1158+ let _guard = tracing:: subscriber:: set_default (
1159+ tracing_subscriber:: registry ( )
1160+ . with ( tracing:: level_filters:: LevelFilter :: INFO )
1161+ . with ( capture. clone ( ) ) ,
1162+ ) ;
1163+
1164+ let options = InstrumentationOptions :: builder ( ) . preview_limit ( 5 ) . build ( ) ;
1165+ let plan = two_partition_plan_with_options ( & options) ;
1166+ let task_ctx_1 = Arc :: new ( TaskContext :: default ( ) ) ;
1167+ let task_ctx_2 = Arc :: new ( TaskContext :: default ( ) ) ;
1168+ let mut stream_1 = plan. execute ( 0 , task_ctx_1) . unwrap ( ) ;
1169+ let mut stream_2 = plan. execute ( 1 , task_ctx_2) . unwrap ( ) ;
1170+
1171+ while let Some ( batch) = stream_1. next ( ) . await {
1172+ batch. unwrap ( ) ;
1173+ }
1174+ while let Some ( batch) = stream_2. next ( ) . await {
1175+ batch. unwrap ( ) ;
1176+ }
1177+ drop ( stream_1) ;
1178+ drop ( stream_2) ;
1179+
1180+ let previews =
1181+ capture. closed_field_values ( "InstrumentedExec" , "datafusion.preview" ) ;
1182+ assert_eq ! (
1183+ previews. len( ) ,
1184+ 2 ,
1185+ "independent recorder groups should each close with a preview"
1186+ ) ;
1187+ assert ! (
1188+ previews. iter( ) . any( |preview| preview. contains( "| 1 |" ) ) ,
1189+ "one recorder group should preview partition 0"
1190+ ) ;
1191+ assert ! (
1192+ previews. iter( ) . any( |preview| preview. contains( "| 2 |" ) ) ,
1193+ "one recorder group should preview partition 1"
1194+ ) ;
1195+ assert ! (
1196+ previews
1197+ . iter( )
1198+ . all( |preview| !preview. contains( "| 1 |\n |---|\n | 2 |" ) ) ,
1199+ "independent recorder group previews must not be concatenated"
1200+ ) ;
1201+ }
1202+
1203+ /// Metrics are still DataFusion's native plan metrics, but they should be
1204+ /// recorded when the stream-owned recorder group closes.
1205+ #[ tokio:: test]
1206+ async fn metrics_are_recorded_when_stream_group_closes ( ) {
1207+ let capture = SpanCapture :: default ( ) ;
1208+ let _guard = tracing:: subscriber:: set_default (
1209+ tracing_subscriber:: registry ( )
1210+ . with ( tracing:: level_filters:: LevelFilter :: INFO )
1211+ . with ( capture. clone ( ) ) ,
1212+ ) ;
1213+
1214+ let options = InstrumentationOptions :: builder ( )
1215+ . record_metrics ( true )
1216+ . build ( ) ;
1217+ let ctx = make_ctx_with_options ( options) . await ;
1218+ let plan = ctx
1219+ . sql ( "SELECT 1" )
1220+ . await
1221+ . unwrap ( )
1222+ . create_physical_plan ( )
1223+ . await
1224+ . unwrap ( ) ;
1225+ let task_ctx = ctx. task_ctx ( ) ;
1226+
1227+ for partition in 0 ..plan. properties ( ) . partitioning . partition_count ( ) {
1228+ let mut stream = plan. execute ( partition, task_ctx. clone ( ) ) . unwrap ( ) ;
1229+ while let Some ( batch) = stream. next ( ) . await {
1230+ batch. unwrap ( ) ;
1231+ }
1232+ drop ( stream) ;
1233+ }
1234+
1235+ let output_rows = capture
1236+ . closed_field_values ( "InstrumentedExec" , "datafusion.metrics.output_rows" ) ;
1237+ assert ! (
1238+ output_rows. iter( ) . any( |value| value == "1" ) ,
1239+ "stream group close should record native DataFusion output row metrics"
1240+ ) ;
1241+ }
10861242}
0 commit comments