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