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