Skip to main content

servers/http/
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
15pub mod v3;
16
17use std::collections::{BTreeMap, HashMap};
18use std::fmt;
19use std::str::FromStr;
20use std::sync::Arc;
21
22use axum::Extension;
23use axum::extract::{Path, Query, State};
24use axum::http::{HeaderMap, StatusCode as HttpStatusCode};
25use axum::response::IntoResponse;
26use axum_extra::TypedHeader;
27use common_catalog::consts::{PARENT_SPAN_ID_COLUMN, TRACE_TABLE_NAME};
28use common_error::ext::ErrorExt;
29use common_error::status_code::StatusCode;
30use common_query::{Output, OutputData};
31use common_recordbatch::util;
32use common_telemetry::{debug, error, tracing, warn};
33use headers::UserAgent;
34use serde::{Deserialize, Deserializer, Serialize, de};
35use serde_json::Value as JsonValue;
36use session::context::{Channel, QueryContext};
37use snafu::{OptionExt, ResultExt};
38
39use crate::error::{
40    CollectRecordbatchSnafu, Error, InvalidJaegerQuerySnafu, Result, status_code_to_http_status,
41};
42use crate::http::HttpRecordsOutput;
43use crate::http::extractor::TraceTableName;
44use crate::metrics::METRIC_JAEGER_QUERY_ELAPSED;
45use crate::otlp::trace::{
46    DURATION_NANO_COLUMN, KEY_OTEL_SCOPE_NAME, KEY_OTEL_SCOPE_VERSION, KEY_OTEL_STATUS_CODE,
47    KEY_OTEL_STATUS_ERROR_KEY, KEY_OTEL_STATUS_MESSAGE, KEY_OTEL_TRACE_STATE, KEY_SERVICE_NAME,
48    KEY_SPAN_KIND, RESOURCE_ATTRIBUTES_COLUMN, SCOPE_NAME_COLUMN, SCOPE_VERSION_COLUMN,
49    SERVICE_NAME_COLUMN, SPAN_ATTRIBUTES_COLUMN, SPAN_EVENTS_COLUMN, SPAN_ID_COLUMN,
50    SPAN_KIND_COLUMN, SPAN_KIND_PREFIX, SPAN_LINKS_COLUMN, SPAN_NAME_COLUMN, SPAN_STATUS_CODE,
51    SPAN_STATUS_ERROR, SPAN_STATUS_MESSAGE_COLUMN, SPAN_STATUS_PREFIX, SPAN_STATUS_UNSET,
52    TIMESTAMP_COLUMN, TRACE_ID_COLUMN, TRACE_STATE_COLUMN,
53};
54use crate::query_handler::JaegerQueryHandlerRef;
55
56pub const JAEGER_QUERY_TABLE_NAME_KEY: &str = "jaeger_query_table_name";
57
58const REF_TYPE_CHILD_OF: &str = "CHILD_OF";
59const SPAN_KIND_TIME_FMTS: [&str; 2] = ["%Y-%m-%d %H:%M:%S%.6f%z", "%Y-%m-%d %H:%M:%S%.9f%z"];
60
61const TRACE_NOT_FOUND_ERROR_CODE: i32 = 404;
62const TRACE_NOT_FOUND_ERROR_MSG: &str = "trace not found";
63
64/// JaegerAPIResponse is the response of Jaeger HTTP API.
65/// The original version is `structuredResponse` which is defined in https://github.com/jaegertracing/jaeger/blob/main/cmd/query/app/http_handler.go.
66#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
67pub struct JaegerAPIResponse {
68    pub data: Option<JaegerData>,
69    pub total: usize,
70    pub limit: usize,
71    pub offset: usize,
72    pub errors: Vec<JaegerAPIError>,
73}
74
75impl JaegerAPIResponse {
76    pub fn trace_not_found() -> Self {
77        Self {
78            data: None,
79            total: 0,
80            limit: 0,
81            offset: 0,
82            errors: vec![JaegerAPIError {
83                code: TRACE_NOT_FOUND_ERROR_CODE,
84                msg: TRACE_NOT_FOUND_ERROR_MSG.to_string(),
85                trace_id: None,
86            }],
87        }
88    }
89}
90
91/// JaegerData is the query result of Jaeger HTTP API.
92#[derive(Debug, Serialize, Deserialize, PartialEq)]
93#[serde(untagged)]
94pub enum JaegerData {
95    ServiceNames(Vec<String>),
96    OperationsNames(Vec<String>),
97    Operations(Vec<Operation>),
98    Traces(Vec<Trace>),
99}
100
101/// JaegerAPIError is the error of Jaeger HTTP API.
102#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
103#[serde(rename_all = "camelCase")]
104pub struct JaegerAPIError {
105    pub code: i32,
106    pub msg: String,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub trace_id: Option<String>,
109}
110
111/// Operation is an operation in a service.
112#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
113#[serde(rename_all = "camelCase")]
114pub struct Operation {
115    pub name: String,
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub span_kind: Option<String>,
118}
119
120/// Trace is a collection of spans.
121#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
122#[serde(rename_all = "camelCase")]
123pub struct Trace {
124    #[serde(rename = "traceID")]
125    pub trace_id: String,
126    pub spans: Vec<Span>,
127
128    #[serde(skip_serializing_if = "HashMap::is_empty")]
129    pub processes: HashMap<String, Process>,
130
131    #[serde(skip_serializing_if = "Vec::is_empty")]
132    pub warnings: Vec<String>,
133}
134
135/// Span is a single operation within a trace.
136#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
137#[serde(rename_all = "camelCase")]
138pub struct Span {
139    #[serde(rename = "traceID")]
140    pub trace_id: String,
141
142    #[serde(rename = "spanID")]
143    pub span_id: String,
144
145    #[serde(rename = "parentSpanID")]
146    #[serde(skip_serializing_if = "String::is_empty")]
147    pub parent_span_id: String,
148
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub flags: Option<u32>,
151
152    pub operation_name: String,
153    pub references: Vec<Reference>,
154    pub start_time: u64, // microseconds since unix epoch
155    pub duration: u64,   // microseconds
156    pub tags: Vec<KeyValue>,
157    pub logs: Vec<Log>,
158
159    #[serde(rename = "processID")]
160    #[serde(skip_serializing_if = "String::is_empty")]
161    pub process_id: String,
162
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub process: Option<Process>,
165
166    #[serde(skip_serializing_if = "Vec::is_empty")]
167    pub warnings: Vec<String>,
168}
169
170/// Reference is a reference from one span to another.
171#[derive(Debug, Serialize, Deserialize, PartialEq)]
172#[serde(rename_all = "camelCase")]
173pub struct Reference {
174    #[serde(rename = "traceID")]
175    pub trace_id: String,
176    #[serde(rename = "spanID")]
177    pub span_id: String,
178    pub ref_type: String,
179}
180
181/// Process is the process emitting a set of spans.
182#[derive(Debug, Serialize, Deserialize, PartialEq)]
183#[serde(rename_all = "camelCase")]
184pub struct Process {
185    pub service_name: String,
186    pub tags: Vec<KeyValue>,
187}
188
189/// Log is a log emitted in a span.
190#[derive(Debug, Serialize, Deserialize, PartialEq)]
191#[serde(rename_all = "camelCase")]
192pub struct Log {
193    pub timestamp: u64,
194    pub fields: Vec<KeyValue>,
195}
196
197/// KeyValue is a key-value pair with typed value.
198#[derive(Debug, Serialize, Deserialize, PartialEq)]
199#[serde(rename_all = "camelCase")]
200pub struct KeyValue {
201    pub key: String,
202    #[serde(rename = "type")]
203    pub value_type: ValueType,
204    pub value: Value,
205}
206
207/// Value is the value of a key-value pair in Jaeger Span attributes.
208#[derive(Debug, Serialize, Deserialize, PartialEq)]
209#[serde(untagged)]
210#[serde(rename_all = "camelCase")]
211pub enum Value {
212    String(String),
213    Int64(i64),
214    Float64(f64),
215    Boolean(bool),
216    Binary(Vec<u8>),
217}
218
219/// ValueType is the type of a value stored in KeyValue struct.
220#[derive(Debug, Serialize, Deserialize, PartialEq)]
221#[serde(rename_all = "lowercase")]
222pub enum ValueType {
223    String,
224    Int64,
225    Float64,
226    Boolean,
227    Binary,
228}
229
230/// JaegerQueryParams is the query parameters of Jaeger HTTP API.
231#[derive(Default, Debug, Serialize, Deserialize)]
232#[serde(rename_all = "camelCase")]
233pub struct JaegerQueryParams {
234    /// Service name of the trace.
235    #[serde(rename = "service")]
236    pub service_name: Option<String>,
237
238    /// Operation name of the trace.
239    #[serde(rename = "operation")]
240    pub operation_name: Option<String>,
241
242    /// Limit the return data.
243    #[serde(default, deserialize_with = "empty_string_as_none")]
244    pub limit: Option<usize>,
245
246    /// Start time of the trace in microseconds since unix epoch.
247    pub start: Option<i64>,
248
249    /// End time of the trace in microseconds since unix epoch.
250    pub end: Option<i64>,
251
252    /// Max duration string value of the trace. Units can be `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`.
253    #[serde(default, deserialize_with = "empty_string_as_none")]
254    pub max_duration: Option<String>,
255
256    /// Min duration string value of the trace. Units can be `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`.
257    #[serde(default, deserialize_with = "empty_string_as_none")]
258    pub min_duration: Option<String>,
259
260    /// Tags of the trace in JSON format. It will be URL encoded in the raw query.
261    /// The decoded format is like: tags="{\"http.status_code\":\"200\",\"latency\":\"11.234\",\"error\":\"false\",\"http.method\":\"GET\",\"http.path\":\"/api/v1/users\"}".
262    /// The key and value of the map are both strings. The key and value is the attribute name and value of the span. The value will be converted to the corresponding type when querying.
263    pub tags: Option<String>,
264
265    /// The span kind of the trace.
266    pub span_kind: Option<String>,
267}
268
269/// Serde deserialization decorator to map empty Strings to None.
270fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
271where
272    D: Deserializer<'de>,
273    T: FromStr,
274    T::Err: fmt::Display,
275{
276    let opt = Option::<String>::deserialize(de)?;
277    match opt.as_deref() {
278        None | Some("") => Ok(None),
279        Some(s) => FromStr::from_str(s).map_err(de::Error::custom).map(Some),
280    }
281}
282
283fn update_query_context(query_ctx: &mut QueryContext, table_name: Option<String>) {
284    // db should be already handled by middlewares
285    query_ctx.set_channel(Channel::Jaeger);
286    if let Some(table) = table_name {
287        query_ctx.set_extension(JAEGER_QUERY_TABLE_NAME_KEY, table);
288    }
289}
290
291impl QueryTraceParams {
292    fn from_jaeger_query_params(query_params: JaegerQueryParams) -> Result<Self> {
293        let mut internal_query_params: QueryTraceParams = QueryTraceParams {
294            service_name: Some(query_params.service_name.context(InvalidJaegerQuerySnafu {
295                reason: "service_name is required".to_string(),
296            })?),
297            operation_name: query_params.operation_name,
298            // Convert start time from microseconds to nanoseconds.
299            start_time: query_params.start.map(|start| start * 1000),
300            end_time: query_params.end.map(|end| end * 1000),
301            ..Default::default()
302        };
303
304        if let Some(max_duration) = query_params.max_duration {
305            let duration = humantime::parse_duration(&max_duration).map_err(|e| {
306                InvalidJaegerQuerySnafu {
307                    reason: format!("parse maxDuration '{}' failed: {}", max_duration, e),
308                }
309                .build()
310            })?;
311            internal_query_params.max_duration = Some(duration.as_nanos() as u64);
312        }
313
314        if let Some(min_duration) = query_params.min_duration {
315            let duration = humantime::parse_duration(&min_duration).map_err(|e| {
316                InvalidJaegerQuerySnafu {
317                    reason: format!("parse minDuration '{}' failed: {}", min_duration, e),
318                }
319                .build()
320            })?;
321            internal_query_params.min_duration = Some(duration.as_nanos() as u64);
322        }
323
324        if let Some(tags) = query_params.tags {
325            // Serialize the tags to a JSON map.
326            let mut tags_map: HashMap<String, JsonValue> =
327                serde_json::from_str(&tags).map_err(|e| {
328                    InvalidJaegerQuerySnafu {
329                        reason: format!("parse tags '{}' failed: {}", tags, e),
330                    }
331                    .build()
332                })?;
333            for v in tags_map.values_mut() {
334                if let Some(number) = convert_string_to_number(v) {
335                    *v = number;
336                }
337                if let Some(boolean) = convert_string_to_boolean(v) {
338                    *v = boolean;
339                }
340            }
341            internal_query_params.tags = Some(tags_map);
342        }
343
344        internal_query_params.limit = query_params.limit;
345
346        Ok(internal_query_params)
347    }
348}
349
350#[derive(Debug, Default, PartialEq)]
351pub struct QueryTraceParams {
352    pub service_name: Option<String>,
353    pub operation_name: Option<String>,
354
355    // The limit of the number of traces to return.
356    pub limit: Option<usize>,
357
358    // Select the traces with the given tags(span attributes).
359    pub tags: Option<HashMap<String, JsonValue>>,
360
361    // The unit of the following time related parameters is nanoseconds.
362    pub start_time: Option<i64>,
363    pub end_time: Option<i64>,
364    pub min_duration: Option<u64>,
365    pub max_duration: Option<u64>,
366
367    /// Fetch all spans of matching traces, including spans outside the search window.
368    pub fetch_full_trace: bool,
369
370    // The user agent of the trace query, mainly find traces
371    pub user_agent: TraceUserAgent,
372}
373
374#[derive(Debug, Default, PartialEq, Eq)]
375pub enum TraceUserAgent {
376    Grafana,
377    // Jaeger-UI does not actually send user agent
378    // But it's a jaeger API, so let's treat it as jaeger
379    #[default]
380    Jaeger,
381}
382
383impl From<UserAgent> for TraceUserAgent {
384    fn from(value: UserAgent) -> Self {
385        let ua_str = value.as_str().to_lowercase();
386        debug!("received user agent: {}", ua_str);
387        if ua_str.contains("grafana") {
388            Self::Grafana
389        } else {
390            Self::Jaeger
391        }
392    }
393}
394
395/// Handle the GET `/api/services` request.
396#[axum_macros::debug_handler]
397#[tracing::instrument(skip_all, fields(protocol = "jaeger", request_type = "get_services"))]
398pub async fn handle_get_services(
399    State(handler): State<JaegerQueryHandlerRef>,
400    Query(query_params): Query<JaegerQueryParams>,
401    Extension(mut query_ctx): Extension<QueryContext>,
402    TraceTableName(table_name): TraceTableName,
403) -> impl IntoResponse {
404    debug!(
405        "Received Jaeger '/api/services' request, query_params: {:?}, query_ctx: {:?}",
406        query_params, query_ctx
407    );
408
409    query_ctx.set_channel(Channel::Jaeger);
410    if let Some(table) = table_name {
411        query_ctx.set_extension(JAEGER_QUERY_TABLE_NAME_KEY, table);
412    }
413
414    let query_ctx = Arc::new(query_ctx);
415    let db = query_ctx.get_db_string();
416
417    // Record the query time histogram.
418    let _timer = METRIC_JAEGER_QUERY_ELAPSED
419        .with_label_values(&[db.as_str(), "/api/services"])
420        .start_timer();
421
422    match handler.get_services(query_ctx).await {
423        Ok(output) => match covert_to_records(output).await {
424            Ok(Some(records)) => match services_from_records(records) {
425                Ok(services) => {
426                    let services_num = services.len();
427                    (
428                        HttpStatusCode::OK,
429                        axum::Json(JaegerAPIResponse {
430                            data: Some(JaegerData::ServiceNames(services)),
431                            total: services_num,
432                            ..Default::default()
433                        }),
434                    )
435                }
436                Err(err) => {
437                    error!("Failed to get services: {:?}", err);
438                    error_response(err)
439                }
440            },
441            Ok(None) => (HttpStatusCode::OK, axum::Json(JaegerAPIResponse::default())),
442            Err(err) => {
443                error!("Failed to get services: {:?}", err);
444                error_response(err)
445            }
446        },
447        Err(err) => handle_query_error(err, "Failed to get services", &db),
448    }
449}
450
451/// Handle the GET `/api/traces/{trace_id}` request.
452#[axum_macros::debug_handler]
453#[tracing::instrument(skip_all, fields(protocol = "jaeger", request_type = "get_trace"))]
454pub async fn handle_get_trace(
455    State(handler): State<JaegerQueryHandlerRef>,
456    Path(trace_id): Path<String>,
457    Query(query_params): Query<JaegerQueryParams>,
458    Extension(mut query_ctx): Extension<QueryContext>,
459    TraceTableName(table_name): TraceTableName,
460) -> impl IntoResponse {
461    debug!(
462        "Received Jaeger '/api/traces/{}' request, query_params: {:?}, query_ctx: {:?}",
463        trace_id, query_params, query_ctx
464    );
465
466    update_query_context(&mut query_ctx, table_name);
467    let query_ctx = Arc::new(query_ctx);
468    let db = query_ctx.get_db_string();
469
470    // Record the query time histogram.
471    let _timer = METRIC_JAEGER_QUERY_ELAPSED
472        .with_label_values(&[db.as_str(), "/api/traces"])
473        .start_timer();
474
475    // Convert start time and end time from microseconds to nanoseconds.
476    let start_time_ns = query_params.start.map(|start_us| start_us * 1000);
477    let end_time_ns = query_params.end.map(|end_us| end_us * 1000);
478
479    let output = match handler
480        .get_trace(
481            query_ctx,
482            &trace_id,
483            start_time_ns,
484            end_time_ns,
485            query_params.limit,
486        )
487        .await
488    {
489        Ok(output) => output,
490        Err(err) => {
491            return handle_query_error(
492                err,
493                &format!("Failed to get trace for '{}'", trace_id),
494                &db,
495            );
496        }
497    };
498
499    match covert_to_records(output).await {
500        Ok(Some(records)) => match traces_from_records(records) {
501            Ok(traces) if traces.is_empty() => (
502                HttpStatusCode::NOT_FOUND,
503                axum::Json(JaegerAPIResponse::trace_not_found()),
504            ),
505            Ok(traces) => (
506                HttpStatusCode::OK,
507                axum::Json(JaegerAPIResponse {
508                    data: Some(JaegerData::Traces(traces)),
509                    ..Default::default()
510                }),
511            ),
512            Err(err) => {
513                error!("Failed to get trace '{}': {:?}", trace_id, err);
514                error_response(err)
515            }
516        },
517        Ok(None) => (
518            HttpStatusCode::NOT_FOUND,
519            axum::Json(JaegerAPIResponse::trace_not_found()),
520        ),
521        Err(err) => {
522            error!("Failed to get trace '{}': {:?}", trace_id, err);
523            error_response(err)
524        }
525    }
526}
527
528/// Handle the GET `/api/traces` request.
529#[axum_macros::debug_handler]
530#[tracing::instrument(skip_all, fields(protocol = "jaeger", request_type = "find_traces"))]
531pub async fn handle_find_traces(
532    State(handler): State<JaegerQueryHandlerRef>,
533    Query(query_params): Query<JaegerQueryParams>,
534    Extension(mut query_ctx): Extension<QueryContext>,
535    TraceTableName(table_name): TraceTableName,
536    optional_user_agent: Option<TypedHeader<UserAgent>>,
537) -> impl IntoResponse {
538    debug!(
539        "Received Jaeger '/api/traces' request, query_params: {:?}, query_ctx: {:?}",
540        query_params, query_ctx
541    );
542
543    update_query_context(&mut query_ctx, table_name);
544    let query_ctx = Arc::new(query_ctx);
545    let db = query_ctx.get_db_string();
546
547    // Record the query time histogram.
548    let _timer = METRIC_JAEGER_QUERY_ELAPSED
549        .with_label_values(&[db.as_str(), "/api/traces"])
550        .start_timer();
551
552    match QueryTraceParams::from_jaeger_query_params(query_params) {
553        Ok(mut query_params) => {
554            if let Some(TypedHeader(user_agent)) = optional_user_agent {
555                query_params.user_agent = user_agent.into();
556            }
557            let output = handler.find_traces(query_ctx, query_params).await;
558            match output {
559                Ok(output) => match covert_to_records(output).await {
560                    Ok(Some(records)) => match traces_from_records(records) {
561                        Ok(traces) => (
562                            HttpStatusCode::OK,
563                            axum::Json(JaegerAPIResponse {
564                                data: Some(JaegerData::Traces(traces)),
565                                ..Default::default()
566                            }),
567                        ),
568                        Err(err) => {
569                            error!("Failed to find traces: {:?}", err);
570                            error_response(err)
571                        }
572                    },
573                    Ok(None) => (HttpStatusCode::OK, axum::Json(JaegerAPIResponse::default())),
574                    Err(err) => error_response(err),
575                },
576                Err(err) => handle_query_error(err, "Failed to find traces", &db),
577            }
578        }
579        Err(e) => error_response(e),
580    }
581}
582
583/// Handle the GET `/api/operations` request.
584#[axum_macros::debug_handler]
585#[tracing::instrument(skip_all, fields(protocol = "jaeger", request_type = "get_operations"))]
586pub async fn handle_get_operations(
587    State(handler): State<JaegerQueryHandlerRef>,
588    Query(query_params): Query<JaegerQueryParams>,
589    Extension(mut query_ctx): Extension<QueryContext>,
590    TraceTableName(table_name): TraceTableName,
591    headers: HeaderMap,
592) -> impl IntoResponse {
593    debug!(
594        "Received Jaeger '/api/operations' request, query_params: {:?}, query_ctx: {:?}, headers: {:?}",
595        query_params, query_ctx, headers
596    );
597
598    if let Some(service_name) = &query_params.service_name {
599        update_query_context(&mut query_ctx, table_name);
600        let query_ctx = Arc::new(query_ctx);
601        let db = query_ctx.get_db_string();
602
603        // Record the query time histogram.
604        let _timer = METRIC_JAEGER_QUERY_ELAPSED
605            .with_label_values(&[db.as_str(), "/api/operations"])
606            .start_timer();
607
608        match handler
609            .get_operations(query_ctx, service_name, query_params.span_kind.as_deref())
610            .await
611        {
612            Ok(output) => match covert_to_records(output).await {
613                Ok(Some(records)) => match operations_from_records(records, true) {
614                    Ok(operations) => {
615                        let total = operations.len();
616                        (
617                            HttpStatusCode::OK,
618                            axum::Json(JaegerAPIResponse {
619                                data: Some(JaegerData::Operations(operations)),
620                                total,
621                                ..Default::default()
622                            }),
623                        )
624                    }
625                    Err(err) => {
626                        error!("Failed to get operations: {:?}", err);
627                        error_response(err)
628                    }
629                },
630                Ok(None) => (HttpStatusCode::OK, axum::Json(JaegerAPIResponse::default())),
631                Err(err) => error_response(err),
632            },
633            Err(err) => handle_query_error(
634                err,
635                &format!("Failed to get operations for service '{}'", service_name),
636                &db,
637            ),
638        }
639    } else {
640        (
641            HttpStatusCode::BAD_REQUEST,
642            axum::Json(JaegerAPIResponse {
643                errors: vec![JaegerAPIError {
644                    code: 400,
645                    msg: "parameter 'service' is required".to_string(),
646                    trace_id: None,
647                }],
648                ..Default::default()
649            }),
650        )
651    }
652}
653
654/// Handle the GET `/api/services/{service_name}/operations` request.
655#[axum_macros::debug_handler]
656#[tracing::instrument(
657    skip_all,
658    fields(protocol = "jaeger", request_type = "get_operations_by_service")
659)]
660pub async fn handle_get_operations_by_service(
661    State(handler): State<JaegerQueryHandlerRef>,
662    Path(service_name): Path<String>,
663    Query(query_params): Query<JaegerQueryParams>,
664    Extension(mut query_ctx): Extension<QueryContext>,
665    TraceTableName(table_name): TraceTableName,
666    headers: HeaderMap,
667) -> impl IntoResponse {
668    debug!(
669        "Received Jaeger '/api/services/{}/operations' request, query_params: {:?}, query_ctx: {:?}, headers: {:?}",
670        service_name, query_params, query_ctx, headers
671    );
672
673    update_query_context(&mut query_ctx, table_name);
674    let query_ctx = Arc::new(query_ctx);
675    let db = query_ctx.get_db_string();
676
677    // Record the query time histogram.
678    let _timer = METRIC_JAEGER_QUERY_ELAPSED
679        .with_label_values(&[db.as_str(), "/api/services"])
680        .start_timer();
681
682    match handler.get_operations(query_ctx, &service_name, None).await {
683        Ok(output) => match covert_to_records(output).await {
684            Ok(Some(records)) => match operations_from_records(records, false) {
685                Ok(operations) => {
686                    let operations: Vec<String> =
687                        operations.into_iter().map(|op| op.name).collect();
688                    let total = operations.len();
689                    (
690                        HttpStatusCode::OK,
691                        axum::Json(JaegerAPIResponse {
692                            data: Some(JaegerData::OperationsNames(operations)),
693                            total,
694                            ..Default::default()
695                        }),
696                    )
697                }
698                Err(err) => {
699                    error!(
700                        "Failed to get operations for service '{}': {:?}",
701                        service_name, err
702                    );
703                    error_response(err)
704                }
705            },
706            Ok(None) => (HttpStatusCode::OK, axum::Json(JaegerAPIResponse::default())),
707            Err(err) => error_response(err),
708        },
709        Err(err) => handle_query_error(
710            err,
711            &format!("Failed to get operations for service '{}'", service_name),
712            &db,
713        ),
714    }
715}
716
717async fn covert_to_records(output: Output) -> Result<Option<HttpRecordsOutput>> {
718    match output.data {
719        OutputData::Stream(stream) => {
720            let records = HttpRecordsOutput::try_new(
721                stream.schema().clone(),
722                util::collect(stream)
723                    .await
724                    .context(CollectRecordbatchSnafu)?,
725            )?;
726            debug!(
727                "The query records: {}",
728                serde_json::to_string(&records).unwrap()
729            );
730            Ok(Some(records))
731        }
732        // It's unlikely to happen. However, if the output is not a stream, return None.
733        _ => Ok(None),
734    }
735}
736
737fn handle_query_error(
738    err: Error,
739    prompt: &str,
740    db: &str,
741) -> (HttpStatusCode, axum::Json<JaegerAPIResponse>) {
742    // To compatible with the Jaeger API, if the trace table is not found, return an empty response instead of an error.
743    if err.status_code() == StatusCode::TableNotFound {
744        warn!(
745            "No trace table '{}' found in database '{}'",
746            TRACE_TABLE_NAME, db
747        );
748        (HttpStatusCode::OK, axum::Json(JaegerAPIResponse::default()))
749    } else {
750        error!("{}: {:?}", prompt, err);
751        error_response(err)
752    }
753}
754
755fn error_response(err: Error) -> (HttpStatusCode, axum::Json<JaegerAPIResponse>) {
756    (
757        status_code_to_http_status(&err.status_code()),
758        axum::Json(JaegerAPIResponse {
759            errors: vec![JaegerAPIError {
760                code: err.status_code() as i32,
761                msg: err.to_string(),
762                ..Default::default()
763            }],
764            ..Default::default()
765        }),
766    )
767}
768
769fn traces_from_records(records: HttpRecordsOutput) -> Result<Vec<Trace>> {
770    // maintain the mapping: trace_id -> (process_id -> service_name).
771    let mut trace_id_to_processes: HashMap<String, HashMap<String, String>> = HashMap::new();
772    // maintain the mapping: trace_id -> spans.
773    // use BTreeMap to retain order
774    let mut trace_id_to_spans: BTreeMap<String, Vec<Span>> = BTreeMap::new();
775    // maintain the mapping: service.name -> resource.attributes.
776    let mut service_to_resource_attributes: HashMap<String, Vec<KeyValue>> = HashMap::new();
777
778    let is_span_attributes_flatten = !records
779        .schema
780        .column_schemas
781        .iter()
782        .any(|c| c.name == SPAN_ATTRIBUTES_COLUMN);
783
784    for row in records.rows.into_iter() {
785        let mut span = Span::default();
786        let mut service_name = None;
787        let mut parent_span_id = None;
788        let mut resource_tags = vec![];
789
790        for (idx, cell) in row.into_iter().enumerate() {
791            // safe to use index here
792            let column_name = &records.schema.column_schemas[idx].name;
793
794            match column_name.as_str() {
795                TRACE_ID_COLUMN => {
796                    if let JsonValue::String(trace_id) = cell {
797                        span.trace_id = trace_id.clone();
798                        trace_id_to_processes.entry(trace_id).or_default();
799                    }
800                }
801                TIMESTAMP_COLUMN => {
802                    span.start_time = cell.as_u64().context(InvalidJaegerQuerySnafu {
803                        reason: "Failed to convert timestamp to u64".to_string(),
804                    })? / 1000;
805                }
806                DURATION_NANO_COLUMN => {
807                    span.duration = cell.as_u64().context(InvalidJaegerQuerySnafu {
808                        reason: "Failed to convert duration to u64".to_string(),
809                    })? / 1000;
810                }
811                SERVICE_NAME_COLUMN => {
812                    if let JsonValue::String(name) = cell {
813                        service_name = Some(name);
814                    }
815                }
816                SPAN_NAME_COLUMN => {
817                    if let JsonValue::String(span_name) = cell {
818                        span.operation_name = span_name;
819                    }
820                }
821                SPAN_ID_COLUMN => {
822                    if let JsonValue::String(span_id) = cell {
823                        span.span_id = span_id;
824                    }
825                }
826                SPAN_ATTRIBUTES_COLUMN => {
827                    // For v0 and v2, span_attributes are nested as a JSON
828                    // data structure
829                    if let JsonValue::Object(span_attrs) = cell {
830                        span.tags.extend(object_to_tags(span_attrs));
831                    }
832                }
833                RESOURCE_ATTRIBUTES_COLUMN => {
834                    // For v0 and v2, resource_attributes are nested as a JSON
835                    // data structure
836
837                    if let JsonValue::Object(mut resource_attrs) = cell {
838                        resource_attrs.remove(KEY_SERVICE_NAME);
839                        resource_tags = object_to_tags(resource_attrs);
840                    }
841                }
842                PARENT_SPAN_ID_COLUMN => {
843                    if let JsonValue::String(id) = cell
844                        && !id.is_empty()
845                    {
846                        parent_span_id = Some(id);
847                    }
848                }
849                SPAN_LINKS_COLUMN => {
850                    if let JsonValue::Array(links) = cell {
851                        for link in links {
852                            if let (Some(trace_id), Some(span_id)) = (
853                                link.get("trace_id").and_then(JsonValue::as_str),
854                                link.get("span_id").and_then(JsonValue::as_str),
855                            ) {
856                                span.references.push(Reference {
857                                    trace_id: trace_id.to_string(),
858                                    span_id: span_id.to_string(),
859                                    ref_type: "FOLLOWS_FROM".to_string(),
860                                });
861                            }
862                        }
863                    }
864                }
865                SPAN_EVENTS_COLUMN => {
866                    if let JsonValue::Array(events) = cell {
867                        for event in events {
868                            if let JsonValue::Object(mut obj) = event {
869                                let Some(action) = obj.get("name").and_then(|v| v.as_str()) else {
870                                    continue;
871                                };
872
873                                let Some(t) =
874                                    obj.get("time").and_then(|t| t.as_str()).and_then(|s| {
875                                        SPAN_KIND_TIME_FMTS
876                                            .iter()
877                                            .find_map(|fmt| {
878                                                chrono::DateTime::parse_from_str(s, fmt).ok()
879                                            })
880                                            .map(|dt| dt.timestamp_micros() as u64)
881                                    })
882                                else {
883                                    continue;
884                                };
885
886                                let mut fields = vec![KeyValue {
887                                    key: "event".to_string(),
888                                    value_type: ValueType::String,
889                                    value: Value::String(action.to_string()),
890                                }];
891
892                                // Add event attributes as fields
893                                if let Some(JsonValue::Object(attrs)) = obj.remove("attributes") {
894                                    fields.extend(object_to_tags(attrs));
895                                }
896
897                                span.logs.push(Log {
898                                    timestamp: t,
899                                    fields,
900                                });
901                            }
902                        }
903                    }
904                }
905                SCOPE_NAME_COLUMN => {
906                    if let JsonValue::String(scope_name) = cell
907                        && !scope_name.is_empty()
908                    {
909                        span.tags.push(KeyValue {
910                            key: KEY_OTEL_SCOPE_NAME.to_string(),
911                            value_type: ValueType::String,
912                            value: Value::String(scope_name),
913                        });
914                    }
915                }
916                SCOPE_VERSION_COLUMN => {
917                    if let JsonValue::String(scope_version) = cell
918                        && !scope_version.is_empty()
919                    {
920                        span.tags.push(KeyValue {
921                            key: KEY_OTEL_SCOPE_VERSION.to_string(),
922                            value_type: ValueType::String,
923                            value: Value::String(scope_version),
924                        });
925                    }
926                }
927                SPAN_KIND_COLUMN => {
928                    if let JsonValue::String(span_kind) = cell
929                        && !span_kind.is_empty()
930                    {
931                        span.tags.push(KeyValue {
932                            key: KEY_SPAN_KIND.to_string(),
933                            value_type: ValueType::String,
934                            value: Value::String(normalize_span_kind(&span_kind)),
935                        });
936                    }
937                }
938                SPAN_STATUS_CODE => {
939                    if let JsonValue::String(span_status) = cell
940                        && span_status != SPAN_STATUS_UNSET
941                        && !span_status.is_empty()
942                    {
943                        span.tags.push(KeyValue {
944                            key: KEY_OTEL_STATUS_CODE.to_string(),
945                            value_type: ValueType::String,
946                            value: Value::String(normalize_status_code(&span_status)),
947                        });
948                        // set error to comply with the Jaeger API
949                        if span_status == SPAN_STATUS_ERROR {
950                            span.tags.push(KeyValue {
951                                key: KEY_OTEL_STATUS_ERROR_KEY.to_string(),
952                                value_type: ValueType::Boolean,
953                                value: Value::Boolean(true),
954                            });
955                        }
956                    }
957                }
958
959                SPAN_STATUS_MESSAGE_COLUMN => {
960                    if let JsonValue::String(span_status_message) = cell
961                        && !span_status_message.is_empty()
962                    {
963                        span.tags.push(KeyValue {
964                            key: KEY_OTEL_STATUS_MESSAGE.to_string(),
965                            value_type: ValueType::String,
966                            value: Value::String(span_status_message),
967                        });
968                    }
969                }
970
971                TRACE_STATE_COLUMN => {
972                    if let JsonValue::String(trace_state) = cell
973                        && !trace_state.is_empty()
974                    {
975                        span.tags.push(KeyValue {
976                            key: KEY_OTEL_TRACE_STATE.to_string(),
977                            value_type: ValueType::String,
978                            value: Value::String(trace_state),
979                        });
980                    }
981                }
982
983                _ => {
984                    // this this v1 data model
985                    if is_span_attributes_flatten {
986                        const SPAN_ATTR_PREFIX: &str = "span_attributes.";
987                        const RESOURCE_ATTR_PREFIX: &str = "resource_attributes.";
988                        // a span attributes column
989                        if column_name.starts_with(SPAN_ATTR_PREFIX) {
990                            if let Some(keyvalue) = to_keyvalue(
991                                column_name
992                                    .strip_prefix(SPAN_ATTR_PREFIX)
993                                    .unwrap_or_default()
994                                    .to_string(),
995                                cell,
996                            ) {
997                                span.tags.push(keyvalue);
998                            }
999                        } else if column_name.starts_with(RESOURCE_ATTR_PREFIX)
1000                            && let Some(keyvalue) = to_keyvalue(
1001                                column_name
1002                                    .strip_prefix(RESOURCE_ATTR_PREFIX)
1003                                    .unwrap_or_default()
1004                                    .to_string(),
1005                                cell,
1006                            )
1007                        {
1008                            resource_tags.push(keyvalue);
1009                        }
1010                    }
1011                }
1012            }
1013        }
1014
1015        if let Some(parent_span_id) = parent_span_id {
1016            span.references.insert(
1017                0,
1018                Reference {
1019                    trace_id: span.trace_id.clone(),
1020                    span_id: parent_span_id,
1021                    ref_type: REF_TYPE_CHILD_OF.to_string(),
1022                },
1023            );
1024        }
1025
1026        if let Some(service_name) = service_name {
1027            if !service_to_resource_attributes.contains_key(&service_name) {
1028                service_to_resource_attributes.insert(service_name.clone(), resource_tags);
1029            }
1030
1031            if let Some(process) = trace_id_to_processes.get_mut(&span.trace_id) {
1032                if let Some(process_id) = process.get(&service_name) {
1033                    span.process_id = process_id.clone();
1034                } else {
1035                    // Allocate a new process id.
1036                    let process_id = format!("p{}", process.len() + 1);
1037                    process.insert(service_name, process_id.clone());
1038                    span.process_id = process_id;
1039                }
1040            }
1041        }
1042
1043        // ensure span tags order
1044        span.tags.sort_by(|a, b| a.key.cmp(&b.key));
1045
1046        if let Some(spans) = trace_id_to_spans.get_mut(&span.trace_id) {
1047            spans.push(span);
1048        } else {
1049            trace_id_to_spans.insert(span.trace_id.clone(), vec![span]);
1050        }
1051    }
1052
1053    let mut traces = Vec::new();
1054    for (trace_id, spans) in trace_id_to_spans {
1055        let mut trace = Trace {
1056            trace_id,
1057            spans,
1058            ..Default::default()
1059        };
1060
1061        if let Some(processes) = trace_id_to_processes.remove(&trace.trace_id) {
1062            let mut process_id_to_process = HashMap::new();
1063            for (service_name, process_id) in processes.into_iter() {
1064                let tags = service_to_resource_attributes
1065                    .remove(&service_name)
1066                    .unwrap_or_default();
1067                process_id_to_process.insert(process_id, Process { service_name, tags });
1068            }
1069            trace.processes = process_id_to_process;
1070        }
1071        traces.push(trace);
1072    }
1073
1074    Ok(traces)
1075}
1076
1077fn to_keyvalue(key: String, value: JsonValue) -> Option<KeyValue> {
1078    match value {
1079        JsonValue::String(value) => Some(KeyValue {
1080            key,
1081            value_type: ValueType::String,
1082            value: Value::String(value.clone()),
1083        }),
1084        JsonValue::Number(value) => {
1085            if value.is_i64() {
1086                Some(KeyValue {
1087                    key,
1088                    value_type: ValueType::Int64,
1089                    value: Value::Int64(value.as_i64()?),
1090                })
1091            } else {
1092                Some(KeyValue {
1093                    key,
1094                    value_type: ValueType::Float64,
1095                    value: Value::Float64(value.as_f64()?),
1096                })
1097            }
1098        }
1099        JsonValue::Bool(value) => Some(KeyValue {
1100            key,
1101            value_type: ValueType::Boolean,
1102            value: Value::Boolean(value),
1103        }),
1104        JsonValue::Array(value) => Some(KeyValue {
1105            key,
1106            value_type: ValueType::String,
1107            value: Value::String(serde_json::to_string(&value).unwrap()),
1108        }),
1109        JsonValue::Object(value) => Some(KeyValue {
1110            key,
1111            value_type: ValueType::String,
1112            value: Value::String(serde_json::to_string(&value).unwrap()),
1113        }),
1114        JsonValue::Null => None,
1115    }
1116}
1117
1118fn object_to_tags(object: serde_json::map::Map<String, JsonValue>) -> Vec<KeyValue> {
1119    object
1120        .into_iter()
1121        .filter_map(|(key, value)| to_keyvalue(key, value))
1122        .collect()
1123}
1124
1125fn services_from_records(records: HttpRecordsOutput) -> Result<Vec<String>> {
1126    let expected_schema = vec![(SERVICE_NAME_COLUMN, "String")];
1127    check_schema(&records, &expected_schema)?;
1128
1129    let mut services = Vec::with_capacity(records.total_rows);
1130    for row in records.rows.into_iter() {
1131        for value in row.into_iter() {
1132            if let JsonValue::String(service_name) = value {
1133                services.push(service_name);
1134            }
1135        }
1136    }
1137    Ok(services)
1138}
1139
1140// Construct Jaeger operations from records.
1141fn operations_from_records(
1142    records: HttpRecordsOutput,
1143    contain_span_kind: bool,
1144) -> Result<Vec<Operation>> {
1145    let expected_schema = vec![(SPAN_NAME_COLUMN, "String"), (SPAN_KIND_COLUMN, "String")];
1146    check_schema(&records, &expected_schema)?;
1147
1148    let mut operations = Vec::with_capacity(records.total_rows);
1149    for row in records.rows.into_iter() {
1150        let mut row_iter = row.into_iter();
1151        if let Some(JsonValue::String(operation)) = row_iter.next() {
1152            let mut operation = Operation {
1153                name: operation,
1154                span_kind: None,
1155            };
1156            if contain_span_kind {
1157                if let Some(JsonValue::String(span_kind)) = row_iter.next() {
1158                    operation.span_kind = Some(normalize_span_kind(&span_kind));
1159                }
1160            } else {
1161                // skip span kind.
1162                row_iter.next();
1163            }
1164            operations.push(operation);
1165        }
1166    }
1167
1168    Ok(operations)
1169}
1170
1171// Check whether the schema of the records is correct.
1172fn check_schema(records: &HttpRecordsOutput, expected_schema: &[(&str, &str)]) -> Result<()> {
1173    for (i, column) in records.schema.column_schemas.iter().enumerate() {
1174        if column.name != expected_schema[i].0 || column.data_type != expected_schema[i].1 {
1175            InvalidJaegerQuerySnafu {
1176                reason: "query result schema is not correct".to_string(),
1177            }
1178            .fail()?
1179        }
1180    }
1181    Ok(())
1182}
1183
1184// By default, the span kind is stored as `SPAN_KIND_<kind>` in GreptimeDB.
1185// However, in Jaeger API, the span kind is returned as `<kind>` which is the lowercase of the span kind and without the `SPAN_KIND_` prefix.
1186fn normalize_span_kind(span_kind: &str) -> String {
1187    // If the span_kind starts with `SPAN_KIND_` prefix, remove it and convert to lowercase.
1188    if let Some(stripped) = span_kind.strip_prefix(SPAN_KIND_PREFIX) {
1189        stripped.to_lowercase()
1190    } else {
1191        // It's unlikely to happen. However, we still convert it to lowercase for consistency.
1192        span_kind.to_lowercase()
1193    }
1194}
1195
1196// By default, the status code is stored as `STATUS_CODE_<code>` in GreptimeDB.
1197// However, in Jaeger API, the status code is returned as `<code>` without the `STATUS_CODE_` prefix.
1198fn normalize_status_code(status_code: &str) -> String {
1199    // If the span_kind starts with `SPAN_KIND_` prefix, remove it and convert to lowercase.
1200    if let Some(stripped) = status_code.strip_prefix(SPAN_STATUS_PREFIX) {
1201        stripped.to_string()
1202    } else {
1203        // It's unlikely to happen
1204        status_code.to_string()
1205    }
1206}
1207
1208fn convert_string_to_number(input: &serde_json::Value) -> Option<serde_json::Value> {
1209    if let Some(data) = input.as_str() {
1210        if let Ok(number) = data.parse::<i64>() {
1211            return Some(serde_json::Value::Number(serde_json::Number::from(number)));
1212        }
1213        if let Ok(number) = data.parse::<f64>()
1214            && let Some(number) = serde_json::Number::from_f64(number)
1215        {
1216            return Some(serde_json::Value::Number(number));
1217        }
1218    }
1219
1220    None
1221}
1222
1223fn convert_string_to_boolean(input: &serde_json::Value) -> Option<serde_json::Value> {
1224    if let Some(data) = input.as_str() {
1225        if data == "true" {
1226            return Some(serde_json::Value::Bool(true));
1227        }
1228        if data == "false" {
1229            return Some(serde_json::Value::Bool(false));
1230        }
1231    }
1232
1233    None
1234}
1235
1236#[cfg(test)]
1237mod tests {
1238    use serde_json::{Number, Value as JsonValue, json};
1239
1240    use super::*;
1241    use crate::http::{ColumnSchema, HttpRecordsOutput, OutputSchema};
1242
1243    #[test]
1244    fn test_numeric_tag_types() {
1245        for (input, value_type, value) in [
1246            (json!(42), ValueType::Int64, Value::Int64(42)),
1247            (json!(1.5), ValueType::Float64, Value::Float64(1.5)),
1248        ] {
1249            assert_eq!(
1250                to_keyvalue("tag".to_string(), input),
1251                Some(KeyValue {
1252                    key: "tag".to_string(),
1253                    value_type,
1254                    value,
1255                })
1256            );
1257        }
1258    }
1259
1260    #[test]
1261    fn test_services_from_records() {
1262        // The tests is the tuple of `(test_records, expected)`.
1263        let tests = vec![(
1264            HttpRecordsOutput {
1265                schema: OutputSchema {
1266                    column_schemas: vec![ColumnSchema {
1267                        name: "service_name".to_string(),
1268                        data_type: "String".to_string(),
1269                    }],
1270                },
1271                rows: vec![
1272                    vec![JsonValue::String("test-service-0".to_string())],
1273                    vec![JsonValue::String("test-service-1".to_string())],
1274                ],
1275                total_rows: 2,
1276                metrics: HashMap::new(),
1277            },
1278            vec!["test-service-0".to_string(), "test-service-1".to_string()],
1279        )];
1280
1281        for (records, expected) in tests {
1282            let services = services_from_records(records).unwrap();
1283            assert_eq!(services, expected);
1284        }
1285    }
1286
1287    #[test]
1288    fn test_operations_from_records() {
1289        // The tests is the tuple of `(test_records, contain_span_kind, expected)`.
1290        let tests = vec![
1291            (
1292                HttpRecordsOutput {
1293                    schema: OutputSchema {
1294                        column_schemas: vec![
1295                            ColumnSchema {
1296                                name: "span_name".to_string(),
1297                                data_type: "String".to_string(),
1298                            },
1299                            ColumnSchema {
1300                                name: "span_kind".to_string(),
1301                                data_type: "String".to_string(),
1302                            },
1303                        ],
1304                    },
1305                    rows: vec![
1306                        vec![
1307                            JsonValue::String("access-mysql".to_string()),
1308                            JsonValue::String("SPAN_KIND_SERVER".to_string()),
1309                        ],
1310                        vec![
1311                            JsonValue::String("access-redis".to_string()),
1312                            JsonValue::String("SPAN_KIND_CLIENT".to_string()),
1313                        ],
1314                    ],
1315                    total_rows: 2,
1316                    metrics: HashMap::new(),
1317                },
1318                false,
1319                vec![
1320                    Operation {
1321                        name: "access-mysql".to_string(),
1322                        span_kind: None,
1323                    },
1324                    Operation {
1325                        name: "access-redis".to_string(),
1326                        span_kind: None,
1327                    },
1328                ],
1329            ),
1330            (
1331                HttpRecordsOutput {
1332                    schema: OutputSchema {
1333                        column_schemas: vec![
1334                            ColumnSchema {
1335                                name: "span_name".to_string(),
1336                                data_type: "String".to_string(),
1337                            },
1338                            ColumnSchema {
1339                                name: "span_kind".to_string(),
1340                                data_type: "String".to_string(),
1341                            },
1342                        ],
1343                    },
1344                    rows: vec![
1345                        vec![
1346                            JsonValue::String("access-mysql".to_string()),
1347                            JsonValue::String("SPAN_KIND_SERVER".to_string()),
1348                        ],
1349                        vec![
1350                            JsonValue::String("access-redis".to_string()),
1351                            JsonValue::String("SPAN_KIND_CLIENT".to_string()),
1352                        ],
1353                    ],
1354                    total_rows: 2,
1355                    metrics: HashMap::new(),
1356                },
1357                true,
1358                vec![
1359                    Operation {
1360                        name: "access-mysql".to_string(),
1361                        span_kind: Some("server".to_string()),
1362                    },
1363                    Operation {
1364                        name: "access-redis".to_string(),
1365                        span_kind: Some("client".to_string()),
1366                    },
1367                ],
1368            ),
1369        ];
1370
1371        for (records, contain_span_kind, expected) in tests {
1372            let operations = operations_from_records(records, contain_span_kind).unwrap();
1373            assert_eq!(operations, expected);
1374        }
1375    }
1376
1377    #[test]
1378    fn test_traces_from_records() {
1379        // The tests is the tuple of `(test_records, expected)`.
1380        let tests = vec![(
1381            HttpRecordsOutput {
1382                schema: OutputSchema {
1383                    column_schemas: vec![
1384                        ColumnSchema {
1385                            name: "trace_id".to_string(),
1386                            data_type: "String".to_string(),
1387                        },
1388                        ColumnSchema {
1389                            name: "timestamp".to_string(),
1390                            data_type: "TimestampNanosecond".to_string(),
1391                        },
1392                        ColumnSchema {
1393                            name: "duration_nano".to_string(),
1394                            data_type: "UInt64".to_string(),
1395                        },
1396                        ColumnSchema {
1397                            name: "service_name".to_string(),
1398                            data_type: "String".to_string(),
1399                        },
1400                        ColumnSchema {
1401                            name: "span_name".to_string(),
1402                            data_type: "String".to_string(),
1403                        },
1404                        ColumnSchema {
1405                            name: "span_id".to_string(),
1406                            data_type: "String".to_string(),
1407                        },
1408                        ColumnSchema {
1409                            name: "span_attributes".to_string(),
1410                            data_type: "Json".to_string(),
1411                        },
1412                    ],
1413                },
1414                rows: vec![
1415                    vec![
1416                        JsonValue::String("5611dce1bc9ebed65352d99a027b08ea".to_string()),
1417                        JsonValue::Number(Number::from_u128(1738726754492422000).unwrap()),
1418                        JsonValue::Number(Number::from_u128(100000000).unwrap()),
1419                        JsonValue::String("test-service-0".to_string()),
1420                        JsonValue::String("access-mysql".to_string()),
1421                        JsonValue::String("008421dbbd33a3e9".to_string()),
1422                        JsonValue::Object(
1423                            json!({
1424                                "operation.type": "access-mysql",
1425                            })
1426                            .as_object()
1427                            .unwrap()
1428                            .clone(),
1429                        ),
1430                    ],
1431                    vec![
1432                        JsonValue::String("5611dce1bc9ebed65352d99a027b08ea".to_string()),
1433                        JsonValue::Number(Number::from_u128(1738726754642422000).unwrap()),
1434                        JsonValue::Number(Number::from_u128(100000000).unwrap()),
1435                        JsonValue::String("test-service-0".to_string()),
1436                        JsonValue::String("access-redis".to_string()),
1437                        JsonValue::String("ffa03416a7b9ea48".to_string()),
1438                        JsonValue::Object(
1439                            json!({
1440                                "operation.type": "access-redis",
1441                            })
1442                            .as_object()
1443                            .unwrap()
1444                            .clone(),
1445                        ),
1446                    ],
1447                ],
1448                total_rows: 2,
1449                metrics: HashMap::new(),
1450            },
1451            vec![Trace {
1452                trace_id: "5611dce1bc9ebed65352d99a027b08ea".to_string(),
1453                spans: vec![
1454                    Span {
1455                        trace_id: "5611dce1bc9ebed65352d99a027b08ea".to_string(),
1456                        span_id: "008421dbbd33a3e9".to_string(),
1457                        operation_name: "access-mysql".to_string(),
1458                        start_time: 1738726754492422,
1459                        duration: 100000,
1460                        tags: vec![KeyValue {
1461                            key: "operation.type".to_string(),
1462                            value_type: ValueType::String,
1463                            value: Value::String("access-mysql".to_string()),
1464                        }],
1465                        process_id: "p1".to_string(),
1466                        ..Default::default()
1467                    },
1468                    Span {
1469                        trace_id: "5611dce1bc9ebed65352d99a027b08ea".to_string(),
1470                        span_id: "ffa03416a7b9ea48".to_string(),
1471                        operation_name: "access-redis".to_string(),
1472                        start_time: 1738726754642422,
1473                        duration: 100000,
1474                        tags: vec![KeyValue {
1475                            key: "operation.type".to_string(),
1476                            value_type: ValueType::String,
1477                            value: Value::String("access-redis".to_string()),
1478                        }],
1479                        process_id: "p1".to_string(),
1480                        ..Default::default()
1481                    },
1482                ],
1483                processes: HashMap::from([(
1484                    "p1".to_string(),
1485                    Process {
1486                        service_name: "test-service-0".to_string(),
1487                        tags: vec![],
1488                    },
1489                )]),
1490                ..Default::default()
1491            }],
1492        )];
1493
1494        for (records, expected) in tests {
1495            let traces = traces_from_records(records).unwrap();
1496            assert_eq!(traces, expected);
1497        }
1498    }
1499
1500    #[test]
1501    fn test_traces_from_v1_records() {
1502        // The tests is the tuple of `(test_records, expected)`.
1503        let tests = vec![(
1504            HttpRecordsOutput {
1505                schema: OutputSchema {
1506                    column_schemas: vec![
1507                        ColumnSchema {
1508                            name: "trace_id".to_string(),
1509                            data_type: "String".to_string(),
1510                        },
1511                        ColumnSchema {
1512                            name: "timestamp".to_string(),
1513                            data_type: "TimestampNanosecond".to_string(),
1514                        },
1515                        ColumnSchema {
1516                            name: "duration_nano".to_string(),
1517                            data_type: "UInt64".to_string(),
1518                        },
1519                        ColumnSchema {
1520                            name: "service_name".to_string(),
1521                            data_type: "String".to_string(),
1522                        },
1523                        ColumnSchema {
1524                            name: "span_name".to_string(),
1525                            data_type: "String".to_string(),
1526                        },
1527                        ColumnSchema {
1528                            name: "span_id".to_string(),
1529                            data_type: "String".to_string(),
1530                        },
1531                        ColumnSchema {
1532                            name: "span_attributes.http.request.method".to_string(),
1533                            data_type: "String".to_string(),
1534                        },
1535                        ColumnSchema {
1536                            name: "span_attributes.http.request.url".to_string(),
1537                            data_type: "String".to_string(),
1538                        },
1539                        ColumnSchema {
1540                            name: "span_attributes.http.status_code".to_string(),
1541                            data_type: "UInt64".to_string(),
1542                        },
1543                    ],
1544                },
1545                rows: vec![
1546                    vec![
1547                        JsonValue::String("5611dce1bc9ebed65352d99a027b08ea".to_string()),
1548                        JsonValue::Number(Number::from_u128(1738726754492422000).unwrap()),
1549                        JsonValue::Number(Number::from_u128(100000000).unwrap()),
1550                        JsonValue::String("test-service-0".to_string()),
1551                        JsonValue::String("access-mysql".to_string()),
1552                        JsonValue::String("008421dbbd33a3e9".to_string()),
1553                        JsonValue::String("GET".to_string()),
1554                        JsonValue::String("/data".to_string()),
1555                        JsonValue::Number(Number::from_u128(200).unwrap()),
1556                    ],
1557                    vec![
1558                        JsonValue::String("5611dce1bc9ebed65352d99a027b08ea".to_string()),
1559                        JsonValue::Number(Number::from_u128(1738726754642422000).unwrap()),
1560                        JsonValue::Number(Number::from_u128(100000000).unwrap()),
1561                        JsonValue::String("test-service-0".to_string()),
1562                        JsonValue::String("access-redis".to_string()),
1563                        JsonValue::String("ffa03416a7b9ea48".to_string()),
1564                        JsonValue::String("POST".to_string()),
1565                        JsonValue::String("/create".to_string()),
1566                        JsonValue::Number(Number::from_u128(400).unwrap()),
1567                    ],
1568                ],
1569                total_rows: 2,
1570                metrics: HashMap::new(),
1571            },
1572            vec![Trace {
1573                trace_id: "5611dce1bc9ebed65352d99a027b08ea".to_string(),
1574                spans: vec![
1575                    Span {
1576                        trace_id: "5611dce1bc9ebed65352d99a027b08ea".to_string(),
1577                        span_id: "008421dbbd33a3e9".to_string(),
1578                        operation_name: "access-mysql".to_string(),
1579                        start_time: 1738726754492422,
1580                        duration: 100000,
1581                        tags: vec![
1582                            KeyValue {
1583                                key: "http.request.method".to_string(),
1584                                value_type: ValueType::String,
1585                                value: Value::String("GET".to_string()),
1586                            },
1587                            KeyValue {
1588                                key: "http.request.url".to_string(),
1589                                value_type: ValueType::String,
1590                                value: Value::String("/data".to_string()),
1591                            },
1592                            KeyValue {
1593                                key: "http.status_code".to_string(),
1594                                value_type: ValueType::Int64,
1595                                value: Value::Int64(200),
1596                            },
1597                        ],
1598                        process_id: "p1".to_string(),
1599                        ..Default::default()
1600                    },
1601                    Span {
1602                        trace_id: "5611dce1bc9ebed65352d99a027b08ea".to_string(),
1603                        span_id: "ffa03416a7b9ea48".to_string(),
1604                        operation_name: "access-redis".to_string(),
1605                        start_time: 1738726754642422,
1606                        duration: 100000,
1607                        tags: vec![
1608                            KeyValue {
1609                                key: "http.request.method".to_string(),
1610                                value_type: ValueType::String,
1611                                value: Value::String("POST".to_string()),
1612                            },
1613                            KeyValue {
1614                                key: "http.request.url".to_string(),
1615                                value_type: ValueType::String,
1616                                value: Value::String("/create".to_string()),
1617                            },
1618                            KeyValue {
1619                                key: "http.status_code".to_string(),
1620                                value_type: ValueType::Int64,
1621                                value: Value::Int64(400),
1622                            },
1623                        ],
1624                        process_id: "p1".to_string(),
1625                        ..Default::default()
1626                    },
1627                ],
1628                processes: HashMap::from([(
1629                    "p1".to_string(),
1630                    Process {
1631                        service_name: "test-service-0".to_string(),
1632                        tags: vec![],
1633                    },
1634                )]),
1635                ..Default::default()
1636            }],
1637        )];
1638
1639        for (records, expected) in tests {
1640            let traces = traces_from_records(records).unwrap();
1641            assert_eq!(traces, expected);
1642        }
1643    }
1644
1645    #[test]
1646    fn test_from_jaeger_query_params() {
1647        // The tests is the tuple of `(test_query_params, expected)`.
1648        let tests = vec![
1649            (
1650                JaegerQueryParams {
1651                    service_name: Some("test-service-0".to_string()),
1652                    ..Default::default()
1653                },
1654                QueryTraceParams {
1655                    service_name: Some("test-service-0".to_string()),
1656                    ..Default::default()
1657                },
1658            ),
1659            (
1660                JaegerQueryParams {
1661                    service_name: Some("test-service-0".to_string()),
1662                    operation_name: Some("access-mysql".to_string()),
1663                    start: Some(1738726754492422),
1664                    end: Some(1738726754642422),
1665                    max_duration: Some("100ms".to_string()),
1666                    min_duration: Some("50ms".to_string()),
1667                    limit: Some(10),
1668                    tags: Some("{\"http.status_code\":\"200\",\"latency\":\"11.234\",\"error\":\"false\",\"http.method\":\"GET\",\"http.path\":\"/api/v1/users\"}".to_string()),
1669                    ..Default::default()
1670                },
1671                QueryTraceParams {
1672                    service_name: Some("test-service-0".to_string()),
1673                    operation_name: Some("access-mysql".to_string()),
1674                    start_time: Some(1738726754492422000),
1675                    end_time: Some(1738726754642422000),
1676                    min_duration: Some(50000000),
1677                    max_duration: Some(100000000),
1678                    limit: Some(10),
1679                    tags: Some(HashMap::from([
1680                        ("http.status_code".to_string(), JsonValue::Number(Number::from(200))),
1681                        ("latency".to_string(), JsonValue::Number(Number::from_f64(11.234).unwrap())),
1682                        ("error".to_string(), JsonValue::Bool(false)),
1683                        ("http.method".to_string(), JsonValue::String("GET".to_string())),
1684                        ("http.path".to_string(), JsonValue::String("/api/v1/users".to_string())),
1685                    ])),
1686                    user_agent: TraceUserAgent::Jaeger,
1687                    fetch_full_trace: false,
1688                },
1689            ),
1690        ];
1691
1692        for (query_params, expected) in tests {
1693            let query_params = QueryTraceParams::from_jaeger_query_params(query_params).unwrap();
1694            assert_eq!(query_params, expected);
1695        }
1696    }
1697
1698    #[test]
1699    fn test_check_schema() {
1700        // The tests is the tuple of `(test_records, expected_schema, is_ok)`.
1701        let tests = vec![(
1702            HttpRecordsOutput {
1703                schema: OutputSchema {
1704                    column_schemas: vec![
1705                        ColumnSchema {
1706                            name: "trace_id".to_string(),
1707                            data_type: "String".to_string(),
1708                        },
1709                        ColumnSchema {
1710                            name: "timestamp".to_string(),
1711                            data_type: "TimestampNanosecond".to_string(),
1712                        },
1713                        ColumnSchema {
1714                            name: "duration_nano".to_string(),
1715                            data_type: "UInt64".to_string(),
1716                        },
1717                        ColumnSchema {
1718                            name: "service_name".to_string(),
1719                            data_type: "String".to_string(),
1720                        },
1721                        ColumnSchema {
1722                            name: "span_name".to_string(),
1723                            data_type: "String".to_string(),
1724                        },
1725                        ColumnSchema {
1726                            name: "span_id".to_string(),
1727                            data_type: "String".to_string(),
1728                        },
1729                        ColumnSchema {
1730                            name: "span_attributes".to_string(),
1731                            data_type: "Json".to_string(),
1732                        },
1733                    ],
1734                },
1735                rows: vec![],
1736                total_rows: 0,
1737                metrics: HashMap::new(),
1738            },
1739            vec![
1740                (TRACE_ID_COLUMN, "String"),
1741                (TIMESTAMP_COLUMN, "TimestampNanosecond"),
1742                (DURATION_NANO_COLUMN, "UInt64"),
1743                (SERVICE_NAME_COLUMN, "String"),
1744                (SPAN_NAME_COLUMN, "String"),
1745                (SPAN_ID_COLUMN, "String"),
1746                (SPAN_ATTRIBUTES_COLUMN, "Json"),
1747            ],
1748            true,
1749        )];
1750
1751        for (records, expected_schema, is_ok) in tests {
1752            let result = check_schema(&records, &expected_schema);
1753            assert_eq!(result.is_ok(), is_ok);
1754        }
1755    }
1756
1757    #[test]
1758    fn test_normalize_span_kind() {
1759        let tests = vec![
1760            ("SPAN_KIND_SERVER".to_string(), "server".to_string()),
1761            ("SPAN_KIND_CLIENT".to_string(), "client".to_string()),
1762        ];
1763
1764        for (input, expected) in tests {
1765            let result = normalize_span_kind(&input);
1766            assert_eq!(result, expected);
1767        }
1768    }
1769
1770    #[test]
1771    fn test_convert_string_to_number() {
1772        let tests = vec![
1773            (
1774                JsonValue::String("123".to_string()),
1775                Some(JsonValue::Number(Number::from(123))),
1776            ),
1777            (
1778                JsonValue::String("123.456".to_string()),
1779                Some(JsonValue::Number(Number::from_f64(123.456).unwrap())),
1780            ),
1781        ];
1782
1783        for (input, expected) in tests {
1784            let result = convert_string_to_number(&input);
1785            assert_eq!(result, expected);
1786        }
1787    }
1788
1789    #[test]
1790    fn test_convert_string_to_boolean() {
1791        let tests = vec![
1792            (
1793                JsonValue::String("true".to_string()),
1794                Some(JsonValue::Bool(true)),
1795            ),
1796            (
1797                JsonValue::String("false".to_string()),
1798                Some(JsonValue::Bool(false)),
1799            ),
1800        ];
1801
1802        for (input, expected) in tests {
1803            let result = convert_string_to_boolean(&input);
1804            assert_eq!(result, expected);
1805        }
1806    }
1807}