Skip to main content

frontend/instance/
otlp.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::sync::Arc;
16
17use api::helper::ColumnDataTypeWrapper;
18use api::v1::alter_table_expr::Kind;
19use api::v1::{
20    AlterTableExpr, ColumnDataType, ModifyColumnType, ModifyColumnTypes, RowInsertRequests,
21};
22use async_trait::async_trait;
23use auth::{PermissionChecker, PermissionCheckerRef, PermissionReq};
24use client::Output;
25use common_error::ext::{BoxedError, ErrorExt};
26use common_error::status_code::StatusCode;
27use common_query::prelude::GREPTIME_PHYSICAL_TABLE;
28use common_telemetry::{tracing, warn};
29use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest;
30use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
31use otel_arrow_rust::proto::opentelemetry::collector::metrics::v1::ExportMetricsServiceRequest;
32use pipeline::{GreptimePipelineParams, PipelineWay};
33use servers::error::{self, AuthSnafu, Result as ServerResult};
34use servers::http::prom_store::PHYSICAL_TABLE_PARAM;
35use servers::interceptor::{OpenTelemetryProtocolInterceptor, OpenTelemetryProtocolInterceptorRef};
36use servers::otlp;
37use servers::otlp::coerce::{coerce_value_data, trace_value_datatype};
38use servers::otlp::trace::TraceAuxData;
39use servers::otlp::trace::span::{TraceSpan, TraceSpanGroup};
40use servers::query_handler::{
41    OpenTelemetryProtocolHandler, PipelineHandlerRef, TraceIngestOutcome,
42};
43use session::context::QueryContextRef;
44use snafu::{IntoError, ResultExt};
45use table::requests::{
46    OTLP_METRIC_COMPAT_KEY, OTLP_METRIC_COMPAT_PROM, SEMANTIC_PER_TABLE_INDEX_KEY,
47    SEMANTIC_PIPELINE, SEMANTIC_SIGNAL_TYPE, SEMANTIC_SOURCE, SEMANTIC_TRACE_CONVENTIONS,
48    SEMANTIC_VALUE_MIXED, SEMANTIC_VALUE_UNKNOWN, SIGNAL_TYPE_LOG, SIGNAL_TYPE_METRIC,
49    SIGNAL_TYPE_TRACE, SOURCE_OPENTELEMETRY, TABLE_DATA_MODEL_TRACE_V1,
50};
51
52use crate::instance::Instance;
53use crate::instance::otlp::trace_semconv::trace_semconv_fixed_type;
54use crate::instance::otlp::trace_types::{
55    PendingTraceColumnRewrite, choose_trace_reconcile_decision, enrich_trace_reconcile_error,
56    is_trace_reconcile_candidate_type, push_observed_trace_type, validate_trace_column_rewrites,
57};
58use crate::metrics::{
59    OTLP_LOGS_ROWS, OTLP_METRICS_ROWS, OTLP_TRACES_FAILURE_COUNT, OTLP_TRACES_ROWS,
60};
61
62pub mod trace_semconv;
63pub mod trace_types;
64
65const TRACE_FAILURE_MESSAGE_LIMIT: usize = 4;
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68enum ChunkFailureReaction {
69    RetryPerSpan,
70    DiscardChunk,
71    Propagate,
72}
73
74impl ChunkFailureReaction {
75    fn as_metric_label(self) -> &'static str {
76        match self {
77            Self::RetryPerSpan => "retry_per_span",
78            Self::DiscardChunk => "discard_chunk",
79            Self::Propagate => "propagate_failure",
80        }
81    }
82}
83
84struct TraceChunkIngestContext<'a> {
85    pipeline_handler: PipelineHandlerRef,
86    pipeline: &'a PipelineWay,
87    pipeline_params: &'a GreptimePipelineParams,
88    table_name: &'a str,
89    is_trace_v1_model: bool,
90}
91
92struct TraceIngestState {
93    aux_data: TraceAuxData,
94    outcome: TraceIngestOutcome,
95    failure_messages: Vec<String>,
96}
97
98#[async_trait]
99impl OpenTelemetryProtocolHandler for Instance {
100    #[tracing::instrument(skip_all)]
101    async fn metrics(
102        &self,
103        request: ExportMetricsServiceRequest,
104        ctx: QueryContextRef,
105    ) -> ServerResult<Output> {
106        self.plugins
107            .get::<PermissionCheckerRef>()
108            .as_ref()
109            .check_permission(ctx.current_user(), PermissionReq::Otlp)
110            .context(AuthSnafu)?;
111
112        let interceptor_ref = self
113            .plugins
114            .get::<OpenTelemetryProtocolInterceptorRef<servers::error::Error>>();
115        interceptor_ref.pre_execute(ctx.clone())?;
116
117        let input_names = request
118            .resource_metrics
119            .iter()
120            .flat_map(|r| r.scope_metrics.iter())
121            .flat_map(|s| s.metrics.iter().map(|m| &m.name))
122            .collect::<Vec<_>>();
123
124        // See [`OtlpMetricCtx`] for details
125        let is_legacy = self.check_otlp_legacy(&input_names, ctx.clone()).await?;
126
127        let mut metric_ctx = ctx
128            .protocol_ctx()
129            .get_otlp_metric_ctx()
130            .cloned()
131            .unwrap_or_default();
132        metric_ctx.is_legacy = is_legacy;
133
134        let (requests, rows, semantic_index) =
135            otlp::metrics::to_grpc_insert_requests(request, &mut metric_ctx)?;
136        OTLP_METRICS_ROWS.inc_by(rows as u64);
137
138        let ctx = {
139            let mut c = (*ctx).clone();
140            c.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_METRIC);
141            c.set_extension(SEMANTIC_SOURCE, SOURCE_OPENTELEMETRY);
142            // Per-table metric specifics + resource/scope lineage ride this
143            // internal channel; the auto-create path folds them per table name.
144            if let Some(index) = semantic_index.encode() {
145                c.set_extension(SEMANTIC_PER_TABLE_INDEX_KEY, index);
146            }
147            if !is_legacy {
148                c.set_extension(OTLP_METRIC_COMPAT_KEY, OTLP_METRIC_COMPAT_PROM.to_string());
149            }
150            Arc::new(c)
151        };
152
153        // If the user uses the legacy path, it is by default without metric engine.
154        if metric_ctx.is_legacy || !metric_ctx.with_metric_engine {
155            self.handle_row_inserts(requests, ctx, false, false)
156                .await
157                .map_err(BoxedError::new)
158                .context(error::ExecuteGrpcQuerySnafu)
159        } else {
160            let physical_table = ctx
161                .extension(PHYSICAL_TABLE_PARAM)
162                .unwrap_or(GREPTIME_PHYSICAL_TABLE)
163                .to_string();
164            self.handle_metric_row_inserts(requests, ctx, physical_table.clone())
165                .await
166                .map_err(BoxedError::new)
167                .context(error::ExecuteGrpcQuerySnafu)
168        }
169    }
170
171    #[tracing::instrument(skip_all)]
172    async fn traces(
173        &self,
174        pipeline_handler: PipelineHandlerRef,
175        request: ExportTraceServiceRequest,
176        pipeline: PipelineWay,
177        pipeline_params: GreptimePipelineParams,
178        table_name: String,
179        ctx: QueryContextRef,
180    ) -> ServerResult<TraceIngestOutcome> {
181        self.plugins
182            .get::<PermissionCheckerRef>()
183            .as_ref()
184            .check_permission(ctx.current_user(), PermissionReq::Otlp)
185            .context(AuthSnafu)?;
186
187        let interceptor_ref = self
188            .plugins
189            .get::<OpenTelemetryProtocolInterceptorRef<servers::error::Error>>();
190        interceptor_ref.pre_execute(ctx.clone())?;
191
192        // `schema_url` is consumed by `parse`, so derive conventions first.
193        let conventions = trace_conventions(&request);
194        let spans = otlp::trace::span::parse(request);
195        self.ingest_trace_spans(
196            pipeline_handler,
197            &pipeline,
198            &pipeline_params,
199            table_name,
200            spans,
201            &conventions,
202            ctx,
203        )
204        .await
205    }
206
207    #[tracing::instrument(skip_all)]
208    async fn logs(
209        &self,
210        pipeline_handler: PipelineHandlerRef,
211        request: ExportLogsServiceRequest,
212        pipeline: PipelineWay,
213        pipeline_params: GreptimePipelineParams,
214        table_name: String,
215        ctx: QueryContextRef,
216    ) -> ServerResult<Vec<Output>> {
217        self.plugins
218            .get::<PermissionCheckerRef>()
219            .as_ref()
220            .check_permission(ctx.current_user(), PermissionReq::Otlp)
221            .context(AuthSnafu)?;
222
223        let interceptor_ref = self
224            .plugins
225            .get::<OpenTelemetryProtocolInterceptorRef<servers::error::Error>>();
226        interceptor_ref.pre_execute(ctx.clone())?;
227
228        // `as_req_iter` clones this ctx into each `temp_ctx`, so identity set here
229        // reaches the context that drives table auto-create.
230        let ctx = {
231            let mut c = (*ctx).clone();
232            c.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_LOG);
233            c.set_extension(SEMANTIC_SOURCE, SOURCE_OPENTELEMETRY);
234            Arc::new(c)
235        };
236
237        let opt_req = otlp::logs::to_grpc_insert_requests(
238            request,
239            pipeline,
240            pipeline_params,
241            table_name,
242            &ctx,
243            pipeline_handler,
244        )
245        .await?;
246
247        let mut outputs = vec![];
248
249        for (temp_ctx, requests) in opt_req.as_req_iter(ctx) {
250            let cnt = requests
251                .inserts
252                .iter()
253                .filter_map(|r| r.rows.as_ref().map(|r| r.rows.len()))
254                .sum::<usize>();
255
256            let o = self
257                .handle_log_inserts(requests, temp_ctx)
258                .await
259                .inspect(|_| OTLP_LOGS_ROWS.inc_by(cnt as u64))
260                .map_err(BoxedError::new)
261                .context(error::ExecuteGrpcQuerySnafu)?;
262            outputs.push(o);
263        }
264
265        Ok(outputs)
266    }
267}
268
269impl Instance {
270    /// Ingest OTLP trace spans with chunk-level writes and span-level fallback on
271    /// deterministic chunk failures.
272    #[allow(clippy::too_many_arguments)]
273    async fn ingest_trace_spans(
274        &self,
275        pipeline_handler: PipelineHandlerRef,
276        pipeline: &PipelineWay,
277        pipeline_params: &GreptimePipelineParams,
278        table_name: String,
279        groups: Vec<TraceSpanGroup>,
280        conventions: &str,
281        ctx: QueryContextRef,
282    ) -> ServerResult<TraceIngestOutcome> {
283        let is_trace_v1_model = matches!(pipeline, PipelineWay::OtlpTraceDirectV1);
284
285        // Only the main span table gets the identity; the derived `_services` /
286        // `_operations` lookup tables keep the unstamped `ctx`.
287        let main_ctx = {
288            let mut c = (*ctx).clone();
289            c.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_TRACE);
290            c.set_extension(SEMANTIC_SOURCE, SOURCE_OPENTELEMETRY);
291            if is_trace_v1_model {
292                c.set_extension(SEMANTIC_PIPELINE, TABLE_DATA_MODEL_TRACE_V1);
293                c.set_extension(SEMANTIC_TRACE_CONVENTIONS, conventions);
294            }
295            Arc::new(c)
296        };
297
298        let ingest_ctx = TraceChunkIngestContext {
299            pipeline_handler,
300            pipeline,
301            pipeline_params,
302            table_name: &table_name,
303            is_trace_v1_model,
304        };
305        let mut ingest_state = TraceIngestState {
306            aux_data: TraceAuxData::default(),
307            outcome: TraceIngestOutcome::default(),
308            failure_messages: Vec::new(),
309        };
310
311        for group in groups {
312            let chunks = chunk_owned(group.spans, self.trace_ingest_chunk_size);
313            for chunk in chunks {
314                self.ingest_trace_chunk(&ingest_ctx, chunk, main_ctx.clone(), &mut ingest_state)
315                    .await?;
316            }
317        }
318
319        OTLP_TRACES_ROWS.inc_by(ingest_state.outcome.accepted_spans as u64);
320
321        if !ingest_state.aux_data.is_empty() {
322            // Auxiliary trace tables are derived from spans whose main-table
323            // writes are already confirmed, so they never create new accepted
324            // spans and they do not affect rejected span counts.
325            let (aux_requests, _) = otlp::trace::to_grpc_insert_requests_for_aux_tables(
326                std::mem::take(&mut ingest_state.aux_data),
327                ingest_ctx.pipeline,
328                ingest_ctx.table_name,
329            )?;
330
331            if !aux_requests.inserts.is_empty() {
332                match self
333                    .insert_trace_requests(aux_requests, ingest_ctx.is_trace_v1_model, ctx)
334                    .await
335                {
336                    Ok(output) => {
337                        Self::add_trace_write_cost(&mut ingest_state.outcome, output.meta.cost);
338                    }
339                    Err(err) => {
340                        Self::push_trace_failure_message(
341                            &mut ingest_state.failure_messages,
342                            "aux_table_update_failed",
343                            format!(
344                                "Auxiliary trace tables were not fully updated ({})",
345                                err.status_code().as_ref()
346                            ),
347                        );
348                    }
349                }
350            }
351        }
352
353        ingest_state.outcome.error_message = Self::finish_trace_failure_message(
354            ingest_state.outcome.accepted_spans,
355            ingest_state.outcome.rejected_spans,
356            ingest_state.failure_messages,
357        );
358
359        Ok(ingest_state.outcome)
360    }
361
362    /// Ingest one owned trace chunk so successful spans can be moved into the
363    /// accepted set without extra cloning.
364    async fn ingest_trace_chunk(
365        &self,
366        ingest_ctx: &TraceChunkIngestContext<'_>,
367        chunk: Vec<TraceSpan>,
368        ctx: QueryContextRef,
369        ingest_state: &mut TraceIngestState,
370    ) -> ServerResult<()> {
371        // Try the fast path first so healthy batches keep their original
372        // throughput and write amplification stays low.
373        let (requests, chunk_rows) = otlp::trace::to_grpc_insert_requests_from_spans(
374            &chunk,
375            ingest_ctx.pipeline,
376            ingest_ctx.pipeline_params,
377            ingest_ctx.table_name,
378            &ctx,
379            ingest_ctx.pipeline_handler.clone(),
380        )?;
381
382        match self
383            .insert_trace_requests(requests, ingest_ctx.is_trace_v1_model, ctx.clone())
384            .await
385        {
386            Ok(output) => {
387                Self::add_trace_write_cost(&mut ingest_state.outcome, output.meta.cost);
388                ingest_state.outcome.accepted_spans += chunk_rows;
389                for span in &chunk {
390                    ingest_state.aux_data.observe_span(span);
391                }
392            }
393            Err(err) => match Self::classify_trace_chunk_failure(err.status_code()) {
394                ChunkFailureReaction::RetryPerSpan => {
395                    Self::push_trace_failure_message(
396                        &mut ingest_state.failure_messages,
397                        ChunkFailureReaction::RetryPerSpan.as_metric_label(),
398                        format!("Chunk fallback triggered by {}", err.status_code().as_ref()),
399                    );
400                    // Only deterministic failures are retried span by span.
401                    // This includes schemaless table or column creation paths for
402                    // trace ingestion. Ambiguous failures are handled below
403                    // without retrying because the chunk may already have been
404                    // ingested.
405                    self.ingest_trace_chunk_span_by_span(
406                        ingest_ctx,
407                        chunk,
408                        ctx.clone(),
409                        ingest_state,
410                    )
411                    .await?;
412                }
413                ChunkFailureReaction::DiscardChunk => {
414                    ingest_state.outcome.rejected_spans += chunk.len();
415                    Self::push_trace_failure_message(
416                        &mut ingest_state.failure_messages,
417                        ChunkFailureReaction::DiscardChunk.as_metric_label(),
418                        format!(
419                            "Discarded {} spans after ambiguous chunk failure ({})",
420                            chunk.len(),
421                            err.status_code().as_ref()
422                        ),
423                    );
424                    // TODO(shuiyisong): Add an idempotent retry-safe recovery path for
425                    // ambiguous chunk failures such as timeout-like errors.
426                }
427                // Retryable or ambiguous failures must fail the request instead of
428                // becoming partial success. This path is not retry-safe because the
429                // chunk may already have been committed before the error surfaced.
430                ChunkFailureReaction::Propagate => {
431                    Self::push_trace_failure_message(
432                        &mut ingest_state.failure_messages,
433                        ChunkFailureReaction::Propagate.as_metric_label(),
434                        format!(
435                            "Propagating retryable chunk failure ({})",
436                            err.status_code().as_ref()
437                        ),
438                    );
439                    return Err(err);
440                }
441            },
442        }
443
444        Ok(())
445    }
446
447    /// Retry spans one by one only after a deterministic chunk failure.
448    async fn ingest_trace_chunk_span_by_span(
449        &self,
450        ingest_ctx: &TraceChunkIngestContext<'_>,
451        chunk: Vec<TraceSpan>,
452        ctx: QueryContextRef,
453        ingest_state: &mut TraceIngestState,
454    ) -> ServerResult<()> {
455        for span in chunk {
456            let (requests, rows) = otlp::trace::to_grpc_insert_requests_from_spans(
457                std::slice::from_ref(&span),
458                ingest_ctx.pipeline,
459                ingest_ctx.pipeline_params,
460                ingest_ctx.table_name,
461                &ctx,
462                ingest_ctx.pipeline_handler.clone(),
463            )?;
464
465            match self
466                .insert_trace_requests(requests, ingest_ctx.is_trace_v1_model, ctx.clone())
467                .await
468            {
469                Ok(output) => {
470                    Self::add_trace_write_cost(&mut ingest_state.outcome, output.meta.cost);
471                    ingest_state.outcome.accepted_spans += rows;
472                    ingest_state.aux_data.observe_span(&span);
473                }
474                Err(err) => {
475                    if Self::should_propagate_trace_span_failure(err.status_code()) {
476                        Self::push_trace_failure_message(
477                            &mut ingest_state.failure_messages,
478                            ChunkFailureReaction::Propagate.as_metric_label(),
479                            format!(
480                                "Propagating retryable span failure for {}:{} ({})",
481                                span.trace_id,
482                                span.span_id,
483                                err.status_code().as_ref()
484                            ),
485                        );
486                        return Err(err);
487                    }
488
489                    ingest_state.outcome.rejected_spans += 1;
490                    Self::push_trace_failure_message(
491                        &mut ingest_state.failure_messages,
492                        "span_rejected",
493                        format!(
494                            "Rejected span {}:{} ({})",
495                            span.trace_id,
496                            span.span_id,
497                            err.status_code().as_ref()
498                        ),
499                    );
500                }
501            }
502        }
503
504        Ok(())
505    }
506
507    /// Reconcile and insert one trace request batch.
508    async fn insert_trace_requests(
509        &self,
510        mut requests: RowInsertRequests,
511        is_trace_v1_model: bool,
512        ctx: QueryContextRef,
513    ) -> ServerResult<Output> {
514        if is_trace_v1_model {
515            self.reconcile_trace_column_types(&mut requests, &ctx)
516                .await?;
517            self.handle_trace_inserts(requests, ctx)
518                .await
519                .map_err(BoxedError::new)
520                .context(error::ExecuteGrpcQuerySnafu)
521        } else {
522            self.handle_log_inserts(requests, ctx)
523                .await
524                .map_err(BoxedError::new)
525                .context(error::ExecuteGrpcQuerySnafu)
526        }
527    }
528
529    fn classify_trace_chunk_failure(status: StatusCode) -> ChunkFailureReaction {
530        match status {
531            StatusCode::InvalidArguments
532            | StatusCode::InvalidSyntax
533            | StatusCode::Unsupported
534            | StatusCode::TableNotFound
535            | StatusCode::TableColumnNotFound => ChunkFailureReaction::RetryPerSpan,
536            StatusCode::DatabaseNotFound => ChunkFailureReaction::DiscardChunk,
537            StatusCode::Cancelled | StatusCode::DeadlineExceeded => ChunkFailureReaction::Propagate,
538            StatusCode::StorageUnavailable
539            | StatusCode::RuntimeResourcesExhausted
540            | StatusCode::Internal
541            | StatusCode::RegionNotReady
542            | StatusCode::TableUnavailable
543            | StatusCode::RegionBusy => ChunkFailureReaction::Propagate,
544            _ => ChunkFailureReaction::DiscardChunk,
545        }
546    }
547
548    fn should_propagate_trace_span_failure(status: StatusCode) -> bool {
549        matches!(
550            Self::classify_trace_chunk_failure(status),
551            ChunkFailureReaction::Propagate
552        )
553    }
554
555    fn add_trace_write_cost(outcome: &mut TraceIngestOutcome, cost: usize) {
556        outcome.write_cost += cost;
557    }
558
559    fn push_trace_failure_message(messages: &mut Vec<String>, label: &str, message: String) {
560        OTLP_TRACES_FAILURE_COUNT.with_label_values(&[label]).inc();
561
562        if messages.len() < TRACE_FAILURE_MESSAGE_LIMIT {
563            messages.push(message);
564        } else if messages.len() == TRACE_FAILURE_MESSAGE_LIMIT {
565            tracing::debug!(
566                label,
567                limit = TRACE_FAILURE_MESSAGE_LIMIT,
568                "Trace ingest failure message limit reached; suppressing additional failure details"
569            );
570        }
571    }
572
573    fn finish_trace_failure_message(
574        accepted_spans: usize,
575        rejected_spans: usize,
576        messages: Vec<String>,
577    ) -> Option<String> {
578        if rejected_spans == 0 && messages.is_empty() {
579            return None;
580        }
581
582        let mut summary = format!(
583            "Accepted {} spans, rejected {} spans",
584            accepted_spans, rejected_spans
585        );
586
587        if !messages.is_empty() {
588            summary.push_str(": ");
589            summary.push_str(&messages.join("; "));
590        }
591
592        Some(summary)
593    }
594
595    /// Widen existing trace table columns to Float64 before request rewrite.
596    async fn alter_trace_table_columns_to_float64(
597        &self,
598        ctx: &QueryContextRef,
599        table_name: &str,
600        column_names: &[String],
601    ) -> ServerResult<()> {
602        let catalog_name = ctx.current_catalog().to_string();
603        let schema_name = ctx.current_schema();
604        let alter_expr = AlterTableExpr {
605            catalog_name: catalog_name.clone(),
606            schema_name: schema_name.clone(),
607            table_name: table_name.to_string(),
608            kind: Some(Kind::ModifyColumnTypes(ModifyColumnTypes {
609                modify_column_types: column_names
610                    .iter()
611                    .map(|column_name| ModifyColumnType {
612                        column_name: column_name.clone(),
613                        target_type: ColumnDataType::Float64 as i32,
614                        target_type_extension: None,
615                    })
616                    .collect(),
617            })),
618        };
619
620        if let Err(err) = self
621            .statement_executor
622            .alter_table_inner(alter_expr, ctx.clone())
623            .await
624        {
625            let table = self
626                .catalog_manager
627                .table(&catalog_name, &schema_name, table_name, None)
628                .await
629                .map_err(servers::error::Error::from)?;
630            let alter_already_applied = table
631                .map(|table| {
632                    let table_schema = table.schema();
633                    column_names.iter().all(|column_name| {
634                        table_schema
635                            .column_schema_by_name(column_name)
636                            .and_then(|table_col| {
637                                ColumnDataTypeWrapper::try_from(table_col.data_type.clone())
638                                    .ok()
639                                    .map(|wrapper| wrapper.datatype())
640                            })
641                            == Some(ColumnDataType::Float64)
642                    })
643                })
644                .unwrap_or(false);
645
646            if alter_already_applied {
647                return Ok(());
648            }
649
650            warn!(
651                table_name,
652                columns = ?column_names,
653                error = %err,
654                "failed to widen trace columns before insert"
655            );
656
657            return Err(wrap_trace_alter_failure(err));
658        }
659
660        Ok(())
661    }
662
663    /// Coerce request column types and values to match the existing table schema
664    /// for compatible type pairs. Existing table schema wins when present;
665    /// otherwise the full request batch decides a stable target type.
666    async fn reconcile_trace_column_types(
667        &self,
668        requests: &mut RowInsertRequests,
669        ctx: &QueryContextRef,
670    ) -> ServerResult<()> {
671        let catalog = ctx.current_catalog();
672        let schema = ctx.current_schema();
673
674        for req in &mut requests.inserts {
675            let table = self
676                .catalog_manager
677                .table(catalog, &schema, &req.table_name, None)
678                .await?;
679
680            let Some(rows) = req.rows.as_mut() else {
681                continue;
682            };
683
684            let table_schema = table.map(|table| table.schema());
685            let mut pending_rewrites = Vec::new();
686            let mut pending_alter_columns = Vec::new();
687
688            for (col_idx, col_schema) in rows.schema.iter().enumerate() {
689                let Some(current_type) = ColumnDataType::try_from(col_schema.datatype).ok() else {
690                    continue;
691                };
692
693                let mut observed_types = Vec::new();
694                push_observed_trace_type(&mut observed_types, current_type);
695
696                // Scan the full request first so the final type decision is not affected
697                // by row order inside the batch.
698                for row in &rows.rows {
699                    let Some(value) = row
700                        .values
701                        .get(col_idx)
702                        .and_then(|value| value.value_data.as_ref())
703                    else {
704                        continue;
705                    };
706
707                    let Some(value_type) = trace_value_datatype(value) else {
708                        continue;
709                    };
710                    push_observed_trace_type(&mut observed_types, value_type);
711                }
712
713                let existing_type = table_schema
714                    .as_ref()
715                    .and_then(|schema| schema.column_schema_by_name(&col_schema.column_name))
716                    .and_then(|table_col| {
717                        ColumnDataTypeWrapper::try_from(table_col.data_type.clone())
718                            .ok()
719                            .map(|wrapper| wrapper.datatype())
720                    });
721                let fixed_type = trace_semconv_fixed_type(&col_schema.column_name);
722
723                if !observed_types
724                    .iter()
725                    .copied()
726                    .any(is_trace_reconcile_candidate_type)
727                    && existing_type
728                        .map(|datatype| !is_trace_reconcile_candidate_type(datatype))
729                        .unwrap_or(true)
730                    && fixed_type.is_none()
731                {
732                    continue;
733                }
734
735                // Decide the final type once per column, then rewrite all affected cells
736                // together in one row pass below.
737                let Some(decision) = choose_trace_reconcile_decision(
738                    &col_schema.column_name,
739                    &observed_types,
740                    existing_type,
741                )
742                .map_err(|_| {
743                    enrich_trace_reconcile_error(
744                        &req.table_name,
745                        &col_schema.column_name,
746                        &observed_types,
747                        existing_type,
748                        fixed_type,
749                    )
750                })?
751                else {
752                    continue;
753                };
754                let target_type = decision.target_type();
755
756                if !decision.requires_alter()
757                    && observed_types
758                        .iter()
759                        .all(|observed| *observed == target_type)
760                    && col_schema.datatype == target_type as i32
761                {
762                    continue;
763                }
764
765                if decision.requires_alter()
766                    && !pending_alter_columns.contains(&col_schema.column_name)
767                {
768                    pending_alter_columns.push(col_schema.column_name.clone());
769                }
770
771                pending_rewrites.push(PendingTraceColumnRewrite {
772                    col_idx,
773                    target_type,
774                    column_name: col_schema.column_name.clone(),
775                });
776            }
777
778            if pending_rewrites.is_empty() {
779                continue;
780            }
781
782            validate_trace_column_rewrites(&rows.rows, &pending_rewrites, &req.table_name)?;
783
784            if !pending_alter_columns.is_empty() {
785                self.alter_trace_table_columns_to_float64(
786                    ctx,
787                    &req.table_name,
788                    &pending_alter_columns,
789                )
790                .await?;
791            }
792
793            // Update schema metadata before mutating row values so both stay in sync.
794            for pending_rewrite in &pending_rewrites {
795                rows.schema[pending_rewrite.col_idx].datatype = pending_rewrite.target_type as i32;
796            }
797
798            // Apply all pending column rewrites in one row pass.
799            for row in &mut rows.rows {
800                for pending_rewrite in &pending_rewrites {
801                    let Some(value) = row.values.get_mut(pending_rewrite.col_idx) else {
802                        continue;
803                    };
804                    let Some(request_type) =
805                        value.value_data.as_ref().and_then(trace_value_datatype)
806                    else {
807                        continue;
808                    };
809                    if request_type == pending_rewrite.target_type {
810                        continue;
811                    }
812
813                    value.value_data = coerce_value_data(
814                        &value.value_data,
815                        pending_rewrite.target_type,
816                        request_type,
817                    )
818                    .map_err(|_| {
819                        error::InvalidParameterSnafu {
820                            reason: format!(
821                                "failed to coerce trace column '{}' in table '{}' from {:?} to {:?}",
822                                pending_rewrite.column_name,
823                                req.table_name,
824                                request_type,
825                                pending_rewrite.target_type
826                            ),
827                        }
828                        .build()
829                    })?;
830                }
831            }
832        }
833
834        Ok(())
835    }
836}
837
838fn chunk_owned<T>(items: Vec<T>, chunk_size: usize) -> Vec<Vec<T>> {
839    if items.is_empty() {
840        return Vec::new();
841    }
842
843    if chunk_size == 0 {
844        return vec![items];
845    }
846
847    let mut chunks = Vec::with_capacity(items.len().div_ceil(chunk_size));
848    let mut iter = items.into_iter();
849    while iter.len() > 0 {
850        chunks.push(iter.by_ref().take(chunk_size).collect());
851    }
852    chunks
853}
854
855/// Preserve the original alter failure status so chunk retry behavior stays correct.
856fn wrap_trace_alter_failure<E>(err: E) -> servers::error::Error
857where
858    E: ErrorExt + Send + Sync + 'static,
859{
860    error::ExecuteGrpcQuerySnafu.into_error(BoxedError::new(err))
861}
862
863/// Derives `trace.conventions` from the request's resource/scope `schema_url`s.
864/// A single distinct non-empty value is concrete; multiple distinct values are
865/// `mixed`; none is `unknown`. `schema_url` is row-level in OTLP, so the
866/// table-level value is best-effort per the RFC conflict rule.
867fn trace_conventions(request: &ExportTraceServiceRequest) -> String {
868    let mut seen: Option<&str> = None;
869    let mut mixed = false;
870
871    for resource_spans in &request.resource_spans {
872        let urls = std::iter::once(resource_spans.schema_url.as_str()).chain(
873            resource_spans
874                .scope_spans
875                .iter()
876                .map(|s| s.schema_url.as_str()),
877        );
878        for url in urls {
879            if url.is_empty() {
880                continue;
881            }
882            match seen {
883                None => seen = Some(url),
884                Some(prev) if prev == url => {}
885                Some(_) => {
886                    mixed = true;
887                    break;
888                }
889            }
890        }
891        if mixed {
892            break;
893        }
894    }
895
896    if mixed {
897        SEMANTIC_VALUE_MIXED.to_string()
898    } else {
899        seen.map(str::to_string)
900            .unwrap_or_else(|| SEMANTIC_VALUE_UNKNOWN.to_string())
901    }
902}
903
904#[cfg(test)]
905mod tests {
906    use common_error::ext::ErrorExt;
907    use common_error::status_code::StatusCode;
908    use servers::query_handler::TraceIngestOutcome;
909
910    use super::{ChunkFailureReaction, Instance, chunk_owned, wrap_trace_alter_failure};
911    use crate::metrics::OTLP_TRACES_FAILURE_COUNT;
912
913    #[test]
914    fn test_chunk_owned() {
915        let chunks = chunk_owned(vec![1, 2, 3], 2);
916        assert_eq!(chunks.iter().map(Vec::len).collect::<Vec<_>>(), vec![2, 1]);
917
918        let chunks = chunk_owned(vec![1, 2, 3], 0);
919        assert_eq!(chunks.len(), 1);
920        assert_eq!(chunks[0].len(), 3);
921
922        assert!(chunk_owned::<i32>(Vec::new(), 0).is_empty());
923    }
924
925    #[test]
926    fn test_classify_trace_chunk_failure() {
927        assert_eq!(
928            Instance::classify_trace_chunk_failure(StatusCode::InvalidArguments),
929            ChunkFailureReaction::RetryPerSpan
930        );
931        assert_eq!(
932            Instance::classify_trace_chunk_failure(StatusCode::InvalidSyntax),
933            ChunkFailureReaction::RetryPerSpan
934        );
935        assert_eq!(
936            Instance::classify_trace_chunk_failure(StatusCode::Unsupported),
937            ChunkFailureReaction::RetryPerSpan
938        );
939        assert_eq!(
940            Instance::classify_trace_chunk_failure(StatusCode::TableColumnNotFound),
941            ChunkFailureReaction::RetryPerSpan
942        );
943        assert_eq!(
944            Instance::classify_trace_chunk_failure(StatusCode::TableNotFound),
945            ChunkFailureReaction::RetryPerSpan
946        );
947        assert_eq!(
948            Instance::classify_trace_chunk_failure(StatusCode::DatabaseNotFound),
949            ChunkFailureReaction::DiscardChunk
950        );
951        assert_eq!(
952            Instance::classify_trace_chunk_failure(StatusCode::DeadlineExceeded),
953            ChunkFailureReaction::Propagate
954        );
955        assert_eq!(
956            Instance::classify_trace_chunk_failure(StatusCode::Cancelled),
957            ChunkFailureReaction::Propagate
958        );
959        assert_eq!(
960            Instance::classify_trace_chunk_failure(StatusCode::StorageUnavailable),
961            ChunkFailureReaction::Propagate
962        );
963        assert_eq!(
964            Instance::classify_trace_chunk_failure(StatusCode::Internal),
965            ChunkFailureReaction::Propagate
966        );
967        assert_eq!(
968            Instance::classify_trace_chunk_failure(StatusCode::RegionNotReady),
969            ChunkFailureReaction::Propagate
970        );
971        assert_eq!(
972            Instance::classify_trace_chunk_failure(StatusCode::TableUnavailable),
973            ChunkFailureReaction::Propagate
974        );
975        assert_eq!(
976            Instance::classify_trace_chunk_failure(StatusCode::RegionBusy),
977            ChunkFailureReaction::Propagate
978        );
979        assert_eq!(
980            Instance::classify_trace_chunk_failure(StatusCode::RuntimeResourcesExhausted),
981            ChunkFailureReaction::Propagate
982        );
983    }
984
985    #[test]
986    fn test_classify_trace_span_failure() {
987        assert!(Instance::should_propagate_trace_span_failure(
988            StatusCode::DeadlineExceeded
989        ));
990        assert!(Instance::should_propagate_trace_span_failure(
991            StatusCode::StorageUnavailable
992        ));
993        assert!(!Instance::should_propagate_trace_span_failure(
994            StatusCode::InvalidArguments
995        ));
996    }
997
998    #[test]
999    fn test_add_trace_write_cost() {
1000        let mut outcome = TraceIngestOutcome::default();
1001        Instance::add_trace_write_cost(&mut outcome, 3);
1002        Instance::add_trace_write_cost(&mut outcome, 5);
1003        assert_eq!(outcome.write_cost, 8);
1004    }
1005
1006    #[test]
1007    fn test_finish_trace_failure_message() {
1008        let message = Instance::finish_trace_failure_message(
1009            3,
1010            2,
1011            vec!["Rejected span trace:span (InvalidArguments)".to_string()],
1012        )
1013        .unwrap();
1014        assert!(message.contains("Accepted 3 spans, rejected 2 spans"));
1015        assert!(message.contains("Rejected span trace:span"));
1016
1017        assert_eq!(Instance::finish_trace_failure_message(2, 0, vec![]), None);
1018    }
1019
1020    #[test]
1021    fn test_finish_trace_failure_message_without_detail_messages() {
1022        assert_eq!(
1023            Instance::finish_trace_failure_message(0, 2, vec![]),
1024            Some("Accepted 0 spans, rejected 2 spans".to_string())
1025        );
1026    }
1027
1028    #[test]
1029    fn test_push_trace_failure_message_increments_labeled_counter() {
1030        let label = "retry_per_span_counter_test";
1031        let initial = OTLP_TRACES_FAILURE_COUNT.with_label_values(&[label]).get();
1032        let mut messages = Vec::new();
1033
1034        Instance::push_trace_failure_message(
1035            &mut messages,
1036            label,
1037            "Chunk fallback triggered by InvalidArguments".to_string(),
1038        );
1039
1040        assert_eq!(messages.len(), 1);
1041        assert_eq!(
1042            OTLP_TRACES_FAILURE_COUNT.with_label_values(&[label]).get(),
1043            initial + 1
1044        );
1045    }
1046
1047    #[test]
1048    fn test_push_trace_failure_message_caps_recorded_messages() {
1049        let label = "retry_per_span_limit_test";
1050        let mut messages = Vec::new();
1051
1052        for idx in 0..=4 {
1053            Instance::push_trace_failure_message(&mut messages, label, format!("failure-{idx}"));
1054        }
1055
1056        assert_eq!(messages.len(), 4);
1057        assert_eq!(
1058            messages,
1059            vec![
1060                "failure-0".to_string(),
1061                "failure-1".to_string(),
1062                "failure-2".to_string(),
1063                "failure-3".to_string()
1064            ]
1065        );
1066    }
1067
1068    #[test]
1069    fn test_classify_trace_chunk_failure_defaults_to_discard() {
1070        assert_eq!(
1071            Instance::classify_trace_chunk_failure(StatusCode::Unknown),
1072            ChunkFailureReaction::DiscardChunk
1073        );
1074    }
1075
1076    #[test]
1077    fn test_wrap_trace_alter_failure_preserves_status_code() {
1078        let err = wrap_trace_alter_failure(
1079            servers::error::TableNotFoundSnafu {
1080                catalog: "greptime".to_string(),
1081                schema: "public".to_string(),
1082                table: "trace_type_missing".to_string(),
1083            }
1084            .build(),
1085        );
1086
1087        assert_eq!(err.status_code(), StatusCode::TableNotFound);
1088    }
1089
1090    use opentelemetry_proto::tonic::trace::v1::{ResourceSpans, ScopeSpans};
1091
1092    use super::{ExportTraceServiceRequest, trace_conventions};
1093
1094    fn resource_spans(resource_url: &str, scope_urls: &[&str]) -> ResourceSpans {
1095        ResourceSpans {
1096            schema_url: resource_url.to_string(),
1097            scope_spans: scope_urls
1098                .iter()
1099                .map(|u| ScopeSpans {
1100                    schema_url: u.to_string(),
1101                    ..Default::default()
1102                })
1103                .collect(),
1104            ..Default::default()
1105        }
1106    }
1107
1108    #[test]
1109    fn test_trace_conventions() {
1110        let unknown = ExportTraceServiceRequest::default();
1111        assert_eq!(trace_conventions(&unknown), "unknown");
1112
1113        let url = "https://opentelemetry.io/schemas/1.27.0";
1114        let single = ExportTraceServiceRequest {
1115            resource_spans: vec![resource_spans("", &[url, url])],
1116        };
1117        assert_eq!(trace_conventions(&single), url);
1118
1119        let resource_level = ExportTraceServiceRequest {
1120            resource_spans: vec![resource_spans(url, &[""])],
1121        };
1122        assert_eq!(trace_conventions(&resource_level), url);
1123
1124        let conflicting = ExportTraceServiceRequest {
1125            resource_spans: vec![resource_spans(
1126                "",
1127                &[url, "https://opentelemetry.io/schemas/1.30.0"],
1128            )],
1129        };
1130        assert_eq!(trace_conventions(&conflicting), "mixed");
1131    }
1132}