Skip to main content

frontend/instance/
jaeger.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17
18use async_trait::async_trait;
19use auth::{JAEGER_QUERY, PermissionReq, PermissionTableTarget, PermissionTableTargets};
20use catalog::CatalogManagerRef;
21use common_catalog::consts::{
22    TRACE_TABLE_NAME, trace_operations_table_name, trace_services_table_name,
23};
24use common_function::function::FunctionRef;
25use common_function::scalars::json::json_get::{
26    JsonGetBool, JsonGetFloat, JsonGetInt, JsonGetString, JsonGetWithType,
27};
28use common_function::scalars::udf::create_udf;
29use common_query::{Output, OutputData};
30use common_recordbatch::adapter::RecordBatchStreamAdapter;
31use common_recordbatch::util;
32use common_telemetry::warn;
33use datafusion::common::ScalarValue;
34use datafusion::dataframe::DataFrame;
35use datafusion::execution::SessionStateBuilder;
36use datafusion::execution::context::SessionContext;
37use datafusion::functions::core::expr_fn::coalesce;
38use datafusion::functions_aggregate::expr_fn::max;
39use datafusion::functions_window::expr_fn::row_number;
40use datafusion_expr::select_expr::SelectExpr;
41use datafusion_expr::{Expr, ExprFunctionExt, SortExpr, col, lit, lit_timestamp_nano, wildcard};
42use query::QueryEngineRef;
43use serde_json::Value as JsonValue;
44use servers::error::{
45    AuthSnafu, CollectRecordbatchSnafu, DataFusionSnafu, Result as ServerResult, TableNotFoundSnafu,
46};
47use servers::http::jaeger::{JAEGER_QUERY_TABLE_NAME_KEY, QueryTraceParams, TraceUserAgent};
48use servers::otlp::trace::{
49    DURATION_NANO_COLUMN, KEY_OTEL_STATUS_ERROR_KEY, RESOURCE_ATTRIBUTES_COLUMN,
50    SERVICE_NAME_COLUMN, SPAN_ATTRIBUTES_COLUMN, SPAN_KIND_COLUMN, SPAN_KIND_PREFIX,
51    SPAN_NAME_COLUMN, SPAN_STATUS_CODE, SPAN_STATUS_ERROR, TIMESTAMP_COLUMN, TRACE_ID_COLUMN,
52};
53use servers::query_handler::JaegerQueryHandler;
54use session::context::QueryContextRef;
55use snafu::{OptionExt, ResultExt};
56use table::TableRef;
57use table::table::adapter::DfTableProviderAdapter;
58
59use crate::instance::Instance;
60
61const DEFAULT_LIMIT: usize = 2000;
62const KEY_RN: &str = "greptime_rn";
63const KEY_LATEST_MATCH: &str = "greptime_latest_match";
64
65impl Instance {
66    async fn check_jaeger_query_permission(&self, ctx: &QueryContextRef) -> ServerResult<()> {
67        let table = ctx
68            .extension(JAEGER_QUERY_TABLE_NAME_KEY)
69            .unwrap_or(TRACE_TABLE_NAME);
70        let targets = PermissionTableTargets::resolved(vec![PermissionTableTarget::new(
71            ctx.current_catalog(),
72            ctx.current_schema(),
73            table,
74        )]);
75        let targets = self.resolve_query_permission_targets(targets, ctx).await?;
76        self.check_table_permission(ctx, PermissionReq::Action(JAEGER_QUERY), targets)
77            .context(AuthSnafu)?;
78        Ok(())
79    }
80}
81
82#[async_trait]
83impl JaegerQueryHandler for Instance {
84    async fn get_services(&self, ctx: QueryContextRef) -> ServerResult<Output> {
85        self.check_jaeger_query_permission(&ctx).await?;
86
87        // It's equivalent to `SELECT DISTINCT(service_name) FROM {db}.{trace_table}`.
88        Ok(query_trace_table(
89            ctx,
90            self,
91            vec![SelectExpr::from(col(SERVICE_NAME_COLUMN))],
92            vec![],
93            vec![],
94            None,
95            None,
96            vec![col(SERVICE_NAME_COLUMN)],
97        )
98        .await?)
99    }
100
101    async fn get_operations(
102        &self,
103        ctx: QueryContextRef,
104        service_name: &str,
105        span_kind: Option<&str>,
106    ) -> ServerResult<Output> {
107        self.check_jaeger_query_permission(&ctx).await?;
108
109        let mut filters = vec![col(SERVICE_NAME_COLUMN).eq(lit(service_name))];
110
111        if let Some(span_kind) = span_kind {
112            filters.push(col(SPAN_KIND_COLUMN).eq(lit(format!(
113                "{}{}",
114                SPAN_KIND_PREFIX,
115                span_kind.to_uppercase()
116            ))));
117        }
118
119        // It's equivalent to the following SQL query:
120        //
121        // ```
122        // SELECT DISTINCT span_name, span_kind
123        // FROM
124        //   {db}.{trace_table}
125        // WHERE
126        //   service_name = '{service_name}' AND
127        //   span_kind = '{span_kind}'
128        // ORDER BY
129        //   span_name ASC
130        // ```.
131        Ok(query_trace_table(
132            ctx,
133            self,
134            vec![
135                SelectExpr::from(col(SPAN_NAME_COLUMN)),
136                SelectExpr::from(col(SPAN_KIND_COLUMN)),
137                SelectExpr::from(col(SERVICE_NAME_COLUMN)),
138                SelectExpr::from(col(TIMESTAMP_COLUMN)),
139            ],
140            filters,
141            vec![col(SPAN_NAME_COLUMN).sort(true, false)], // Sort by span_name in ascending order.
142            Some(DEFAULT_LIMIT),
143            None,
144            vec![col(SPAN_NAME_COLUMN), col(SPAN_KIND_COLUMN)],
145        )
146        .await?)
147    }
148
149    async fn get_trace(
150        &self,
151        ctx: QueryContextRef,
152        trace_id: &str,
153        start_time: Option<i64>,
154        end_time: Option<i64>,
155        limit: Option<usize>,
156    ) -> ServerResult<Output> {
157        self.check_jaeger_query_permission(&ctx).await?;
158
159        // It's equivalent to the following SQL query:
160        //
161        // ```
162        // SELECT
163        //   *
164        // FROM
165        //   {db}.{trace_table}
166        // WHERE
167        //   trace_id = '{trace_id}' AND
168        //   timestamp >= {start_time} AND
169        //   timestamp <= {end_time}
170        // ORDER BY
171        //   timestamp DESC
172        // ```.
173        let selects = vec![wildcard()];
174
175        let mut filters = vec![col(TRACE_ID_COLUMN).eq(lit(trace_id))];
176
177        if let Some(start_time) = start_time {
178            filters.push(col(TIMESTAMP_COLUMN).gt_eq(lit_timestamp_nano(start_time)));
179        }
180
181        if let Some(end_time) = end_time {
182            filters.push(col(TIMESTAMP_COLUMN).lt_eq(lit_timestamp_nano(end_time)));
183        }
184
185        Ok(query_trace_table(
186            ctx,
187            self,
188            selects,
189            filters,
190            vec![col(TIMESTAMP_COLUMN).sort(false, false)], // Sort by timestamp in descending order.
191            limit,
192            None,
193            vec![],
194        )
195        .await?)
196    }
197
198    async fn find_traces(
199        &self,
200        ctx: QueryContextRef,
201        query_params: QueryTraceParams,
202    ) -> ServerResult<Output> {
203        self.check_jaeger_query_permission(&ctx).await?;
204
205        let mut filters = vec![];
206
207        if let Some(service_name) = query_params.service_name {
208            filters.push(col(SERVICE_NAME_COLUMN).eq(lit(service_name)));
209        }
210
211        if let Some(operation_name) = query_params.operation_name {
212            filters.push(col(SPAN_NAME_COLUMN).eq(lit(operation_name)));
213        }
214
215        if let Some(start_time) = query_params.start_time {
216            filters.push(col(TIMESTAMP_COLUMN).gt_eq(lit_timestamp_nano(start_time)));
217        }
218
219        if let Some(end_time) = query_params.end_time {
220            filters.push(col(TIMESTAMP_COLUMN).lt_eq(lit_timestamp_nano(end_time)));
221        }
222
223        if let Some(min_duration) = query_params.min_duration {
224            filters.push(col(DURATION_NANO_COLUMN).gt_eq(lit(min_duration)));
225        }
226
227        if let Some(max_duration) = query_params.max_duration {
228            filters.push(col(DURATION_NANO_COLUMN).lt_eq(lit(max_duration)));
229        }
230
231        // Get the ids of the most recent traces that match the filters.
232        // It's equivalent to the following SQL query:
233        //
234        // ```
235        // SELECT trace_id
236        // FROM
237        //   {db}.{trace_table}
238        // WHERE
239        //   service_name = '{service_name}' AND
240        //   operation_name = '{operation_name}' AND
241        //   timestamp >= {start_time} AND
242        //   timestamp <= {end_time} AND
243        //   duration >= {min_duration} AND
244        //   duration <= {max_duration}
245        // GROUP BY trace_id
246        // ORDER BY max(timestamp) DESC, trace_id
247        // LIMIT {limit}
248        // ```.
249        let output = find_latest_trace_ids(
250            ctx.clone(),
251            self,
252            filters,
253            query_params.limit.unwrap_or(DEFAULT_LIMIT),
254            query_params.tags,
255        )
256        .await?;
257
258        // Get all traces that match the trace ids from the previous query.
259        // API v3 applies the time window only to the search, not to the returned spans.
260        // It's equivalent to the following SQL query:
261        //
262        // ```
263        // SELECT *
264        // FROM
265        //   {db}.{trace_table}
266        // WHERE
267        //   trace_id IN ({trace_ids}) AND
268        //   timestamp >= {start_time} AND
269        //   timestamp <= {end_time}
270        // ```
271        let mut filters = vec![
272            col(TRACE_ID_COLUMN).in_list(
273                trace_ids_from_output(output)
274                    .await?
275                    .iter()
276                    .map(lit)
277                    .collect::<Vec<Expr>>(),
278                false,
279            ),
280        ];
281
282        if !query_params.fetch_full_trace
283            && let Some(start_time) = query_params.start_time
284        {
285            filters.push(col(TIMESTAMP_COLUMN).gt_eq(lit_timestamp_nano(start_time)));
286        }
287
288        if !query_params.fetch_full_trace
289            && let Some(end_time) = query_params.end_time
290        {
291            filters.push(col(TIMESTAMP_COLUMN).lt_eq(lit_timestamp_nano(end_time)));
292        }
293
294        match query_params.user_agent {
295            TraceUserAgent::Grafana => {
296                // grafana only use trace id and timestamp
297                // clicking the trace id will invoke the query trace api
298                // so we only need to return 1 span for each trace
299                let table_name = ctx
300                    .extension(JAEGER_QUERY_TABLE_NAME_KEY)
301                    .unwrap_or(TRACE_TABLE_NAME);
302
303                let table = get_table(ctx.clone(), self.catalog_manager(), table_name).await?;
304
305                Ok(find_traces_rank_3(
306                    table,
307                    self.query_engine(),
308                    filters,
309                    vec![col(TIMESTAMP_COLUMN).sort(false, false)], // Sort by timestamp in descending order.
310                )
311                .await?)
312            }
313            _ => {
314                // query all spans
315                Ok(query_trace_table(
316                    ctx,
317                    self,
318                    vec![wildcard()],
319                    filters,
320                    vec![col(TIMESTAMP_COLUMN).sort(false, false)], // Sort by timestamp in descending order.
321                    None,
322                    None,
323                    vec![],
324                )
325                .await?)
326            }
327        }
328    }
329}
330
331#[allow(clippy::too_many_arguments)]
332async fn query_trace_table(
333    ctx: QueryContextRef,
334    instance: &Instance,
335    selects: Vec<SelectExpr>,
336    filters: Vec<Expr>,
337    sorts: Vec<SortExpr>,
338    limit: Option<usize>,
339    tags: Option<HashMap<String, JsonValue>>,
340    distincts: Vec<Expr>,
341) -> ServerResult<Output> {
342    let dataframe =
343        build_trace_dataframe(&ctx, instance, selects, filters, tags, &distincts).await?;
344
345    // Apply the distinct if needed.
346    let dataframe = if !distincts.is_empty() {
347        dataframe
348            .distinct_on(distincts.clone(), distincts, None)
349            .context(DataFusionSnafu)?
350    } else {
351        dataframe
352    };
353
354    // Apply the sorts if needed.
355    let dataframe = if !sorts.is_empty() {
356        dataframe.sort(sorts).context(DataFusionSnafu)?
357    } else {
358        dataframe
359    };
360
361    // Apply the limit if needed.
362    let dataframe = if let Some(limit) = limit {
363        dataframe.limit(0, Some(limit)).context(DataFusionSnafu)?
364    } else {
365        dataframe
366    };
367
368    execute_trace_dataframe(dataframe).await
369}
370
371/// Returns the ids of the `limit` traces whose latest span matching `filters`
372/// and `tags` is the most recent, newest first.
373///
374/// Ordering before the limit keeps a search that matches more traces than the
375/// limit deterministic.
376async fn find_latest_trace_ids(
377    ctx: QueryContextRef,
378    instance: &Instance,
379    filters: Vec<Expr>,
380    limit: usize,
381    tags: Option<HashMap<String, JsonValue>>,
382) -> ServerResult<Output> {
383    let dataframe =
384        build_trace_dataframe(&ctx, instance, vec![wildcard()], filters, tags, &[]).await?;
385    let dataframe = dataframe
386        .aggregate(
387            vec![col(TRACE_ID_COLUMN)],
388            vec![max(col(TIMESTAMP_COLUMN)).alias(KEY_LATEST_MATCH)],
389        )
390        .context(DataFusionSnafu)?
391        .sort(vec![
392            col(KEY_LATEST_MATCH).sort(false, false),
393            col(TRACE_ID_COLUMN).sort(true, false),
394        ])
395        .context(DataFusionSnafu)?
396        .limit(0, Some(limit))
397        .context(DataFusionSnafu)?
398        .select(vec![col(TRACE_ID_COLUMN)])
399        .context(DataFusionSnafu)?;
400
401    execute_trace_dataframe(dataframe).await
402}
403
404/// Reads the trace table, or the services or operations lookup table that
405/// answers `selects` and `distincts`, then projects `selects` and applies
406/// `filters` and the Jaeger tag filters.
407async fn build_trace_dataframe(
408    ctx: &QueryContextRef,
409    instance: &Instance,
410    selects: Vec<SelectExpr>,
411    filters: Vec<Expr>,
412    tags: Option<HashMap<String, JsonValue>>,
413    distincts: &[Expr],
414) -> ServerResult<DataFrame> {
415    let trace_table_name = ctx
416        .extension(JAEGER_QUERY_TABLE_NAME_KEY)
417        .unwrap_or(TRACE_TABLE_NAME);
418
419    // If only select services, use the trace services table.
420    // If querying operations (distinct by span_name and span_kind), use the trace operations table.
421    let table_name = {
422        if match selects.as_slice() {
423            [SelectExpr::Expression(x)] => x == &col(SERVICE_NAME_COLUMN),
424            _ => false,
425        } {
426            &trace_services_table_name(trace_table_name)
427        } else if !distincts.is_empty()
428            && distincts.contains(&col(SPAN_NAME_COLUMN))
429            && distincts.contains(&col(SPAN_KIND_COLUMN))
430        {
431            &trace_operations_table_name(trace_table_name)
432        } else {
433            trace_table_name
434        }
435    };
436
437    let table = instance
438        .catalog_manager()
439        .table(
440            ctx.current_catalog(),
441            &ctx.current_schema(),
442            table_name,
443            Some(ctx),
444        )
445        .await?
446        .with_context(|| TableNotFoundSnafu {
447            table: table_name,
448            catalog: ctx.current_catalog(),
449            schema: ctx.current_schema(),
450        })?;
451
452    let table_info = table.table_info();
453    let data_model = table_info.meta.options.data_model();
454
455    // collect to set
456    let col_names = table_info
457        .meta
458        .field_column_names()
459        .map(|s| format!("\"{}\"", s))
460        .collect::<HashSet<String>>();
461
462    let df_context = create_df_context(instance.query_engine())?;
463
464    let dataframe = df_context
465        .read_table(Arc::new(DfTableProviderAdapter::new(table)))
466        .context(DataFusionSnafu)?;
467
468    let dataframe = dataframe.select(selects).context(DataFusionSnafu)?;
469
470    // Apply all filters.
471    let dataframe = filters
472        .into_iter()
473        .chain(tags.map_or(Ok(vec![]), |t| {
474            tags_filters(&dataframe, t, data_model, &col_names)
475        })?)
476        .try_fold(dataframe, |df, expr| {
477            df.filter(expr).context(DataFusionSnafu)
478        })?;
479
480    Ok(dataframe)
481}
482
483/// Executes `dataframe` and wraps its stream into an [`Output`] with dictionary
484/// columns decoded to values.
485async fn execute_trace_dataframe(dataframe: DataFrame) -> ServerResult<Output> {
486    let stream = dataframe.execute_stream().await.context(DataFusionSnafu)?;
487
488    let output = Output::new_with_stream(Box::pin(
489        RecordBatchStreamAdapter::try_new(stream).context(CollectRecordbatchSnafu)?,
490    ));
491
492    output
493        .map_dictionary_to_values()
494        .context(CollectRecordbatchSnafu)
495}
496
497async fn get_table(
498    ctx: QueryContextRef,
499    catalog_manager: &CatalogManagerRef,
500    table_name: &str,
501) -> ServerResult<TableRef> {
502    catalog_manager
503        .table(
504            ctx.current_catalog(),
505            &ctx.current_schema(),
506            table_name,
507            Some(&ctx),
508        )
509        .await?
510        .with_context(|| TableNotFoundSnafu {
511            table: table_name,
512            catalog: ctx.current_catalog(),
513            schema: ctx.current_schema(),
514        })
515}
516
517async fn find_traces_rank_3(
518    table: TableRef,
519    query_engine: &QueryEngineRef,
520    filters: Vec<Expr>,
521    sorts: Vec<SortExpr>,
522) -> ServerResult<Output> {
523    let df_context = create_df_context(query_engine)?;
524
525    let dataframe = df_context
526        .read_table(Arc::new(DfTableProviderAdapter::new(table)))
527        .context(DataFusionSnafu)?;
528
529    let dataframe = dataframe
530        .select(vec![wildcard()])
531        .context(DataFusionSnafu)?;
532
533    // Apply all filters.
534    let dataframe = filters.into_iter().try_fold(dataframe, |df, expr| {
535        df.filter(expr).context(DataFusionSnafu)
536    })?;
537
538    // Apply the sorts if needed.
539    let dataframe = if !sorts.is_empty() {
540        dataframe.sort(sorts).context(DataFusionSnafu)?
541    } else {
542        dataframe
543    };
544
545    // create rank column, for each trace, get the earliest 3 spans
546    let trace_id_col = vec![col(TRACE_ID_COLUMN)];
547    let timestamp_asc = vec![col(TIMESTAMP_COLUMN).sort(true, false)];
548
549    let dataframe = dataframe
550        .with_column(
551            KEY_RN,
552            row_number()
553                .partition_by(trace_id_col)
554                .order_by(timestamp_asc)
555                .build()
556                .context(DataFusionSnafu)?,
557        )
558        .context(DataFusionSnafu)?;
559
560    let dataframe = dataframe
561        .filter(col(KEY_RN).lt_eq(lit(3)))
562        .context(DataFusionSnafu)?;
563
564    // Execute the query and collect the result.
565    let stream = dataframe.execute_stream().await.context(DataFusionSnafu)?;
566
567    let output = Output::new_with_stream(Box::pin(
568        RecordBatchStreamAdapter::try_new(stream).context(CollectRecordbatchSnafu)?,
569    ));
570
571    output
572        .map_dictionary_to_values()
573        .context(CollectRecordbatchSnafu)
574}
575
576// The current implementation registers UDFs during the planning stage, which makes it difficult
577// to utilize them through DataFrame APIs. To address this limitation, we create a new session
578// context and register the required UDFs, allowing them to be decoupled from the global context.
579// TODO(zyy17): Is it possible or necessary to reuse the existing session context?
580fn create_df_context(query_engine: &QueryEngineRef) -> ServerResult<SessionContext> {
581    let df_context = SessionContext::new_with_state(
582        SessionStateBuilder::new_from_existing(query_engine.engine_state().session_state()).build(),
583    );
584
585    // JSON UDFs used by the v0 and v2 tag filters.
586    let udfs: Vec<FunctionRef> = vec![
587        Arc::new(JsonGetWithType::default()),
588        Arc::new(JsonGetInt::default()),
589        Arc::new(JsonGetFloat::default()),
590        Arc::new(JsonGetBool::default()),
591        Arc::new(JsonGetString::default()),
592    ];
593
594    for udf in udfs {
595        df_context.register_udf(create_udf(udf));
596    }
597
598    Ok(df_context)
599}
600
601fn json_tag_filters(
602    dataframe: &DataFrame,
603    tags: HashMap<String, JsonValue>,
604) -> ServerResult<Vec<Expr>> {
605    let has_resource_attributes = dataframe
606        .schema()
607        .has_column_with_unqualified_name(RESOURCE_ATTRIBUTES_COLUMN);
608    let mut filters = vec![];
609
610    // NOTE: The key of the tags may contain `.`, for example: `http.status_code`, so we need to use `["http.status_code"]` in json path to access the value.
611    for (key, value) in tags.iter() {
612        let (udf, value) = match value {
613            JsonValue::String(value) => (JsonGetString::NAME, lit(value)),
614            JsonValue::Number(value) if value.is_i64() => {
615                (JsonGetInt::NAME, lit(value.as_i64().unwrap()))
616            }
617            JsonValue::Number(value) if value.is_f64() => {
618                (JsonGetFloat::NAME, lit(value.as_f64().unwrap()))
619            }
620            JsonValue::Bool(value) => (JsonGetBool::NAME, lit(*value)),
621            _ => continue,
622        };
623        let get = dataframe.registry().udf(udf).context(DataFusionSnafu)?;
624        let path = lit(format!("[\"{}\"]", key));
625        let span_value = get.call(vec![col(SPAN_ATTRIBUTES_COLUMN), path.clone()]);
626        // Same precedence as `json2_tag_filters`: a span attribute shadows a resource attribute.
627        let attribute = if has_resource_attributes {
628            coalesce(vec![
629                span_value,
630                get.call(vec![col(RESOURCE_ATTRIBUTES_COLUMN), path]),
631            ])
632        } else {
633            span_value
634        };
635        filters.push(attribute.eq(value));
636    }
637
638    Ok(filters)
639}
640
641/// Resolves each tag per row, falling back to Resource when the typed Span value is null.
642fn json2_tag_filters(
643    dataframe: &DataFrame,
644    tags: HashMap<String, JsonValue>,
645) -> ServerResult<Vec<Expr>> {
646    let get = dataframe
647        .registry()
648        .udf(JsonGetWithType::NAME)
649        .context(DataFusionSnafu)?;
650    tags.into_iter()
651        .map(|(key, value)| {
652            if key == KEY_OTEL_STATUS_ERROR_KEY && value == JsonValue::Bool(true) {
653                return Ok(col(SPAN_STATUS_CODE).eq(lit(SPAN_STATUS_ERROR)));
654            }
655            // JSON quoting preserves literal dots, quotes, and backslashes in attribute keys.
656            let path = lit(format!("$[{}]", JsonValue::String(key)));
657            let (value, value_type) = match value {
658                JsonValue::String(value) => (
659                    ScalarValue::Utf8View(Some(value)),
660                    ScalarValue::Utf8View(None),
661                ),
662                JsonValue::Bool(value) => (
663                    ScalarValue::Boolean(Some(value)),
664                    ScalarValue::Boolean(None),
665                ),
666                JsonValue::Number(value) => {
667                    if let Some(value) = value.as_i64() {
668                        (ScalarValue::Int64(Some(value)), ScalarValue::Int64(None))
669                    } else if let Some(value) = value.as_u64() {
670                        (ScalarValue::UInt64(Some(value)), ScalarValue::UInt64(None))
671                    } else if let Some(value) = value.as_f64() {
672                        (
673                            ScalarValue::Float64(Some(value)),
674                            ScalarValue::Float64(None),
675                        )
676                    } else {
677                        return Ok(lit(false));
678                    }
679                }
680                JsonValue::Null => (ScalarValue::Utf8View(None), ScalarValue::Utf8View(None)),
681                JsonValue::Array(_) | JsonValue::Object(_) => return Ok(lit(false)),
682            };
683            let attribute = coalesce(vec![
684                get.call(vec![
685                    col(SPAN_ATTRIBUTES_COLUMN),
686                    path.clone(),
687                    lit(value_type.clone()),
688                ]),
689                get.call(vec![col(RESOURCE_ATTRIBUTES_COLUMN), path, lit(value_type)]),
690            ]);
691            Ok(if value.is_null() {
692                attribute.is_null()
693            } else {
694                attribute.eq(lit(value))
695            })
696        })
697        .collect()
698}
699
700/// Helper function to check if span_key or resource_key exists in col_names and create an expression.
701/// If neither exists, logs a warning and returns None.
702#[inline]
703fn check_col_and_build_expr<F>(
704    span_key: String,
705    resource_key: String,
706    key: &str,
707    col_names: &HashSet<String>,
708    expr_builder: F,
709) -> Option<Expr>
710where
711    F: FnOnce(String) -> Expr,
712{
713    if col_names.contains(&span_key) {
714        return Some(expr_builder(span_key));
715    }
716    if col_names.contains(&resource_key) {
717        return Some(expr_builder(resource_key));
718    }
719    warn!("tag key {} not found in table columns", key);
720    None
721}
722
723fn flatten_tag_filters(
724    tags: HashMap<String, JsonValue>,
725    col_names: &HashSet<String>,
726) -> ServerResult<Vec<Expr>> {
727    let filters = tags
728        .into_iter()
729        .filter_map(|(key, value)| {
730            if key == KEY_OTEL_STATUS_ERROR_KEY && value == JsonValue::Bool(true) {
731                return Some(col(SPAN_STATUS_CODE).eq(lit(SPAN_STATUS_ERROR)));
732            }
733
734            // TODO(shuiyisong): add more precise mapping from key to col name
735            let span_key = format!("\"span_attributes.{}\"", key);
736            let resource_key = format!("\"resource_attributes.{}\"", key);
737            match value {
738                JsonValue::String(value) => {
739                    check_col_and_build_expr(span_key, resource_key, &key, col_names, |k| {
740                        col(k).eq(lit(value))
741                    })
742                }
743                JsonValue::Number(value) => {
744                    if value.is_f64() {
745                        // safe to unwrap as checked previously
746                        let value = value.as_f64().unwrap();
747                        check_col_and_build_expr(span_key, resource_key, &key, col_names, |k| {
748                            col(k).eq(lit(value))
749                        })
750                    } else {
751                        let value = value.as_i64().unwrap();
752                        check_col_and_build_expr(span_key, resource_key, &key, col_names, |k| {
753                            col(k).eq(lit(value))
754                        })
755                    }
756                }
757                JsonValue::Bool(value) => {
758                    check_col_and_build_expr(span_key, resource_key, &key, col_names, |k| {
759                        col(k).eq(lit(value))
760                    })
761                }
762                JsonValue::Null => {
763                    check_col_and_build_expr(span_key, resource_key, &key, col_names, |k| {
764                        col(k).is_null()
765                    })
766                }
767                // not supported at the moment
768                JsonValue::Array(_value) => None,
769                JsonValue::Object(_value) => None,
770            }
771        })
772        .collect();
773    Ok(filters)
774}
775
776fn tags_filters(
777    dataframe: &DataFrame,
778    tags: HashMap<String, JsonValue>,
779    data_model: Option<&str>,
780    col_names: &HashSet<String>,
781) -> ServerResult<Vec<Expr>> {
782    match data_model {
783        Some(table::requests::TABLE_DATA_MODEL_TRACE_V1) => flatten_tag_filters(tags, col_names),
784        Some(table::requests::TABLE_DATA_MODEL_TRACE_V2) => json2_tag_filters(dataframe, tags),
785        _ => json_tag_filters(dataframe, tags),
786    }
787}
788
789// Get trace ids from the output in recordbatches.
790async fn trace_ids_from_output(output: Output) -> ServerResult<Vec<String>> {
791    if let OutputData::Stream(stream) = output.data {
792        let schema = stream.schema().clone();
793        let recordbatches = util::collect(stream)
794            .await
795            .context(CollectRecordbatchSnafu)?;
796
797        // Only contains `trace_id` column in string type.
798        if !recordbatches.is_empty()
799            && schema.num_columns() == 1
800            && schema.contains_column(TRACE_ID_COLUMN)
801        {
802            let mut trace_ids = vec![];
803            for recordbatch in recordbatches {
804                recordbatch
805                    .iter_column_as_string(0)
806                    .flatten()
807                    .for_each(|x| trace_ids.push(x));
808            }
809
810            return Ok(trace_ids);
811        }
812    }
813
814    Ok(vec![])
815}