1use std::fmt;
24use std::ops::Range;
25use std::sync::atomic::{AtomicU64, Ordering};
26use std::sync::{Arc, OnceLock};
27
28use datafusion::logical_expr::Expr;
29use datafusion::physical_plan::metrics::{ExecutionPlanMetricsSet, MetricBuilder, MetricsSet};
30use opentelemetry_proto::tonic::{
31 collector::trace::v1::ExportTraceServiceRequest,
32 common::v1::any_value::Value,
33 common::v1::{AnyValue, KeyValue},
34 resource::v1::Resource,
35 trace::v1::{ResourceSpans, ScopeSpans, Span, span::SpanKind},
36};
37use re_async::AsyncRuntimeHandle;
38use re_dataframe::QueryExpression;
39use re_protos::cloud::v1alpha1::SystemTableKind;
40use re_protos::cloud::v1alpha1::ext::ProviderDetails;
41use re_redap_client::ConnectionAnalyticsExporter;
42use re_uri::Origin;
43use web_time::{Duration, Instant, SystemTime};
44
45use crate::metrics_capture::{QueryMetrics, QuerySnapshot, build_query_snapshot};
46
47#[derive(Clone)]
56pub(crate) struct ConnectionAnalytics {
57 inner: Arc<Inner>,
58}
59
60impl fmt::Debug for ConnectionAnalytics {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 f.debug_struct("ConnectionAnalytics")
63 .field("origin", &self.inner.origin)
64 .finish_non_exhaustive()
65 }
66}
67
68struct Inner {
69 origin: Origin,
70 async_runtime: Option<AsyncRuntimeHandle>,
71
72 exporter: Option<ConnectionAnalyticsExporter>,
76}
77
78impl ConnectionAnalytics {
79 pub fn new(exporter: ConnectionAnalyticsExporter, async_runtime: AsyncRuntimeHandle) -> Self {
84 let origin = exporter.origin().clone();
85 Self {
86 inner: Arc::new(Inner {
87 origin,
88 async_runtime: Some(async_runtime),
89 exporter: Some(exporter),
90 }),
91 }
92 }
93
94 #[cfg(test)]
95 pub(crate) fn disabled_for_test(origin: Origin) -> Self {
96 Self {
97 inner: Arc::new(Inner {
98 origin,
99 async_runtime: None,
100 exporter: None,
101 }),
102 }
103 }
104
105 pub fn begin_table_query(
110 &self,
111 info: TableQueryInfo,
112 scan_start: Instant,
113 ) -> PendingTableQueryAnalytics {
114 PendingTableQueryAnalytics {
115 inner: Arc::new(PendingTableInner {
116 connection: self.clone(),
117 info,
118 stats: SharedTableScanStats::default(),
119 scan_start,
120 time_to_first_response: OnceLock::new(),
121 time_to_first_batch: OnceLock::new(),
122 trace_id: OnceLock::new(),
123 error_kind: OnceLock::new(),
124 }),
125 }
126 }
127
128 fn send_span(&self, span: Span, trace_id: Option<opentelemetry::TraceId>) {
130 let this = self.clone();
131
132 let fut = async move {
133 if let Err(err) = this.send_span_impl(span, trace_id).await {
134 re_log::debug_once!(
135 "Failed to send analytics to Rerun Hub: {} ({})",
136 err.code(),
137 err.message()
138 );
139 }
140 };
141
142 if let Some(async_runtime) = &self.inner.async_runtime {
143 async_runtime.spawn_future(fut);
144 }
145 }
146
147 async fn send_span_impl(
148 &self,
149 mut span: Span,
150 trace_id: Option<opentelemetry::TraceId>,
151 ) -> tonic::Result<()> {
152 let Some(exporter) = &self.inner.exporter else {
153 return Ok(());
154 };
155 assign_span_identity(&mut span, trace_id)?;
156
157 let mut resource_attributes = vec![kv_string("service.name", "rerun-viewer")];
170 if let Some(analytics) = re_analytics::Analytics::global_get() {
171 resource_attributes.push(kv_string("analytics_id", &analytics.config().analytics_id));
172 }
173
174 let export_request = ExportTraceServiceRequest {
175 resource_spans: vec![ResourceSpans {
176 resource: Some(Resource {
177 attributes: resource_attributes,
178 dropped_attributes_count: 0,
179 entity_refs: Vec::new(),
180 }),
181 scope_spans: vec![ScopeSpans {
182 scope: None,
183 spans: vec![span],
184 schema_url: String::new(),
185 }],
186 schema_url: String::new(),
187 }],
188 };
189
190 exporter.export_trace(export_request, trace_id).await
191 }
192}
193
194fn assign_span_identity(
195 span: &mut Span,
196 correlated_trace_id: Option<opentelemetry::TraceId>,
197) -> tonic::Result<()> {
198 span.trace_id = if let Some(trace_id) =
199 correlated_trace_id.filter(|trace_id| *trace_id != opentelemetry::TraceId::INVALID)
200 {
201 trace_id.to_bytes().to_vec()
202 } else {
203 random_nonzero_id::<16>()?
204 };
205 span.span_id = random_nonzero_id::<8>()?;
206 Ok(())
207}
208
209fn random_nonzero_id<const N: usize>() -> tonic::Result<Vec<u8>> {
210 loop {
211 let mut id = [0; N];
212 getrandom::fill(&mut id).map_err(|err| {
213 tonic::Status::internal(format!("failed to generate OTLP span ID: {err}"))
214 })?;
215 if id.iter().any(|byte| *byte != 0) {
216 return Ok(id.to_vec());
217 }
218 }
219}
220
221pub fn begin_query(
238 connection: Option<ConnectionAnalytics>,
239 query_info: QueryInfo,
240 scan_start: Instant,
241 scan_start_wall: SystemTime,
242) -> PendingQueryAnalytics {
243 PendingQueryAnalytics {
244 inner: Arc::new(PendingInner {
245 connection,
246 metrics: Arc::new(QueryMetrics::new(query_info)),
247 scan_start,
248 scan_start_wall,
249 time_to_first_chunk: OnceLock::new(),
250 direct_terminal_reason: OnceLock::new(),
251 error_kind: OnceLock::new(),
252 }),
253 }
254}
255
256#[derive(Clone, Copy, Debug, PartialEq, Eq)]
258pub enum QueryType {
259 Static,
261
262 LatestAt,
264
265 Range,
267
268 Dataframe,
270
271 FullScan,
273}
274
275impl QueryType {
276 pub(crate) fn classify(query_expression: &QueryExpression) -> Self {
278 if query_expression.is_static() {
279 Self::Static
280 } else {
281 let has_latest_at = query_expression.min_latest_at().is_some();
282 let has_range = query_expression.max_range().is_some();
283 match (has_latest_at, has_range) {
284 (true, true) => Self::Dataframe,
285 (true, false) => Self::LatestAt,
286 (false, true) => Self::Range,
287 (false, false) => Self::FullScan,
288 }
289 }
290 }
291
292 pub const fn as_str(self) -> &'static str {
294 match self {
295 Self::Static => "static",
296 Self::LatestAt => "latest_at",
297 Self::Range => "range",
298 Self::Dataframe => "dataframe",
299 Self::FullScan => "full_scan",
300 }
301 }
302}
303
304#[derive(Clone, Debug, PartialEq)]
306pub struct QueryInfo {
307 pub dataset_id: String,
310
311 pub query_chunks: usize,
313
314 pub query_segments: usize,
316
317 pub query_layers: usize,
319
320 pub query_columns: usize,
322
323 pub query_entities: usize,
325
326 pub query_bytes: u64,
328
329 pub query_chunks_per_segment_min: u32,
331
332 pub query_chunks_per_segment_max: u32,
334
335 pub query_chunks_per_segment_mean: f32,
337
338 pub query_type: QueryType,
340
341 pub primary_index_name: Option<String>,
343
344 pub time_to_first_chunk_info: Option<Duration>,
347
348 pub trace_id: Option<opentelemetry::TraceId>,
350
351 pub filters_pushed_down: usize,
354
355 pub filters_applied_client_side: usize,
358
359 pub entity_path_narrowing_applied: bool,
362
363 pub filters_total: u32,
367
368 pub filters_signatures: String,
371
372 pub filters_signatures_exact: String,
375
376 pub filters_signatures_inexact: String,
379
380 pub filters_signatures_unsupported: String,
383}
384
385#[derive(Clone)]
396pub(crate) struct PendingQueryAnalytics {
397 inner: Arc<PendingInner>,
398}
399
400impl fmt::Debug for PendingQueryAnalytics {
401 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402 f.debug_struct("PendingQueryAnalytics")
403 .finish_non_exhaustive()
404 }
405}
406
407pub(crate) struct PendingInner {
408 connection: Option<ConnectionAnalytics>,
412
413 metrics: Arc<QueryMetrics>,
419
420 scan_start: Instant,
422
423 scan_start_wall: SystemTime,
427
428 time_to_first_chunk: OnceLock<Duration>,
430
431 direct_terminal_reason: OnceLock<DirectFetchFailureReason>,
435
436 error_kind: OnceLock<&'static str>,
439}
440
441#[derive(Debug, Clone, Copy, PartialEq, Eq)]
447#[cfg_attr(target_arch = "wasm32", expect(dead_code))]
448pub enum QueryErrorKind {
449 GrpcFetch,
451
452 DirectFetch,
454
455 Decode,
457
458 Other,
460}
461
462impl QueryErrorKind {
463 pub fn as_str(self) -> &'static str {
465 match self {
466 Self::GrpcFetch => "grpc_fetch",
467 Self::DirectFetch => "direct_fetch",
468 Self::Decode => "decode",
469 Self::Other => "other",
470 }
471 }
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq)]
480pub enum DirectFetchFailureReason {
481 Timeout,
482 Http4xx,
483 Http5xx,
484 Connection,
485 Decode,
486
487 SourceChanged,
490 Other,
491}
492
493impl DirectFetchFailureReason {
494 pub fn as_str(self) -> &'static str {
496 match self {
497 Self::Timeout => "timeout",
498 Self::Http4xx => "http_4xx",
499 Self::Http5xx => "http_5xx",
500 Self::Connection => "connection",
501 Self::Decode => "decode",
502 Self::SourceChanged => "source_changed",
503 Self::Other => "other",
504 }
505 }
506}
507
508impl PendingQueryAnalytics {
509 pub(crate) fn metrics(&self) -> &Arc<QueryMetrics> {
513 &self.inner.metrics
514 }
515
516 #[cfg_attr(target_arch = "wasm32", expect(dead_code))]
519 pub fn record_first_chunk(&self) {
520 self.inner
521 .time_to_first_chunk
522 .get_or_init(|| self.inner.scan_start.elapsed());
523 }
524
525 #[cfg(not(target_arch = "wasm32"))]
528 pub fn record_direct_terminal_failure(&self, reason: DirectFetchFailureReason) {
529 #[expect(clippy::let_underscore_must_use)]
530 let _ = self.inner.direct_terminal_reason.set(reason);
531 }
532
533 pub fn record_error(&self, kind: QueryErrorKind) {
537 #[expect(clippy::let_underscore_must_use)]
538 let _ = self.inner.error_kind.set(kind.as_str());
539 }
540
541 pub fn error_kind(&self) -> Option<&'static str> {
545 self.inner.error_kind.get().copied()
546 }
547
548 pub fn time_to_first_chunk(&self) -> Option<Duration> {
552 self.inner.time_to_first_chunk.get().copied()
553 }
554
555 pub fn direct_terminal_reason(&self) -> Option<DirectFetchFailureReason> {
558 self.inner.direct_terminal_reason.get().copied()
559 }
560
561 pub fn total_duration(&self) -> Duration {
566 self.inner.scan_start.elapsed()
567 }
568}
569
570#[derive(Default)]
581#[must_use]
582pub(crate) struct TaskFetchStats {
583 grpc_bytes: u64,
584 direct_bytes: u64,
585 direct_retries_total: u64,
586 direct_requests_retried: u64,
587 direct_retry_sleep: Duration,
588 direct_max_attempt: u64,
589 direct_original_ranges: u64,
590 direct_merged_ranges: u64,
591}
592
593#[cfg_attr(target_arch = "wasm32", expect(dead_code))]
594impl TaskFetchStats {
595 pub fn record_grpc_bytes(&mut self, bytes: u64) {
596 self.grpc_bytes += bytes;
597 }
598
599 pub fn record_direct_bytes(&mut self, bytes: u64) {
600 self.direct_bytes += bytes;
601 }
602
603 pub fn record_direct_retry(&mut self, sleep: Duration, attempt: u64) {
608 self.direct_retries_total += 1;
609 self.direct_retry_sleep = self.direct_retry_sleep.saturating_add(sleep);
610 self.direct_max_attempt = self.direct_max_attempt.max(attempt);
611 }
612
613 pub fn record_direct_request_was_retried(&mut self) {
616 self.direct_requests_retried += 1;
617 }
618
619 pub fn record_direct_ranges(&mut self, original: u64, merged: u64) {
621 self.direct_original_ranges += original;
622 self.direct_merged_ranges += merged;
623 }
624
625 #[expect(
627 clippy::needless_pass_by_value,
628 reason = "Prevent double-counting stats"
629 )]
630 pub fn merge_from(&mut self, other: Self) {
631 let Self {
632 grpc_bytes,
633 direct_bytes,
634 direct_retries_total,
635 direct_requests_retried,
636 direct_retry_sleep: direct_retry_sleep_us,
637 direct_max_attempt,
638 direct_original_ranges,
639 direct_merged_ranges,
640 } = other;
641 self.grpc_bytes += grpc_bytes;
642 self.direct_bytes += direct_bytes;
643 self.direct_retries_total += direct_retries_total;
644 self.direct_requests_retried += direct_requests_retried;
645 self.direct_retry_sleep += direct_retry_sleep_us;
646 self.direct_max_attempt = self.direct_max_attempt.max(direct_max_attempt);
647 self.direct_original_ranges += direct_original_ranges;
648 self.direct_merged_ranges += direct_merged_ranges;
649 }
650
651 pub fn flush_into(self, metrics: &QueryMetrics) {
657 let Self {
658 grpc_bytes,
659 direct_bytes,
660 direct_retries_total,
661 direct_requests_retried,
662 direct_retry_sleep,
663 direct_max_attempt,
664 direct_original_ranges,
665 direct_merged_ranges,
666 } = self;
667
668 if grpc_bytes != 0 {
671 metrics
672 .fetch_grpc_bytes
673 .fetch_add(grpc_bytes, Ordering::Relaxed);
674 }
675 if direct_bytes != 0 {
676 metrics
677 .fetch_direct_bytes
678 .fetch_add(direct_bytes, Ordering::Relaxed);
679 }
680 if direct_retries_total != 0 {
681 metrics
682 .fetch_direct_retries
683 .fetch_add(direct_retries_total, Ordering::Relaxed);
684 }
685 if direct_requests_retried != 0 {
686 metrics
687 .fetch_direct_requests_retried
688 .fetch_add(direct_requests_retried, Ordering::Relaxed);
689 }
690 if !direct_retry_sleep.is_zero() {
691 metrics
692 .fetch_direct_retry_sleep_us
693 .fetch_add(direct_retry_sleep.as_micros() as u64, Ordering::Relaxed);
694 }
695 if direct_max_attempt != 0 {
696 metrics
697 .fetch_direct_max_attempt
698 .fetch_max(direct_max_attempt, Ordering::Relaxed);
699 }
700 if direct_original_ranges != 0 {
701 metrics
702 .fetch_direct_original_ranges
703 .fetch_add(direct_original_ranges, Ordering::Relaxed);
704 }
705 if direct_merged_ranges != 0 {
706 metrics
707 .fetch_direct_merged_ranges
708 .fetch_add(direct_merged_ranges, Ordering::Relaxed);
709 }
710 }
711
712 pub fn try_flush_into(
715 self,
716 analytics: &PendingQueryAnalytics,
717 result: Result<(), QueryErrorKind>,
718 ) {
719 self.flush_into(analytics.metrics());
720 if let Err(err) = result {
721 analytics.record_error(err);
722 }
723 }
724}
725
726pub(crate) fn build_metrics_set_for_explain(
744 metrics: &QueryMetrics,
745 num_partitions: usize,
746 time_to_first_chunk: Option<Duration>,
747) -> MetricsSet {
748 let set = ExecutionPlanMetricsSet::new();
749 let info = &metrics.query_info;
750 let load = |a: &AtomicU64| a.load(Ordering::Relaxed) as usize;
751
752 let global = |name: &'static str| MetricBuilder::new(&set).global_counter(name);
753 global("query_chunks").add(info.query_chunks);
754 global("query_segments").add(info.query_segments);
755 global("query_layers").add(info.query_layers);
756 global("query_columns").add(info.query_columns);
757 global("query_entities").add(info.query_entities);
758 global("query_bytes").add(info.query_bytes as usize);
759 global("query_chunks_per_segment_min").add(info.query_chunks_per_segment_min as usize);
760 global("query_chunks_per_segment_max").add(info.query_chunks_per_segment_max as usize);
761 global("filters_pushed_down").add(info.filters_pushed_down);
762 global("filters_applied_client_side").add(info.filters_applied_client_side);
763 if info.entity_path_narrowing_applied {
764 global("entity_path_narrowing_applied").add(1);
765 }
766 if let Some(ttfci) = info.time_to_first_chunk_info {
767 global("time_to_first_chunk_info_us").add(ttfci.as_micros() as usize);
768 }
769 global("num_partitions").add(num_partitions);
770
771 global("fetch_grpc_requests").add(load(&metrics.fetch_grpc_requests));
772 global("fetch_grpc_bytes").add(load(&metrics.fetch_grpc_bytes));
773 global("fetch_direct_requests").add(load(&metrics.fetch_direct_requests));
774 global("fetch_direct_bytes").add(load(&metrics.fetch_direct_bytes));
775 global("fetch_direct_retries").add(load(&metrics.fetch_direct_retries));
776 global("fetch_direct_requests_retried").add(load(&metrics.fetch_direct_requests_retried));
777 global("fetch_direct_retry_sleep_us").add(load(&metrics.fetch_direct_retry_sleep_us));
778 global("fetch_direct_max_attempt").add(load(&metrics.fetch_direct_max_attempt));
779 global("fetch_direct_original_ranges").add(load(&metrics.fetch_direct_original_ranges));
780 global("fetch_direct_merged_ranges").add(load(&metrics.fetch_direct_merged_ranges));
781 global("planned_fetch_batches").add(load(&metrics.planned_fetch_batches));
782 global("planned_segment_waves").add(load(&metrics.planned_segment_waves));
783 global("segment_admission_limit").add(load(&metrics.segment_admission_limit));
784 global("segment_admission_candidate_limit")
785 .add(load(&metrics.segment_admission_candidate_limit));
786 global("segment_admission_source_code").add(load(&metrics.segment_admission_source));
787 global("segment_admission_candidate_reason_code")
788 .add(load(&metrics.segment_admission_candidate_reason));
789 global("segment_admission_adaptive_enabled")
790 .add(load(&metrics.segment_admission_adaptive_enabled));
791 global("segment_admission_profile_segment_count")
792 .add(load(&metrics.segment_admission_profile_segment_count));
793 global("segment_admission_profile_complete")
794 .add(load(&metrics.segment_admission_profile_complete));
795 global("segment_admission_p95_segment_bytes")
796 .add(load(&metrics.segment_admission_p95_segment_bytes));
797 global("segment_admission_max_segment_bytes")
798 .add(load(&metrics.segment_admission_max_segment_bytes));
799 global("segment_admission_largest_window_bytes")
800 .add(load(&metrics.segment_admission_largest_window_bytes));
801 global("max_segments_per_fetch_batch").add(load(&metrics.max_segments_per_fetch_batch));
802 global("max_segments_per_wave").add(load(&metrics.max_segments_per_wave));
803 global("peak_active_segments").add(load(&metrics.peak_active_segments));
804 global("pipeline_budget_bytes").add(load(&metrics.pipeline_budget_bytes));
805 global("pipeline_peak_decoded_bytes").add(load(&metrics.pipeline_peak_decoded_bytes));
806 global("pipeline_byte_waits").add(load(&metrics.pipeline_byte_waits));
807 global("segment_admission_waits").add(load(&metrics.segment_admission_waits));
808 global("pipeline_stall_breaker_activations")
809 .add(load(&metrics.pipeline_stall_breaker_activations));
810
811 if let Some(ttfr) = time_to_first_chunk {
812 MetricBuilder::new(&set)
813 .subset_time("time_to_first_chunk", 0)
814 .add_duration(ttfr);
815 }
816
817 set.clone_inner()
818}
819
820impl Drop for PendingInner {
821 fn drop(&mut self) {
822 let Some(connection) = self.connection.as_ref() else {
827 return;
828 };
829
830 let total_duration = self.scan_start.elapsed();
831 let scan_end_wall = SystemTime::now();
832 let time_to_first_chunk = self.time_to_first_chunk.get().copied();
833 let direct_terminal_reason = self.direct_terminal_reason.get().copied();
834 let error_kind = self.error_kind.get().copied();
835 let trace_id = self.metrics.query_info.trace_id;
836
837 let snapshot = build_query_snapshot(
838 &self.metrics,
839 total_duration,
840 time_to_first_chunk,
841 error_kind,
842 direct_terminal_reason,
843 );
844
845 let span = build_query_span(&snapshot, self.scan_start_wall..scan_end_wall);
846
847 connection.send_span(span, trace_id);
848 }
849}
850
851fn build_query_span(snap: &QuerySnapshot, wall_clock_range: Range<SystemTime>) -> Span {
857 let start_time_unix_nano = nanos_since_epoch(&wall_clock_range.start);
858 let end_time_unix_nano = nanos_since_epoch(&wall_clock_range.end);
859
860 let QuerySnapshot {
861 query_info:
862 QueryInfo {
863 dataset_id,
864 query_chunks,
865 query_segments,
866 query_layers,
867 query_columns,
868 query_entities,
869 query_bytes,
870 query_chunks_per_segment_min,
871 query_chunks_per_segment_max,
872 query_chunks_per_segment_mean,
873 query_type,
874 primary_index_name,
875 time_to_first_chunk_info,
876 trace_id: _,
877 filters_pushed_down,
878 filters_applied_client_side,
879 entity_path_narrowing_applied,
880 filters_total,
881 filters_signatures,
882 filters_signatures_exact,
883 filters_signatures_inexact,
884 filters_signatures_unsupported,
885 },
886 total_duration,
887 time_to_first_chunk,
888 error_kind,
889 direct_terminal_reason,
890 fetch_grpc_requests,
891 fetch_grpc_bytes,
892 fetch_direct_requests,
893 fetch_direct_bytes,
894 fetch_direct_retries,
895 fetch_direct_requests_retried,
896 fetch_direct_retry_sleep,
897 fetch_direct_max_attempt,
898 fetch_direct_original_ranges,
899 fetch_direct_merged_ranges,
900 planned_fetch_batches,
901 planned_segment_waves,
902 segment_admission_limit,
903 segment_admission_candidate_limit,
904 segment_admission_source,
905 segment_admission_candidate_reason,
906 segment_admission_adaptive_enabled,
907 segment_admission_profile_segment_count,
908 segment_admission_profile_complete,
909 segment_admission_p95_segment_bytes,
910 segment_admission_max_segment_bytes,
911 segment_admission_largest_window_bytes,
912 max_segments_per_fetch_batch,
913 max_segments_per_wave,
914 peak_active_segments,
915 pipeline_budget_bytes,
916 pipeline_peak_decoded_bytes,
917 pipeline_byte_waits,
918 segment_admission_waits,
919 pipeline_stall_breaker_activations,
920 } = snap;
921
922 #[expect(
923 clippy::cast_possible_wrap,
924 reason = "OTLP proto uses i64 for int values"
925 )]
926 let mut attributes = vec![
927 kv_string("dataset_id", dataset_id),
928 kv_int("query_chunks", *query_chunks as i64),
929 kv_int("query_segments", *query_segments as i64),
930 kv_int("query_layers", *query_layers as i64),
931 kv_int("query_columns", *query_columns as i64),
932 kv_int("query_entities", *query_entities as i64),
933 kv_int("query_bytes", *query_bytes as i64),
934 kv_int(
935 "query_chunks_per_segment_min",
936 i64::from(*query_chunks_per_segment_min),
937 ),
938 kv_int(
939 "query_chunks_per_segment_max",
940 i64::from(*query_chunks_per_segment_max),
941 ),
942 kv_double(
943 "query_chunks_per_segment_mean",
944 f64::from(*query_chunks_per_segment_mean),
945 ),
946 kv_string("query_type", query_type.as_str()),
947 kv_int("total_duration_us", total_duration.as_micros() as i64),
948 kv_bool("is_success", error_kind.is_none()),
949 kv_int("fetch_grpc_requests", *fetch_grpc_requests as i64),
951 kv_int("fetch_grpc_bytes", *fetch_grpc_bytes as i64),
952 kv_int("fetch_direct_requests", *fetch_direct_requests as i64),
955 kv_int("fetch_direct_bytes", *fetch_direct_bytes as i64),
956 kv_int("fetch_direct_retries", *fetch_direct_retries as i64),
957 kv_int(
958 "fetch_direct_requests_retried",
959 *fetch_direct_requests_retried as i64,
960 ),
961 kv_int(
962 "fetch_direct_retry_sleep_us",
963 fetch_direct_retry_sleep.as_micros() as i64,
964 ),
965 kv_int("fetch_direct_max_attempt", *fetch_direct_max_attempt as i64),
966 kv_int(
967 "fetch_direct_original_ranges",
968 *fetch_direct_original_ranges as i64,
969 ),
970 kv_int(
971 "fetch_direct_merged_ranges",
972 *fetch_direct_merged_ranges as i64,
973 ),
974 kv_int("planned_fetch_batches", *planned_fetch_batches as i64),
975 kv_int("planned_segment_waves", *planned_segment_waves as i64),
976 kv_int("segment_admission_limit", *segment_admission_limit as i64),
977 kv_int(
978 "segment_admission_candidate_limit",
979 *segment_admission_candidate_limit as i64,
980 ),
981 kv_string("segment_admission_source", segment_admission_source),
982 kv_string(
983 "segment_admission_candidate_reason",
984 segment_admission_candidate_reason,
985 ),
986 kv_bool(
987 "segment_admission_adaptive_enabled",
988 *segment_admission_adaptive_enabled,
989 ),
990 kv_int(
991 "segment_admission_profile_segment_count",
992 *segment_admission_profile_segment_count as i64,
993 ),
994 kv_bool(
995 "segment_admission_profile_complete",
996 *segment_admission_profile_complete,
997 ),
998 kv_int(
999 "segment_admission_p95_segment_bytes",
1000 *segment_admission_p95_segment_bytes as i64,
1001 ),
1002 kv_int(
1003 "segment_admission_max_segment_bytes",
1004 *segment_admission_max_segment_bytes as i64,
1005 ),
1006 kv_int(
1007 "segment_admission_largest_window_bytes",
1008 *segment_admission_largest_window_bytes as i64,
1009 ),
1010 kv_int(
1011 "max_segments_per_fetch_batch",
1012 *max_segments_per_fetch_batch as i64,
1013 ),
1014 kv_int("max_segments_per_wave", *max_segments_per_wave as i64),
1015 kv_int("peak_active_segments", *peak_active_segments as i64),
1016 kv_int("pipeline_budget_bytes", *pipeline_budget_bytes as i64),
1017 kv_int(
1018 "pipeline_peak_decoded_bytes",
1019 *pipeline_peak_decoded_bytes as i64,
1020 ),
1021 kv_int("pipeline_byte_waits", *pipeline_byte_waits as i64),
1022 kv_int("segment_admission_waits", *segment_admission_waits as i64),
1023 kv_int(
1024 "pipeline_stall_breaker_activations",
1025 *pipeline_stall_breaker_activations as i64,
1026 ),
1027 kv_int("filters_pushed_down", *filters_pushed_down as i64),
1028 kv_int(
1029 "filters_applied_client_side",
1030 *filters_applied_client_side as i64,
1031 ),
1032 kv_bool(
1033 "entity_path_narrowing_applied",
1034 *entity_path_narrowing_applied,
1035 ),
1036 ];
1037
1038 if *filters_total > 0 {
1039 attributes.push(kv_int("filters_total", i64::from(*filters_total)));
1040 }
1041 if !filters_signatures.is_empty() {
1042 attributes.push(kv_string("filters_signatures", filters_signatures));
1043 }
1044 if !filters_signatures_exact.is_empty() {
1045 attributes.push(kv_string(
1046 "filters_signatures_exact",
1047 filters_signatures_exact,
1048 ));
1049 }
1050 if !filters_signatures_inexact.is_empty() {
1051 attributes.push(kv_string(
1052 "filters_signatures_inexact",
1053 filters_signatures_inexact,
1054 ));
1055 }
1056 if !filters_signatures_unsupported.is_empty() {
1057 attributes.push(kv_string(
1058 "filters_signatures_unsupported",
1059 filters_signatures_unsupported,
1060 ));
1061 }
1062
1063 if let Some(name) = primary_index_name.as_deref() {
1064 attributes.push(kv_string("primary_index_name", name));
1065 }
1066
1067 if let Some(ttfci) = time_to_first_chunk_info {
1068 attributes.push(kv_int(
1069 "time_to_first_chunk_info_us",
1070 ttfci.as_micros() as i64,
1071 ));
1072 }
1073
1074 if let Some(ttfr) = time_to_first_chunk {
1075 attributes.push(kv_int("time_to_first_chunk_us", ttfr.as_micros() as i64));
1076 }
1077
1078 if let Some(reason) = direct_terminal_reason {
1079 attributes.push(kv_string("fetch_direct_terminal_reason", reason.as_str()));
1080 }
1081
1082 if let Some(kind) = error_kind {
1083 attributes.push(kv_string("error_kind", kind));
1084 }
1085
1086 Span {
1087 name: "cloud_query_dataset".to_owned(),
1088 kind: SpanKind::Client.into(),
1089 start_time_unix_nano,
1090 end_time_unix_nano,
1091 attributes,
1092 ..Default::default()
1093 }
1094}
1095
1096pub(crate) fn expr_filter_signature(expr: &Expr) -> String {
1110 let sql = datafusion::sql::unparser::expr_to_sql(expr)
1111 .map(|e| e.to_string())
1112 .unwrap_or_else(|_| expr.variant_name().to_owned());
1113 escape_sig_str(&sql)
1114}
1115
1116fn escape_sig_str(s: &str) -> String {
1118 s.replace('\\', "\\\\").replace(';', "\\;")
1119}
1120
1121#[derive(Clone, Copy, Debug)]
1126pub enum TableKind {
1127 Lance,
1129
1130 SystemEntries,
1132
1133 SystemNamespaces,
1135
1136 Unknown,
1138}
1139
1140impl TableKind {
1141 const fn as_str(self) -> &'static str {
1143 match self {
1144 Self::Lance => "lance",
1145 Self::SystemEntries => "system_entries",
1146 Self::SystemNamespaces => "system_namespaces",
1147 Self::Unknown => "unknown",
1148 }
1149 }
1150}
1151
1152impl From<&ProviderDetails> for TableKind {
1153 fn from(details: &ProviderDetails) -> Self {
1154 match details {
1155 ProviderDetails::LanceTable(_) => Self::Lance,
1156 ProviderDetails::SystemTable(t) => match t.kind {
1157 SystemTableKind::Entries => Self::SystemEntries,
1158 SystemTableKind::Namespaces => Self::SystemNamespaces,
1159 SystemTableKind::Unspecified => Self::Unknown,
1160 },
1161 }
1162 }
1163}
1164
1165#[derive(Clone, Copy, Debug)]
1170pub enum TableQueryCaller {
1171 CatalogResolver,
1174
1175 EntriesTable,
1177
1178 BrowserDetailView,
1180}
1181
1182impl TableQueryCaller {
1183 const fn as_str(self) -> &'static str {
1185 match self {
1186 Self::CatalogResolver => "catalog_resolver",
1187 Self::EntriesTable => "entries_table",
1188 Self::BrowserDetailView => "browser_detail_view",
1189 }
1190 }
1191}
1192
1193#[derive(Clone, Debug)]
1195pub struct TableQueryInfo {
1196 pub table_id: String,
1199
1200 pub table_kind: TableKind,
1202
1203 pub caller: TableQueryCaller,
1205
1206 pub schema_total_columns: u32,
1208
1209 pub projected_columns: u32,
1212
1213 pub has_limit: bool,
1215
1216 pub limit_value: Option<u64>,
1218
1219 pub time_range: Range<SystemTime>,
1221
1222 pub filters_total: u32,
1225
1226 pub filters_signatures: String,
1230}
1231
1232#[derive(Default)]
1237pub(crate) struct SharedTableScanStats {
1238 grpc_requests: AtomicU64,
1239 batches: AtomicU64,
1240 rows_returned: AtomicU64,
1241 bytes_returned: AtomicU64,
1242}
1243
1244#[derive(Default, Clone, Copy)]
1249pub(crate) struct TableScanStatsSnapshot {
1250 pub grpc_requests: u64,
1251 pub batches: u64,
1252 pub rows_returned: u64,
1253 pub bytes_returned: u64,
1254}
1255
1256impl SharedTableScanStats {
1257 fn snapshot(&self) -> TableScanStatsSnapshot {
1259 TableScanStatsSnapshot {
1260 grpc_requests: self.grpc_requests.load(Ordering::Relaxed),
1261 batches: self.batches.load(Ordering::Relaxed),
1262 rows_returned: self.rows_returned.load(Ordering::Relaxed),
1263 bytes_returned: self.bytes_returned.load(Ordering::Relaxed),
1264 }
1265 }
1266}
1267
1268#[derive(Clone)]
1272pub(crate) struct PendingTableQueryAnalytics {
1273 inner: Arc<PendingTableInner>,
1274}
1275
1276impl fmt::Debug for PendingTableQueryAnalytics {
1277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1278 f.debug_struct("PendingTableQueryAnalytics")
1279 .finish_non_exhaustive()
1280 }
1281}
1282
1283struct PendingTableInner {
1284 connection: ConnectionAnalytics,
1285 info: TableQueryInfo,
1286 stats: SharedTableScanStats,
1287
1288 scan_start: Instant,
1290
1291 time_to_first_response: OnceLock<Duration>,
1293
1294 time_to_first_batch: OnceLock<Duration>,
1299
1300 trace_id: OnceLock<opentelemetry::TraceId>,
1303
1304 error_kind: OnceLock<&'static str>,
1307}
1308
1309impl PendingTableQueryAnalytics {
1310 pub fn record_trace_id(&self, trace_id: opentelemetry::TraceId) {
1313 #[expect(clippy::let_underscore_must_use)]
1314 let _ = self.inner.trace_id.set(trace_id);
1315 }
1316
1317 pub fn record_first_response(&self) {
1320 self.inner
1321 .time_to_first_response
1322 .get_or_init(|| self.inner.scan_start.elapsed());
1323 }
1324
1325 pub fn record_first_batch(&self) {
1328 self.inner
1329 .time_to_first_batch
1330 .get_or_init(|| self.inner.scan_start.elapsed());
1331 }
1332
1333 pub fn record_batch(&self, num_rows: u64, num_bytes: u64) {
1335 self.inner
1336 .stats
1337 .grpc_requests
1338 .fetch_add(1, Ordering::Relaxed);
1339 self.inner.stats.batches.fetch_add(1, Ordering::Relaxed);
1340 if num_rows != 0 {
1341 self.inner
1342 .stats
1343 .rows_returned
1344 .fetch_add(num_rows, Ordering::Relaxed);
1345 }
1346 if num_bytes != 0 {
1347 self.inner
1348 .stats
1349 .bytes_returned
1350 .fetch_add(num_bytes, Ordering::Relaxed);
1351 }
1352 }
1353
1354 pub fn record_error(&self, kind: QueryErrorKind) {
1357 #[expect(clippy::let_underscore_must_use)]
1358 let _ = self.inner.error_kind.set(kind.as_str());
1359 }
1360
1361 #[cfg(test)]
1365 pub(crate) fn build_span_for_test(&self) -> Span {
1366 let mut span = self.inner.build_span();
1367 assign_span_identity(&mut span, self.inner.trace_id.get().copied()).unwrap();
1368 span
1369 }
1370}
1371
1372impl PendingTableInner {
1373 fn build_span(&self) -> Span {
1380 let total_duration = self.scan_start.elapsed();
1381 let scan_end_wall = SystemTime::now();
1382 let stats = self.stats.snapshot();
1383 build_table_query_span(
1384 &self.info,
1385 stats,
1386 self.info.time_range.start..scan_end_wall,
1387 total_duration,
1388 self.time_to_first_response.get().copied(),
1389 self.time_to_first_batch.get().copied(),
1390 self.trace_id.get().copied(),
1391 self.error_kind.get().copied(),
1392 )
1393 }
1394}
1395
1396impl Drop for PendingTableInner {
1397 fn drop(&mut self) {
1398 let span = self.build_span();
1399 let trace_id = self.trace_id.get().copied();
1400 self.connection.send_span(span, trace_id);
1401 }
1402}
1403
1404pub(crate) fn build_table_query_span(
1411 info: &TableQueryInfo,
1412 stats: TableScanStatsSnapshot,
1413 wall_clock_range: Range<SystemTime>,
1414 total_duration: Duration,
1415 time_to_first_response: Option<Duration>,
1416 time_to_first_batch: Option<Duration>,
1417 _trace_id: Option<opentelemetry::TraceId>,
1418 error_kind: Option<&'static str>,
1419) -> Span {
1420 let TableQueryInfo {
1421 ref table_id,
1422 table_kind,
1423 caller,
1424 schema_total_columns,
1425 projected_columns,
1426 has_limit,
1427 limit_value,
1428 time_range: _,
1429 filters_total,
1430 ref filters_signatures,
1431 } = *info;
1432
1433 let start_time_unix_nano = nanos_since_epoch(&wall_clock_range.start);
1434 let end_time_unix_nano = nanos_since_epoch(&wall_clock_range.end);
1435
1436 #[expect(
1437 clippy::cast_possible_wrap,
1438 reason = "OTLP proto uses i64 for int values"
1439 )]
1440 let mut attributes = vec![
1441 kv_string("table_id", table_id),
1443 kv_string("table_kind", table_kind.as_str()),
1444 kv_string("caller", caller.as_str()),
1445 kv_int("schema_total_columns", i64::from(schema_total_columns)),
1447 kv_int("projected_columns", i64::from(projected_columns)),
1448 kv_bool("has_limit", has_limit),
1450 kv_bool("is_success", error_kind.is_none()),
1452 kv_int("total_duration_us", total_duration.as_micros() as i64),
1454 kv_int("fetch_grpc_requests", stats.grpc_requests as i64),
1456 kv_int("num_record_batches", stats.batches as i64),
1458 kv_int("rows_returned", stats.rows_returned as i64),
1459 kv_int("bytes_returned", stats.bytes_returned as i64),
1460 ];
1461
1462 if let Some(value) = limit_value {
1463 #[expect(
1464 clippy::cast_possible_wrap,
1465 reason = "OTLP proto uses i64 for int values"
1466 )]
1467 attributes.push(kv_int("limit_value", value as i64));
1468 }
1469
1470 if let Some(ttfr) = time_to_first_response {
1471 attributes.push(kv_int("time_to_first_response_us", ttfr.as_micros() as i64));
1472 }
1473
1474 if let Some(ttfb) = time_to_first_batch {
1475 attributes.push(kv_int("time_to_first_batch_us", ttfb.as_micros() as i64));
1476 }
1477
1478 if let Some(kind) = error_kind {
1479 attributes.push(kv_string("error_kind", kind));
1480 }
1481
1482 if filters_total > 0 {
1483 attributes.push(kv_int("filters_total", i64::from(filters_total)));
1484 }
1485 if !filters_signatures.is_empty() {
1486 attributes.push(kv_string("filters_signatures", filters_signatures));
1487 }
1488
1489 Span {
1490 name: "cloud_scan_table".to_owned(),
1491 kind: SpanKind::Client.into(),
1492 start_time_unix_nano,
1493 end_time_unix_nano,
1494 attributes,
1495 ..Default::default()
1496 }
1497}
1498
1499fn nanos_since_epoch(time: &SystemTime) -> u64 {
1502 time.duration_since(SystemTime::UNIX_EPOCH)
1503 .unwrap_or_default()
1504 .as_nanos() as u64
1505}
1506
1507fn kv_string(key: &str, value: &str) -> KeyValue {
1508 KeyValue {
1509 key: key.to_owned(),
1510 value: Some(AnyValue {
1511 value: Some(Value::StringValue(value.to_owned())),
1512 }),
1513 key_strindex: 0,
1514 }
1515}
1516
1517fn kv_int(key: &str, value: impl Into<i64>) -> KeyValue {
1518 KeyValue {
1519 key: key.to_owned(),
1520 value: Some(AnyValue {
1521 value: Some(Value::IntValue(value.into())),
1522 }),
1523 key_strindex: 0,
1524 }
1525}
1526
1527fn kv_bool(key: &str, value: bool) -> KeyValue {
1528 KeyValue {
1529 key: key.to_owned(),
1530 value: Some(AnyValue {
1531 value: Some(Value::BoolValue(value)),
1532 }),
1533 key_strindex: 0,
1534 }
1535}
1536
1537fn kv_double(key: &str, value: f64) -> KeyValue {
1538 KeyValue {
1539 key: key.to_owned(),
1540 value: Some(AnyValue {
1541 value: Some(Value::DoubleValue(value)),
1542 }),
1543 key_strindex: 0,
1544 }
1545}
1546
1547#[cfg(test)]
1548mod tests {
1549 use std::collections::HashSet;
1550
1551 use super::*;
1552
1553 fn dummy_query_info() -> QueryInfo {
1554 QueryInfo {
1555 dataset_id: "ds-123".to_owned(),
1556 query_chunks: 42,
1557 query_segments: 5,
1558 query_layers: 2,
1559 query_columns: 7,
1560 query_entities: 3,
1561 query_bytes: 1234,
1562 query_chunks_per_segment_min: 4,
1563 query_chunks_per_segment_max: 12,
1564 query_chunks_per_segment_mean: 8.4,
1565 query_type: QueryType::LatestAt,
1566 primary_index_name: None,
1567 time_to_first_chunk_info: None,
1568 trace_id: None,
1569 filters_pushed_down: 0,
1570 filters_applied_client_side: 0,
1571 entity_path_narrowing_applied: false,
1572 filters_total: 0,
1573 filters_signatures: String::new(),
1574 filters_signatures_exact: String::new(),
1575 filters_signatures_inexact: String::new(),
1576 filters_signatures_unsupported: String::new(),
1577 }
1578 }
1579
1580 #[test]
1581 fn assign_span_identity_uses_correlated_trace_id() {
1582 let trace_id = opentelemetry::TraceId::from_bytes([7; 16]);
1583 let mut span = Span::default();
1584
1585 assign_span_identity(&mut span, Some(trace_id)).unwrap();
1586
1587 assert_eq!(span.trace_id, trace_id.to_bytes());
1588 assert_eq!(span.span_id.len(), 8);
1589 assert!(span.span_id.iter().any(|byte| *byte != 0));
1590 }
1591
1592 #[test]
1593 fn assign_span_identity_generates_trace_id_without_correlation() {
1594 let mut span = Span::default();
1595
1596 assign_span_identity(&mut span, None).unwrap();
1597
1598 assert_eq!(span.trace_id.len(), 16);
1599 assert!(span.trace_id.iter().any(|byte| *byte != 0));
1600 assert_eq!(span.span_id.len(), 8);
1601 assert!(span.span_id.iter().any(|byte| *byte != 0));
1602 }
1603
1604 fn attribute_keys(span: &Span) -> HashSet<&str> {
1605 let keys: HashSet<_> = span.attributes.iter().map(|kv| kv.key.as_str()).collect();
1606 re_log::debug_assert_eq!(
1607 keys.len(),
1608 span.attributes.len(),
1609 "span contains duplicate attribute keys"
1610 );
1611 keys
1612 }
1613
1614 fn find_int(span: &Span, key: &str) -> Option<i64> {
1615 span.attributes
1616 .iter()
1617 .find(|kv| kv.key == key)
1618 .and_then(|kv| match kv.value.as_ref()?.value.as_ref()? {
1619 Value::IntValue(i) => Some(*i),
1620 _ => None,
1621 })
1622 }
1623
1624 fn find_string<'a>(span: &'a Span, key: &str) -> Option<&'a str> {
1625 span.attributes
1626 .iter()
1627 .find(|kv| kv.key == key)
1628 .and_then(|kv| match kv.value.as_ref()?.value.as_ref()? {
1629 Value::StringValue(s) => Some(s.as_str()),
1630 _ => None,
1631 })
1632 }
1633
1634 fn find_double(span: &Span, key: &str) -> Option<f64> {
1635 span.attributes
1636 .iter()
1637 .find(|kv| kv.key == key)
1638 .and_then(|kv| match kv.value.as_ref()?.value.as_ref()? {
1639 Value::DoubleValue(d) => Some(*d),
1640 _ => None,
1641 })
1642 }
1643
1644 fn find_bool(span: &Span, key: &str) -> Option<bool> {
1645 span.attributes
1646 .iter()
1647 .find(|kv| kv.key == key)
1648 .and_then(|kv| match kv.value.as_ref()?.value.as_ref()? {
1649 Value::BoolValue(b) => Some(*b),
1650 _ => None,
1651 })
1652 }
1653
1654 const REQUIRED_KEYS: &[&str] = &[
1657 "dataset_id",
1658 "query_chunks",
1659 "query_segments",
1660 "query_layers",
1661 "query_columns",
1662 "query_entities",
1663 "query_bytes",
1664 "query_chunks_per_segment_min",
1665 "query_chunks_per_segment_max",
1666 "query_chunks_per_segment_mean",
1667 "query_type",
1668 "total_duration_us",
1669 "is_success",
1670 "fetch_grpc_requests",
1671 "fetch_grpc_bytes",
1672 "fetch_direct_requests",
1673 "fetch_direct_bytes",
1674 "fetch_direct_retries",
1675 "fetch_direct_requests_retried",
1676 "fetch_direct_retry_sleep_us",
1677 "fetch_direct_max_attempt",
1678 "fetch_direct_original_ranges",
1679 "fetch_direct_merged_ranges",
1680 "planned_fetch_batches",
1681 "planned_segment_waves",
1682 "segment_admission_limit",
1683 "segment_admission_candidate_limit",
1684 "segment_admission_source",
1685 "segment_admission_candidate_reason",
1686 "segment_admission_adaptive_enabled",
1687 "segment_admission_profile_segment_count",
1688 "segment_admission_profile_complete",
1689 "segment_admission_p95_segment_bytes",
1690 "segment_admission_max_segment_bytes",
1691 "segment_admission_largest_window_bytes",
1692 "max_segments_per_fetch_batch",
1693 "max_segments_per_wave",
1694 "peak_active_segments",
1695 "pipeline_budget_bytes",
1696 "pipeline_peak_decoded_bytes",
1697 "pipeline_byte_waits",
1698 "segment_admission_waits",
1699 "pipeline_stall_breaker_activations",
1700 "filters_pushed_down",
1701 "filters_applied_client_side",
1702 "entity_path_narrowing_applied",
1703 ];
1704
1705 fn snapshot_from_info(query_info: QueryInfo) -> QuerySnapshot {
1709 QuerySnapshot {
1710 query_info,
1711 total_duration: Duration::ZERO,
1712 time_to_first_chunk: None,
1713 error_kind: None,
1714 direct_terminal_reason: None,
1715 fetch_grpc_requests: 0,
1716 fetch_grpc_bytes: 0,
1717 fetch_direct_requests: 0,
1718 fetch_direct_bytes: 0,
1719 fetch_direct_retries: 0,
1720 fetch_direct_requests_retried: 0,
1721 fetch_direct_retry_sleep: Duration::ZERO,
1722 fetch_direct_max_attempt: 0,
1723 fetch_direct_original_ranges: 0,
1724 fetch_direct_merged_ranges: 0,
1725 planned_fetch_batches: 0,
1726 planned_segment_waves: 0,
1727 segment_admission_limit: 0,
1728 segment_admission_candidate_limit: 0,
1729 segment_admission_source: "metrics_only",
1730 segment_admission_candidate_reason: "eligible",
1731 segment_admission_adaptive_enabled: false,
1732 segment_admission_profile_segment_count: 0,
1733 segment_admission_profile_complete: false,
1734 segment_admission_p95_segment_bytes: 0,
1735 segment_admission_max_segment_bytes: 0,
1736 segment_admission_largest_window_bytes: 0,
1737 max_segments_per_fetch_batch: 0,
1738 max_segments_per_wave: 0,
1739 peak_active_segments: 0,
1740 pipeline_budget_bytes: 0,
1741 pipeline_peak_decoded_bytes: 0,
1742 pipeline_byte_waits: 0,
1743 segment_admission_waits: 0,
1744 pipeline_stall_breaker_activations: 0,
1745 }
1746 }
1747
1748 #[test]
1749 fn build_query_span_minimal_emits_only_required_attributes() {
1750 let qi = dummy_query_info();
1751 let mut snap = snapshot_from_info(qi);
1752 snap.total_duration = Duration::from_micros(500);
1753
1754 let span = build_query_span(
1755 &snap,
1756 SystemTime::UNIX_EPOCH..SystemTime::UNIX_EPOCH + Duration::from_secs(1),
1757 );
1758
1759 assert_eq!(span.name, "cloud_query_dataset");
1761 assert_eq!(span.kind, i32::from(SpanKind::Client));
1762 assert!(span.links.is_empty());
1763
1764 let expected: HashSet<&str> = REQUIRED_KEYS.iter().copied().collect();
1766 let actual = attribute_keys(&span);
1767 assert_eq!(
1768 actual,
1769 expected,
1770 "extra/missing attribute keys: {:?}",
1771 actual.symmetric_difference(&expected).collect::<Vec<_>>()
1772 );
1773
1774 assert_eq!(find_string(&span, "dataset_id"), Some("ds-123"));
1776 assert_eq!(find_int(&span, "query_chunks"), Some(42));
1777 assert_eq!(find_int(&span, "query_chunks_per_segment_min"), Some(4));
1778 assert_eq!(find_int(&span, "query_chunks_per_segment_max"), Some(12));
1779 assert_eq!(
1780 find_double(&span, "query_chunks_per_segment_mean"),
1781 Some(f64::from(8.4_f32))
1782 );
1783 assert_eq!(find_string(&span, "query_type"), Some("latest_at"));
1784 assert_eq!(find_int(&span, "total_duration_us"), Some(500));
1785 assert_eq!(find_bool(&span, "is_success"), Some(true));
1786 }
1787
1788 #[test]
1789 fn build_query_span_records_fetch_stats() {
1790 let qi = dummy_query_info();
1791 let mut snap = snapshot_from_info(qi);
1792 snap.total_duration = Duration::from_millis(1);
1793 snap.fetch_grpc_requests = 2;
1794 snap.fetch_grpc_bytes = 5_000;
1795 snap.fetch_direct_requests = 1;
1796 snap.fetch_direct_bytes = 10_000;
1797 snap.fetch_direct_retries = 2;
1798 snap.fetch_direct_requests_retried = 1;
1799 snap.fetch_direct_retry_sleep = Duration::from_millis(12);
1800 snap.fetch_direct_max_attempt = 3;
1801 snap.fetch_direct_original_ranges = 8;
1802 snap.fetch_direct_merged_ranges = 4;
1803 snap.planned_fetch_batches = 16;
1804 snap.planned_segment_waves = 1_332;
1805 snap.segment_admission_limit = 3;
1806 snap.segment_admission_candidate_limit = 16;
1807 snap.segment_admission_source = "metrics_only";
1808 snap.segment_admission_candidate_reason = "eligible";
1809 snap.segment_admission_adaptive_enabled = true;
1810 snap.segment_admission_profile_segment_count = 32;
1811 snap.segment_admission_profile_complete = true;
1812 snap.segment_admission_p95_segment_bytes = 1024;
1813 snap.segment_admission_max_segment_bytes = 2048;
1814 snap.segment_admission_largest_window_bytes = 16_384;
1815 snap.max_segments_per_fetch_batch = 3;
1816 snap.max_segments_per_wave = 3;
1817 snap.peak_active_segments = 3;
1818 snap.pipeline_budget_bytes = 4 * 1024 * 1024 * 1024;
1819 snap.pipeline_peak_decoded_bytes = 96 * 1024 * 1024;
1820 snap.pipeline_byte_waits = 2;
1821 snap.segment_admission_waits = 1_329;
1822 snap.pipeline_stall_breaker_activations = 1;
1823
1824 let span = build_query_span(
1825 &snap,
1826 SystemTime::UNIX_EPOCH..SystemTime::UNIX_EPOCH + Duration::from_secs(1),
1827 );
1828
1829 assert_eq!(find_int(&span, "fetch_grpc_requests"), Some(2));
1830 assert_eq!(find_int(&span, "fetch_grpc_bytes"), Some(5_000));
1831 assert_eq!(find_int(&span, "fetch_direct_requests"), Some(1));
1832 assert_eq!(find_int(&span, "fetch_direct_bytes"), Some(10_000));
1833 assert_eq!(find_int(&span, "fetch_direct_retries"), Some(2));
1834 assert_eq!(find_int(&span, "fetch_direct_requests_retried"), Some(1));
1835 assert_eq!(find_int(&span, "fetch_direct_retry_sleep_us"), Some(12_000));
1836 assert_eq!(find_int(&span, "fetch_direct_max_attempt"), Some(3));
1837 assert_eq!(find_int(&span, "fetch_direct_original_ranges"), Some(8));
1838 assert_eq!(find_int(&span, "fetch_direct_merged_ranges"), Some(4));
1839 assert_eq!(find_int(&span, "planned_fetch_batches"), Some(16));
1840 assert_eq!(find_int(&span, "planned_segment_waves"), Some(1_332));
1841 assert_eq!(find_int(&span, "segment_admission_limit"), Some(3));
1842 assert_eq!(
1843 find_int(&span, "segment_admission_candidate_limit"),
1844 Some(16)
1845 );
1846 assert_eq!(
1847 find_string(&span, "segment_admission_source"),
1848 Some("metrics_only")
1849 );
1850 assert_eq!(
1851 find_string(&span, "segment_admission_candidate_reason"),
1852 Some("eligible")
1853 );
1854 assert_eq!(
1855 find_bool(&span, "segment_admission_adaptive_enabled"),
1856 Some(true)
1857 );
1858 assert_eq!(
1859 find_int(&span, "segment_admission_profile_segment_count"),
1860 Some(32)
1861 );
1862 assert_eq!(
1863 find_bool(&span, "segment_admission_profile_complete"),
1864 Some(true)
1865 );
1866 assert_eq!(
1867 find_int(&span, "segment_admission_p95_segment_bytes"),
1868 Some(1024)
1869 );
1870 assert_eq!(
1871 find_int(&span, "segment_admission_max_segment_bytes"),
1872 Some(2048)
1873 );
1874 assert_eq!(
1875 find_int(&span, "segment_admission_largest_window_bytes"),
1876 Some(16_384)
1877 );
1878 assert_eq!(find_int(&span, "max_segments_per_fetch_batch"), Some(3));
1879 assert_eq!(find_int(&span, "max_segments_per_wave"), Some(3));
1880 assert_eq!(find_int(&span, "peak_active_segments"), Some(3));
1881 assert_eq!(
1882 find_int(&span, "pipeline_budget_bytes"),
1883 Some(4 * 1024 * 1024 * 1024)
1884 );
1885 assert_eq!(
1886 find_int(&span, "pipeline_peak_decoded_bytes"),
1887 Some(96 * 1024 * 1024)
1888 );
1889 assert_eq!(find_int(&span, "pipeline_byte_waits"), Some(2));
1890 assert_eq!(find_int(&span, "segment_admission_waits"), Some(1_329));
1891 assert_eq!(
1892 find_int(&span, "pipeline_stall_breaker_activations"),
1893 Some(1)
1894 );
1895 }
1896
1897 #[test]
1898 fn build_query_span_emits_all_optional_attributes_when_present() {
1899 let trace_id = opentelemetry::TraceId::from_bytes([7u8; 16]);
1900 let mut qi = dummy_query_info();
1901 qi.primary_index_name = Some("log_time".to_owned());
1902 qi.time_to_first_chunk_info = Some(Duration::from_micros(123));
1903 qi.trace_id = Some(trace_id);
1904
1905 let mut snap = snapshot_from_info(qi);
1906 snap.total_duration = Duration::from_micros(999);
1907 snap.time_to_first_chunk = Some(Duration::from_micros(456));
1908 snap.direct_terminal_reason = Some(DirectFetchFailureReason::Http5xx);
1909 snap.error_kind = Some(QueryErrorKind::DirectFetch.as_str());
1910
1911 let span = build_query_span(
1912 &snap,
1913 SystemTime::UNIX_EPOCH..SystemTime::UNIX_EPOCH + Duration::from_secs(1),
1914 );
1915
1916 let optional = [
1918 "primary_index_name",
1919 "time_to_first_chunk_info_us",
1920 "time_to_first_chunk_us",
1921 "fetch_direct_terminal_reason",
1922 "error_kind",
1923 ];
1924 let keys = attribute_keys(&span);
1925 for k in optional {
1926 assert!(keys.contains(k), "missing optional attribute: {k}");
1927 }
1928
1929 assert_eq!(find_bool(&span, "is_success"), Some(false));
1931
1932 assert_eq!(find_string(&span, "primary_index_name"), Some("log_time"));
1933 assert_eq!(find_int(&span, "time_to_first_chunk_info_us"), Some(123));
1934 assert_eq!(find_int(&span, "time_to_first_chunk_us"), Some(456));
1935 assert_eq!(
1936 find_string(&span, "fetch_direct_terminal_reason"),
1937 Some("http_5xx")
1938 );
1939 assert_eq!(find_string(&span, "error_kind"), Some("direct_fetch"));
1940 assert!(span.links.is_empty());
1941 }
1942
1943 #[test]
1948 fn explain_metrics_set_includes_chunks_per_segment_min_and_max() {
1949 let metrics = QueryMetrics::new(dummy_query_info());
1950 let set = build_metrics_set_for_explain(&metrics, 1, None);
1951
1952 let aggregated = set.aggregate_by_name();
1953 let names: std::collections::HashSet<_> = aggregated
1954 .iter()
1955 .filter_map(|m| match m.value() {
1956 datafusion::physical_plan::metrics::MetricValue::Count { name, .. } => {
1957 Some(name.as_ref().to_owned())
1958 }
1959 _ => None,
1960 })
1961 .collect();
1962
1963 assert!(
1964 names.contains("query_chunks_per_segment_min"),
1965 "expected query_chunks_per_segment_min in explain metrics: {names:?}"
1966 );
1967 assert!(
1968 names.contains("query_chunks_per_segment_max"),
1969 "expected query_chunks_per_segment_max in explain metrics: {names:?}"
1970 );
1971 }
1972
1973 #[test]
1974 fn build_query_span_uses_wall_clock_range() {
1975 let qi = dummy_query_info();
1976 let snap = snapshot_from_info(qi);
1977 let start = SystemTime::UNIX_EPOCH + Duration::from_secs(2);
1978 let end = SystemTime::UNIX_EPOCH + Duration::from_millis(2_500);
1979
1980 let span = build_query_span(&snap, start..end);
1981
1982 assert_eq!(span.start_time_unix_nano, 2_000_000_000);
1983 assert_eq!(span.end_time_unix_nano, 2_500_000_000);
1984 }
1985}
1986
1987#[cfg(test)]
1988mod table_query_tests {
1989 use std::collections::HashSet;
1990
1991 use re_protos::cloud::v1alpha1::ext::{LanceTable, ProviderDetails, SystemTable};
1992
1993 use super::*;
1994
1995 fn lance_provider_details() -> ProviderDetails {
1996 let proto = re_protos::cloud::v1alpha1::LanceTable {
1999 table_url: "s3://bucket/path".to_owned(),
2000 };
2001 ProviderDetails::LanceTable(LanceTable::try_from(proto).unwrap())
2002 }
2003
2004 fn dummy_table_query_info() -> TableQueryInfo {
2007 TableQueryInfo {
2008 table_id: "tbl-42".to_owned(),
2009 table_kind: TableKind::Lance,
2010 caller: TableQueryCaller::BrowserDetailView,
2011 schema_total_columns: 12,
2012 projected_columns: 5,
2013 has_limit: false,
2014 limit_value: None,
2015 time_range: SystemTime::UNIX_EPOCH..SystemTime::UNIX_EPOCH + Duration::from_secs(1),
2016 filters_total: 0,
2017 filters_signatures: String::new(),
2018 }
2019 }
2020
2021 fn empty_stats() -> TableScanStatsSnapshot {
2022 TableScanStatsSnapshot::default()
2023 }
2024
2025 fn attribute_keys(span: &Span) -> HashSet<&str> {
2026 let keys: HashSet<_> = span.attributes.iter().map(|kv| kv.key.as_str()).collect();
2027 assert_eq!(
2028 keys.len(),
2029 span.attributes.len(),
2030 "span contains duplicate attribute keys"
2031 );
2032 keys
2033 }
2034
2035 fn find_int(span: &Span, key: &str) -> Option<i64> {
2036 span.attributes
2037 .iter()
2038 .find(|kv| kv.key == key)
2039 .and_then(|kv| match kv.value.as_ref()?.value.as_ref()? {
2040 Value::IntValue(i) => Some(*i),
2041 _ => None,
2042 })
2043 }
2044
2045 fn find_string<'a>(span: &'a Span, key: &str) -> Option<&'a str> {
2046 span.attributes
2047 .iter()
2048 .find(|kv| kv.key == key)
2049 .and_then(|kv| match kv.value.as_ref()?.value.as_ref()? {
2050 Value::StringValue(s) => Some(s.as_str()),
2051 _ => None,
2052 })
2053 }
2054
2055 fn find_bool(span: &Span, key: &str) -> Option<bool> {
2056 span.attributes
2057 .iter()
2058 .find(|kv| kv.key == key)
2059 .and_then(|kv| match kv.value.as_ref()?.value.as_ref()? {
2060 Value::BoolValue(b) => Some(*b),
2061 _ => None,
2062 })
2063 }
2064
2065 const REQUIRED_KEYS: &[&str] = &[
2069 "table_id",
2070 "table_kind",
2071 "caller",
2072 "schema_total_columns",
2073 "projected_columns",
2074 "has_limit",
2075 "is_success",
2076 "total_duration_us",
2077 "fetch_grpc_requests",
2078 "num_record_batches",
2079 "rows_returned",
2080 "bytes_returned",
2081 ];
2082
2083 #[test]
2086 fn build_table_query_span_minimal_emits_only_required_attributes() {
2087 let info = dummy_table_query_info();
2088
2089 let span = build_table_query_span(
2090 &info,
2091 empty_stats(),
2092 SystemTime::UNIX_EPOCH..SystemTime::UNIX_EPOCH + Duration::from_secs(1),
2093 Duration::from_micros(500),
2094 None,
2095 None,
2096 None,
2097 None,
2098 );
2099
2100 assert_eq!(span.name, "cloud_scan_table");
2102 assert_eq!(span.kind, i32::from(SpanKind::Client));
2103 assert!(span.links.is_empty());
2104
2105 let expected: HashSet<&str> = REQUIRED_KEYS.iter().copied().collect();
2107 let actual = attribute_keys(&span);
2108 assert_eq!(
2109 actual,
2110 expected,
2111 "extra/missing attribute keys: {:?}",
2112 actual.symmetric_difference(&expected).collect::<Vec<_>>()
2113 );
2114
2115 assert_eq!(find_string(&span, "table_id"), Some("tbl-42"));
2117 assert_eq!(find_string(&span, "table_kind"), Some("lance"));
2118 assert_eq!(find_string(&span, "caller"), Some("browser_detail_view"));
2119 assert_eq!(find_int(&span, "schema_total_columns"), Some(12));
2120 assert_eq!(find_int(&span, "projected_columns"), Some(5));
2121 assert_eq!(find_bool(&span, "has_limit"), Some(false));
2122 assert_eq!(find_bool(&span, "is_success"), Some(true));
2123 assert_eq!(find_int(&span, "total_duration_us"), Some(500));
2124 }
2125
2126 #[test]
2127 fn build_table_query_span_records_scan_stats() {
2128 let info = dummy_table_query_info();
2129 let stats = TableScanStatsSnapshot {
2130 grpc_requests: 7,
2131 batches: 7,
2132 rows_returned: 12_345,
2133 bytes_returned: 4_567_890,
2134 };
2135
2136 let span = build_table_query_span(
2137 &info,
2138 stats,
2139 SystemTime::UNIX_EPOCH..SystemTime::UNIX_EPOCH + Duration::from_secs(1),
2140 Duration::from_millis(2),
2141 None,
2142 None,
2143 None,
2144 None,
2145 );
2146
2147 assert_eq!(find_int(&span, "fetch_grpc_requests"), Some(7));
2148 assert_eq!(find_int(&span, "num_record_batches"), Some(7));
2149 assert_eq!(find_int(&span, "rows_returned"), Some(12_345));
2150 assert_eq!(find_int(&span, "bytes_returned"), Some(4_567_890));
2151 }
2152
2153 #[test]
2154 fn build_table_query_span_emits_optional_attributes_when_present() {
2155 let trace_id = opentelemetry::TraceId::from_bytes([3u8; 16]);
2156 let mut info = dummy_table_query_info();
2157 info.has_limit = true;
2158 info.limit_value = Some(500);
2159
2160 let span = build_table_query_span(
2161 &info,
2162 empty_stats(),
2163 SystemTime::UNIX_EPOCH..SystemTime::UNIX_EPOCH + Duration::from_secs(1),
2164 Duration::from_millis(1),
2165 Some(Duration::from_micros(50)),
2166 Some(Duration::from_micros(75)),
2167 Some(trace_id),
2168 Some(QueryErrorKind::Decode.as_str()),
2169 );
2170
2171 let optional = [
2173 "limit_value",
2174 "time_to_first_response_us",
2175 "time_to_first_batch_us",
2176 "error_kind",
2177 ];
2178 let keys = attribute_keys(&span);
2179 for k in optional {
2180 assert!(keys.contains(k), "missing optional attribute: {k}");
2181 }
2182
2183 assert_eq!(find_bool(&span, "is_success"), Some(false));
2185
2186 assert_eq!(find_int(&span, "limit_value"), Some(500));
2187 assert_eq!(find_int(&span, "time_to_first_response_us"), Some(50));
2188 assert_eq!(find_int(&span, "time_to_first_batch_us"), Some(75));
2189 assert_eq!(find_string(&span, "error_kind"), Some("decode"));
2190 assert!(span.links.is_empty());
2191 }
2192
2193 #[test]
2194 fn build_table_query_span_uses_wall_clock_range() {
2195 let info = dummy_table_query_info();
2196 let start = SystemTime::UNIX_EPOCH + Duration::from_secs(2);
2197 let end = SystemTime::UNIX_EPOCH + Duration::from_millis(2_500);
2198
2199 let span = build_table_query_span(
2200 &info,
2201 empty_stats(),
2202 start..end,
2203 Duration::from_micros(0),
2204 None,
2205 None,
2206 None,
2207 None,
2208 );
2209
2210 assert_eq!(span.start_time_unix_nano, 2_000_000_000);
2211 assert_eq!(span.end_time_unix_nano, 2_500_000_000);
2212 }
2213
2214 #[test]
2215 fn build_table_query_span_records_table_kind_and_caller_strings() {
2216 let cases = [
2219 (TableKind::Lance, "lance"),
2220 (TableKind::SystemEntries, "system_entries"),
2221 (TableKind::SystemNamespaces, "system_namespaces"),
2222 (TableKind::Unknown, "unknown"),
2223 ];
2224 for (kind, expected) in cases {
2225 let mut info = dummy_table_query_info();
2226 info.table_kind = kind;
2227 let span = build_table_query_span(
2228 &info,
2229 empty_stats(),
2230 SystemTime::UNIX_EPOCH..SystemTime::UNIX_EPOCH,
2231 Duration::ZERO,
2232 None,
2233 None,
2234 None,
2235 None,
2236 );
2237 assert_eq!(find_string(&span, "table_kind"), Some(expected));
2238 }
2239
2240 let cases = [
2241 (TableQueryCaller::CatalogResolver, "catalog_resolver"),
2242 (TableQueryCaller::EntriesTable, "entries_table"),
2243 (TableQueryCaller::BrowserDetailView, "browser_detail_view"),
2244 ];
2245 for (caller, expected) in cases {
2246 let mut info = dummy_table_query_info();
2247 info.caller = caller;
2248 let span = build_table_query_span(
2249 &info,
2250 empty_stats(),
2251 SystemTime::UNIX_EPOCH..SystemTime::UNIX_EPOCH,
2252 Duration::ZERO,
2253 None,
2254 None,
2255 None,
2256 None,
2257 );
2258 assert_eq!(find_string(&span, "caller"), Some(expected));
2259 }
2260 }
2261
2262 #[test]
2263 fn build_table_query_span_no_limit_value_when_no_limit() {
2264 let info = dummy_table_query_info();
2267 let span = build_table_query_span(
2268 &info,
2269 empty_stats(),
2270 SystemTime::UNIX_EPOCH..SystemTime::UNIX_EPOCH,
2271 Duration::ZERO,
2272 None,
2273 None,
2274 None,
2275 None,
2276 );
2277 assert!(!attribute_keys(&span).contains("limit_value"));
2278 }
2279
2280 #[test]
2283 fn table_kind_from_lance_provider() {
2284 assert!(matches!(
2285 TableKind::from(&lance_provider_details()),
2286 TableKind::Lance
2287 ));
2288 }
2289
2290 #[test]
2291 fn table_kind_from_system_entries_provider() {
2292 let pd = ProviderDetails::SystemTable(SystemTable {
2293 kind: SystemTableKind::Entries,
2294 });
2295 assert!(matches!(TableKind::from(&pd), TableKind::SystemEntries));
2296 }
2297
2298 #[test]
2299 fn table_kind_from_system_namespaces_provider() {
2300 let pd = ProviderDetails::SystemTable(SystemTable {
2301 kind: SystemTableKind::Namespaces,
2302 });
2303 assert!(matches!(TableKind::from(&pd), TableKind::SystemNamespaces));
2304 }
2305
2306 #[test]
2307 fn table_kind_from_system_unspecified_falls_back_to_unknown() {
2308 let pd = ProviderDetails::SystemTable(SystemTable {
2309 kind: SystemTableKind::Unspecified,
2310 });
2311 assert!(matches!(TableKind::from(&pd), TableKind::Unknown));
2312 }
2313
2314 fn make_pending() -> PendingTableQueryAnalytics {
2320 let origin: Origin = "rerun+http://localhost:51234".parse().unwrap();
2321 let analytics = ConnectionAnalytics::disabled_for_test(origin);
2322 analytics.begin_table_query(dummy_table_query_info(), Instant::now())
2323 }
2324
2325 #[tokio::test]
2326 async fn record_first_response_is_once_only() {
2327 let pending = make_pending();
2328 pending.record_first_response();
2329 let first = pending.inner.time_to_first_response.get().copied().unwrap();
2330 std::thread::sleep(Duration::from_millis(2));
2331 pending.record_first_response();
2332 let second = pending.inner.time_to_first_response.get().copied().unwrap();
2333 assert_eq!(first, second, "second call must not overwrite");
2334 }
2335
2336 #[tokio::test]
2337 async fn record_first_batch_is_once_only() {
2338 let pending = make_pending();
2339 pending.record_first_batch();
2340 let first = pending.inner.time_to_first_batch.get().copied().unwrap();
2341 std::thread::sleep(Duration::from_millis(2));
2342 pending.record_first_batch();
2343 let second = pending.inner.time_to_first_batch.get().copied().unwrap();
2344 assert_eq!(first, second);
2345 }
2346
2347 #[tokio::test]
2348 async fn record_error_is_once_only() {
2349 let pending = make_pending();
2350 pending.record_error(QueryErrorKind::GrpcFetch);
2351 pending.record_error(QueryErrorKind::Decode);
2352 assert_eq!(
2353 pending.inner.error_kind.get().copied(),
2354 Some(QueryErrorKind::GrpcFetch.as_str())
2355 );
2356 }
2357
2358 #[tokio::test]
2359 async fn record_trace_id_is_once_only() {
2360 let pending = make_pending();
2361 let first = opentelemetry::TraceId::from_bytes([1u8; 16]);
2362 let second = opentelemetry::TraceId::from_bytes([2u8; 16]);
2363 pending.record_trace_id(first);
2364 pending.record_trace_id(second);
2365 assert_eq!(pending.inner.trace_id.get().copied(), Some(first));
2366 }
2367
2368 #[tokio::test]
2369 async fn record_batch_accumulates_across_calls() {
2370 let pending = make_pending();
2371 pending.record_batch(100, 1_000);
2372 pending.record_batch(50, 500);
2373 pending.record_batch(0, 0); let stats = pending.inner.stats.snapshot();
2375 assert_eq!(stats.grpc_requests, 3);
2376 assert_eq!(stats.batches, 3);
2377 assert_eq!(stats.rows_returned, 150);
2378 assert_eq!(stats.bytes_returned, 1_500);
2379 }
2380}
2381
2382#[cfg(test)]
2383mod explain_metrics_set_tests {
2384 use super::*;
2390
2391 fn dummy_query_info(
2392 filters_pushed_down: usize,
2393 filters_applied_client_side: usize,
2394 entity_path_narrowing_applied: bool,
2395 ) -> QueryInfo {
2396 QueryInfo {
2397 dataset_id: "ds-test".to_owned(),
2398 query_chunks: 7,
2399 query_segments: 3,
2400 query_layers: 2,
2401 query_columns: 11,
2402 query_entities: 5,
2403 query_bytes: 12_345,
2404 query_chunks_per_segment_min: 1,
2405 query_chunks_per_segment_max: 4,
2406 query_chunks_per_segment_mean: 2.5,
2407 query_type: QueryType::LatestAt,
2408 primary_index_name: Some("log_time".to_owned()),
2409 time_to_first_chunk_info: Some(Duration::from_millis(2)),
2410 trace_id: None,
2411 filters_pushed_down,
2412 filters_applied_client_side,
2413 entity_path_narrowing_applied,
2414 filters_total: 0,
2415 filters_signatures: String::new(),
2416 filters_signatures_exact: String::new(),
2417 filters_signatures_inexact: String::new(),
2418 filters_signatures_unsupported: String::new(),
2419 }
2420 }
2421
2422 fn metric_value_by_name(set: &MetricsSet, name: &str) -> Option<usize> {
2424 set.iter()
2425 .find(|m| m.value().name() == name)
2426 .map(|m| m.value().as_usize())
2427 }
2428
2429 #[test]
2430 fn emits_chunk_segment_byte_counts() {
2431 let metrics = QueryMetrics::new(dummy_query_info(0, 0, false));
2432 let set = build_metrics_set_for_explain(&metrics, 1, None);
2433
2434 assert_eq!(metric_value_by_name(&set, "query_chunks"), Some(7));
2435 assert_eq!(metric_value_by_name(&set, "query_segments"), Some(3));
2436 assert_eq!(metric_value_by_name(&set, "query_layers"), Some(2));
2437 assert_eq!(metric_value_by_name(&set, "query_columns"), Some(11));
2438 assert_eq!(metric_value_by_name(&set, "query_entities"), Some(5));
2439 assert_eq!(metric_value_by_name(&set, "query_bytes"), Some(12_345));
2440 assert_eq!(
2441 metric_value_by_name(&set, "query_chunks_per_segment_max"),
2442 Some(4),
2443 );
2444 assert_eq!(
2445 metric_value_by_name(&set, "time_to_first_chunk_info_us"),
2446 Some(2_000),
2447 );
2448 }
2449
2450 #[test]
2451 fn emits_filter_pushdown_counters() {
2452 let metrics = QueryMetrics::new(dummy_query_info(2, 1, false));
2453 let set = build_metrics_set_for_explain(&metrics, 1, None);
2454
2455 assert_eq!(metric_value_by_name(&set, "filters_pushed_down"), Some(2));
2456 assert_eq!(
2457 metric_value_by_name(&set, "filters_applied_client_side"),
2458 Some(1),
2459 );
2460 assert_eq!(
2462 metric_value_by_name(&set, "entity_path_narrowing_applied"),
2463 None,
2464 );
2465 }
2466
2467 #[test]
2468 fn emits_entity_path_narrowing_when_applied() {
2469 let metrics = QueryMetrics::new(dummy_query_info(0, 0, true));
2470 let set = build_metrics_set_for_explain(&metrics, 1, None);
2471
2472 assert_eq!(
2473 metric_value_by_name(&set, "entity_path_narrowing_applied"),
2474 Some(1),
2475 );
2476 }
2477
2478 #[test]
2479 fn emits_runtime_counters_and_partition_count() {
2480 let metrics = QueryMetrics::new(dummy_query_info(0, 0, false));
2481 metrics.fetch_grpc_bytes.fetch_add(1_000, Ordering::Relaxed);
2483 metrics.fetch_grpc_bytes.fetch_add(2_500, Ordering::Relaxed);
2484 metrics
2485 .fetch_direct_max_attempt
2486 .fetch_max(3, Ordering::Relaxed);
2487 metrics
2488 .fetch_direct_max_attempt
2489 .fetch_max(5, Ordering::Relaxed);
2490 metrics
2491 .fetch_direct_max_attempt
2492 .fetch_max(2, Ordering::Relaxed);
2493 metrics
2494 .planned_fetch_batches
2495 .fetch_add(16, Ordering::Relaxed);
2496 metrics
2497 .planned_segment_waves
2498 .fetch_add(1_332, Ordering::Relaxed);
2499 metrics
2500 .segment_admission_limit
2501 .fetch_max(3, Ordering::Relaxed);
2502 metrics.segment_admission_source.store(
2503 crate::metrics_capture::SegmentAdmissionSource::MetricsOnly as u64,
2504 Ordering::Relaxed,
2505 );
2506 metrics.segment_admission_candidate_reason.store(
2507 crate::metrics_capture::SegmentAdmissionCandidateReason::Eligible as u64,
2508 Ordering::Relaxed,
2509 );
2510 metrics
2511 .max_segments_per_fetch_batch
2512 .fetch_max(2, Ordering::Relaxed);
2513 metrics
2514 .max_segments_per_wave
2515 .fetch_max(3, Ordering::Relaxed);
2516 metrics.peak_active_segments.fetch_max(3, Ordering::Relaxed);
2517 metrics
2518 .pipeline_budget_bytes
2519 .store(4 * 1024 * 1024 * 1024, Ordering::Relaxed);
2520 metrics
2521 .pipeline_peak_decoded_bytes
2522 .fetch_max(96 * 1024 * 1024, Ordering::Relaxed);
2523 metrics.pipeline_byte_waits.fetch_add(4, Ordering::Relaxed);
2524 metrics
2525 .segment_admission_waits
2526 .fetch_add(20, Ordering::Relaxed);
2527 metrics
2528 .pipeline_stall_breaker_activations
2529 .fetch_add(1, Ordering::Relaxed);
2530
2531 let set = build_metrics_set_for_explain(&metrics, 4, None);
2532
2533 assert_eq!(metric_value_by_name(&set, "fetch_grpc_bytes"), Some(3_500));
2534 assert_eq!(
2536 metric_value_by_name(&set, "fetch_direct_max_attempt"),
2537 Some(5),
2538 );
2539 assert_eq!(metric_value_by_name(&set, "num_partitions"), Some(4));
2540 assert_eq!(
2541 metric_value_by_name(&set, "planned_fetch_batches"),
2542 Some(16)
2543 );
2544 assert_eq!(
2545 metric_value_by_name(&set, "planned_segment_waves"),
2546 Some(1_332)
2547 );
2548 assert_eq!(
2549 metric_value_by_name(&set, "segment_admission_limit"),
2550 Some(3)
2551 );
2552 assert_eq!(
2553 metric_value_by_name(&set, "segment_admission_source_code"),
2554 Some(crate::metrics_capture::SegmentAdmissionSource::MetricsOnly as usize)
2555 );
2556 assert_eq!(
2557 metric_value_by_name(&set, "segment_admission_candidate_reason_code"),
2558 Some(crate::metrics_capture::SegmentAdmissionCandidateReason::Eligible as usize)
2559 );
2560 assert_eq!(
2561 metric_value_by_name(&set, "max_segments_per_fetch_batch"),
2562 Some(2)
2563 );
2564 assert_eq!(metric_value_by_name(&set, "max_segments_per_wave"), Some(3));
2565 assert_eq!(metric_value_by_name(&set, "peak_active_segments"), Some(3));
2566 assert_eq!(
2567 metric_value_by_name(&set, "pipeline_budget_bytes"),
2568 Some(4 * 1024 * 1024 * 1024)
2569 );
2570 assert_eq!(
2571 metric_value_by_name(&set, "pipeline_peak_decoded_bytes"),
2572 Some(96 * 1024 * 1024)
2573 );
2574 assert_eq!(metric_value_by_name(&set, "pipeline_byte_waits"), Some(4));
2575 assert_eq!(
2576 metric_value_by_name(&set, "segment_admission_waits"),
2577 Some(20)
2578 );
2579 assert_eq!(
2580 metric_value_by_name(&set, "pipeline_stall_breaker_activations"),
2581 Some(1)
2582 );
2583 }
2584}
2585
2586#[cfg(test)]
2587mod expr_filter_signature_tests {
2588 use datafusion::logical_expr::expr::InList;
2589 use datafusion::logical_expr::{Between, col, lit};
2590
2591 use super::*;
2592
2593 #[test]
2594 fn binary_expr_column_on_left() {
2595 let expr = col("frame_nr").gt(lit(100i64));
2596 assert_eq!(expr_filter_signature(&expr), "(frame_nr > 100)");
2597 }
2598
2599 #[test]
2600 fn binary_expr_column_on_right() {
2601 let expr = lit(100i64).lt(col("frame_nr"));
2602 assert_eq!(expr_filter_signature(&expr), "(100 < frame_nr)");
2603 }
2604
2605 #[test]
2606 fn binary_expr_equality() {
2607 let expr = col("rerun_segment_id").eq(lit("some-segment"));
2608 assert_eq!(
2609 expr_filter_signature(&expr),
2610 "(rerun_segment_id = 'some-segment')"
2611 );
2612 }
2613
2614 #[test]
2615 fn between_expr() {
2616 let expr = Expr::Between(Between {
2617 expr: Box::new(col("log_time")),
2618 negated: false,
2619 low: Box::new(lit(0i64)),
2620 high: Box::new(lit(1000i64)),
2621 });
2622 assert_eq!(
2623 expr_filter_signature(&expr),
2624 "(log_time BETWEEN 0 AND 1000)"
2625 );
2626 }
2627
2628 #[test]
2629 fn in_list_expr() {
2630 let expr = Expr::InList(InList {
2631 expr: Box::new(col("rerun_segment_id")),
2632 list: vec![lit("a"), lit("b")],
2633 negated: false,
2634 });
2635 assert_eq!(
2636 expr_filter_signature(&expr),
2637 "rerun_segment_id IN ('a', 'b')"
2638 );
2639 }
2640
2641 #[test]
2642 fn alias_is_transparent() {
2643 let expr = col("frame_nr").gt(lit(5i64)).alias("my_filter");
2644 assert_eq!(expr_filter_signature(&expr), "(frame_nr > 5)");
2645 }
2646
2647 #[test]
2648 fn plain_column_reference() {
2649 let expr = col("something");
2650 assert_eq!(expr_filter_signature(&expr), "something");
2651 }
2652
2653 #[test]
2654 fn semicolon_in_column_name_is_escaped() {
2655 let expr = col("my;col").gt(lit(0i64));
2657 assert_eq!(expr_filter_signature(&expr), "(\"my\\;col\" > 0)");
2658 }
2659
2660 #[test]
2661 fn backslash_in_column_name_is_escaped() {
2662 let expr = col("my\\col").gt(lit(0i64));
2664 assert_eq!(expr_filter_signature(&expr), "(\"my\\\\col\" > 0)");
2665 }
2666}
2667
2668#[cfg(test)]
2669mod filter_capture_span_tests {
2670 use std::collections::HashSet;
2671
2672 use super::*;
2673
2674 fn dummy_info_with_filters(offered: u32, signatures: &str) -> TableQueryInfo {
2675 TableQueryInfo {
2676 table_id: "tbl-1".to_owned(),
2677 table_kind: TableKind::Lance,
2678 caller: TableQueryCaller::CatalogResolver,
2679 schema_total_columns: 4,
2680 projected_columns: 4,
2681 has_limit: false,
2682 limit_value: None,
2683 time_range: web_time::SystemTime::UNIX_EPOCH
2684 ..web_time::SystemTime::UNIX_EPOCH + web_time::Duration::from_secs(1),
2685 filters_total: offered,
2686 filters_signatures: signatures.to_owned(),
2687 }
2688 }
2689
2690 fn attribute_keys(span: &opentelemetry_proto::tonic::trace::v1::Span) -> HashSet<&str> {
2691 span.attributes.iter().map(|kv| kv.key.as_str()).collect()
2692 }
2693
2694 fn find_int(span: &opentelemetry_proto::tonic::trace::v1::Span, key: &str) -> Option<i64> {
2695 use opentelemetry_proto::tonic::common::v1::any_value::Value;
2696 span.attributes
2697 .iter()
2698 .find(|kv| kv.key == key)
2699 .and_then(|kv| match kv.value.as_ref()?.value.as_ref()? {
2700 Value::IntValue(i) => Some(*i),
2701 _ => None,
2702 })
2703 }
2704
2705 fn find_string<'a>(
2706 span: &'a opentelemetry_proto::tonic::trace::v1::Span,
2707 key: &str,
2708 ) -> Option<&'a str> {
2709 use opentelemetry_proto::tonic::common::v1::any_value::Value;
2710 span.attributes
2711 .iter()
2712 .find(|kv| kv.key == key)
2713 .and_then(|kv| match kv.value.as_ref()?.value.as_ref()? {
2714 Value::StringValue(s) => Some(s.as_str()),
2715 _ => None,
2716 })
2717 }
2718
2719 #[test]
2720 fn filters_omitted_when_none_offered() {
2721 let info = dummy_info_with_filters(0, "");
2722 let span = build_table_query_span(
2723 &info,
2724 TableScanStatsSnapshot::default(),
2725 web_time::SystemTime::UNIX_EPOCH
2726 ..web_time::SystemTime::UNIX_EPOCH + web_time::Duration::from_secs(1),
2727 web_time::Duration::ZERO,
2728 None,
2729 None,
2730 None,
2731 None,
2732 );
2733 let keys = attribute_keys(&span);
2734 assert!(!keys.contains("filters_total"), "must be absent when zero");
2735 assert!(
2736 !keys.contains("filters_signatures"),
2737 "must be absent when empty"
2738 );
2739 }
2740
2741 #[test]
2742 fn filters_emitted_when_present() {
2743 let sigs = "(frame_nr > 100);rerun_segment_id IN ('a', 'b')";
2744 let info = dummy_info_with_filters(2, sigs);
2745 let span = build_table_query_span(
2746 &info,
2747 TableScanStatsSnapshot::default(),
2748 web_time::SystemTime::UNIX_EPOCH
2749 ..web_time::SystemTime::UNIX_EPOCH + web_time::Duration::from_secs(1),
2750 web_time::Duration::ZERO,
2751 None,
2752 None,
2753 None,
2754 None,
2755 );
2756 assert_eq!(find_int(&span, "filters_total"), Some(2));
2757 assert_eq!(find_string(&span, "filters_signatures"), Some(sigs));
2758 }
2759}