Skip to content

Commit c3c18a8

Browse files
antonmrygeoffreyclaude
authored andcommitted
test(instrumented_exec): verify preview isolation and metrics recording
- Capture span fields on open/record/close to assert recorded values - Add test ensuring independent recorder groups keep previews separate - Add test ensuring native metrics are recorded on stream group close
1 parent 9277e4b commit c3c18a8

1 file changed

Lines changed: 175 additions & 19 deletions

File tree

datafusion-tracing/src/instrumented_exec.rs

Lines changed: 175 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)