1pub 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#[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#[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#[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#[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#[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#[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, pub duration: u64, 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#[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#[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#[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#[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#[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#[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#[derive(Default, Debug, Serialize, Deserialize)]
232#[serde(rename_all = "camelCase")]
233pub struct JaegerQueryParams {
234 #[serde(rename = "service")]
236 pub service_name: Option<String>,
237
238 #[serde(rename = "operation")]
240 pub operation_name: Option<String>,
241
242 #[serde(default, deserialize_with = "empty_string_as_none")]
244 pub limit: Option<usize>,
245
246 pub start: Option<i64>,
248
249 pub end: Option<i64>,
251
252 #[serde(default, deserialize_with = "empty_string_as_none")]
254 pub max_duration: Option<String>,
255
256 #[serde(default, deserialize_with = "empty_string_as_none")]
258 pub min_duration: Option<String>,
259
260 pub tags: Option<String>,
264
265 pub span_kind: Option<String>,
267}
268
269fn 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 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 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 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 pub limit: Option<usize>,
357
358 pub tags: Option<HashMap<String, JsonValue>>,
360
361 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 pub fetch_full_trace: bool,
369
370 pub user_agent: TraceUserAgent,
372}
373
374#[derive(Debug, Default, PartialEq, Eq)]
375pub enum TraceUserAgent {
376 Grafana,
377 #[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#[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 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#[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 let _timer = METRIC_JAEGER_QUERY_ELAPSED
472 .with_label_values(&[db.as_str(), "/api/traces"])
473 .start_timer();
474
475 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#[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 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#[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 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#[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 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 _ => Ok(None),
734 }
735}
736
737fn handle_query_error(
738 err: Error,
739 prompt: &str,
740 db: &str,
741) -> (HttpStatusCode, axum::Json<JaegerAPIResponse>) {
742 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 let mut trace_id_to_processes: HashMap<String, HashMap<String, String>> = HashMap::new();
772 let mut trace_id_to_spans: BTreeMap<String, Vec<Span>> = BTreeMap::new();
775 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 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 if let JsonValue::Object(span_attrs) = cell {
830 span.tags.extend(object_to_tags(span_attrs));
831 }
832 }
833 RESOURCE_ATTRIBUTES_COLUMN => {
834 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 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 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 if is_span_attributes_flatten {
986 const SPAN_ATTR_PREFIX: &str = "span_attributes.";
987 const RESOURCE_ATTR_PREFIX: &str = "resource_attributes.";
988 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 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 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
1140fn 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 row_iter.next();
1163 }
1164 operations.push(operation);
1165 }
1166 }
1167
1168 Ok(operations)
1169}
1170
1171fn 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
1184fn normalize_span_kind(span_kind: &str) -> String {
1187 if let Some(stripped) = span_kind.strip_prefix(SPAN_KIND_PREFIX) {
1189 stripped.to_lowercase()
1190 } else {
1191 span_kind.to_lowercase()
1193 }
1194}
1195
1196fn normalize_status_code(status_code: &str) -> String {
1199 if let Some(stripped) = status_code.strip_prefix(SPAN_STATUS_PREFIX) {
1201 stripped.to_string()
1202 } else {
1203 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 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 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 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 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 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 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}