1use std::collections::{BTreeMap, HashMap, HashSet};
16use std::sync::Arc;
17
18use api::helper::ColumnDataTypeWrapper;
19use api::v1::alter_table_expr::Kind;
20use api::v1::{
21 AlterTableExpr, ColumnDataType, ColumnSchema, ModifyColumnType, ModifyColumnTypes,
22 RowInsertRequest, RowInsertRequests, Rows, Value,
23};
24use client::Output;
25use common_error::ext::{BoxedError, ErrorExt};
26use common_error::status_code::StatusCode;
27use common_meta::rpc::ddl::TriggerReason;
28use common_telemetry::{debug, warn};
29use datatypes::prelude::ConcreteDataType;
30use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
31use pipeline::{GreptimePipelineParams, PipelineWay};
32use servers::error::{self, Result as ServerResult};
33use servers::otlp;
34use servers::otlp::coerce::{coerce_value_data, is_supported_trace_coercion, trace_value_datatype};
35use servers::otlp::trace::span::{TraceSpan, TraceSpanGroup};
36use servers::otlp::trace::v1::{TraceBatchSchema, TraceBinaryType, TraceRetryColumns};
37use servers::otlp::trace::{SERVICE_NAME_COLUMN, TraceAuxData};
38use servers::query_handler::{PipelineHandlerRef, TraceIngestOutcome};
39use session::context::QueryContextRef;
40use snafu::{IntoError, ResultExt};
41use table::requests::{
42 SEMANTIC_ENTITY_SERVICE_ID, SEMANTIC_PIPELINE, SEMANTIC_SIGNAL_TYPE, SEMANTIC_SOURCE,
43 SEMANTIC_TRACE_CONVENTIONS, SEMANTIC_VALUE_MIXED, SEMANTIC_VALUE_UNKNOWN, SIGNAL_TYPE_TRACE,
44 SOURCE_OPENTELEMETRY, TABLE_DATA_MODEL_TRACE_V1,
45};
46
47use crate::instance::Instance;
48use crate::instance::otlp::trace_semconv::trace_semconv_fixed_type;
49use crate::instance::otlp::trace_types::{
50 PendingTraceColumnRewrite, PreparedTraceColumnRewrites, TraceColumnRewriteError,
51 choose_trace_reconcile_decision, enrich_trace_reconcile_error,
52 is_trace_reconcile_candidate_type, prepare_trace_column_rewrites, push_observed_trace_type,
53 truncate_for_diagnostics,
54};
55use crate::metrics::{OTLP_TRACES_FAILURE_COUNT, OTLP_TRACES_ROWS};
56
57const TRACE_FAILURE_MESSAGE_LIMIT: usize = 4;
58
59const TRACE_FAILURE_CAUSE_LIMIT: usize = 256;
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64enum ChunkFailureReaction {
65 RetryPerSpan,
66 DiscardChunk,
67 Propagate,
68}
69
70impl ChunkFailureReaction {
71 fn as_metric_label(self) -> &'static str {
72 match self {
73 Self::RetryPerSpan => "retry_per_span",
74 Self::DiscardChunk => "discard_chunk",
75 Self::Propagate => "propagate_failure",
76 }
77 }
78}
79
80struct TraceChunkIngestContext<'a> {
82 pipeline_handler: PipelineHandlerRef,
83 pipeline: &'a PipelineWay,
84 pipeline_params: &'a GreptimePipelineParams,
85 table_name: &'a str,
86 is_trace_v1_model: bool,
87}
88
89struct TraceIngestState {
91 aux_data: TraceAuxData,
92 outcome: TraceIngestOutcome,
93 failure_messages: TraceFailureMessages,
94}
95
96#[derive(Debug, Default)]
102struct TraceFailureMessages {
103 entries: Vec<TraceFailureEntry>,
104 suppressed_occurrences: usize,
105}
106
107#[derive(Debug)]
108struct TraceFailureEntry {
109 label: &'static str,
110 key: String,
113 message: String,
114 occurrences: usize,
115}
116
117impl TraceFailureMessages {
118 fn is_empty(&self) -> bool {
119 self.entries.is_empty()
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125enum TraceChunkSchemaState {
126 Prepared,
127 ReconcilePerChunk,
128 SpanFallbackOnly,
129}
130
131impl TraceChunkSchemaState {
132 fn mark_for_reconcile(&mut self) {
133 if matches!(self, Self::Prepared) {
134 *self = Self::ReconcilePerChunk;
135 }
136 }
137}
138
139struct TraceChunkRetry {
141 table_name: String,
142 schema: Vec<ColumnSchema>,
143 rows: Vec<TraceRetryRow>,
144}
145
146struct TraceRetryRow {
148 span_metadata: TraceSpanMetadata,
149 values: Vec<TraceRetryValue>,
150}
151
152struct TraceRetryValue {
154 column_index: usize,
155 value: Value,
156 schema_type: Option<TraceRetrySchemaType>,
157}
158
159enum TraceRetrySchemaType {
161 Scalar(ColumnDataType),
162 Binary(TraceBinaryType),
163}
164
165struct TraceSpanMetadata {
167 trace_id: String,
168 span_id: String,
169 operation: Option<(String, String, String)>,
170}
171
172impl From<&TraceSpan> for TraceSpanMetadata {
173 fn from(span: &TraceSpan) -> Self {
174 Self {
175 trace_id: span.trace_id.clone(),
176 span_id: span.span_id.clone(),
177 operation: span.service_name.as_ref().map(|service_name| {
178 (
179 service_name.clone(),
180 span.span_name.clone(),
181 span.span_kind.clone(),
182 )
183 }),
184 }
185 }
186}
187
188impl TraceSpanMetadata {
189 fn add_to_aux_data(self, aux_data: &mut TraceAuxData) {
190 if let Some((service_name, span_name, span_kind)) = self.operation {
191 aux_data.services.insert(service_name.clone());
192 aux_data
193 .operations
194 .insert((service_name, span_name, span_kind));
195 }
196 }
197}
198
199impl TraceChunkRetry {
200 fn try_new(
201 table_name: String,
202 rows: Rows,
203 span_metadata: Vec<TraceSpanMetadata>,
204 retry_columns: TraceRetryColumns,
205 ) -> ServerResult<Self> {
206 let Rows { schema, rows } = rows;
207 let row_count = rows.len();
208 if row_count != span_metadata.len() {
209 return error::InternalSnafu {
210 err_msg: format!(
211 "trace row count {} does not match span metadata count {}",
212 row_count,
213 span_metadata.len()
214 ),
215 }
216 .fail();
217 }
218
219 let retry_columns_by_index = schema
220 .iter()
221 .map(|column| retry_columns.get(&column.column_name))
222 .collect::<Vec<_>>();
223 let mut fixed_columns = Vec::with_capacity(schema.len());
224 let mut sparse_columns = vec![Vec::new(); rows.len()];
225 for (column_index, retry_column) in retry_columns_by_index.iter().enumerate() {
226 let Some(retry_column) = retry_column else {
227 fixed_columns.push(column_index);
228 continue;
229 };
230 for present_row in &retry_column.present_rows {
231 if *present_row < sparse_columns.len() {
232 sparse_columns[*present_row].push(column_index);
233 }
234 }
235 }
236
237 let mut retry_rows = Vec::with_capacity(rows.len());
238 for (row_index, (mut row, span_metadata)) in rows.into_iter().zip(span_metadata).enumerate()
239 {
240 if row.values.len() > schema.len() {
241 return error::InternalSnafu {
242 err_msg: format!(
243 "trace row column count {} exceeds schema column count {}",
244 row.values.len(),
245 schema.len()
246 ),
247 }
248 .fail();
249 }
250 let sparse_row = &mut sparse_columns[row_index];
251 let mut selected_columns = Vec::with_capacity(fixed_columns.len() + sparse_row.len());
252 selected_columns.extend_from_slice(&fixed_columns);
253 selected_columns.append(sparse_row);
254 selected_columns.sort_unstable();
255 selected_columns.dedup();
256
257 let mut values = Vec::with_capacity(selected_columns.len());
258 for column_index in selected_columns {
259 let column = &schema[column_index];
260 let value = row
261 .values
262 .get_mut(column_index)
263 .map(std::mem::take)
264 .unwrap_or_default();
265 let value_type = value.value_data.as_ref().and_then(trace_value_datatype);
266 let binary_type = if value_type == Some(ColumnDataType::Binary) {
267 retry_columns_by_index[column_index].and_then(|retry_column| {
268 retry_column
269 .binary_types
270 .binary_search_by_key(&row_index, |(row_index, _)| *row_index)
271 .ok()
272 .map(|index| retry_column.binary_types[index].1)
273 })
274 } else {
275 None
276 };
277 let schema_type = binary_type.map(TraceRetrySchemaType::Binary).or_else(|| {
278 value_type
279 .filter(|datatype| {
280 is_trace_reconcile_candidate_type(*datatype)
281 && ColumnDataType::try_from(column.datatype).ok() != Some(*datatype)
282 })
283 .map(TraceRetrySchemaType::Scalar)
284 });
285 values.push(TraceRetryValue {
286 column_index,
287 value,
288 schema_type,
289 });
290 }
291 retry_rows.push(TraceRetryRow {
292 span_metadata,
293 values,
294 });
295 }
296
297 Ok(Self {
298 table_name,
299 schema,
300 rows: retry_rows,
301 })
302 }
303
304 fn to_request(&self) -> ServerResult<RowInsertRequest> {
305 let mut rows = Vec::with_capacity(self.rows.len());
306 for retry_row in &self.rows {
307 let mut values = vec![Value::default(); self.schema.len()];
308 for retry_value in &retry_row.values {
309 let Some(value) = values.get_mut(retry_value.column_index) else {
310 return error::InternalSnafu {
311 err_msg: format!(
312 "trace retry column index {} is out of bounds",
313 retry_value.column_index
314 ),
315 }
316 .fail();
317 };
318 *value = retry_value.value.clone();
319 }
320 rows.push(api::v1::Row { values });
321 }
322
323 Ok(RowInsertRequest {
324 table_name: self.table_name.clone(),
325 rows: Some(Rows {
326 schema: self.schema.clone(),
327 rows,
328 }),
329 })
330 }
331
332 fn into_single_span_chunks(self) -> ServerResult<Vec<Self>> {
333 let Self {
334 table_name,
335 schema,
336 rows,
337 } = self;
338 rows.into_iter()
339 .map(|row| {
340 let (span_metadata, rows_for_insert) = row.into_rows(&schema)?;
341 Self::try_new(
342 table_name.clone(),
343 rows_for_insert,
344 vec![span_metadata],
345 TraceRetryColumns::default(),
346 )
347 })
348 .collect()
349 }
350
351 fn add_to_aux_data(self, aux_data: &mut TraceAuxData) {
352 for row in self.rows {
353 row.span_metadata.add_to_aux_data(aux_data);
354 }
355 }
356}
357
358impl TraceRetryRow {
359 fn into_rows(self, schema: &[ColumnSchema]) -> ServerResult<(TraceSpanMetadata, Rows)> {
360 let mut projected_schema = Vec::with_capacity(self.values.len());
361 let mut projected_values = Vec::with_capacity(self.values.len());
362 for retry_value in self.values {
363 let Some(mut column) = schema.get(retry_value.column_index).cloned() else {
364 return error::InternalSnafu {
365 err_msg: format!(
366 "trace fallback column index {} is out of bounds",
367 retry_value.column_index
368 ),
369 }
370 .fail();
371 };
372 match retry_value.schema_type {
373 Some(TraceRetrySchemaType::Scalar(datatype)) => {
374 column.datatype = datatype as i32;
375 column.datatype_extension = None;
376 }
377 Some(TraceRetrySchemaType::Binary(binary_type)) => {
378 binary_type.apply_to_schema(&mut column);
379 }
380 None => {}
381 }
382 projected_schema.push(column);
383 projected_values.push(retry_value.value);
384 }
385
386 Ok((
387 self.span_metadata,
388 Rows {
389 schema: projected_schema,
390 rows: vec![api::v1::Row {
391 values: projected_values,
392 }],
393 },
394 ))
395 }
396}
397
398#[derive(Default)]
400struct TraceRequestSchema {
401 columns: Vec<TraceColumnRequestSchema>,
402 column_indexes: HashMap<String, usize>,
403}
404
405struct TraceColumnRequestSchema {
407 schema: ColumnSchema,
408 batches: BTreeMap<usize, TraceBatchColumnObservation>,
409 target_type: Option<ColumnDataType>,
410}
411
412struct TraceBatchColumnObservation {
414 value_types: Vec<ColumnDataType>,
415 schema_type: ColumnDataType,
416 concrete_type: ConcreteDataType,
417}
418
419type TraceSchemaExclusions = HashMap<String, HashSet<usize>>;
421
422struct TraceTablePreAlter {
424 ensure_columns: Vec<ColumnSchema>,
425 modify_float64_columns: Vec<String>,
426 alter_existing: bool,
427}
428
429enum TraceRequestSchemaPlan {
431 Ready(TraceTablePreAlter),
432 IncompatibleObservations(TraceSchemaExclusions),
433}
434
435impl TraceTablePreAlter {
436 fn requires_ddl(&self) -> bool {
437 !self.ensure_columns.is_empty() || !self.modify_float64_columns.is_empty()
438 }
439}
440
441impl TraceRequestSchema {
442 fn observe_batch_schema(
443 &mut self,
444 batch_index: usize,
445 schema: &[ColumnSchema],
446 batch_schema: &TraceBatchSchema,
447 ) {
448 for column in schema {
449 if batch_schema.has_incompatible_logical_types_for(&column.column_name) {
450 continue;
451 }
452 let observed_types = batch_schema.value_types(&column.column_name);
453 self.observe_trace_column(batch_index, column, None);
454 if let Some(observed_types) = observed_types {
455 for value_type in observed_types {
456 self.observe_trace_column(batch_index, column, Some(*value_type));
457 }
458 }
459 }
460 }
461
462 fn observe_retry_chunk(
463 &mut self,
464 batch_index: usize,
465 chunk: &TraceChunkRetry,
466 ) -> ServerResult<()> {
467 for row in &chunk.rows {
470 for retry_value in &row.values {
471 let Some(column) = chunk.schema.get(retry_value.column_index) else {
472 return error::InternalSnafu {
473 err_msg: format!(
474 "trace retry column index {} is out of bounds",
475 retry_value.column_index
476 ),
477 }
478 .fail();
479 };
480 let value_type = retry_value
481 .value
482 .value_data
483 .as_ref()
484 .and_then(trace_value_datatype);
485 self.observe_trace_column(batch_index, column, value_type);
486 }
487 }
488 Ok(())
489 }
490
491 fn observe_trace_column(
492 &mut self,
493 batch_index: usize,
494 schema: &ColumnSchema,
495 value_type: Option<ColumnDataType>,
496 ) {
497 let column_schema = self.observe_column(schema);
498 if let Ok(current_type) = ColumnDataType::try_from(schema.datatype) {
499 let observation = column_schema.batches.entry(batch_index).or_insert_with(|| {
500 let wrapper =
501 ColumnDataTypeWrapper::new(current_type, schema.datatype_extension.clone());
502 TraceBatchColumnObservation {
503 value_types: Vec::new(),
504 schema_type: current_type,
505 concrete_type: ConcreteDataType::from(wrapper),
506 }
507 });
508 push_observed_trace_type(&mut observation.value_types, current_type);
509 }
510 if let Some(value_type) = value_type {
511 column_schema.observe_type(batch_index, value_type);
512 }
513 }
514
515 fn incompatible_schema_observations(
516 &self,
517 table_schema: Option<&datatypes::schema::Schema>,
518 ) -> TraceSchemaExclusions {
519 let mut exclusions = HashMap::new();
520 for column in &self.columns {
521 let existing_type = table_schema
522 .and_then(|schema| schema.column_schema_by_name(&column.schema.column_name))
523 .and_then(|table_col| {
524 ColumnDataTypeWrapper::try_from(table_col.data_type.clone())
525 .ok()
526 .map(|wrapper| {
527 let datatype = wrapper.datatype();
528 (datatype, ConcreteDataType::from(wrapper))
529 })
530 });
531 let incompatible_batches = column.incompatible_schema_batches(existing_type);
532 if !incompatible_batches.is_empty() {
533 exclusions.insert(column.schema.column_name.clone(), incompatible_batches);
534 }
535 }
536 exclusions
537 }
538
539 fn resolve_table_schema(
540 &mut self,
541 table_schema: Option<&datatypes::schema::Schema>,
542 ) -> TraceRequestSchemaPlan {
543 let mut pre_alter = TraceTablePreAlter {
544 ensure_columns: Vec::new(),
545 modify_float64_columns: Vec::new(),
546 alter_existing: table_schema.is_some(),
547 };
548
549 for column in &mut self.columns {
550 let Some(current_type) = ColumnDataType::try_from(column.schema.datatype).ok() else {
551 continue;
552 };
553 let observed_types = column.observed_types();
554
555 let existing_type = table_schema
556 .and_then(|schema| schema.column_schema_by_name(&column.schema.column_name))
557 .and_then(|table_col| {
558 ColumnDataTypeWrapper::try_from(table_col.data_type.clone())
559 .ok()
560 .map(|wrapper| wrapper.datatype())
561 });
562 let fixed_type = trace_semconv_fixed_type(&column.schema.column_name);
563
564 let needs_reconcile = observed_types
565 .iter()
566 .copied()
567 .any(is_trace_reconcile_candidate_type)
568 || existing_type
569 .map(is_trace_reconcile_candidate_type)
570 .unwrap_or(false)
571 || fixed_type.is_some();
572
573 let target_type = if needs_reconcile {
574 let decision = match choose_trace_reconcile_decision(
575 &column.schema.column_name,
576 &observed_types,
577 existing_type,
578 ) {
579 Ok(decision) => decision,
580 Err(_) => {
581 return TraceRequestSchemaPlan::IncompatibleObservations(HashMap::from([
582 (
583 column.schema.column_name.clone(),
584 column.batches.keys().copied().collect(),
585 ),
586 ]));
587 }
588 };
589 decision
590 .map(|decision| {
591 if decision.requires_alter() {
592 pre_alter
593 .modify_float64_columns
594 .push(column.schema.column_name.clone());
595 }
596 decision.target_type()
597 })
598 .unwrap_or(current_type)
599 } else {
600 current_type
601 };
602
603 column.target_type = Some(target_type);
604
605 if table_schema
606 .map(|schema| {
607 schema
608 .column_schema_by_name(&column.schema.column_name)
609 .is_none()
610 })
611 .unwrap_or(false)
612 {
613 pre_alter.ensure_columns.push(column.target_schema());
614 }
615 }
616
617 if table_schema.is_none() {
618 pre_alter.ensure_columns = self
619 .columns
620 .iter()
621 .map(TraceColumnRequestSchema::target_schema)
622 .collect();
623 }
624
625 TraceRequestSchemaPlan::Ready(pre_alter)
626 }
627
628 fn prepare_request_rewrite(
629 &self,
630 batch_index: usize,
631 request: &RowInsertRequest,
632 ) -> Result<Option<PreparedTraceColumnRewrites>, TraceColumnRewriteError> {
633 let Some(rows) = request.rows.as_ref() else {
634 return Ok(None);
635 };
636 let pending_rewrites = self.pending_batch_rewrites(batch_index, rows);
637 if pending_rewrites.is_empty() {
638 return Ok(None);
639 }
640 prepare_trace_column_rewrites(&rows.rows, pending_rewrites, &request.table_name).map(Some)
641 }
642
643 fn apply_request_rewrite(
644 request: &mut RowInsertRequest,
645 prepared: PreparedTraceColumnRewrites,
646 ) {
647 if let Some(rows) = request.rows.as_mut() {
648 prepared.apply(rows);
649 }
650 }
651
652 fn pending_batch_rewrites(
653 &self,
654 batch_index: usize,
655 rows: &api::v1::Rows,
656 ) -> Vec<PendingTraceColumnRewrite> {
657 let mut pending_rewrites = Vec::new();
658 for (col_idx, col_schema) in rows.schema.iter().enumerate() {
659 let Some(global_idx) = self.column_indexes.get(&col_schema.column_name).copied() else {
660 continue;
661 };
662 let global_column = &self.columns[global_idx];
663 let Some(target_type) = global_column.target_type() else {
664 continue;
665 };
666 if !global_column
667 .batches
668 .get(&batch_index)
669 .is_some_and(|observation| {
670 observation
671 .value_types
672 .iter()
673 .any(|datatype| *datatype != target_type)
674 })
675 {
676 continue;
677 }
678 pending_rewrites.push(PendingTraceColumnRewrite {
679 col_idx,
680 target_type,
681 column_name: global_column.schema.column_name.clone(),
682 });
683 }
684
685 pending_rewrites
686 }
687
688 #[cfg(test)]
689 fn remove_batches(&mut self, batch_indexes: &HashSet<usize>) {
690 for column in &mut self.columns {
691 column
692 .batches
693 .retain(|batch_index, _| !batch_indexes.contains(batch_index));
694 column.target_type = None;
695 }
696 self.columns.retain(|column| !column.batches.is_empty());
697
698 self.column_indexes.clear();
699 for (index, column) in self.columns.iter().enumerate() {
700 self.column_indexes
701 .insert(column.schema.column_name.clone(), index);
702 }
703 }
704
705 #[cfg(test)]
706 fn resolved_target_types(&self) -> Vec<Option<ColumnDataType>> {
707 self.columns
708 .iter()
709 .map(|column| column.target_type)
710 .collect()
711 }
712
713 fn observe_column(&mut self, schema: &ColumnSchema) -> &mut TraceColumnRequestSchema {
714 if let Some(index) = self.column_indexes.get(&schema.column_name).copied() {
715 return &mut self.columns[index];
716 }
717
718 let index = self.columns.len();
719 self.columns.push(TraceColumnRequestSchema {
720 schema: schema.clone(),
721 batches: BTreeMap::new(),
722 target_type: None,
723 });
724 self.column_indexes
725 .insert(schema.column_name.clone(), index);
726 &mut self.columns[index]
727 }
728}
729
730impl TraceColumnRequestSchema {
731 fn observe_type(&mut self, batch_index: usize, datatype: ColumnDataType) {
732 if let Some(observation) = self.batches.get_mut(&batch_index) {
733 push_observed_trace_type(&mut observation.value_types, datatype);
734 }
735 }
736
737 fn observed_types(&self) -> Vec<ColumnDataType> {
738 let mut observed_types = Vec::new();
739 for datatype in self
740 .batches
741 .values()
742 .flat_map(|observation| &observation.value_types)
743 {
744 push_observed_trace_type(&mut observed_types, *datatype);
745 }
746 observed_types
747 }
748
749 fn incompatible_schema_batches(
750 &self,
751 existing_type: Option<(ColumnDataType, ConcreteDataType)>,
752 ) -> HashSet<usize> {
753 let mut incompatible_batches = HashSet::new();
754 if let Some((existing_datatype, existing_concrete_type)) = existing_type {
755 for (batch_index, observation) in &self.batches {
756 if trace_logical_types_incompatible(
757 observation.schema_type,
758 &observation.concrete_type,
759 existing_datatype,
760 &existing_concrete_type,
761 ) {
762 incompatible_batches.insert(*batch_index);
763 }
764 }
765 return incompatible_batches;
766 }
767
768 let mut reconcile_candidates = Vec::<&TraceBatchColumnObservation>::new();
769 for observation in self
770 .batches
771 .values()
772 .filter(|observation| is_trace_reconcile_candidate_type(observation.schema_type))
773 {
774 if !reconcile_candidates.iter().any(|candidate| {
775 candidate.schema_type == observation.schema_type
776 && candidate.concrete_type == observation.concrete_type
777 }) {
778 reconcile_candidates.push(observation);
779 }
780 }
781 if !reconcile_candidates.is_empty() {
782 for (batch_index, observation) in &self.batches {
783 if reconcile_candidates.iter().any(|candidate| {
784 trace_logical_types_incompatible(
785 observation.schema_type,
786 &observation.concrete_type,
787 candidate.schema_type,
788 &candidate.concrete_type,
789 )
790 }) {
791 incompatible_batches.insert(*batch_index);
792 }
793 }
794 return incompatible_batches;
795 }
796
797 let mut schemas = Vec::<(ColumnDataType, ConcreteDataType, bool)>::new();
798 for observation in self.batches.values() {
799 if schemas.iter().any(|(observed_datatype, observed_type, _)| {
800 *observed_datatype == observation.schema_type
801 && observed_type == &observation.concrete_type
802 }) {
803 continue;
804 }
805
806 let mut conflicts = false;
807 for (observed_datatype, observed_type, observed_conflicts) in &mut schemas {
808 if trace_logical_types_incompatible(
809 *observed_datatype,
810 observed_type,
811 observation.schema_type,
812 &observation.concrete_type,
813 ) {
814 *observed_conflicts = true;
815 conflicts = true;
816 }
817 }
818 schemas.push((
819 observation.schema_type,
820 observation.concrete_type.clone(),
821 conflicts,
822 ));
823 }
824
825 for (batch_index, observation) in &self.batches {
826 if schemas
827 .iter()
828 .any(|(observed_datatype, observed_type, conflict)| {
829 *observed_datatype == observation.schema_type
830 && observed_type == &observation.concrete_type
831 && *conflict
832 })
833 {
834 incompatible_batches.insert(*batch_index);
835 }
836 }
837
838 incompatible_batches
839 }
840
841 fn target_type(&self) -> Option<ColumnDataType> {
842 self.target_type
843 .or_else(|| ColumnDataType::try_from(self.schema.datatype).ok())
844 }
845
846 fn target_schema(&self) -> ColumnSchema {
847 let mut schema = self.schema.clone();
848 if let Some(target_type) = self.target_type() {
849 schema.datatype = target_type as i32;
850 }
851 schema
852 }
853}
854
855impl Instance {
856 #[allow(clippy::too_many_arguments)]
859 pub(super) async fn ingest_trace_spans(
860 &self,
861 pipeline_handler: PipelineHandlerRef,
862 pipeline: &PipelineWay,
863 pipeline_params: &GreptimePipelineParams,
864 table_name: String,
865 groups: Vec<TraceSpanGroup>,
866 conventions: &str,
867 ctx: QueryContextRef,
868 ) -> ServerResult<TraceIngestOutcome> {
869 let is_trace_v1_model = matches!(pipeline, PipelineWay::OtlpTraceDirectV1);
870
871 let main_ctx = {
874 let mut c = (*ctx).clone();
875 c.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_TRACE);
876 c.set_extension(SEMANTIC_SOURCE, SOURCE_OPENTELEMETRY);
877 c.set_extension(SEMANTIC_ENTITY_SERVICE_ID, SERVICE_NAME_COLUMN);
880 if is_trace_v1_model {
881 c.set_extension(SEMANTIC_PIPELINE, TABLE_DATA_MODEL_TRACE_V1);
882 c.set_extension(SEMANTIC_TRACE_CONVENTIONS, conventions);
883 }
884 Arc::new(c)
885 };
886
887 let ingest_ctx = TraceChunkIngestContext {
888 pipeline_handler,
889 pipeline,
890 pipeline_params,
891 table_name: &table_name,
892 is_trace_v1_model,
893 };
894 let mut ingest_state = TraceIngestState {
895 aux_data: TraceAuxData::default(),
896 outcome: TraceIngestOutcome::default(),
897 failure_messages: TraceFailureMessages::default(),
898 };
899
900 let main_result: ServerResult<()> = async {
901 if is_trace_v1_model {
902 let mut request_schema = TraceRequestSchema::default();
903 let mut chunks = Vec::new();
904 let mut chunk_states = Vec::new();
905 for chunk in groups
908 .into_iter()
909 .flat_map(|group| chunk_owned(group.spans, self.trace_ingest_chunk_size))
910 {
911 let batch_index = chunks.len();
912 let span_metadata = chunk.iter().map(TraceSpanMetadata::from).collect();
913 let (table_data, batch_schema) =
914 otlp::trace::v1::v1_to_main_table_data_with_schema(chunk)?;
915 let schema_state = if batch_schema.has_incompatible_logical_types() {
916 TraceChunkSchemaState::SpanFallbackOnly
917 } else {
918 TraceChunkSchemaState::Prepared
919 };
920 let (schema, rows) = table_data.into_schema_and_rows();
921 if schema_state == TraceChunkSchemaState::Prepared {
922 request_schema.observe_batch_schema(batch_index, &schema, &batch_schema);
923 }
924 chunks.push(TraceChunkRetry::try_new(
925 ingest_ctx.table_name.to_string(),
926 Rows { schema, rows },
927 span_metadata,
928 batch_schema.into_retry_columns(),
929 )?);
930 chunk_states.push(schema_state);
931 }
932
933 if !chunks.is_empty() {
934 self.ingest_trace_v1_prepared_chunks(
935 &ingest_ctx,
936 request_schema,
937 chunks,
938 chunk_states,
939 main_ctx.clone(),
940 &mut ingest_state,
941 )
942 .await?;
943 }
944 } else {
945 for group in groups {
946 let chunks = chunk_owned(group.spans, self.trace_ingest_chunk_size);
947 for chunk in chunks {
948 self.ingest_trace_chunk(
949 &ingest_ctx,
950 chunk,
951 main_ctx.clone(),
952 &mut ingest_state,
953 )
954 .await?;
955 }
956 }
957 }
958
959 Ok(())
960 }
961 .await;
962
963 OTLP_TRACES_ROWS.inc_by(ingest_state.outcome.accepted_spans as u64);
964
965 if !ingest_state.aux_data.is_empty() {
966 let aux_requests = otlp::trace::to_grpc_insert_requests_for_aux_tables(
970 std::mem::take(&mut ingest_state.aux_data),
971 ingest_ctx.pipeline,
972 ingest_ctx.table_name,
973 );
974
975 match aux_requests {
976 Ok((aux_requests, _)) if !aux_requests.inserts.is_empty() => {
977 match self
978 .insert_trace_requests(aux_requests, ingest_ctx.is_trace_v1_model, ctx)
979 .await
980 {
981 Ok(output) => {
982 Self::add_trace_write_cost(&mut ingest_state.outcome, output.meta.cost);
983 }
984 Err(err) => {
985 Self::push_trace_failure_message(
986 &mut ingest_state.failure_messages,
987 "aux_table_update_failed",
988 err.status_code().as_ref(),
989 format!(
990 "Auxiliary trace tables were not fully updated ({})",
991 err.status_code().as_ref()
992 ),
993 );
994 }
995 }
996 }
997 Err(err) if main_result.is_ok() => return Err(err),
1000 Err(err) => Self::push_trace_failure_message(
1001 &mut ingest_state.failure_messages,
1002 "aux_table_update_failed",
1003 err.status_code().as_ref(),
1004 format!(
1005 "Auxiliary trace tables were not fully updated ({})",
1006 err.status_code().as_ref()
1007 ),
1008 ),
1009 Ok(_) => {}
1010 }
1011 }
1012
1013 main_result?;
1014
1015 ingest_state.outcome.error_message = Self::finish_trace_failure_message(
1016 ingest_state.outcome.accepted_spans,
1017 ingest_state.outcome.rejected_spans,
1018 ingest_state.failure_messages,
1019 );
1020
1021 if let Some(error_message) = &ingest_state.outcome.error_message {
1022 let accepted_spans = ingest_state.outcome.accepted_spans;
1023 let rejected_spans = ingest_state.outcome.rejected_spans;
1024 if accepted_spans == 0 && rejected_spans > 0 {
1033 warn!(
1034 table_name = ingest_ctx.table_name,
1035 rejected_spans,
1036 error_message = ?error_message,
1037 "OTLP trace ingest rejected every span"
1038 );
1039 } else {
1040 debug!(
1041 table_name = ingest_ctx.table_name,
1042 accepted_spans,
1043 rejected_spans,
1044 error_message = ?error_message,
1045 "OTLP trace ingest reported failures"
1046 );
1047 }
1048 }
1049
1050 Ok(ingest_state.outcome)
1051 }
1052
1053 async fn ingest_trace_chunk(
1056 &self,
1057 ingest_ctx: &TraceChunkIngestContext<'_>,
1058 chunk: Vec<TraceSpan>,
1059 ctx: QueryContextRef,
1060 ingest_state: &mut TraceIngestState,
1061 ) -> ServerResult<()> {
1062 let (requests, chunk_rows) = otlp::trace::to_grpc_insert_requests_from_spans(
1065 &chunk,
1066 ingest_ctx.pipeline,
1067 ingest_ctx.pipeline_params,
1068 ingest_ctx.table_name,
1069 &ctx,
1070 ingest_ctx.pipeline_handler.clone(),
1071 )?;
1072
1073 let output = match self
1074 .insert_trace_requests(requests, ingest_ctx.is_trace_v1_model, ctx)
1075 .await
1076 {
1077 Ok(output) => output,
1078 Err(err) => {
1079 Self::push_trace_failure_message(
1082 &mut ingest_state.failure_messages,
1083 ChunkFailureReaction::Propagate.as_metric_label(),
1084 err.status_code().as_ref(),
1085 format!(
1086 "Propagating chunk write failure ({})",
1087 err.status_code().as_ref()
1088 ),
1089 );
1090 return Err(err);
1091 }
1092 };
1093
1094 Self::add_trace_write_cost(&mut ingest_state.outcome, output.meta.cost);
1095 ingest_state.outcome.accepted_spans += chunk_rows;
1096 for span in &chunk {
1097 ingest_state.aux_data.observe_span(span);
1098 }
1099
1100 Ok(())
1101 }
1102
1103 async fn ingest_trace_v1_prepared_chunks(
1104 &self,
1105 ingest_ctx: &TraceChunkIngestContext<'_>,
1106 mut request_schema: TraceRequestSchema,
1107 mut chunks: Vec<TraceChunkRetry>,
1108 mut chunk_states: Vec<TraceChunkSchemaState>,
1109 ctx: QueryContextRef,
1110 ingest_state: &mut TraceIngestState,
1111 ) -> ServerResult<()> {
1112 if let Err(err) = self
1113 .prepare_trace_v1_request_schema(
1114 ingest_ctx.table_name,
1115 &mut request_schema,
1116 &mut chunks,
1117 &mut chunk_states,
1118 &ctx,
1119 )
1120 .await
1121 {
1122 if matches!(
1123 Self::classify_trace_prewrite_failure(err.status_code(), err.is_retryable()),
1124 ChunkFailureReaction::Propagate
1125 ) {
1126 return Err(err);
1127 }
1128
1129 let span_count = chunks.iter().map(|chunk| chunk.rows.len()).sum::<usize>();
1134 ingest_state.outcome.rejected_spans += span_count;
1135 let (cause, shown_cause) = Self::trace_failure_cause(&err);
1136 Self::push_trace_failure_message(
1137 &mut ingest_state.failure_messages,
1138 ChunkFailureReaction::DiscardChunk.as_metric_label(),
1139 &format!("{span_count}:{cause}"),
1142 format!(
1143 "Discarded {} spans after pre-write request failure ({}): {}",
1144 span_count,
1145 err.status_code().as_ref(),
1146 shown_cause
1147 ),
1148 );
1149 return Ok(());
1150 }
1151
1152 for (batch_index, (retry, schema_state)) in chunks.into_iter().zip(chunk_states).enumerate()
1153 {
1154 self.ingest_trace_v1_chunk(
1155 &request_schema,
1156 batch_index,
1157 retry,
1158 schema_state,
1159 ctx.clone(),
1160 ingest_state,
1161 )
1162 .await?;
1163 }
1164
1165 Ok(())
1166 }
1167
1168 async fn ingest_trace_v1_chunk(
1169 &self,
1170 request_schema: &TraceRequestSchema,
1171 batch_index: usize,
1172 retry: TraceChunkRetry,
1173 mut schema_state: TraceChunkSchemaState,
1174 ctx: QueryContextRef,
1175 ingest_state: &mut TraceIngestState,
1176 ) -> ServerResult<()> {
1177 if schema_state == TraceChunkSchemaState::SpanFallbackOnly {
1178 Self::push_trace_failure_message(
1179 &mut ingest_state.failure_messages,
1180 ChunkFailureReaction::RetryPerSpan.as_metric_label(),
1181 "incompatible_binary_and_json",
1182 "Chunk fallback triggered by incompatible binary and JSON values".to_string(),
1183 );
1184 return self
1185 .ingest_trace_v1_rows_span_by_span(retry, ctx, ingest_state)
1186 .await;
1187 }
1188
1189 let mut request = retry.to_request()?;
1190 let _ = Self::prepare_trace_v1_chunk_rewrite(
1191 request_schema,
1192 batch_index,
1193 &mut request,
1194 &mut schema_state,
1195 )?;
1196 let span_count = retry.rows.len();
1197 let mut requests = RowInsertRequests {
1198 inserts: vec![request],
1199 };
1200 if schema_state == TraceChunkSchemaState::ReconcilePerChunk
1201 && let Err(err) = self.reconcile_trace_column_types(&mut requests, &ctx).await
1202 {
1203 match Self::classify_trace_prewrite_failure(err.status_code(), err.is_retryable()) {
1204 ChunkFailureReaction::RetryPerSpan => {
1205 Self::push_trace_failure_message(
1206 &mut ingest_state.failure_messages,
1207 ChunkFailureReaction::RetryPerSpan.as_metric_label(),
1208 err.status_code().as_ref(),
1209 format!("Chunk fallback triggered by {}", err.status_code().as_ref()),
1210 );
1211 return self
1212 .ingest_trace_v1_rows_span_by_span(retry, ctx, ingest_state)
1213 .await;
1214 }
1215 ChunkFailureReaction::DiscardChunk => {
1216 ingest_state.outcome.rejected_spans += span_count;
1217 let (cause, shown_cause) = Self::trace_failure_cause(&err);
1218 Self::push_trace_failure_message(
1219 &mut ingest_state.failure_messages,
1220 ChunkFailureReaction::DiscardChunk.as_metric_label(),
1221 &format!("{span_count}:{cause}"),
1225 format!(
1226 "Discarded {} spans after pre-write chunk failure ({}): {}",
1227 span_count,
1228 err.status_code().as_ref(),
1229 shown_cause
1230 ),
1231 );
1232 return Ok(());
1233 }
1234 ChunkFailureReaction::Propagate => {
1235 Self::push_trace_failure_message(
1236 &mut ingest_state.failure_messages,
1237 ChunkFailureReaction::Propagate.as_metric_label(),
1238 err.status_code().as_ref(),
1239 format!(
1240 "Propagating pre-write chunk failure ({})",
1241 err.status_code().as_ref()
1242 ),
1243 );
1244 return Err(err);
1245 }
1246 }
1247 }
1248
1249 let output = match self
1250 .handle_trace_inserts(requests, ctx)
1251 .await
1252 .map_err(BoxedError::new)
1253 .context(error::ExecuteGrpcQuerySnafu)
1254 {
1255 Ok(output) => output,
1256 Err(err) => {
1257 Self::push_trace_failure_message(
1260 &mut ingest_state.failure_messages,
1261 ChunkFailureReaction::Propagate.as_metric_label(),
1262 err.status_code().as_ref(),
1263 format!(
1264 "Propagating chunk write failure ({})",
1265 err.status_code().as_ref()
1266 ),
1267 );
1268 return Err(err);
1269 }
1270 };
1271
1272 Self::add_trace_write_cost(&mut ingest_state.outcome, output.meta.cost);
1273 ingest_state.outcome.accepted_spans += span_count;
1274 retry.add_to_aux_data(&mut ingest_state.aux_data);
1275
1276 Ok(())
1277 }
1278
1279 async fn ingest_trace_v1_rows_span_by_span(
1280 &self,
1281 retry: TraceChunkRetry,
1282 ctx: QueryContextRef,
1283 ingest_state: &mut TraceIngestState,
1284 ) -> ServerResult<()> {
1285 for row in retry.rows {
1286 let (span, rows) = row.into_rows(&retry.schema)?;
1287 let mut requests = RowInsertRequests {
1288 inserts: vec![RowInsertRequest {
1289 table_name: retry.table_name.clone(),
1290 rows: Some(rows),
1291 }],
1292 };
1293
1294 if let Err(err) = self.reconcile_trace_column_types(&mut requests, &ctx).await {
1295 if matches!(
1296 Self::classify_trace_prewrite_failure(err.status_code(), err.is_retryable()),
1297 ChunkFailureReaction::Propagate
1298 ) {
1299 Self::push_trace_failure_message(
1300 &mut ingest_state.failure_messages,
1301 ChunkFailureReaction::Propagate.as_metric_label(),
1302 err.status_code().as_ref(),
1303 format!(
1304 "Propagating pre-write span failure for {}:{} ({})",
1305 span.trace_id,
1306 span.span_id,
1307 err.status_code().as_ref()
1308 ),
1309 );
1310 return Err(err);
1311 }
1312
1313 ingest_state.outcome.rejected_spans += 1;
1314 let (cause, shown_cause) = Self::trace_failure_cause(&err);
1317 Self::push_trace_failure_message(
1318 &mut ingest_state.failure_messages,
1319 "span_rejected",
1320 &cause,
1321 format!(
1322 "Rejected span {}:{} ({}): {}",
1323 span.trace_id,
1324 span.span_id,
1325 err.status_code().as_ref(),
1326 shown_cause
1327 ),
1328 );
1329 continue;
1330 }
1331
1332 let output = match self
1333 .handle_trace_inserts(requests, ctx.clone())
1334 .await
1335 .map_err(BoxedError::new)
1336 .context(error::ExecuteGrpcQuerySnafu)
1337 {
1338 Ok(output) => output,
1339 Err(err) => {
1340 Self::push_trace_failure_message(
1341 &mut ingest_state.failure_messages,
1342 ChunkFailureReaction::Propagate.as_metric_label(),
1343 err.status_code().as_ref(),
1344 format!(
1345 "Propagating span write failure for {}:{} ({})",
1346 span.trace_id,
1347 span.span_id,
1348 err.status_code().as_ref()
1349 ),
1350 );
1351 return Err(err);
1352 }
1353 };
1354
1355 Self::add_trace_write_cost(&mut ingest_state.outcome, output.meta.cost);
1356 ingest_state.outcome.accepted_spans += 1;
1357 span.add_to_aux_data(&mut ingest_state.aux_data);
1358 }
1359
1360 Ok(())
1361 }
1362
1363 async fn insert_trace_requests(
1365 &self,
1366 mut requests: RowInsertRequests,
1367 is_trace_v1_model: bool,
1368 ctx: QueryContextRef,
1369 ) -> ServerResult<Output> {
1370 if is_trace_v1_model {
1371 self.reconcile_trace_column_types(&mut requests, &ctx)
1372 .await?;
1373 self.handle_trace_inserts(requests, ctx)
1374 .await
1375 .map_err(BoxedError::new)
1376 .context(error::ExecuteGrpcQuerySnafu)
1377 } else {
1378 self.handle_log_inserts(requests, ctx)
1379 .await
1380 .map_err(BoxedError::new)
1381 .context(error::ExecuteGrpcQuerySnafu)
1382 }
1383 }
1384
1385 async fn plan_trace_v1_request_schema(
1386 &self,
1387 table_name: &str,
1388 request_schema: &mut TraceRequestSchema,
1389 ctx: &QueryContextRef,
1390 ) -> ServerResult<TraceRequestSchemaPlan> {
1391 let catalog = ctx.current_catalog();
1392 let schema = ctx.current_schema();
1393 let table = self
1394 .catalog_manager
1395 .table(catalog, &schema, table_name, None)
1396 .await?;
1397 let table_schema = table.as_ref().map(|table| table.schema());
1398 let exclusions = request_schema.incompatible_schema_observations(table_schema.as_deref());
1399 if !exclusions.is_empty() {
1400 return Ok(TraceRequestSchemaPlan::IncompatibleObservations(exclusions));
1401 }
1402
1403 Ok(request_schema.resolve_table_schema(table_schema.as_deref()))
1404 }
1405
1406 async fn prepare_trace_v1_request_schema(
1407 &self,
1408 table_name: &str,
1409 request_schema: &mut TraceRequestSchema,
1410 chunks: &mut Vec<TraceChunkRetry>,
1411 chunk_states: &mut Vec<TraceChunkSchemaState>,
1412 ctx: &QueryContextRef,
1413 ) -> ServerResult<()> {
1414 loop {
1415 let exclusions = Self::prevalidate_trace_v1_fixed_columns(chunks, chunk_states)?;
1418 if !exclusions.is_empty() {
1419 Self::exclude_trace_v1_schema_observations(
1420 request_schema,
1421 chunks,
1422 chunk_states,
1423 &exclusions,
1424 )?;
1425 continue;
1426 }
1427
1428 if request_schema.columns.is_empty() {
1429 return Ok(());
1430 }
1431
1432 let pre_alter = match self
1433 .plan_trace_v1_request_schema(table_name, request_schema, ctx)
1434 .await?
1435 {
1436 TraceRequestSchemaPlan::Ready(pre_alter) => pre_alter,
1437 TraceRequestSchemaPlan::IncompatibleObservations(exclusions) => {
1438 Self::exclude_trace_v1_schema_observations(
1439 request_schema,
1440 chunks,
1441 chunk_states,
1442 &exclusions,
1443 )?;
1444 continue;
1445 }
1446 };
1447 if pre_alter.requires_ddl() {
1448 let exclusions = Self::prevalidate_trace_v1_chunk_rewrites(
1449 request_schema,
1450 chunks,
1451 chunk_states,
1452 )?;
1453 if !exclusions.is_empty() {
1454 Self::exclude_trace_v1_schema_observations(
1455 request_schema,
1456 chunks,
1457 chunk_states,
1458 &exclusions,
1459 )?;
1460 continue;
1461 }
1462
1463 self.apply_trace_v1_pre_alter(ctx, table_name, pre_alter)
1464 .await?;
1465 let final_plan = self
1466 .plan_trace_v1_request_schema(table_name, request_schema, ctx)
1467 .await?;
1468 let schema_converged = matches!(
1469 final_plan,
1470 TraceRequestSchemaPlan::Ready(final_plan) if !final_plan.requires_ddl()
1471 );
1472 if !schema_converged {
1473 for state in chunk_states.iter_mut() {
1474 state.mark_for_reconcile();
1475 }
1476 return Ok(());
1477 }
1478
1479 return Ok(());
1480 }
1481
1482 return Ok(());
1483 }
1484 }
1485
1486 fn prevalidate_trace_v1_fixed_columns(
1487 chunks: &[TraceChunkRetry],
1488 chunk_states: &[TraceChunkSchemaState],
1489 ) -> ServerResult<TraceSchemaExclusions> {
1490 let mut exclusions = HashMap::<String, HashSet<usize>>::new();
1491 for (batch_index, (chunk, state)) in chunks.iter().zip(chunk_states).enumerate() {
1492 if *state != TraceChunkSchemaState::Prepared {
1493 continue;
1494 }
1495
1496 'rows: for row in &chunk.rows {
1497 for retry_value in &row.values {
1498 let Some(column) = chunk.schema.get(retry_value.column_index) else {
1499 return error::InternalSnafu {
1500 err_msg: format!(
1501 "trace retry column index {} is out of bounds",
1502 retry_value.column_index
1503 ),
1504 }
1505 .fail();
1506 };
1507 let Some(target_type) = trace_semconv_fixed_type(&column.column_name) else {
1508 continue;
1509 };
1510 let Some(request_type) = retry_value
1511 .value
1512 .value_data
1513 .as_ref()
1514 .and_then(trace_value_datatype)
1515 else {
1516 continue;
1517 };
1518 if request_type != target_type
1519 && coerce_value_data(
1520 &retry_value.value.value_data,
1521 target_type,
1522 request_type,
1523 )
1524 .is_err()
1525 {
1526 exclusions
1527 .entry(column.column_name.clone())
1528 .or_default()
1529 .insert(batch_index);
1530 break 'rows;
1531 }
1532 }
1533 }
1534 }
1535 Ok(exclusions)
1536 }
1537
1538 fn prevalidate_trace_v1_chunk_rewrites(
1539 request_schema: &TraceRequestSchema,
1540 chunks: &[TraceChunkRetry],
1541 chunk_states: &mut [TraceChunkSchemaState],
1542 ) -> ServerResult<TraceSchemaExclusions> {
1543 let mut exclusions = HashMap::<String, HashSet<usize>>::new();
1544 for (batch_index, (chunk, state)) in chunks.iter().zip(chunk_states).enumerate() {
1545 if *state != TraceChunkSchemaState::Prepared {
1546 continue;
1547 }
1548 let mut request = chunk.to_request()?;
1549 if let Some(column_name) = Self::prepare_trace_v1_chunk_rewrite(
1550 request_schema,
1551 batch_index,
1552 &mut request,
1553 state,
1554 )? {
1555 exclusions
1556 .entry(column_name)
1557 .or_default()
1558 .insert(batch_index);
1559 }
1560 }
1561 Ok(exclusions)
1562 }
1563
1564 fn prepare_trace_v1_chunk_rewrite(
1565 request_schema: &TraceRequestSchema,
1566 batch_index: usize,
1567 request: &mut RowInsertRequest,
1568 state: &mut TraceChunkSchemaState,
1569 ) -> ServerResult<Option<String>> {
1570 if *state != TraceChunkSchemaState::Prepared {
1571 return Ok(None);
1572 }
1573
1574 match request_schema.prepare_request_rewrite(batch_index, request) {
1575 Ok(Some(rewrite)) => {
1576 TraceRequestSchema::apply_request_rewrite(request, rewrite);
1577 Ok(None)
1578 }
1579 Ok(None) => Ok(None),
1580 Err(failure) => {
1581 if !matches!(
1582 Self::classify_trace_prewrite_failure(
1583 failure.error.status_code(),
1584 failure.error.is_retryable(),
1585 ),
1586 ChunkFailureReaction::RetryPerSpan
1587 ) {
1588 return Err(failure.error);
1589 }
1590 state.mark_for_reconcile();
1591 Ok(Some(failure.column_name))
1592 }
1593 }
1594 }
1595
1596 fn exclude_trace_v1_schema_observations(
1597 request_schema: &mut TraceRequestSchema,
1598 chunks: &mut Vec<TraceChunkRetry>,
1599 chunk_states: &mut Vec<TraceChunkSchemaState>,
1600 exclusions: &TraceSchemaExclusions,
1601 ) -> ServerResult<()> {
1602 if chunks.len() != chunk_states.len() {
1603 return error::InternalSnafu {
1604 err_msg: format!(
1605 "trace chunk count {} does not match state count {}",
1606 chunks.len(),
1607 chunk_states.len()
1608 ),
1609 }
1610 .fail();
1611 }
1612
1613 let batch_indexes = exclusions
1614 .values()
1615 .flatten()
1616 .copied()
1617 .collect::<HashSet<_>>();
1618 let previous_chunks = std::mem::take(chunks);
1619 let previous_states = std::mem::take(chunk_states);
1620 for (batch_index, (chunk, mut state)) in
1621 previous_chunks.into_iter().zip(previous_states).enumerate()
1622 {
1623 if !batch_indexes.contains(&batch_index) {
1624 chunks.push(chunk);
1625 chunk_states.push(state);
1626 } else if chunk.rows.len() <= 1 {
1627 state.mark_for_reconcile();
1628 chunks.push(chunk);
1629 chunk_states.push(state);
1630 } else {
1631 let split_chunks = chunk.into_single_span_chunks()?;
1632 chunk_states.extend(std::iter::repeat_n(
1633 TraceChunkSchemaState::Prepared,
1634 split_chunks.len(),
1635 ));
1636 chunks.extend(split_chunks);
1637 }
1638 }
1639
1640 *request_schema = TraceRequestSchema::default();
1641 for (batch_index, (chunk, state)) in chunks.iter().zip(chunk_states.iter()).enumerate() {
1642 if *state == TraceChunkSchemaState::Prepared {
1643 request_schema.observe_retry_chunk(batch_index, chunk)?;
1644 }
1645 }
1646
1647 Ok(())
1648 }
1649
1650 async fn apply_trace_v1_pre_alter(
1651 &self,
1652 ctx: &QueryContextRef,
1653 table_name: &str,
1654 pre_alter: TraceTablePreAlter,
1655 ) -> ServerResult<()> {
1656 let TraceTablePreAlter {
1657 ensure_columns,
1658 modify_float64_columns,
1659 alter_existing,
1660 } = pre_alter;
1661
1662 if !ensure_columns.is_empty() {
1663 self.inserter
1664 .ensure_trace_table_on_demand(
1665 table_name,
1666 ensure_columns,
1667 alter_existing,
1668 ctx,
1669 &self.statement_executor,
1670 )
1671 .await
1672 .map_err(BoxedError::new)
1673 .context(error::ExecuteGrpcQuerySnafu)?;
1674 }
1675
1676 if !modify_float64_columns.is_empty() {
1677 self.alter_trace_table_columns_to_float64(ctx, table_name, &modify_float64_columns)
1678 .await?;
1679 }
1680
1681 Ok(())
1682 }
1683
1684 fn classify_trace_prewrite_failure(
1685 status: StatusCode,
1686 retryable: bool,
1687 ) -> ChunkFailureReaction {
1688 if retryable {
1689 return ChunkFailureReaction::Propagate;
1690 }
1691
1692 match status {
1693 StatusCode::InvalidArguments
1694 | StatusCode::InvalidSyntax
1695 | StatusCode::Unsupported
1696 | StatusCode::TableNotFound
1697 | StatusCode::TableColumnNotFound => ChunkFailureReaction::RetryPerSpan,
1698 StatusCode::DatabaseNotFound => ChunkFailureReaction::DiscardChunk,
1699 _ => ChunkFailureReaction::Propagate,
1700 }
1701 }
1702
1703 fn add_trace_write_cost(outcome: &mut TraceIngestOutcome, cost: usize) {
1704 outcome.write_cost += cost;
1705 }
1706
1707 fn trace_failure_cause(err: &error::Error) -> (String, String) {
1713 let cause = err.output_msg();
1714 let display = truncate_for_diagnostics(&cause, TRACE_FAILURE_CAUSE_LIMIT);
1715 (cause, display)
1716 }
1717
1718 fn push_trace_failure_message(
1720 messages: &mut TraceFailureMessages,
1721 label: &'static str,
1722 key: &str,
1723 message: String,
1724 ) {
1725 OTLP_TRACES_FAILURE_COUNT.with_label_values(&[label]).inc();
1726
1727 if let Some(entry) = messages
1728 .entries
1729 .iter_mut()
1730 .find(|entry| entry.label == label && entry.key == key)
1731 {
1732 entry.occurrences += 1;
1733 return;
1734 }
1735
1736 if messages.entries.len() >= TRACE_FAILURE_MESSAGE_LIMIT {
1737 messages.suppressed_occurrences += 1;
1738 return;
1739 }
1740
1741 messages.entries.push(TraceFailureEntry {
1742 label,
1743 key: key.to_string(),
1744 message,
1745 occurrences: 1,
1746 });
1747 }
1748
1749 fn finish_trace_failure_message(
1750 accepted_spans: usize,
1751 rejected_spans: usize,
1752 messages: TraceFailureMessages,
1753 ) -> Option<String> {
1754 if rejected_spans == 0 && messages.is_empty() {
1755 return None;
1756 }
1757
1758 let mut summary = format!(
1759 "Accepted {} spans, rejected {} spans",
1760 accepted_spans, rejected_spans
1761 );
1762
1763 if !messages.is_empty() {
1764 let details = messages
1765 .entries
1766 .into_iter()
1767 .map(|entry| {
1768 if entry.occurrences > 1 {
1769 format!("{} (x{})", entry.message, entry.occurrences)
1770 } else {
1771 entry.message
1772 }
1773 })
1774 .collect::<Vec<_>>()
1775 .join("; ");
1776 summary.push_str(": ");
1777 summary.push_str(&details);
1778 }
1779
1780 if messages.suppressed_occurrences > 0 {
1781 summary.push_str(&format!(
1782 "; {} additional failures suppressed",
1783 messages.suppressed_occurrences
1784 ));
1785 }
1786
1787 Some(summary)
1788 }
1789
1790 async fn alter_trace_table_columns_to_float64(
1792 &self,
1793 ctx: &QueryContextRef,
1794 table_name: &str,
1795 column_names: &[String],
1796 ) -> ServerResult<()> {
1797 let catalog_name = ctx.current_catalog().to_string();
1798 let schema_name = ctx.current_schema();
1799 let alter_expr = AlterTableExpr {
1800 catalog_name: catalog_name.clone(),
1801 schema_name: schema_name.clone(),
1802 table_name: table_name.to_string(),
1803 kind: Some(Kind::ModifyColumnTypes(ModifyColumnTypes {
1804 modify_column_types: column_names
1805 .iter()
1806 .map(|column_name| ModifyColumnType {
1807 column_name: column_name.clone(),
1808 target_type: ColumnDataType::Float64 as i32,
1809 target_type_extension: None,
1810 })
1811 .collect(),
1812 })),
1813 };
1814
1815 if let Err(err) = self
1816 .statement_executor
1817 .alter_table_inner(alter_expr, ctx.clone(), TriggerReason::AutoAlter)
1818 .await
1819 {
1820 let table = self
1821 .catalog_manager
1822 .table(&catalog_name, &schema_name, table_name, None)
1823 .await
1824 .map_err(servers::error::Error::from)?;
1825 let alter_already_applied = table
1826 .map(|table| {
1827 let table_schema = table.schema();
1828 column_names.iter().all(|column_name| {
1829 table_schema
1830 .column_schema_by_name(column_name)
1831 .and_then(|table_col| {
1832 ColumnDataTypeWrapper::try_from(table_col.data_type.clone())
1833 .ok()
1834 .map(|wrapper| wrapper.datatype())
1835 })
1836 == Some(ColumnDataType::Float64)
1837 })
1838 })
1839 .unwrap_or(false);
1840
1841 if alter_already_applied {
1842 return Ok(());
1843 }
1844
1845 warn!(
1846 table_name,
1847 columns = ?column_names,
1848 error = %err,
1849 "failed to widen trace columns before insert"
1850 );
1851
1852 return Err(wrap_trace_alter_failure(err));
1853 }
1854
1855 Ok(())
1856 }
1857
1858 async fn reconcile_trace_column_types(
1862 &self,
1863 requests: &mut RowInsertRequests,
1864 ctx: &QueryContextRef,
1865 ) -> ServerResult<()> {
1866 let catalog = ctx.current_catalog();
1867 let schema = ctx.current_schema();
1868
1869 for req in &mut requests.inserts {
1870 let table = self
1871 .catalog_manager
1872 .table(catalog, &schema, &req.table_name, None)
1873 .await?;
1874
1875 let Some(rows) = req.rows.as_mut() else {
1876 continue;
1877 };
1878
1879 let table_schema = table.map(|table| table.schema());
1880 let mut pending_rewrites = Vec::new();
1881 let mut pending_alter_columns = Vec::new();
1882
1883 for (col_idx, col_schema) in rows.schema.iter().enumerate() {
1884 let Some(current_type) = ColumnDataType::try_from(col_schema.datatype).ok() else {
1885 continue;
1886 };
1887
1888 let mut observed_types = Vec::new();
1889 push_observed_trace_type(&mut observed_types, current_type);
1890
1891 for row in &rows.rows {
1894 let Some(value) = row
1895 .values
1896 .get(col_idx)
1897 .and_then(|value| value.value_data.as_ref())
1898 else {
1899 continue;
1900 };
1901
1902 let Some(value_type) = trace_value_datatype(value) else {
1903 continue;
1904 };
1905 push_observed_trace_type(&mut observed_types, value_type);
1906 }
1907
1908 let existing_schema_type = table_schema
1909 .as_ref()
1910 .and_then(|schema| schema.column_schema_by_name(&col_schema.column_name))
1911 .and_then(|table_col| {
1912 ColumnDataTypeWrapper::try_from(table_col.data_type.clone())
1913 .ok()
1914 .map(|wrapper| {
1915 let datatype = wrapper.datatype();
1916 (datatype, ConcreteDataType::from(wrapper))
1917 })
1918 });
1919 let request_concrete_type = ConcreteDataType::from(ColumnDataTypeWrapper::new(
1920 current_type,
1921 col_schema.datatype_extension.clone(),
1922 ));
1923 if let Some((existing_datatype, existing_concrete_type)) =
1924 existing_schema_type.as_ref()
1925 && trace_logical_types_incompatible(
1926 current_type,
1927 &request_concrete_type,
1928 *existing_datatype,
1929 existing_concrete_type,
1930 )
1931 {
1932 return error::InvalidParameterSnafu {
1933 reason: format!(
1934 "incompatible logical types for trace column '{}' in table '{}'",
1935 col_schema.column_name, req.table_name
1936 ),
1937 }
1938 .fail();
1939 }
1940 let existing_type = existing_schema_type.map(|(datatype, _)| datatype);
1941 let fixed_type = trace_semconv_fixed_type(&col_schema.column_name);
1942
1943 if !observed_types
1944 .iter()
1945 .copied()
1946 .any(is_trace_reconcile_candidate_type)
1947 && existing_type
1948 .map(|datatype| !is_trace_reconcile_candidate_type(datatype))
1949 .unwrap_or(true)
1950 && fixed_type.is_none()
1951 {
1952 continue;
1953 }
1954
1955 let Some(decision) = choose_trace_reconcile_decision(
1958 &col_schema.column_name,
1959 &observed_types,
1960 existing_type,
1961 )
1962 .map_err(|_| {
1963 enrich_trace_reconcile_error(
1964 &req.table_name,
1965 &col_schema.column_name,
1966 &observed_types,
1967 existing_type,
1968 fixed_type,
1969 )
1970 })?
1971 else {
1972 continue;
1973 };
1974 let target_type = decision.target_type();
1975
1976 if !decision.requires_alter()
1977 && observed_types
1978 .iter()
1979 .all(|observed| *observed == target_type)
1980 && col_schema.datatype == target_type as i32
1981 {
1982 continue;
1983 }
1984
1985 if decision.requires_alter()
1986 && !pending_alter_columns.contains(&col_schema.column_name)
1987 {
1988 pending_alter_columns.push(col_schema.column_name.clone());
1989 }
1990
1991 pending_rewrites.push(PendingTraceColumnRewrite {
1992 col_idx,
1993 target_type,
1994 column_name: col_schema.column_name.clone(),
1995 });
1996 }
1997
1998 if pending_rewrites.is_empty() {
1999 continue;
2000 }
2001
2002 let prepared_rewrites =
2003 prepare_trace_column_rewrites(&rows.rows, pending_rewrites, &req.table_name)
2004 .map_err(|failure| failure.error)?;
2005
2006 if !pending_alter_columns.is_empty() {
2007 self.alter_trace_table_columns_to_float64(
2008 ctx,
2009 &req.table_name,
2010 &pending_alter_columns,
2011 )
2012 .await?;
2013 }
2014
2015 prepared_rewrites.apply(rows);
2016 }
2017
2018 Ok(())
2019 }
2020}
2021
2022fn trace_logical_types_incompatible(
2023 left_datatype: ColumnDataType,
2024 left_concrete_type: &ConcreteDataType,
2025 right_datatype: ColumnDataType,
2026 right_concrete_type: &ConcreteDataType,
2027) -> bool {
2028 if left_concrete_type == right_concrete_type {
2029 return false;
2030 }
2031 if is_supported_trace_coercion(left_datatype, right_datatype)
2038 || is_supported_trace_coercion(right_datatype, left_datatype)
2039 {
2040 return false;
2041 }
2042 left_datatype == right_datatype
2043 || !is_trace_reconcile_candidate_type(left_datatype)
2044 || !is_trace_reconcile_candidate_type(right_datatype)
2045}
2046
2047fn chunk_owned<T>(items: Vec<T>, chunk_size: usize) -> Vec<Vec<T>> {
2048 if items.is_empty() {
2049 return Vec::new();
2050 }
2051
2052 if chunk_size == 0 {
2053 return vec![items];
2054 }
2055
2056 let mut chunks = Vec::with_capacity(items.len().div_ceil(chunk_size));
2057 let mut iter = items.into_iter();
2058 while iter.len() > 0 {
2059 chunks.push(iter.by_ref().take(chunk_size).collect());
2060 }
2061 chunks
2062}
2063
2064fn wrap_trace_alter_failure<E>(err: E) -> servers::error::Error
2066where
2067 E: ErrorExt + Send + Sync + 'static,
2068{
2069 error::ExecuteGrpcQuerySnafu.into_error(BoxedError::new(err))
2070}
2071
2072pub(super) fn trace_conventions(request: &ExportTraceServiceRequest) -> String {
2077 let mut seen: Option<&str> = None;
2078 let mut mixed = false;
2079
2080 for resource_spans in &request.resource_spans {
2081 let urls = std::iter::once(resource_spans.schema_url.as_str()).chain(
2082 resource_spans
2083 .scope_spans
2084 .iter()
2085 .map(|s| s.schema_url.as_str()),
2086 );
2087 for url in urls {
2088 if url.is_empty() {
2089 continue;
2090 }
2091 match seen {
2092 None => seen = Some(url),
2093 Some(prev) if prev == url => {}
2094 Some(_) => {
2095 mixed = true;
2096 break;
2097 }
2098 }
2099 }
2100 if mixed {
2101 break;
2102 }
2103 }
2104
2105 if mixed {
2106 SEMANTIC_VALUE_MIXED.to_string()
2107 } else {
2108 seen.map(str::to_string)
2109 .unwrap_or_else(|| SEMANTIC_VALUE_UNKNOWN.to_string())
2110 }
2111}
2112
2113#[cfg(test)]
2114mod tests;