use std::fmt;
use std::ops::Range;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};
use datafusion::logical_expr::Expr;
use datafusion::physical_plan::metrics::{ExecutionPlanMetricsSet, MetricBuilder, MetricsSet};
use opentelemetry_proto::tonic::{
collector::trace::v1::ExportTraceServiceRequest,
common::v1::any_value::Value,
common::v1::{AnyValue, KeyValue},
resource::v1::Resource,
trace::v1::{ResourceSpans, ScopeSpans, Span, span::SpanKind},
};
use re_async::AsyncRuntimeHandle;
use re_dataframe::QueryExpression;
use re_protos::cloud::v1alpha1::SystemTableKind;
use re_protos::cloud::v1alpha1::ext::ProviderDetails;
use re_redap_client::ConnectionAnalyticsExporter;
use re_uri::Origin;
use web_time::{Duration, Instant, SystemTime};
use crate::metrics_capture::{QueryMetrics, QuerySnapshot, build_query_snapshot};
#[derive(Clone)]
pub(crate) struct ConnectionAnalytics {
inner: Arc<Inner>,
}
impl fmt::Debug for ConnectionAnalytics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ConnectionAnalytics")
.field("origin", &self.inner.origin)
.finish_non_exhaustive()
}
}
struct Inner {
origin: Origin,
async_runtime: Option<AsyncRuntimeHandle>,
exporter: Option<ConnectionAnalyticsExporter>,
}
impl ConnectionAnalytics {
pub fn new(exporter: ConnectionAnalyticsExporter, async_runtime: AsyncRuntimeHandle) -> Self {
let origin = exporter.origin().clone();
Self {
inner: Arc::new(Inner {
origin,
async_runtime: Some(async_runtime),
exporter: Some(exporter),
}),
}
}
#[cfg(test)]
pub(crate) fn disabled_for_test(origin: Origin) -> Self {
Self {
inner: Arc::new(Inner {
origin,
async_runtime: None,
exporter: None,
}),
}
}
pub fn begin_table_query(
&self,
info: TableQueryInfo,
scan_start: Instant,
) -> PendingTableQueryAnalytics {
PendingTableQueryAnalytics {
inner: Arc::new(PendingTableInner {
connection: self.clone(),
info,
stats: SharedTableScanStats::default(),
scan_start,
time_to_first_response: OnceLock::new(),
time_to_first_batch: OnceLock::new(),
trace_id: OnceLock::new(),
error_kind: OnceLock::new(),
}),
}
}
fn send_span(&self, span: Span, trace_id: Option<opentelemetry::TraceId>) {
let this = self.clone();
let fut = async move {
if let Err(err) = this.send_span_impl(span, trace_id).await {
re_log::debug_once!(
"Failed to send analytics to Rerun Hub: {} ({})",
err.code(),
err.message()
);
}
};
if let Some(async_runtime) = &self.inner.async_runtime {
async_runtime.spawn_future(fut);
}
}
async fn send_span_impl(
&self,
mut span: Span,
trace_id: Option<opentelemetry::TraceId>,
) -> tonic::Result<()> {
let Some(exporter) = &self.inner.exporter else {
return Ok(());
};
assign_span_identity(&mut span, trace_id)?;
let mut resource_attributes = vec![kv_string("service.name", "rerun-viewer")];
if let Some(analytics) = re_analytics::Analytics::global_get() {
resource_attributes.push(kv_string("analytics_id", &analytics.config().analytics_id));
}
let export_request = ExportTraceServiceRequest {
resource_spans: vec![ResourceSpans {
resource: Some(Resource {
attributes: resource_attributes,
dropped_attributes_count: 0,
entity_refs: Vec::new(),
}),
scope_spans: vec![ScopeSpans {
scope: None,
spans: vec![span],
schema_url: String::new(),
}],
schema_url: String::new(),
}],
};
exporter.export_trace(export_request, trace_id).await
}
}
fn assign_span_identity(
span: &mut Span,
correlated_trace_id: Option<opentelemetry::TraceId>,
) -> tonic::Result<()> {
span.trace_id = if let Some(trace_id) =
correlated_trace_id.filter(|trace_id| *trace_id != opentelemetry::TraceId::INVALID)
{
trace_id.to_bytes().to_vec()
} else {
random_nonzero_id::<16>()?
};
span.span_id = random_nonzero_id::<8>()?;
Ok(())
}
fn random_nonzero_id<const N: usize>() -> tonic::Result<Vec<u8>> {
loop {
let mut id = [0; N];
getrandom::fill(&mut id).map_err(|err| {
tonic::Status::internal(format!("failed to generate OTLP span ID: {err}"))
})?;
if id.iter().any(|byte| *byte != 0) {
return Ok(id.to_vec());
}
}
}
pub fn begin_query(
connection: Option<ConnectionAnalytics>,
query_info: QueryInfo,
scan_start: Instant,
scan_start_wall: SystemTime,
) -> PendingQueryAnalytics {
PendingQueryAnalytics {
inner: Arc::new(PendingInner {
connection,
metrics: Arc::new(QueryMetrics::new(query_info)),
scan_start,
scan_start_wall,
time_to_first_chunk: OnceLock::new(),
direct_terminal_reason: OnceLock::new(),
error_kind: OnceLock::new(),
}),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QueryType {
Static,
LatestAt,
Range,
Dataframe,
FullScan,
}
impl QueryType {
pub(crate) fn classify(query_expression: &QueryExpression) -> Self {
if query_expression.is_static() {
Self::Static
} else {
let has_latest_at = query_expression.min_latest_at().is_some();
let has_range = query_expression.max_range().is_some();
match (has_latest_at, has_range) {
(true, true) => Self::Dataframe,
(true, false) => Self::LatestAt,
(false, true) => Self::Range,
(false, false) => Self::FullScan,
}
}
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Static => "static",
Self::LatestAt => "latest_at",
Self::Range => "range",
Self::Dataframe => "dataframe",
Self::FullScan => "full_scan",
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct QueryInfo {
pub dataset_id: String,
pub query_chunks: usize,
pub query_segments: usize,
pub query_layers: usize,
pub query_columns: usize,
pub query_entities: usize,
pub query_bytes: u64,
pub target_partitions: usize,
pub query_chunks_per_segment_min: u32,
pub query_chunks_per_segment_max: u32,
pub query_chunks_per_segment_mean: f32,
pub query_type: QueryType,
pub primary_index_name: Option<String>,
pub time_to_first_chunk_info: Option<Duration>,
pub trace_id: Option<opentelemetry::TraceId>,
pub filters_pushed_down: usize,
pub filters_applied_client_side: usize,
pub entity_path_narrowing_applied: bool,
pub filters_total: u32,
pub filters_signatures: String,
pub filters_signatures_exact: String,
pub filters_signatures_inexact: String,
pub filters_signatures_unsupported: String,
}
#[derive(Clone)]
pub(crate) struct PendingQueryAnalytics {
inner: Arc<PendingInner>,
}
impl fmt::Debug for PendingQueryAnalytics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PendingQueryAnalytics")
.finish_non_exhaustive()
}
}
pub(crate) struct PendingInner {
connection: Option<ConnectionAnalytics>,
metrics: Arc<QueryMetrics>,
scan_start: Instant,
scan_start_wall: SystemTime,
time_to_first_chunk: OnceLock<Duration>,
direct_terminal_reason: OnceLock<DirectFetchFailureReason>,
error_kind: OnceLock<&'static str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(target_arch = "wasm32", expect(dead_code))]
pub enum QueryErrorKind {
GrpcFetch,
DirectFetch,
Decode,
Other,
}
impl QueryErrorKind {
pub fn as_str(self) -> &'static str {
match self {
Self::GrpcFetch => "grpc_fetch",
Self::DirectFetch => "direct_fetch",
Self::Decode => "decode",
Self::Other => "other",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirectFetchFailureReason {
Timeout,
Http4xx,
Http5xx,
Connection,
Decode,
SourceChanged,
Other,
}
impl DirectFetchFailureReason {
pub fn as_str(self) -> &'static str {
match self {
Self::Timeout => "timeout",
Self::Http4xx => "http_4xx",
Self::Http5xx => "http_5xx",
Self::Connection => "connection",
Self::Decode => "decode",
Self::SourceChanged => "source_changed",
Self::Other => "other",
}
}
}
impl PendingQueryAnalytics {
pub(crate) fn metrics(&self) -> &Arc<QueryMetrics> {
&self.inner.metrics
}
#[cfg_attr(target_arch = "wasm32", expect(dead_code))]
pub fn record_first_chunk(&self) {
self.inner
.time_to_first_chunk
.get_or_init(|| self.inner.scan_start.elapsed());
}
#[cfg(not(target_arch = "wasm32"))]
pub fn record_direct_terminal_failure(&self, reason: DirectFetchFailureReason) {
#[expect(clippy::let_underscore_must_use)]
let _ = self.inner.direct_terminal_reason.set(reason);
}
pub fn record_error(&self, kind: QueryErrorKind) {
#[expect(clippy::let_underscore_must_use)]
let _ = self.inner.error_kind.set(kind.as_str());
}
pub fn error_kind(&self) -> Option<&'static str> {
self.inner.error_kind.get().copied()
}
pub fn time_to_first_chunk(&self) -> Option<Duration> {
self.inner.time_to_first_chunk.get().copied()
}
pub fn direct_terminal_reason(&self) -> Option<DirectFetchFailureReason> {
self.inner.direct_terminal_reason.get().copied()
}
pub fn total_duration(&self) -> Duration {
self.inner.scan_start.elapsed()
}
}
#[derive(Default)]
#[must_use]
pub(crate) struct TaskFetchStats {
grpc_bytes: u64,
direct_bytes: u64,
direct_retries_total: u64,
direct_requests_retried: u64,
direct_retry_sleep: Duration,
direct_max_attempt: u64,
direct_original_ranges: u64,
direct_merged_ranges: u64,
decode: Duration,
}
#[cfg_attr(target_arch = "wasm32", expect(dead_code))]
impl TaskFetchStats {
pub fn record_grpc_bytes(&mut self, bytes: u64) {
self.grpc_bytes += bytes;
}
pub fn record_decode(&mut self, elapsed: Duration) {
self.decode = self.decode.saturating_add(elapsed);
}
pub fn record_direct_bytes(&mut self, bytes: u64) {
self.direct_bytes += bytes;
}
pub fn record_direct_retry(&mut self, sleep: Duration, attempt: u64) {
self.direct_retries_total += 1;
self.direct_retry_sleep = self.direct_retry_sleep.saturating_add(sleep);
self.direct_max_attempt = self.direct_max_attempt.max(attempt);
}
pub fn record_direct_request_was_retried(&mut self) {
self.direct_requests_retried += 1;
}
pub fn record_direct_ranges(&mut self, original: u64, merged: u64) {
self.direct_original_ranges += original;
self.direct_merged_ranges += merged;
}
#[expect(
clippy::needless_pass_by_value,
reason = "Prevent double-counting stats"
)]
pub fn merge_from(&mut self, other: Self) {
let Self {
grpc_bytes,
direct_bytes,
direct_retries_total,
direct_requests_retried,
direct_retry_sleep: direct_retry_sleep_us,
direct_max_attempt,
direct_original_ranges,
direct_merged_ranges,
decode,
} = other;
self.grpc_bytes += grpc_bytes;
self.direct_bytes += direct_bytes;
self.direct_retries_total += direct_retries_total;
self.direct_requests_retried += direct_requests_retried;
self.direct_retry_sleep += direct_retry_sleep_us;
self.direct_max_attempt = self.direct_max_attempt.max(direct_max_attempt);
self.direct_original_ranges += direct_original_ranges;
self.direct_merged_ranges += direct_merged_ranges;
self.decode = self.decode.saturating_add(decode);
}
pub fn flush_into(self, metrics: &QueryMetrics) {
let Self {
grpc_bytes,
direct_bytes,
direct_retries_total,
direct_requests_retried,
direct_retry_sleep,
direct_max_attempt,
direct_original_ranges,
direct_merged_ranges,
decode,
} = self;
if grpc_bytes != 0 {
metrics
.fetch_grpc_bytes
.fetch_add(grpc_bytes, Ordering::Relaxed);
}
if direct_bytes != 0 {
metrics
.fetch_direct_bytes
.fetch_add(direct_bytes, Ordering::Relaxed);
}
if direct_retries_total != 0 {
metrics
.fetch_direct_retries
.fetch_add(direct_retries_total, Ordering::Relaxed);
}
if direct_requests_retried != 0 {
metrics
.fetch_direct_requests_retried
.fetch_add(direct_requests_retried, Ordering::Relaxed);
}
if !direct_retry_sleep.is_zero() {
metrics
.fetch_direct_retry_sleep_us
.fetch_add(direct_retry_sleep.as_micros() as u64, Ordering::Relaxed);
}
if direct_max_attempt != 0 {
metrics
.fetch_direct_max_attempt
.fetch_max(direct_max_attempt, Ordering::Relaxed);
}
if direct_original_ranges != 0 {
metrics
.fetch_direct_original_ranges
.fetch_add(direct_original_ranges, Ordering::Relaxed);
}
if direct_merged_ranges != 0 {
metrics
.fetch_direct_merged_ranges
.fetch_add(direct_merged_ranges, Ordering::Relaxed);
}
if !decode.is_zero() {
metrics
.decode_time_us
.fetch_add(decode.as_micros() as u64, Ordering::Relaxed);
}
}
pub fn try_flush_into(
self,
analytics: &PendingQueryAnalytics,
result: Result<(), QueryErrorKind>,
) {
self.flush_into(analytics.metrics());
if let Err(err) = result {
analytics.record_error(err);
}
}
}
pub(crate) fn build_metrics_set_for_explain(
metrics: &QueryMetrics,
num_partitions: usize,
time_to_first_chunk: Option<Duration>,
) -> MetricsSet {
let set = ExecutionPlanMetricsSet::new();
let info = &metrics.query_info;
let load = |a: &AtomicU64| a.load(Ordering::Relaxed) as usize;
let global = |name: &'static str| MetricBuilder::new(&set).global_counter(name);
global("query_chunks").add(info.query_chunks);
global("query_segments").add(info.query_segments);
global("query_layers").add(info.query_layers);
global("query_columns").add(info.query_columns);
global("query_entities").add(info.query_entities);
global("query_bytes").add(info.query_bytes as usize);
global("query_chunks_per_segment_min").add(info.query_chunks_per_segment_min as usize);
global("query_chunks_per_segment_max").add(info.query_chunks_per_segment_max as usize);
global("filters_pushed_down").add(info.filters_pushed_down);
global("filters_applied_client_side").add(info.filters_applied_client_side);
if info.entity_path_narrowing_applied {
global("entity_path_narrowing_applied").add(1);
}
if let Some(ttfci) = info.time_to_first_chunk_info {
global("time_to_first_chunk_info_us").add(ttfci.as_micros() as usize);
}
global("num_partitions").add(num_partitions);
global("fetch_grpc_requests").add(load(&metrics.fetch_grpc_requests));
global("fetch_grpc_bytes").add(load(&metrics.fetch_grpc_bytes));
global("fetch_direct_requests").add(load(&metrics.fetch_direct_requests));
global("fetch_direct_bytes").add(load(&metrics.fetch_direct_bytes));
global("fetch_direct_retries").add(load(&metrics.fetch_direct_retries));
global("fetch_direct_requests_retried").add(load(&metrics.fetch_direct_requests_retried));
global("fetch_direct_retry_sleep_us").add(load(&metrics.fetch_direct_retry_sleep_us));
global("fetch_direct_max_attempt").add(load(&metrics.fetch_direct_max_attempt));
global("fetch_direct_original_ranges").add(load(&metrics.fetch_direct_original_ranges));
global("fetch_direct_merged_ranges").add(load(&metrics.fetch_direct_merged_ranges));
global("planned_fetch_batches").add(load(&metrics.planned_fetch_batches));
global("planned_segment_waves").add(load(&metrics.planned_segment_waves));
global("segment_admission_limit").add(load(&metrics.segment_admission_limit));
global("segment_admission_candidate_limit")
.add(load(&metrics.segment_admission_candidate_limit));
global("segment_admission_source_code").add(load(&metrics.segment_admission_source));
global("segment_admission_candidate_reason_code")
.add(load(&metrics.segment_admission_candidate_reason));
global("segment_admission_adaptive_enabled")
.add(load(&metrics.segment_admission_adaptive_enabled));
global("segment_admission_profile_segment_count")
.add(load(&metrics.segment_admission_profile_segment_count));
global("segment_admission_profile_complete")
.add(load(&metrics.segment_admission_profile_complete));
global("segment_admission_p95_segment_bytes")
.add(load(&metrics.segment_admission_p95_segment_bytes));
global("segment_admission_max_segment_bytes")
.add(load(&metrics.segment_admission_max_segment_bytes));
global("segment_admission_largest_window_bytes")
.add(load(&metrics.segment_admission_largest_window_bytes));
global("max_segments_per_fetch_batch").add(load(&metrics.max_segments_per_fetch_batch));
global("max_segments_per_wave").add(load(&metrics.max_segments_per_wave));
global("peak_active_segments").add(load(&metrics.peak_active_segments));
global("pipeline_budget_bytes").add(load(&metrics.pipeline_budget_bytes));
global("pipeline_peak_decoded_bytes").add(load(&metrics.pipeline_peak_decoded_bytes));
global("pipeline_byte_waits").add(load(&metrics.pipeline_byte_waits));
global("segment_admission_waits").add(load(&metrics.segment_admission_waits));
global("pipeline_stall_breaker_activations")
.add(load(&metrics.pipeline_stall_breaker_activations));
global("delivered_rows").add(load(&metrics.delivered_rows));
global("delivered_bytes").add(load(&metrics.delivered_bytes));
global("decode_duration_us").add(load(&metrics.decode_time_us));
global("peak_inflight_fetches").add(load(&metrics.peak_inflight_fetches));
if let Some(ttfr) = time_to_first_chunk {
MetricBuilder::new(&set)
.subset_time("time_to_first_chunk", 0)
.add_duration(ttfr);
}
set.clone_inner()
}
impl Drop for PendingInner {
fn drop(&mut self) {
let Some(connection) = self.connection.as_ref() else {
return;
};
let total_duration = self.scan_start.elapsed();
let scan_end_wall = SystemTime::now();
let time_to_first_chunk = self.time_to_first_chunk.get().copied();
let direct_terminal_reason = self.direct_terminal_reason.get().copied();
let error_kind = self.error_kind.get().copied();
let trace_id = self.metrics.query_info.trace_id;
let snapshot = build_query_snapshot(
&self.metrics,
total_duration,
time_to_first_chunk,
error_kind,
direct_terminal_reason,
);
let span = build_query_span(&snapshot, self.scan_start_wall..scan_end_wall);
connection.send_span(span, trace_id);
}
}
fn build_query_span(snap: &QuerySnapshot, wall_clock_range: Range<SystemTime>) -> Span {
let start_time_unix_nano = nanos_since_epoch(&wall_clock_range.start);
let end_time_unix_nano = nanos_since_epoch(&wall_clock_range.end);
let QuerySnapshot {
query_info:
QueryInfo {
dataset_id,
query_chunks,
query_segments,
query_layers,
query_columns,
query_entities,
query_bytes,
target_partitions,
query_chunks_per_segment_min,
query_chunks_per_segment_max,
query_chunks_per_segment_mean,
query_type,
primary_index_name,
time_to_first_chunk_info,
trace_id: _,
filters_pushed_down,
filters_applied_client_side,
entity_path_narrowing_applied,
filters_total,
filters_signatures,
filters_signatures_exact,
filters_signatures_inexact,
filters_signatures_unsupported,
},
total_duration,
time_to_first_chunk,
error_kind,
direct_terminal_reason,
fetch_grpc_requests,
fetch_grpc_bytes,
fetch_direct_requests,
fetch_direct_bytes,
fetch_direct_retries,
fetch_direct_requests_retried,
fetch_direct_retry_sleep,
fetch_direct_max_attempt,
fetch_direct_original_ranges,
fetch_direct_merged_ranges,
planned_fetch_batches,
planned_segment_waves,
segment_admission_limit,
segment_admission_candidate_limit,
segment_admission_source,
segment_admission_candidate_reason,
segment_admission_adaptive_enabled,
segment_admission_profile_segment_count,
segment_admission_profile_complete,
segment_admission_p95_segment_bytes,
segment_admission_max_segment_bytes,
segment_admission_largest_window_bytes,
max_segments_per_fetch_batch,
max_segments_per_wave,
peak_active_segments,
pipeline_budget_bytes,
pipeline_peak_decoded_bytes,
pipeline_byte_waits,
segment_admission_waits,
pipeline_stall_breaker_activations,
delivered_rows,
delivered_bytes,
decode_duration,
peak_inflight_fetches,
} = snap;
#[expect(
clippy::cast_possible_wrap,
reason = "OTLP proto uses i64 for int values"
)]
let mut attributes = vec![
kv_string("dataset_id", dataset_id),
kv_int("query_chunks", *query_chunks as i64),
kv_int("query_segments", *query_segments as i64),
kv_int("query_layers", *query_layers as i64),
kv_int("query_columns", *query_columns as i64),
kv_int("query_entities", *query_entities as i64),
kv_int("query_bytes", *query_bytes as i64),
kv_int(
"query_chunks_per_segment_min",
i64::from(*query_chunks_per_segment_min),
),
kv_int(
"query_chunks_per_segment_max",
i64::from(*query_chunks_per_segment_max),
),
kv_double(
"query_chunks_per_segment_mean",
f64::from(*query_chunks_per_segment_mean),
),
kv_string("query_type", query_type.as_str()),
kv_int("total_duration_us", total_duration.as_micros() as i64),
kv_bool("is_success", error_kind.is_none()),
kv_int("fetch_grpc_requests", *fetch_grpc_requests as i64),
kv_int("fetch_grpc_bytes", *fetch_grpc_bytes as i64),
kv_int("fetch_direct_requests", *fetch_direct_requests as i64),
kv_int("fetch_direct_bytes", *fetch_direct_bytes as i64),
kv_int("fetch_direct_retries", *fetch_direct_retries as i64),
kv_int(
"fetch_direct_requests_retried",
*fetch_direct_requests_retried as i64,
),
kv_int(
"fetch_direct_retry_sleep_us",
fetch_direct_retry_sleep.as_micros() as i64,
),
kv_int("fetch_direct_max_attempt", *fetch_direct_max_attempt as i64),
kv_int(
"fetch_direct_original_ranges",
*fetch_direct_original_ranges as i64,
),
kv_int(
"fetch_direct_merged_ranges",
*fetch_direct_merged_ranges as i64,
),
kv_int("planned_fetch_batches", *planned_fetch_batches as i64),
kv_int("planned_segment_waves", *planned_segment_waves as i64),
kv_int("segment_admission_limit", *segment_admission_limit as i64),
kv_int(
"segment_admission_candidate_limit",
*segment_admission_candidate_limit as i64,
),
kv_string("segment_admission_source", segment_admission_source),
kv_string(
"segment_admission_candidate_reason",
segment_admission_candidate_reason,
),
kv_bool(
"segment_admission_adaptive_enabled",
*segment_admission_adaptive_enabled,
),
kv_int(
"segment_admission_profile_segment_count",
*segment_admission_profile_segment_count as i64,
),
kv_bool(
"segment_admission_profile_complete",
*segment_admission_profile_complete,
),
kv_int(
"segment_admission_p95_segment_bytes",
*segment_admission_p95_segment_bytes as i64,
),
kv_int(
"segment_admission_max_segment_bytes",
*segment_admission_max_segment_bytes as i64,
),
kv_int(
"segment_admission_largest_window_bytes",
*segment_admission_largest_window_bytes as i64,
),
kv_int(
"max_segments_per_fetch_batch",
*max_segments_per_fetch_batch as i64,
),
kv_int("max_segments_per_wave", *max_segments_per_wave as i64),
kv_int("peak_active_segments", *peak_active_segments as i64),
kv_int("pipeline_budget_bytes", *pipeline_budget_bytes as i64),
kv_int(
"pipeline_peak_decoded_bytes",
*pipeline_peak_decoded_bytes as i64,
),
kv_int("pipeline_byte_waits", *pipeline_byte_waits as i64),
kv_int("segment_admission_waits", *segment_admission_waits as i64),
kv_int(
"pipeline_stall_breaker_activations",
*pipeline_stall_breaker_activations as i64,
),
kv_int("filters_pushed_down", *filters_pushed_down as i64),
kv_int(
"filters_applied_client_side",
*filters_applied_client_side as i64,
),
kv_bool(
"entity_path_narrowing_applied",
*entity_path_narrowing_applied,
),
kv_int("query_target_partitions", *target_partitions as i64),
kv_int("delivered_rows", *delivered_rows as i64),
kv_int("delivered_bytes", *delivered_bytes as i64),
kv_int("decode_duration_us", decode_duration.as_micros() as i64),
kv_int("peak_inflight_fetches", *peak_inflight_fetches as i64),
];
if *filters_total > 0 {
attributes.push(kv_int("filters_total", i64::from(*filters_total)));
}
if !filters_signatures.is_empty() {
attributes.push(kv_string("filters_signatures", filters_signatures));
}
if !filters_signatures_exact.is_empty() {
attributes.push(kv_string(
"filters_signatures_exact",
filters_signatures_exact,
));
}
if !filters_signatures_inexact.is_empty() {
attributes.push(kv_string(
"filters_signatures_inexact",
filters_signatures_inexact,
));
}
if !filters_signatures_unsupported.is_empty() {
attributes.push(kv_string(
"filters_signatures_unsupported",
filters_signatures_unsupported,
));
}
if let Some(name) = primary_index_name.as_deref() {
attributes.push(kv_string("primary_index_name", name));
}
if let Some(ttfci) = time_to_first_chunk_info {
attributes.push(kv_int(
"time_to_first_chunk_info_us",
ttfci.as_micros() as i64,
));
}
if let Some(ttfr) = time_to_first_chunk {
attributes.push(kv_int("time_to_first_chunk_us", ttfr.as_micros() as i64));
}
if let Some(reason) = direct_terminal_reason {
attributes.push(kv_string("fetch_direct_terminal_reason", reason.as_str()));
}
if let Some(kind) = error_kind {
attributes.push(kv_string("error_kind", kind));
}
Span {
name: "cloud_query_dataset".to_owned(),
kind: SpanKind::Client.into(),
start_time_unix_nano,
end_time_unix_nano,
attributes,
..Default::default()
}
}
fn scrub_expr_literals(expr: &Expr) -> Expr {
use datafusion::common::tree_node::{Transformed, TreeNode as _};
use datafusion::logical_expr::expr::Placeholder;
expr.clone()
.transform(|e| {
if matches!(e, Expr::Literal(..)) {
Ok(Transformed::yes(Expr::Placeholder(
Placeholder::new_with_field("?".to_owned(), None),
)))
} else {
Ok(Transformed::no(e))
}
})
.map(|transformed| transformed.data)
.unwrap_or_else(|_| expr.clone())
}
pub(crate) fn expr_filter_signature(expr: &Expr) -> String {
let scrubbed = scrub_expr_literals(expr);
let sql = datafusion::sql::unparser::expr_to_sql(&scrubbed)
.map(|e| e.to_string())
.unwrap_or_else(|_| scrubbed.variant_name().to_owned());
escape_sig_str(&sql)
}
fn escape_sig_str(s: &str) -> String {
s.replace('\\', "\\\\").replace(';', "\\;")
}
#[derive(Clone, Copy, Debug)]
pub enum TableKind {
Lance,
SystemEntries,
SystemNamespaces,
Unknown,
}
impl TableKind {
const fn as_str(self) -> &'static str {
match self {
Self::Lance => "lance",
Self::SystemEntries => "system_entries",
Self::SystemNamespaces => "system_namespaces",
Self::Unknown => "unknown",
}
}
}
impl From<&ProviderDetails> for TableKind {
fn from(details: &ProviderDetails) -> Self {
match details {
ProviderDetails::LanceTable(_) => Self::Lance,
ProviderDetails::SystemTable(t) => match t.kind {
SystemTableKind::Entries => Self::SystemEntries,
SystemTableKind::Namespaces => Self::SystemNamespaces,
SystemTableKind::Unspecified => Self::Unknown,
},
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum TableQueryCaller {
CatalogResolver,
EntriesTable,
BrowserDetailView,
}
impl TableQueryCaller {
const fn as_str(self) -> &'static str {
match self {
Self::CatalogResolver => "catalog_resolver",
Self::EntriesTable => "entries_table",
Self::BrowserDetailView => "browser_detail_view",
}
}
}
#[derive(Clone, Debug)]
pub struct TableQueryInfo {
pub table_id: String,
pub table_kind: TableKind,
pub caller: TableQueryCaller,
pub schema_total_columns: u32,
pub projected_columns: u32,
pub has_limit: bool,
pub limit_value: Option<u64>,
pub time_range: Range<SystemTime>,
pub filters_total: u32,
pub filters_signatures: String,
}
#[derive(Default)]
pub(crate) struct SharedTableScanStats {
grpc_requests: AtomicU64,
batches: AtomicU64,
rows_returned: AtomicU64,
bytes_returned: AtomicU64,
}
#[derive(Default, Clone, Copy)]
pub(crate) struct TableScanStatsSnapshot {
pub grpc_requests: u64,
pub batches: u64,
pub rows_returned: u64,
pub bytes_returned: u64,
}
impl SharedTableScanStats {
fn snapshot(&self) -> TableScanStatsSnapshot {
TableScanStatsSnapshot {
grpc_requests: self.grpc_requests.load(Ordering::Relaxed),
batches: self.batches.load(Ordering::Relaxed),
rows_returned: self.rows_returned.load(Ordering::Relaxed),
bytes_returned: self.bytes_returned.load(Ordering::Relaxed),
}
}
}
#[derive(Clone)]
pub(crate) struct PendingTableQueryAnalytics {
inner: Arc<PendingTableInner>,
}
impl fmt::Debug for PendingTableQueryAnalytics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PendingTableQueryAnalytics")
.finish_non_exhaustive()
}
}
struct PendingTableInner {
connection: ConnectionAnalytics,
info: TableQueryInfo,
stats: SharedTableScanStats,
scan_start: Instant,
time_to_first_response: OnceLock<Duration>,
time_to_first_batch: OnceLock<Duration>,
trace_id: OnceLock<opentelemetry::TraceId>,
error_kind: OnceLock<&'static str>,
}
impl PendingTableQueryAnalytics {
pub fn record_trace_id(&self, trace_id: opentelemetry::TraceId) {
#[expect(clippy::let_underscore_must_use)]
let _ = self.inner.trace_id.set(trace_id);
}
pub fn record_first_response(&self) {
self.inner
.time_to_first_response
.get_or_init(|| self.inner.scan_start.elapsed());
}
pub fn record_first_batch(&self) {
self.inner
.time_to_first_batch
.get_or_init(|| self.inner.scan_start.elapsed());
}
pub fn record_batch(&self, num_rows: u64, num_bytes: u64) {
self.inner
.stats
.grpc_requests
.fetch_add(1, Ordering::Relaxed);
self.inner.stats.batches.fetch_add(1, Ordering::Relaxed);
if num_rows != 0 {
self.inner
.stats
.rows_returned
.fetch_add(num_rows, Ordering::Relaxed);
}
if num_bytes != 0 {
self.inner
.stats
.bytes_returned
.fetch_add(num_bytes, Ordering::Relaxed);
}
}
pub fn record_error(&self, kind: QueryErrorKind) {
#[expect(clippy::let_underscore_must_use)]
let _ = self.inner.error_kind.set(kind.as_str());
}
#[cfg(test)]
pub(crate) fn build_span_for_test(&self) -> Span {
let mut span = self.inner.build_span();
assign_span_identity(&mut span, self.inner.trace_id.get().copied()).unwrap();
span
}
}
impl PendingTableInner {
fn build_span(&self) -> Span {
let total_duration = self.scan_start.elapsed();
let scan_end_wall = SystemTime::now();
let stats = self.stats.snapshot();
build_table_query_span(
&self.info,
stats,
self.info.time_range.start..scan_end_wall,
total_duration,
self.time_to_first_response.get().copied(),
self.time_to_first_batch.get().copied(),
self.trace_id.get().copied(),
self.error_kind.get().copied(),
)
}
}
impl Drop for PendingTableInner {
fn drop(&mut self) {
let span = self.build_span();
let trace_id = self.trace_id.get().copied();
self.connection.send_span(span, trace_id);
}
}
pub(crate) fn build_table_query_span(
info: &TableQueryInfo,
stats: TableScanStatsSnapshot,
wall_clock_range: Range<SystemTime>,
total_duration: Duration,
time_to_first_response: Option<Duration>,
time_to_first_batch: Option<Duration>,
_trace_id: Option<opentelemetry::TraceId>,
error_kind: Option<&'static str>,
) -> Span {
let TableQueryInfo {
ref table_id,
table_kind,
caller,
schema_total_columns,
projected_columns,
has_limit,
limit_value,
time_range: _,
filters_total,
ref filters_signatures,
} = *info;
let start_time_unix_nano = nanos_since_epoch(&wall_clock_range.start);
let end_time_unix_nano = nanos_since_epoch(&wall_clock_range.end);
#[expect(
clippy::cast_possible_wrap,
reason = "OTLP proto uses i64 for int values"
)]
let mut attributes = vec![
kv_string("table_id", table_id),
kv_string("table_kind", table_kind.as_str()),
kv_string("caller", caller.as_str()),
kv_int("schema_total_columns", i64::from(schema_total_columns)),
kv_int("projected_columns", i64::from(projected_columns)),
kv_bool("has_limit", has_limit),
kv_bool("is_success", error_kind.is_none()),
kv_int("total_duration_us", total_duration.as_micros() as i64),
kv_int("fetch_grpc_requests", stats.grpc_requests as i64),
kv_int("num_record_batches", stats.batches as i64),
kv_int("rows_returned", stats.rows_returned as i64),
kv_int("bytes_returned", stats.bytes_returned as i64),
];
if let Some(value) = limit_value {
#[expect(
clippy::cast_possible_wrap,
reason = "OTLP proto uses i64 for int values"
)]
attributes.push(kv_int("limit_value", value as i64));
}
if let Some(ttfr) = time_to_first_response {
attributes.push(kv_int("time_to_first_response_us", ttfr.as_micros() as i64));
}
if let Some(ttfb) = time_to_first_batch {
attributes.push(kv_int("time_to_first_batch_us", ttfb.as_micros() as i64));
}
if let Some(kind) = error_kind {
attributes.push(kv_string("error_kind", kind));
}
if filters_total > 0 {
attributes.push(kv_int("filters_total", i64::from(filters_total)));
}
if !filters_signatures.is_empty() {
attributes.push(kv_string("filters_signatures", filters_signatures));
}
Span {
name: "cloud_scan_table".to_owned(),
kind: SpanKind::Client.into(),
start_time_unix_nano,
end_time_unix_nano,
attributes,
..Default::default()
}
}
fn nanos_since_epoch(time: &SystemTime) -> u64 {
time.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64
}
fn kv_string(key: &str, value: &str) -> KeyValue {
KeyValue {
key: key.to_owned(),
value: Some(AnyValue {
value: Some(Value::StringValue(value.to_owned())),
}),
key_strindex: 0,
}
}
fn kv_int(key: &str, value: impl Into<i64>) -> KeyValue {
KeyValue {
key: key.to_owned(),
value: Some(AnyValue {
value: Some(Value::IntValue(value.into())),
}),
key_strindex: 0,
}
}
fn kv_bool(key: &str, value: bool) -> KeyValue {
KeyValue {
key: key.to_owned(),
value: Some(AnyValue {
value: Some(Value::BoolValue(value)),
}),
key_strindex: 0,
}
}
fn kv_double(key: &str, value: f64) -> KeyValue {
KeyValue {
key: key.to_owned(),
value: Some(AnyValue {
value: Some(Value::DoubleValue(value)),
}),
key_strindex: 0,
}
}
#[cfg(test)]
mod test_explain_metrics_set;
#[cfg(test)]
mod test_expr_filter_signature;
#[cfg(test)]
mod test_filter_capture_span;
#[cfg(test)]
mod test_table_query;
#[cfg(test)]
mod tests;