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