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,
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::dataframe::DataFrame;
34use datafusion::execution::SessionStateBuilder;
35use datafusion::execution::context::SessionContext;
36use datafusion::functions_window::expr_fn::row_number;
37use datafusion_expr::select_expr::SelectExpr;
38use datafusion_expr::{Expr, ExprFunctionExt, SortExpr, col, lit, lit_timestamp_nano, wildcard};
39use query::QueryEngineRef;
40use serde_json::Value as JsonValue;
41use servers::error::{
42    AuthSnafu, CollectRecordbatchSnafu, DataFusionSnafu, Result as ServerResult, TableNotFoundSnafu,
43};
44use servers::http::jaeger::{JAEGER_QUERY_TABLE_NAME_KEY, QueryTraceParams, TraceUserAgent};
45use servers::otlp::trace::{
46    DURATION_NANO_COLUMN, KEY_OTEL_STATUS_ERROR_KEY, SERVICE_NAME_COLUMN, SPAN_ATTRIBUTES_COLUMN,
47    SPAN_KIND_COLUMN, SPAN_KIND_PREFIX, SPAN_NAME_COLUMN, SPAN_STATUS_CODE, SPAN_STATUS_ERROR,
48    TIMESTAMP_COLUMN, TRACE_ID_COLUMN,
49};
50use servers::query_handler::JaegerQueryHandler;
51use session::context::QueryContextRef;
52use snafu::{OptionExt, ResultExt};
53use table::TableRef;
54use table::requests::{TABLE_DATA_MODEL, TABLE_DATA_MODEL_TRACE_V1};
55use table::table::adapter::DfTableProviderAdapter;
56
57use crate::instance::Instance;
58
59const DEFAULT_LIMIT: usize = 2000;
60const KEY_RN: &str = "greptime_rn";
61
62impl Instance {
63    async fn check_jaeger_query_permission(&self, ctx: &QueryContextRef) -> ServerResult<()> {
64        let table = ctx
65            .extension(JAEGER_QUERY_TABLE_NAME_KEY)
66            .unwrap_or(TRACE_TABLE_NAME);
67        let targets = PermissionTableTargets::resolved(vec![PermissionTableTarget::new(
68            ctx.current_catalog(),
69            ctx.current_schema(),
70            table,
71        )]);
72        let targets = self.resolve_query_permission_targets(targets, ctx).await?;
73        self.check_table_permission(ctx, PermissionReq::Action(JAEGER_QUERY), targets)
74            .context(AuthSnafu)?;
75        Ok(())
76    }
77}
78
79#[async_trait]
80impl JaegerQueryHandler for Instance {
81    async fn get_services(&self, ctx: QueryContextRef) -> ServerResult<Output> {
82        self.check_jaeger_query_permission(&ctx).await?;
83
84        // It's equivalent to `SELECT DISTINCT(service_name) FROM {db}.{trace_table}`.
85        Ok(query_trace_table(
86            ctx,
87            self,
88            vec![SelectExpr::from(col(SERVICE_NAME_COLUMN))],
89            vec![],
90            vec![],
91            None,
92            None,
93            vec![col(SERVICE_NAME_COLUMN)],
94        )
95        .await?)
96    }
97
98    async fn get_operations(
99        &self,
100        ctx: QueryContextRef,
101        service_name: &str,
102        span_kind: Option<&str>,
103    ) -> ServerResult<Output> {
104        self.check_jaeger_query_permission(&ctx).await?;
105
106        let mut filters = vec![col(SERVICE_NAME_COLUMN).eq(lit(service_name))];
107
108        if let Some(span_kind) = span_kind {
109            filters.push(col(SPAN_KIND_COLUMN).eq(lit(format!(
110                "{}{}",
111                SPAN_KIND_PREFIX,
112                span_kind.to_uppercase()
113            ))));
114        }
115
116        // It's equivalent to the following SQL query:
117        //
118        // ```
119        // SELECT DISTINCT span_name, span_kind
120        // FROM
121        //   {db}.{trace_table}
122        // WHERE
123        //   service_name = '{service_name}' AND
124        //   span_kind = '{span_kind}'
125        // ORDER BY
126        //   span_name ASC
127        // ```.
128        Ok(query_trace_table(
129            ctx,
130            self,
131            vec![
132                SelectExpr::from(col(SPAN_NAME_COLUMN)),
133                SelectExpr::from(col(SPAN_KIND_COLUMN)),
134                SelectExpr::from(col(SERVICE_NAME_COLUMN)),
135                SelectExpr::from(col(TIMESTAMP_COLUMN)),
136            ],
137            filters,
138            vec![col(SPAN_NAME_COLUMN).sort(true, false)], // Sort by span_name in ascending order.
139            Some(DEFAULT_LIMIT),
140            None,
141            vec![col(SPAN_NAME_COLUMN), col(SPAN_KIND_COLUMN)],
142        )
143        .await?)
144    }
145
146    async fn get_trace(
147        &self,
148        ctx: QueryContextRef,
149        trace_id: &str,
150        start_time: Option<i64>,
151        end_time: Option<i64>,
152        limit: Option<usize>,
153    ) -> ServerResult<Output> {
154        self.check_jaeger_query_permission(&ctx).await?;
155
156        // It's equivalent to the following SQL query:
157        //
158        // ```
159        // SELECT
160        //   *
161        // FROM
162        //   {db}.{trace_table}
163        // WHERE
164        //   trace_id = '{trace_id}' AND
165        //   timestamp >= {start_time} AND
166        //   timestamp <= {end_time}
167        // ORDER BY
168        //   timestamp DESC
169        // ```.
170        let selects = vec![wildcard()];
171
172        let mut filters = vec![col(TRACE_ID_COLUMN).eq(lit(trace_id))];
173
174        if let Some(start_time) = start_time {
175            filters.push(col(TIMESTAMP_COLUMN).gt_eq(lit_timestamp_nano(start_time)));
176        }
177
178        if let Some(end_time) = end_time {
179            filters.push(col(TIMESTAMP_COLUMN).lt_eq(lit_timestamp_nano(end_time)));
180        }
181
182        Ok(query_trace_table(
183            ctx,
184            self,
185            selects,
186            filters,
187            vec![col(TIMESTAMP_COLUMN).sort(false, false)], // Sort by timestamp in descending order.
188            limit,
189            None,
190            vec![],
191        )
192        .await?)
193    }
194
195    async fn find_traces(
196        &self,
197        ctx: QueryContextRef,
198        query_params: QueryTraceParams,
199    ) -> ServerResult<Output> {
200        self.check_jaeger_query_permission(&ctx).await?;
201
202        let mut filters = vec![];
203
204        // `service_name` is already validated in `from_jaeger_query_params()`, so no additional check needed here.
205        filters.push(col(SERVICE_NAME_COLUMN).eq(lit(query_params.service_name)));
206
207        if let Some(operation_name) = query_params.operation_name {
208            filters.push(col(SPAN_NAME_COLUMN).eq(lit(operation_name)));
209        }
210
211        if let Some(start_time) = query_params.start_time {
212            filters.push(col(TIMESTAMP_COLUMN).gt_eq(lit_timestamp_nano(start_time)));
213        }
214
215        if let Some(end_time) = query_params.end_time {
216            filters.push(col(TIMESTAMP_COLUMN).lt_eq(lit_timestamp_nano(end_time)));
217        }
218
219        if let Some(min_duration) = query_params.min_duration {
220            filters.push(col(DURATION_NANO_COLUMN).gt_eq(lit(min_duration)));
221        }
222
223        if let Some(max_duration) = query_params.max_duration {
224            filters.push(col(DURATION_NANO_COLUMN).lt_eq(lit(max_duration)));
225        }
226
227        // Get all distinct trace ids that match the filters.
228        // It's equivalent to the following SQL query:
229        //
230        // ```
231        // SELECT DISTINCT trace_id
232        // FROM
233        //   {db}.{trace_table}
234        // WHERE
235        //   service_name = '{service_name}' AND
236        //   operation_name = '{operation_name}' AND
237        //   timestamp >= {start_time} AND
238        //   timestamp <= {end_time} AND
239        //   duration >= {min_duration} AND
240        //   duration <= {max_duration}
241        // LIMIT {limit}
242        // ```.
243        let output = query_trace_table(
244            ctx.clone(),
245            self,
246            vec![wildcard()],
247            filters,
248            vec![],
249            Some(query_params.limit.unwrap_or(DEFAULT_LIMIT)),
250            query_params.tags,
251            vec![col(TRACE_ID_COLUMN)],
252        )
253        .await?;
254
255        // Get all traces that match the trace ids from the previous query.
256        // It's equivalent to the following SQL query:
257        //
258        // ```
259        // SELECT *
260        // FROM
261        //   {db}.{trace_table}
262        // WHERE
263        //   trace_id IN ({trace_ids}) AND
264        //   timestamp >= {start_time} AND
265        //   timestamp <= {end_time}
266        // ```
267        let mut filters = vec![
268            col(TRACE_ID_COLUMN).in_list(
269                trace_ids_from_output(output)
270                    .await?
271                    .iter()
272                    .map(lit)
273                    .collect::<Vec<Expr>>(),
274                false,
275            ),
276        ];
277
278        if let Some(start_time) = query_params.start_time {
279            filters.push(col(TIMESTAMP_COLUMN).gt_eq(lit_timestamp_nano(start_time)));
280        }
281
282        if let Some(end_time) = query_params.end_time {
283            filters.push(col(TIMESTAMP_COLUMN).lt_eq(lit_timestamp_nano(end_time)));
284        }
285
286        match query_params.user_agent {
287            TraceUserAgent::Grafana => {
288                // grafana only use trace id and timestamp
289                // clicking the trace id will invoke the query trace api
290                // so we only need to return 1 span for each trace
291                let table_name = ctx
292                    .extension(JAEGER_QUERY_TABLE_NAME_KEY)
293                    .unwrap_or(TRACE_TABLE_NAME);
294
295                let table = get_table(ctx.clone(), self.catalog_manager(), table_name).await?;
296
297                Ok(find_traces_rank_3(
298                    table,
299                    self.query_engine(),
300                    filters,
301                    vec![col(TIMESTAMP_COLUMN).sort(false, false)], // Sort by timestamp in descending order.
302                )
303                .await?)
304            }
305            _ => {
306                // query all spans
307                Ok(query_trace_table(
308                    ctx,
309                    self,
310                    vec![wildcard()],
311                    filters,
312                    vec![col(TIMESTAMP_COLUMN).sort(false, false)], // Sort by timestamp in descending order.
313                    None,
314                    None,
315                    vec![],
316                )
317                .await?)
318            }
319        }
320    }
321}
322
323#[allow(clippy::too_many_arguments)]
324async fn query_trace_table(
325    ctx: QueryContextRef,
326    instance: &Instance,
327    selects: Vec<SelectExpr>,
328    filters: Vec<Expr>,
329    sorts: Vec<SortExpr>,
330    limit: Option<usize>,
331    tags: Option<HashMap<String, JsonValue>>,
332    distincts: Vec<Expr>,
333) -> ServerResult<Output> {
334    let trace_table_name = ctx
335        .extension(JAEGER_QUERY_TABLE_NAME_KEY)
336        .unwrap_or(TRACE_TABLE_NAME);
337
338    // If only select services, use the trace services table.
339    // If querying operations (distinct by span_name and span_kind), use the trace operations table.
340    let table_name = {
341        if match selects.as_slice() {
342            [SelectExpr::Expression(x)] => x == &col(SERVICE_NAME_COLUMN),
343            _ => false,
344        } {
345            &trace_services_table_name(trace_table_name)
346        } else if !distincts.is_empty()
347            && distincts.contains(&col(SPAN_NAME_COLUMN))
348            && distincts.contains(&col(SPAN_KIND_COLUMN))
349        {
350            &trace_operations_table_name(trace_table_name)
351        } else {
352            trace_table_name
353        }
354    };
355
356    let table = instance
357        .catalog_manager()
358        .table(
359            ctx.current_catalog(),
360            &ctx.current_schema(),
361            table_name,
362            Some(&ctx),
363        )
364        .await?
365        .with_context(|| TableNotFoundSnafu {
366            table: table_name,
367            catalog: ctx.current_catalog(),
368            schema: ctx.current_schema(),
369        })?;
370
371    let is_data_model_v1 = table
372        .clone()
373        .table_info()
374        .meta
375        .options
376        .extra_options
377        .get(TABLE_DATA_MODEL)
378        .map(|s| s.as_str())
379        == Some(TABLE_DATA_MODEL_TRACE_V1);
380
381    // collect to set
382    let col_names = table
383        .table_info()
384        .meta
385        .field_column_names()
386        .map(|s| format!("\"{}\"", s))
387        .collect::<HashSet<String>>();
388
389    let df_context = create_df_context(instance.query_engine())?;
390
391    let dataframe = df_context
392        .read_table(Arc::new(DfTableProviderAdapter::new(table)))
393        .context(DataFusionSnafu)?;
394
395    let dataframe = dataframe.select(selects).context(DataFusionSnafu)?;
396
397    // Apply all filters.
398    let dataframe = filters
399        .into_iter()
400        .chain(tags.map_or(Ok(vec![]), |t| {
401            tags_filters(&dataframe, t, is_data_model_v1, &col_names)
402        })?)
403        .try_fold(dataframe, |df, expr| {
404            df.filter(expr).context(DataFusionSnafu)
405        })?;
406
407    // Apply the distinct if needed.
408    let dataframe = if !distincts.is_empty() {
409        dataframe
410            .distinct_on(distincts.clone(), distincts, None)
411            .context(DataFusionSnafu)?
412    } else {
413        dataframe
414    };
415
416    // Apply the sorts if needed.
417    let dataframe = if !sorts.is_empty() {
418        dataframe.sort(sorts).context(DataFusionSnafu)?
419    } else {
420        dataframe
421    };
422
423    // Apply the limit if needed.
424    let dataframe = if let Some(limit) = limit {
425        dataframe.limit(0, Some(limit)).context(DataFusionSnafu)?
426    } else {
427        dataframe
428    };
429
430    // Execute the query and collect the result.
431    let stream = dataframe.execute_stream().await.context(DataFusionSnafu)?;
432
433    let output = Output::new_with_stream(Box::pin(
434        RecordBatchStreamAdapter::try_new(stream).context(CollectRecordbatchSnafu)?,
435    ));
436
437    output
438        .map_dictionary_to_values()
439        .context(CollectRecordbatchSnafu)
440}
441
442async fn get_table(
443    ctx: QueryContextRef,
444    catalog_manager: &CatalogManagerRef,
445    table_name: &str,
446) -> ServerResult<TableRef> {
447    catalog_manager
448        .table(
449            ctx.current_catalog(),
450            &ctx.current_schema(),
451            table_name,
452            Some(&ctx),
453        )
454        .await?
455        .with_context(|| TableNotFoundSnafu {
456            table: table_name,
457            catalog: ctx.current_catalog(),
458            schema: ctx.current_schema(),
459        })
460}
461
462async fn find_traces_rank_3(
463    table: TableRef,
464    query_engine: &QueryEngineRef,
465    filters: Vec<Expr>,
466    sorts: Vec<SortExpr>,
467) -> ServerResult<Output> {
468    let df_context = create_df_context(query_engine)?;
469
470    let dataframe = df_context
471        .read_table(Arc::new(DfTableProviderAdapter::new(table)))
472        .context(DataFusionSnafu)?;
473
474    let dataframe = dataframe
475        .select(vec![wildcard()])
476        .context(DataFusionSnafu)?;
477
478    // Apply all filters.
479    let dataframe = filters.into_iter().try_fold(dataframe, |df, expr| {
480        df.filter(expr).context(DataFusionSnafu)
481    })?;
482
483    // Apply the sorts if needed.
484    let dataframe = if !sorts.is_empty() {
485        dataframe.sort(sorts).context(DataFusionSnafu)?
486    } else {
487        dataframe
488    };
489
490    // create rank column, for each trace, get the earliest 3 spans
491    let trace_id_col = vec![col(TRACE_ID_COLUMN)];
492    let timestamp_asc = vec![col(TIMESTAMP_COLUMN).sort(true, false)];
493
494    let dataframe = dataframe
495        .with_column(
496            KEY_RN,
497            row_number()
498                .partition_by(trace_id_col)
499                .order_by(timestamp_asc)
500                .build()
501                .context(DataFusionSnafu)?,
502        )
503        .context(DataFusionSnafu)?;
504
505    let dataframe = dataframe
506        .filter(col(KEY_RN).lt_eq(lit(3)))
507        .context(DataFusionSnafu)?;
508
509    // Execute the query and collect the result.
510    let stream = dataframe.execute_stream().await.context(DataFusionSnafu)?;
511
512    let output = Output::new_with_stream(Box::pin(
513        RecordBatchStreamAdapter::try_new(stream).context(CollectRecordbatchSnafu)?,
514    ));
515
516    output
517        .map_dictionary_to_values()
518        .context(CollectRecordbatchSnafu)
519}
520
521// The current implementation registers UDFs during the planning stage, which makes it difficult
522// to utilize them through DataFrame APIs. To address this limitation, we create a new session
523// context and register the required UDFs, allowing them to be decoupled from the global context.
524// TODO(zyy17): Is it possible or necessary to reuse the existing session context?
525fn create_df_context(query_engine: &QueryEngineRef) -> ServerResult<SessionContext> {
526    let df_context = SessionContext::new_with_state(
527        SessionStateBuilder::new_from_existing(query_engine.engine_state().session_state()).build(),
528    );
529
530    // The following JSON UDFs will be used for tags filters on v0 data model.
531    let udfs: Vec<FunctionRef> = vec![
532        Arc::new(JsonGetInt::default()),
533        Arc::new(JsonGetFloat::default()),
534        Arc::new(JsonGetBool::default()),
535        Arc::new(JsonGetString::default()),
536    ];
537
538    for udf in udfs {
539        df_context.register_udf(create_udf(udf));
540    }
541
542    Ok(df_context)
543}
544
545fn json_tag_filters(
546    dataframe: &DataFrame,
547    tags: HashMap<String, JsonValue>,
548) -> ServerResult<Vec<Expr>> {
549    let mut filters = vec![];
550
551    // 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.
552    for (key, value) in tags.iter() {
553        if let JsonValue::String(value) = value {
554            filters.push(
555                dataframe
556                    .registry()
557                    .udf(JsonGetString::NAME)
558                    .context(DataFusionSnafu)?
559                    .call(vec![
560                        col(SPAN_ATTRIBUTES_COLUMN),
561                        lit(format!("[\"{}\"]", key)),
562                    ])
563                    .eq(lit(value)),
564            );
565        }
566        if let JsonValue::Number(value) = value {
567            if value.is_i64() {
568                filters.push(
569                    dataframe
570                        .registry()
571                        .udf(JsonGetInt::NAME)
572                        .context(DataFusionSnafu)?
573                        .call(vec![
574                            col(SPAN_ATTRIBUTES_COLUMN),
575                            lit(format!("[\"{}\"]", key)),
576                        ])
577                        .eq(lit(value.as_i64().unwrap())),
578                );
579            }
580            if value.is_f64() {
581                filters.push(
582                    dataframe
583                        .registry()
584                        .udf(JsonGetFloat::NAME)
585                        .context(DataFusionSnafu)?
586                        .call(vec![
587                            col(SPAN_ATTRIBUTES_COLUMN),
588                            lit(format!("[\"{}\"]", key)),
589                        ])
590                        .eq(lit(value.as_f64().unwrap())),
591                );
592            }
593        }
594        if let JsonValue::Bool(value) = value {
595            filters.push(
596                dataframe
597                    .registry()
598                    .udf(JsonGetBool::NAME)
599                    .context(DataFusionSnafu)?
600                    .call(vec![
601                        col(SPAN_ATTRIBUTES_COLUMN),
602                        lit(format!("[\"{}\"]", key)),
603                    ])
604                    .eq(lit(*value)),
605            );
606        }
607    }
608
609    Ok(filters)
610}
611
612/// Helper function to check if span_key or resource_key exists in col_names and create an expression.
613/// If neither exists, logs a warning and returns None.
614#[inline]
615fn check_col_and_build_expr<F>(
616    span_key: String,
617    resource_key: String,
618    key: &str,
619    col_names: &HashSet<String>,
620    expr_builder: F,
621) -> Option<Expr>
622where
623    F: FnOnce(String) -> Expr,
624{
625    if col_names.contains(&span_key) {
626        return Some(expr_builder(span_key));
627    }
628    if col_names.contains(&resource_key) {
629        return Some(expr_builder(resource_key));
630    }
631    warn!("tag key {} not found in table columns", key);
632    None
633}
634
635fn flatten_tag_filters(
636    tags: HashMap<String, JsonValue>,
637    col_names: &HashSet<String>,
638) -> ServerResult<Vec<Expr>> {
639    let filters = tags
640        .into_iter()
641        .filter_map(|(key, value)| {
642            if key == KEY_OTEL_STATUS_ERROR_KEY && value == JsonValue::Bool(true) {
643                return Some(col(SPAN_STATUS_CODE).eq(lit(SPAN_STATUS_ERROR)));
644            }
645
646            // TODO(shuiyisong): add more precise mapping from key to col name
647            let span_key = format!("\"span_attributes.{}\"", key);
648            let resource_key = format!("\"resource_attributes.{}\"", key);
649            match value {
650                JsonValue::String(value) => {
651                    check_col_and_build_expr(span_key, resource_key, &key, col_names, |k| {
652                        col(k).eq(lit(value))
653                    })
654                }
655                JsonValue::Number(value) => {
656                    if value.is_f64() {
657                        // safe to unwrap as checked previously
658                        let value = value.as_f64().unwrap();
659                        check_col_and_build_expr(span_key, resource_key, &key, col_names, |k| {
660                            col(k).eq(lit(value))
661                        })
662                    } else {
663                        let value = value.as_i64().unwrap();
664                        check_col_and_build_expr(span_key, resource_key, &key, col_names, |k| {
665                            col(k).eq(lit(value))
666                        })
667                    }
668                }
669                JsonValue::Bool(value) => {
670                    check_col_and_build_expr(span_key, resource_key, &key, col_names, |k| {
671                        col(k).eq(lit(value))
672                    })
673                }
674                JsonValue::Null => {
675                    check_col_and_build_expr(span_key, resource_key, &key, col_names, |k| {
676                        col(k).is_null()
677                    })
678                }
679                // not supported at the moment
680                JsonValue::Array(_value) => None,
681                JsonValue::Object(_value) => None,
682            }
683        })
684        .collect();
685    Ok(filters)
686}
687
688fn tags_filters(
689    dataframe: &DataFrame,
690    tags: HashMap<String, JsonValue>,
691    is_data_model_v1: bool,
692    col_names: &HashSet<String>,
693) -> ServerResult<Vec<Expr>> {
694    if is_data_model_v1 {
695        flatten_tag_filters(tags, col_names)
696    } else {
697        json_tag_filters(dataframe, tags)
698    }
699}
700
701// Get trace ids from the output in recordbatches.
702async fn trace_ids_from_output(output: Output) -> ServerResult<Vec<String>> {
703    if let OutputData::Stream(stream) = output.data {
704        let schema = stream.schema().clone();
705        let recordbatches = util::collect(stream)
706            .await
707            .context(CollectRecordbatchSnafu)?;
708
709        // Only contains `trace_id` column in string type.
710        if !recordbatches.is_empty()
711            && schema.num_columns() == 1
712            && schema.contains_column(TRACE_ID_COLUMN)
713        {
714            let mut trace_ids = vec![];
715            for recordbatch in recordbatches {
716                recordbatch
717                    .iter_column_as_string(0)
718                    .flatten()
719                    .for_each(|x| trace_ids.push(x));
720            }
721
722            return Ok(trace_ids);
723        }
724    }
725
726    Ok(vec![])
727}