Skip to main content

frontend/instance/otlp/
trace_ingest.rs

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