1use std::sync::Arc;
16
17use api::helper::ColumnDataTypeWrapper;
18use api::v1::alter_table_expr::Kind;
19use api::v1::{
20 AlterTableExpr, ColumnDataType, ModifyColumnType, ModifyColumnTypes, RowInsertRequests,
21};
22use async_trait::async_trait;
23use auth::{PermissionChecker, PermissionCheckerRef, PermissionReq};
24use client::Output;
25use common_error::ext::{BoxedError, ErrorExt};
26use common_error::status_code::StatusCode;
27use common_query::prelude::GREPTIME_PHYSICAL_TABLE;
28use common_telemetry::{tracing, warn};
29use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest;
30use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
31use otel_arrow_rust::proto::opentelemetry::collector::metrics::v1::ExportMetricsServiceRequest;
32use pipeline::{GreptimePipelineParams, PipelineWay};
33use servers::error::{self, AuthSnafu, Result as ServerResult};
34use servers::http::prom_store::PHYSICAL_TABLE_PARAM;
35use servers::interceptor::{OpenTelemetryProtocolInterceptor, OpenTelemetryProtocolInterceptorRef};
36use servers::otlp;
37use servers::otlp::coerce::{coerce_value_data, trace_value_datatype};
38use servers::otlp::trace::TraceAuxData;
39use servers::otlp::trace::span::{TraceSpan, TraceSpanGroup};
40use servers::query_handler::{
41 OpenTelemetryProtocolHandler, PipelineHandlerRef, TraceIngestOutcome,
42};
43use session::context::QueryContextRef;
44use snafu::{IntoError, ResultExt};
45use table::requests::{
46 OTLP_METRIC_COMPAT_KEY, OTLP_METRIC_COMPAT_PROM, SEMANTIC_PER_TABLE_INDEX_KEY,
47 SEMANTIC_PIPELINE, SEMANTIC_SIGNAL_TYPE, SEMANTIC_SOURCE, SEMANTIC_TRACE_CONVENTIONS,
48 SEMANTIC_VALUE_MIXED, SEMANTIC_VALUE_UNKNOWN, SIGNAL_TYPE_LOG, SIGNAL_TYPE_METRIC,
49 SIGNAL_TYPE_TRACE, SOURCE_OPENTELEMETRY, TABLE_DATA_MODEL_TRACE_V1,
50};
51
52use crate::instance::Instance;
53use crate::instance::otlp::trace_semconv::trace_semconv_fixed_type;
54use crate::instance::otlp::trace_types::{
55 PendingTraceColumnRewrite, choose_trace_reconcile_decision, enrich_trace_reconcile_error,
56 is_trace_reconcile_candidate_type, push_observed_trace_type, validate_trace_column_rewrites,
57};
58use crate::metrics::{
59 OTLP_LOGS_ROWS, OTLP_METRICS_ROWS, OTLP_TRACES_FAILURE_COUNT, OTLP_TRACES_ROWS,
60};
61
62pub mod trace_semconv;
63pub mod trace_types;
64
65const TRACE_FAILURE_MESSAGE_LIMIT: usize = 4;
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68enum ChunkFailureReaction {
69 RetryPerSpan,
70 DiscardChunk,
71 Propagate,
72}
73
74impl ChunkFailureReaction {
75 fn as_metric_label(self) -> &'static str {
76 match self {
77 Self::RetryPerSpan => "retry_per_span",
78 Self::DiscardChunk => "discard_chunk",
79 Self::Propagate => "propagate_failure",
80 }
81 }
82}
83
84struct TraceChunkIngestContext<'a> {
85 pipeline_handler: PipelineHandlerRef,
86 pipeline: &'a PipelineWay,
87 pipeline_params: &'a GreptimePipelineParams,
88 table_name: &'a str,
89 is_trace_v1_model: bool,
90}
91
92struct TraceIngestState {
93 aux_data: TraceAuxData,
94 outcome: TraceIngestOutcome,
95 failure_messages: Vec<String>,
96}
97
98#[async_trait]
99impl OpenTelemetryProtocolHandler for Instance {
100 #[tracing::instrument(skip_all)]
101 async fn metrics(
102 &self,
103 request: ExportMetricsServiceRequest,
104 ctx: QueryContextRef,
105 ) -> ServerResult<Output> {
106 self.plugins
107 .get::<PermissionCheckerRef>()
108 .as_ref()
109 .check_permission(ctx.current_user(), PermissionReq::Otlp)
110 .context(AuthSnafu)?;
111
112 let interceptor_ref = self
113 .plugins
114 .get::<OpenTelemetryProtocolInterceptorRef<servers::error::Error>>();
115 interceptor_ref.pre_execute(ctx.clone())?;
116
117 let input_names = request
118 .resource_metrics
119 .iter()
120 .flat_map(|r| r.scope_metrics.iter())
121 .flat_map(|s| s.metrics.iter().map(|m| &m.name))
122 .collect::<Vec<_>>();
123
124 let is_legacy = self.check_otlp_legacy(&input_names, ctx.clone()).await?;
126
127 let mut metric_ctx = ctx
128 .protocol_ctx()
129 .get_otlp_metric_ctx()
130 .cloned()
131 .unwrap_or_default();
132 metric_ctx.is_legacy = is_legacy;
133
134 let (requests, rows, semantic_index) =
135 otlp::metrics::to_grpc_insert_requests(request, &mut metric_ctx)?;
136 OTLP_METRICS_ROWS.inc_by(rows as u64);
137
138 let ctx = {
139 let mut c = (*ctx).clone();
140 c.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_METRIC);
141 c.set_extension(SEMANTIC_SOURCE, SOURCE_OPENTELEMETRY);
142 if let Some(index) = semantic_index.encode() {
145 c.set_extension(SEMANTIC_PER_TABLE_INDEX_KEY, index);
146 }
147 if !is_legacy {
148 c.set_extension(OTLP_METRIC_COMPAT_KEY, OTLP_METRIC_COMPAT_PROM.to_string());
149 }
150 Arc::new(c)
151 };
152
153 if metric_ctx.is_legacy || !metric_ctx.with_metric_engine {
155 self.handle_row_inserts(requests, ctx, false, false)
156 .await
157 .map_err(BoxedError::new)
158 .context(error::ExecuteGrpcQuerySnafu)
159 } else {
160 let physical_table = ctx
161 .extension(PHYSICAL_TABLE_PARAM)
162 .unwrap_or(GREPTIME_PHYSICAL_TABLE)
163 .to_string();
164 self.handle_metric_row_inserts(requests, ctx, physical_table.clone())
165 .await
166 .map_err(BoxedError::new)
167 .context(error::ExecuteGrpcQuerySnafu)
168 }
169 }
170
171 #[tracing::instrument(skip_all)]
172 async fn traces(
173 &self,
174 pipeline_handler: PipelineHandlerRef,
175 request: ExportTraceServiceRequest,
176 pipeline: PipelineWay,
177 pipeline_params: GreptimePipelineParams,
178 table_name: String,
179 ctx: QueryContextRef,
180 ) -> ServerResult<TraceIngestOutcome> {
181 self.plugins
182 .get::<PermissionCheckerRef>()
183 .as_ref()
184 .check_permission(ctx.current_user(), PermissionReq::Otlp)
185 .context(AuthSnafu)?;
186
187 let interceptor_ref = self
188 .plugins
189 .get::<OpenTelemetryProtocolInterceptorRef<servers::error::Error>>();
190 interceptor_ref.pre_execute(ctx.clone())?;
191
192 let conventions = trace_conventions(&request);
194 let spans = otlp::trace::span::parse(request);
195 self.ingest_trace_spans(
196 pipeline_handler,
197 &pipeline,
198 &pipeline_params,
199 table_name,
200 spans,
201 &conventions,
202 ctx,
203 )
204 .await
205 }
206
207 #[tracing::instrument(skip_all)]
208 async fn logs(
209 &self,
210 pipeline_handler: PipelineHandlerRef,
211 request: ExportLogsServiceRequest,
212 pipeline: PipelineWay,
213 pipeline_params: GreptimePipelineParams,
214 table_name: String,
215 ctx: QueryContextRef,
216 ) -> ServerResult<Vec<Output>> {
217 self.plugins
218 .get::<PermissionCheckerRef>()
219 .as_ref()
220 .check_permission(ctx.current_user(), PermissionReq::Otlp)
221 .context(AuthSnafu)?;
222
223 let interceptor_ref = self
224 .plugins
225 .get::<OpenTelemetryProtocolInterceptorRef<servers::error::Error>>();
226 interceptor_ref.pre_execute(ctx.clone())?;
227
228 let ctx = {
231 let mut c = (*ctx).clone();
232 c.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_LOG);
233 c.set_extension(SEMANTIC_SOURCE, SOURCE_OPENTELEMETRY);
234 Arc::new(c)
235 };
236
237 let opt_req = otlp::logs::to_grpc_insert_requests(
238 request,
239 pipeline,
240 pipeline_params,
241 table_name,
242 &ctx,
243 pipeline_handler,
244 )
245 .await?;
246
247 let mut outputs = vec![];
248
249 for (temp_ctx, requests) in opt_req.as_req_iter(ctx) {
250 let cnt = requests
251 .inserts
252 .iter()
253 .filter_map(|r| r.rows.as_ref().map(|r| r.rows.len()))
254 .sum::<usize>();
255
256 let o = self
257 .handle_log_inserts(requests, temp_ctx)
258 .await
259 .inspect(|_| OTLP_LOGS_ROWS.inc_by(cnt as u64))
260 .map_err(BoxedError::new)
261 .context(error::ExecuteGrpcQuerySnafu)?;
262 outputs.push(o);
263 }
264
265 Ok(outputs)
266 }
267}
268
269impl Instance {
270 #[allow(clippy::too_many_arguments)]
273 async fn ingest_trace_spans(
274 &self,
275 pipeline_handler: PipelineHandlerRef,
276 pipeline: &PipelineWay,
277 pipeline_params: &GreptimePipelineParams,
278 table_name: String,
279 groups: Vec<TraceSpanGroup>,
280 conventions: &str,
281 ctx: QueryContextRef,
282 ) -> ServerResult<TraceIngestOutcome> {
283 let is_trace_v1_model = matches!(pipeline, PipelineWay::OtlpTraceDirectV1);
284
285 let main_ctx = {
288 let mut c = (*ctx).clone();
289 c.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_TRACE);
290 c.set_extension(SEMANTIC_SOURCE, SOURCE_OPENTELEMETRY);
291 if is_trace_v1_model {
292 c.set_extension(SEMANTIC_PIPELINE, TABLE_DATA_MODEL_TRACE_V1);
293 c.set_extension(SEMANTIC_TRACE_CONVENTIONS, conventions);
294 }
295 Arc::new(c)
296 };
297
298 let ingest_ctx = TraceChunkIngestContext {
299 pipeline_handler,
300 pipeline,
301 pipeline_params,
302 table_name: &table_name,
303 is_trace_v1_model,
304 };
305 let mut ingest_state = TraceIngestState {
306 aux_data: TraceAuxData::default(),
307 outcome: TraceIngestOutcome::default(),
308 failure_messages: Vec::new(),
309 };
310
311 for group in groups {
312 let chunks = chunk_owned(group.spans, self.trace_ingest_chunk_size);
313 for chunk in chunks {
314 self.ingest_trace_chunk(&ingest_ctx, chunk, main_ctx.clone(), &mut ingest_state)
315 .await?;
316 }
317 }
318
319 OTLP_TRACES_ROWS.inc_by(ingest_state.outcome.accepted_spans as u64);
320
321 if !ingest_state.aux_data.is_empty() {
322 let (aux_requests, _) = otlp::trace::to_grpc_insert_requests_for_aux_tables(
326 std::mem::take(&mut ingest_state.aux_data),
327 ingest_ctx.pipeline,
328 ingest_ctx.table_name,
329 )?;
330
331 if !aux_requests.inserts.is_empty() {
332 match self
333 .insert_trace_requests(aux_requests, ingest_ctx.is_trace_v1_model, ctx)
334 .await
335 {
336 Ok(output) => {
337 Self::add_trace_write_cost(&mut ingest_state.outcome, output.meta.cost);
338 }
339 Err(err) => {
340 Self::push_trace_failure_message(
341 &mut ingest_state.failure_messages,
342 "aux_table_update_failed",
343 format!(
344 "Auxiliary trace tables were not fully updated ({})",
345 err.status_code().as_ref()
346 ),
347 );
348 }
349 }
350 }
351 }
352
353 ingest_state.outcome.error_message = Self::finish_trace_failure_message(
354 ingest_state.outcome.accepted_spans,
355 ingest_state.outcome.rejected_spans,
356 ingest_state.failure_messages,
357 );
358
359 Ok(ingest_state.outcome)
360 }
361
362 async fn ingest_trace_chunk(
365 &self,
366 ingest_ctx: &TraceChunkIngestContext<'_>,
367 chunk: Vec<TraceSpan>,
368 ctx: QueryContextRef,
369 ingest_state: &mut TraceIngestState,
370 ) -> ServerResult<()> {
371 let (requests, chunk_rows) = otlp::trace::to_grpc_insert_requests_from_spans(
374 &chunk,
375 ingest_ctx.pipeline,
376 ingest_ctx.pipeline_params,
377 ingest_ctx.table_name,
378 &ctx,
379 ingest_ctx.pipeline_handler.clone(),
380 )?;
381
382 match self
383 .insert_trace_requests(requests, ingest_ctx.is_trace_v1_model, ctx.clone())
384 .await
385 {
386 Ok(output) => {
387 Self::add_trace_write_cost(&mut ingest_state.outcome, output.meta.cost);
388 ingest_state.outcome.accepted_spans += chunk_rows;
389 for span in &chunk {
390 ingest_state.aux_data.observe_span(span);
391 }
392 }
393 Err(err) => match Self::classify_trace_chunk_failure(err.status_code()) {
394 ChunkFailureReaction::RetryPerSpan => {
395 Self::push_trace_failure_message(
396 &mut ingest_state.failure_messages,
397 ChunkFailureReaction::RetryPerSpan.as_metric_label(),
398 format!("Chunk fallback triggered by {}", err.status_code().as_ref()),
399 );
400 self.ingest_trace_chunk_span_by_span(
406 ingest_ctx,
407 chunk,
408 ctx.clone(),
409 ingest_state,
410 )
411 .await?;
412 }
413 ChunkFailureReaction::DiscardChunk => {
414 ingest_state.outcome.rejected_spans += chunk.len();
415 Self::push_trace_failure_message(
416 &mut ingest_state.failure_messages,
417 ChunkFailureReaction::DiscardChunk.as_metric_label(),
418 format!(
419 "Discarded {} spans after ambiguous chunk failure ({})",
420 chunk.len(),
421 err.status_code().as_ref()
422 ),
423 );
424 }
427 ChunkFailureReaction::Propagate => {
431 Self::push_trace_failure_message(
432 &mut ingest_state.failure_messages,
433 ChunkFailureReaction::Propagate.as_metric_label(),
434 format!(
435 "Propagating retryable chunk failure ({})",
436 err.status_code().as_ref()
437 ),
438 );
439 return Err(err);
440 }
441 },
442 }
443
444 Ok(())
445 }
446
447 async fn ingest_trace_chunk_span_by_span(
449 &self,
450 ingest_ctx: &TraceChunkIngestContext<'_>,
451 chunk: Vec<TraceSpan>,
452 ctx: QueryContextRef,
453 ingest_state: &mut TraceIngestState,
454 ) -> ServerResult<()> {
455 for span in chunk {
456 let (requests, rows) = otlp::trace::to_grpc_insert_requests_from_spans(
457 std::slice::from_ref(&span),
458 ingest_ctx.pipeline,
459 ingest_ctx.pipeline_params,
460 ingest_ctx.table_name,
461 &ctx,
462 ingest_ctx.pipeline_handler.clone(),
463 )?;
464
465 match self
466 .insert_trace_requests(requests, ingest_ctx.is_trace_v1_model, ctx.clone())
467 .await
468 {
469 Ok(output) => {
470 Self::add_trace_write_cost(&mut ingest_state.outcome, output.meta.cost);
471 ingest_state.outcome.accepted_spans += rows;
472 ingest_state.aux_data.observe_span(&span);
473 }
474 Err(err) => {
475 if Self::should_propagate_trace_span_failure(err.status_code()) {
476 Self::push_trace_failure_message(
477 &mut ingest_state.failure_messages,
478 ChunkFailureReaction::Propagate.as_metric_label(),
479 format!(
480 "Propagating retryable span failure for {}:{} ({})",
481 span.trace_id,
482 span.span_id,
483 err.status_code().as_ref()
484 ),
485 );
486 return Err(err);
487 }
488
489 ingest_state.outcome.rejected_spans += 1;
490 Self::push_trace_failure_message(
491 &mut ingest_state.failure_messages,
492 "span_rejected",
493 format!(
494 "Rejected span {}:{} ({})",
495 span.trace_id,
496 span.span_id,
497 err.status_code().as_ref()
498 ),
499 );
500 }
501 }
502 }
503
504 Ok(())
505 }
506
507 async fn insert_trace_requests(
509 &self,
510 mut requests: RowInsertRequests,
511 is_trace_v1_model: bool,
512 ctx: QueryContextRef,
513 ) -> ServerResult<Output> {
514 if is_trace_v1_model {
515 self.reconcile_trace_column_types(&mut requests, &ctx)
516 .await?;
517 self.handle_trace_inserts(requests, ctx)
518 .await
519 .map_err(BoxedError::new)
520 .context(error::ExecuteGrpcQuerySnafu)
521 } else {
522 self.handle_log_inserts(requests, ctx)
523 .await
524 .map_err(BoxedError::new)
525 .context(error::ExecuteGrpcQuerySnafu)
526 }
527 }
528
529 fn classify_trace_chunk_failure(status: StatusCode) -> ChunkFailureReaction {
530 match status {
531 StatusCode::InvalidArguments
532 | StatusCode::InvalidSyntax
533 | StatusCode::Unsupported
534 | StatusCode::TableNotFound
535 | StatusCode::TableColumnNotFound => ChunkFailureReaction::RetryPerSpan,
536 StatusCode::DatabaseNotFound => ChunkFailureReaction::DiscardChunk,
537 StatusCode::Cancelled | StatusCode::DeadlineExceeded => ChunkFailureReaction::Propagate,
538 StatusCode::StorageUnavailable
539 | StatusCode::RuntimeResourcesExhausted
540 | StatusCode::Internal
541 | StatusCode::RegionNotReady
542 | StatusCode::TableUnavailable
543 | StatusCode::RegionBusy => ChunkFailureReaction::Propagate,
544 _ => ChunkFailureReaction::DiscardChunk,
545 }
546 }
547
548 fn should_propagate_trace_span_failure(status: StatusCode) -> bool {
549 matches!(
550 Self::classify_trace_chunk_failure(status),
551 ChunkFailureReaction::Propagate
552 )
553 }
554
555 fn add_trace_write_cost(outcome: &mut TraceIngestOutcome, cost: usize) {
556 outcome.write_cost += cost;
557 }
558
559 fn push_trace_failure_message(messages: &mut Vec<String>, label: &str, message: String) {
560 OTLP_TRACES_FAILURE_COUNT.with_label_values(&[label]).inc();
561
562 if messages.len() < TRACE_FAILURE_MESSAGE_LIMIT {
563 messages.push(message);
564 } else if messages.len() == TRACE_FAILURE_MESSAGE_LIMIT {
565 tracing::debug!(
566 label,
567 limit = TRACE_FAILURE_MESSAGE_LIMIT,
568 "Trace ingest failure message limit reached; suppressing additional failure details"
569 );
570 }
571 }
572
573 fn finish_trace_failure_message(
574 accepted_spans: usize,
575 rejected_spans: usize,
576 messages: Vec<String>,
577 ) -> Option<String> {
578 if rejected_spans == 0 && messages.is_empty() {
579 return None;
580 }
581
582 let mut summary = format!(
583 "Accepted {} spans, rejected {} spans",
584 accepted_spans, rejected_spans
585 );
586
587 if !messages.is_empty() {
588 summary.push_str(": ");
589 summary.push_str(&messages.join("; "));
590 }
591
592 Some(summary)
593 }
594
595 async fn alter_trace_table_columns_to_float64(
597 &self,
598 ctx: &QueryContextRef,
599 table_name: &str,
600 column_names: &[String],
601 ) -> ServerResult<()> {
602 let catalog_name = ctx.current_catalog().to_string();
603 let schema_name = ctx.current_schema();
604 let alter_expr = AlterTableExpr {
605 catalog_name: catalog_name.clone(),
606 schema_name: schema_name.clone(),
607 table_name: table_name.to_string(),
608 kind: Some(Kind::ModifyColumnTypes(ModifyColumnTypes {
609 modify_column_types: column_names
610 .iter()
611 .map(|column_name| ModifyColumnType {
612 column_name: column_name.clone(),
613 target_type: ColumnDataType::Float64 as i32,
614 target_type_extension: None,
615 })
616 .collect(),
617 })),
618 };
619
620 if let Err(err) = self
621 .statement_executor
622 .alter_table_inner(alter_expr, ctx.clone())
623 .await
624 {
625 let table = self
626 .catalog_manager
627 .table(&catalog_name, &schema_name, table_name, None)
628 .await
629 .map_err(servers::error::Error::from)?;
630 let alter_already_applied = table
631 .map(|table| {
632 let table_schema = table.schema();
633 column_names.iter().all(|column_name| {
634 table_schema
635 .column_schema_by_name(column_name)
636 .and_then(|table_col| {
637 ColumnDataTypeWrapper::try_from(table_col.data_type.clone())
638 .ok()
639 .map(|wrapper| wrapper.datatype())
640 })
641 == Some(ColumnDataType::Float64)
642 })
643 })
644 .unwrap_or(false);
645
646 if alter_already_applied {
647 return Ok(());
648 }
649
650 warn!(
651 table_name,
652 columns = ?column_names,
653 error = %err,
654 "failed to widen trace columns before insert"
655 );
656
657 return Err(wrap_trace_alter_failure(err));
658 }
659
660 Ok(())
661 }
662
663 async fn reconcile_trace_column_types(
667 &self,
668 requests: &mut RowInsertRequests,
669 ctx: &QueryContextRef,
670 ) -> ServerResult<()> {
671 let catalog = ctx.current_catalog();
672 let schema = ctx.current_schema();
673
674 for req in &mut requests.inserts {
675 let table = self
676 .catalog_manager
677 .table(catalog, &schema, &req.table_name, None)
678 .await?;
679
680 let Some(rows) = req.rows.as_mut() else {
681 continue;
682 };
683
684 let table_schema = table.map(|table| table.schema());
685 let mut pending_rewrites = Vec::new();
686 let mut pending_alter_columns = Vec::new();
687
688 for (col_idx, col_schema) in rows.schema.iter().enumerate() {
689 let Some(current_type) = ColumnDataType::try_from(col_schema.datatype).ok() else {
690 continue;
691 };
692
693 let mut observed_types = Vec::new();
694 push_observed_trace_type(&mut observed_types, current_type);
695
696 for row in &rows.rows {
699 let Some(value) = row
700 .values
701 .get(col_idx)
702 .and_then(|value| value.value_data.as_ref())
703 else {
704 continue;
705 };
706
707 let Some(value_type) = trace_value_datatype(value) else {
708 continue;
709 };
710 push_observed_trace_type(&mut observed_types, value_type);
711 }
712
713 let existing_type = table_schema
714 .as_ref()
715 .and_then(|schema| schema.column_schema_by_name(&col_schema.column_name))
716 .and_then(|table_col| {
717 ColumnDataTypeWrapper::try_from(table_col.data_type.clone())
718 .ok()
719 .map(|wrapper| wrapper.datatype())
720 });
721 let fixed_type = trace_semconv_fixed_type(&col_schema.column_name);
722
723 if !observed_types
724 .iter()
725 .copied()
726 .any(is_trace_reconcile_candidate_type)
727 && existing_type
728 .map(|datatype| !is_trace_reconcile_candidate_type(datatype))
729 .unwrap_or(true)
730 && fixed_type.is_none()
731 {
732 continue;
733 }
734
735 let Some(decision) = choose_trace_reconcile_decision(
738 &col_schema.column_name,
739 &observed_types,
740 existing_type,
741 )
742 .map_err(|_| {
743 enrich_trace_reconcile_error(
744 &req.table_name,
745 &col_schema.column_name,
746 &observed_types,
747 existing_type,
748 fixed_type,
749 )
750 })?
751 else {
752 continue;
753 };
754 let target_type = decision.target_type();
755
756 if !decision.requires_alter()
757 && observed_types
758 .iter()
759 .all(|observed| *observed == target_type)
760 && col_schema.datatype == target_type as i32
761 {
762 continue;
763 }
764
765 if decision.requires_alter()
766 && !pending_alter_columns.contains(&col_schema.column_name)
767 {
768 pending_alter_columns.push(col_schema.column_name.clone());
769 }
770
771 pending_rewrites.push(PendingTraceColumnRewrite {
772 col_idx,
773 target_type,
774 column_name: col_schema.column_name.clone(),
775 });
776 }
777
778 if pending_rewrites.is_empty() {
779 continue;
780 }
781
782 validate_trace_column_rewrites(&rows.rows, &pending_rewrites, &req.table_name)?;
783
784 if !pending_alter_columns.is_empty() {
785 self.alter_trace_table_columns_to_float64(
786 ctx,
787 &req.table_name,
788 &pending_alter_columns,
789 )
790 .await?;
791 }
792
793 for pending_rewrite in &pending_rewrites {
795 rows.schema[pending_rewrite.col_idx].datatype = pending_rewrite.target_type as i32;
796 }
797
798 for row in &mut rows.rows {
800 for pending_rewrite in &pending_rewrites {
801 let Some(value) = row.values.get_mut(pending_rewrite.col_idx) else {
802 continue;
803 };
804 let Some(request_type) =
805 value.value_data.as_ref().and_then(trace_value_datatype)
806 else {
807 continue;
808 };
809 if request_type == pending_rewrite.target_type {
810 continue;
811 }
812
813 value.value_data = coerce_value_data(
814 &value.value_data,
815 pending_rewrite.target_type,
816 request_type,
817 )
818 .map_err(|_| {
819 error::InvalidParameterSnafu {
820 reason: format!(
821 "failed to coerce trace column '{}' in table '{}' from {:?} to {:?}",
822 pending_rewrite.column_name,
823 req.table_name,
824 request_type,
825 pending_rewrite.target_type
826 ),
827 }
828 .build()
829 })?;
830 }
831 }
832 }
833
834 Ok(())
835 }
836}
837
838fn chunk_owned<T>(items: Vec<T>, chunk_size: usize) -> Vec<Vec<T>> {
839 if items.is_empty() {
840 return Vec::new();
841 }
842
843 if chunk_size == 0 {
844 return vec![items];
845 }
846
847 let mut chunks = Vec::with_capacity(items.len().div_ceil(chunk_size));
848 let mut iter = items.into_iter();
849 while iter.len() > 0 {
850 chunks.push(iter.by_ref().take(chunk_size).collect());
851 }
852 chunks
853}
854
855fn wrap_trace_alter_failure<E>(err: E) -> servers::error::Error
857where
858 E: ErrorExt + Send + Sync + 'static,
859{
860 error::ExecuteGrpcQuerySnafu.into_error(BoxedError::new(err))
861}
862
863fn trace_conventions(request: &ExportTraceServiceRequest) -> String {
868 let mut seen: Option<&str> = None;
869 let mut mixed = false;
870
871 for resource_spans in &request.resource_spans {
872 let urls = std::iter::once(resource_spans.schema_url.as_str()).chain(
873 resource_spans
874 .scope_spans
875 .iter()
876 .map(|s| s.schema_url.as_str()),
877 );
878 for url in urls {
879 if url.is_empty() {
880 continue;
881 }
882 match seen {
883 None => seen = Some(url),
884 Some(prev) if prev == url => {}
885 Some(_) => {
886 mixed = true;
887 break;
888 }
889 }
890 }
891 if mixed {
892 break;
893 }
894 }
895
896 if mixed {
897 SEMANTIC_VALUE_MIXED.to_string()
898 } else {
899 seen.map(str::to_string)
900 .unwrap_or_else(|| SEMANTIC_VALUE_UNKNOWN.to_string())
901 }
902}
903
904#[cfg(test)]
905mod tests {
906 use common_error::ext::ErrorExt;
907 use common_error::status_code::StatusCode;
908 use servers::query_handler::TraceIngestOutcome;
909
910 use super::{ChunkFailureReaction, Instance, chunk_owned, wrap_trace_alter_failure};
911 use crate::metrics::OTLP_TRACES_FAILURE_COUNT;
912
913 #[test]
914 fn test_chunk_owned() {
915 let chunks = chunk_owned(vec![1, 2, 3], 2);
916 assert_eq!(chunks.iter().map(Vec::len).collect::<Vec<_>>(), vec![2, 1]);
917
918 let chunks = chunk_owned(vec![1, 2, 3], 0);
919 assert_eq!(chunks.len(), 1);
920 assert_eq!(chunks[0].len(), 3);
921
922 assert!(chunk_owned::<i32>(Vec::new(), 0).is_empty());
923 }
924
925 #[test]
926 fn test_classify_trace_chunk_failure() {
927 assert_eq!(
928 Instance::classify_trace_chunk_failure(StatusCode::InvalidArguments),
929 ChunkFailureReaction::RetryPerSpan
930 );
931 assert_eq!(
932 Instance::classify_trace_chunk_failure(StatusCode::InvalidSyntax),
933 ChunkFailureReaction::RetryPerSpan
934 );
935 assert_eq!(
936 Instance::classify_trace_chunk_failure(StatusCode::Unsupported),
937 ChunkFailureReaction::RetryPerSpan
938 );
939 assert_eq!(
940 Instance::classify_trace_chunk_failure(StatusCode::TableColumnNotFound),
941 ChunkFailureReaction::RetryPerSpan
942 );
943 assert_eq!(
944 Instance::classify_trace_chunk_failure(StatusCode::TableNotFound),
945 ChunkFailureReaction::RetryPerSpan
946 );
947 assert_eq!(
948 Instance::classify_trace_chunk_failure(StatusCode::DatabaseNotFound),
949 ChunkFailureReaction::DiscardChunk
950 );
951 assert_eq!(
952 Instance::classify_trace_chunk_failure(StatusCode::DeadlineExceeded),
953 ChunkFailureReaction::Propagate
954 );
955 assert_eq!(
956 Instance::classify_trace_chunk_failure(StatusCode::Cancelled),
957 ChunkFailureReaction::Propagate
958 );
959 assert_eq!(
960 Instance::classify_trace_chunk_failure(StatusCode::StorageUnavailable),
961 ChunkFailureReaction::Propagate
962 );
963 assert_eq!(
964 Instance::classify_trace_chunk_failure(StatusCode::Internal),
965 ChunkFailureReaction::Propagate
966 );
967 assert_eq!(
968 Instance::classify_trace_chunk_failure(StatusCode::RegionNotReady),
969 ChunkFailureReaction::Propagate
970 );
971 assert_eq!(
972 Instance::classify_trace_chunk_failure(StatusCode::TableUnavailable),
973 ChunkFailureReaction::Propagate
974 );
975 assert_eq!(
976 Instance::classify_trace_chunk_failure(StatusCode::RegionBusy),
977 ChunkFailureReaction::Propagate
978 );
979 assert_eq!(
980 Instance::classify_trace_chunk_failure(StatusCode::RuntimeResourcesExhausted),
981 ChunkFailureReaction::Propagate
982 );
983 }
984
985 #[test]
986 fn test_classify_trace_span_failure() {
987 assert!(Instance::should_propagate_trace_span_failure(
988 StatusCode::DeadlineExceeded
989 ));
990 assert!(Instance::should_propagate_trace_span_failure(
991 StatusCode::StorageUnavailable
992 ));
993 assert!(!Instance::should_propagate_trace_span_failure(
994 StatusCode::InvalidArguments
995 ));
996 }
997
998 #[test]
999 fn test_add_trace_write_cost() {
1000 let mut outcome = TraceIngestOutcome::default();
1001 Instance::add_trace_write_cost(&mut outcome, 3);
1002 Instance::add_trace_write_cost(&mut outcome, 5);
1003 assert_eq!(outcome.write_cost, 8);
1004 }
1005
1006 #[test]
1007 fn test_finish_trace_failure_message() {
1008 let message = Instance::finish_trace_failure_message(
1009 3,
1010 2,
1011 vec!["Rejected span trace:span (InvalidArguments)".to_string()],
1012 )
1013 .unwrap();
1014 assert!(message.contains("Accepted 3 spans, rejected 2 spans"));
1015 assert!(message.contains("Rejected span trace:span"));
1016
1017 assert_eq!(Instance::finish_trace_failure_message(2, 0, vec![]), None);
1018 }
1019
1020 #[test]
1021 fn test_finish_trace_failure_message_without_detail_messages() {
1022 assert_eq!(
1023 Instance::finish_trace_failure_message(0, 2, vec![]),
1024 Some("Accepted 0 spans, rejected 2 spans".to_string())
1025 );
1026 }
1027
1028 #[test]
1029 fn test_push_trace_failure_message_increments_labeled_counter() {
1030 let label = "retry_per_span_counter_test";
1031 let initial = OTLP_TRACES_FAILURE_COUNT.with_label_values(&[label]).get();
1032 let mut messages = Vec::new();
1033
1034 Instance::push_trace_failure_message(
1035 &mut messages,
1036 label,
1037 "Chunk fallback triggered by InvalidArguments".to_string(),
1038 );
1039
1040 assert_eq!(messages.len(), 1);
1041 assert_eq!(
1042 OTLP_TRACES_FAILURE_COUNT.with_label_values(&[label]).get(),
1043 initial + 1
1044 );
1045 }
1046
1047 #[test]
1048 fn test_push_trace_failure_message_caps_recorded_messages() {
1049 let label = "retry_per_span_limit_test";
1050 let mut messages = Vec::new();
1051
1052 for idx in 0..=4 {
1053 Instance::push_trace_failure_message(&mut messages, label, format!("failure-{idx}"));
1054 }
1055
1056 assert_eq!(messages.len(), 4);
1057 assert_eq!(
1058 messages,
1059 vec![
1060 "failure-0".to_string(),
1061 "failure-1".to_string(),
1062 "failure-2".to_string(),
1063 "failure-3".to_string()
1064 ]
1065 );
1066 }
1067
1068 #[test]
1069 fn test_classify_trace_chunk_failure_defaults_to_discard() {
1070 assert_eq!(
1071 Instance::classify_trace_chunk_failure(StatusCode::Unknown),
1072 ChunkFailureReaction::DiscardChunk
1073 );
1074 }
1075
1076 #[test]
1077 fn test_wrap_trace_alter_failure_preserves_status_code() {
1078 let err = wrap_trace_alter_failure(
1079 servers::error::TableNotFoundSnafu {
1080 catalog: "greptime".to_string(),
1081 schema: "public".to_string(),
1082 table: "trace_type_missing".to_string(),
1083 }
1084 .build(),
1085 );
1086
1087 assert_eq!(err.status_code(), StatusCode::TableNotFound);
1088 }
1089
1090 use opentelemetry_proto::tonic::trace::v1::{ResourceSpans, ScopeSpans};
1091
1092 use super::{ExportTraceServiceRequest, trace_conventions};
1093
1094 fn resource_spans(resource_url: &str, scope_urls: &[&str]) -> ResourceSpans {
1095 ResourceSpans {
1096 schema_url: resource_url.to_string(),
1097 scope_spans: scope_urls
1098 .iter()
1099 .map(|u| ScopeSpans {
1100 schema_url: u.to_string(),
1101 ..Default::default()
1102 })
1103 .collect(),
1104 ..Default::default()
1105 }
1106 }
1107
1108 #[test]
1109 fn test_trace_conventions() {
1110 let unknown = ExportTraceServiceRequest::default();
1111 assert_eq!(trace_conventions(&unknown), "unknown");
1112
1113 let url = "https://opentelemetry.io/schemas/1.27.0";
1114 let single = ExportTraceServiceRequest {
1115 resource_spans: vec![resource_spans("", &[url, url])],
1116 };
1117 assert_eq!(trace_conventions(&single), url);
1118
1119 let resource_level = ExportTraceServiceRequest {
1120 resource_spans: vec![resource_spans(url, &[""])],
1121 };
1122 assert_eq!(trace_conventions(&resource_level), url);
1123
1124 let conflicting = ExportTraceServiceRequest {
1125 resource_spans: vec![resource_spans(
1126 "",
1127 &[url, "https://opentelemetry.io/schemas/1.30.0"],
1128 )],
1129 };
1130 assert_eq!(trace_conventions(&conflicting), "mixed");
1131 }
1132}