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