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
15mod trace_ingest;
16pub mod trace_semconv;
17pub mod trace_types;
18
19use std::collections::HashSet;
20use std::sync::Arc;
21
22use api::v1::RowInsertRequests;
23use api::v1::value::ValueData;
24use async_trait::async_trait;
25use auth::{
26    OTLP_WRITE, PermissionChecker, PermissionCheckerRef, PermissionReq, PermissionTableTarget,
27    PermissionTableTargets,
28};
29use client::Output;
30use common_catalog::consts::{
31    METRICS_EXEMPLARS_TABLE, trace_operations_table_name, trace_services_table_name,
32};
33use common_error::ext::BoxedError;
34use common_query::prelude::{EXEMPLAR_METRIC_COLUMN, GREPTIME_PHYSICAL_TABLE};
35use common_telemetry::{tracing, warn};
36use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest;
37use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
38use operator::insert::{Inserter, admit_row_insert_batches, admit_write};
39use otel_arrow_rust::proto::opentelemetry::collector::metrics::v1::ExportMetricsServiceRequest;
40use pipeline::{GreptimePipelineParams, PipelineWay};
41use servers::error::{self, AuthSnafu, Result as ServerResult};
42use servers::http::prom_store::PHYSICAL_TABLE_PARAM;
43use servers::interceptor::{OpenTelemetryProtocolInterceptor, OpenTelemetryProtocolInterceptorRef};
44use servers::otlp;
45use servers::otlp::trace::span::TraceSpanGroup;
46use servers::query_handler::{
47    MetricsIngestOutcome, OpenTelemetryProtocolHandler, PipelineHandlerRef, TraceIngestOutcome,
48};
49use servers::request_memory_limiter::ServerMemoryLimiter;
50use session::context::QueryContextRef;
51use session::protocol_ctx::OtlpMetricCtx;
52use session::write_progress::WriteProgress;
53use snafu::{IntoError, ResultExt};
54use table::requests::{
55    OTLP_METRIC_COMPAT_KEY, OTLP_METRIC_COMPAT_PROM, SEMANTIC_PER_TABLE_INDEX_KEY,
56    SEMANTIC_PIPELINE, SEMANTIC_SIGNAL_TYPE, SEMANTIC_SOURCE, SIGNAL_TYPE_LOG, SIGNAL_TYPE_METRIC,
57    SOURCE_OPENTELEMETRY, TABLE_DATA_MODEL, TABLE_DATA_MODEL_LOG_V1,
58};
59
60pub(crate) use self::trace_ingest::TraceAuxCache;
61use self::trace_ingest::trace_conventions;
62use crate::instance::Instance;
63use crate::metrics::{OTLP_LOGS_ROWS, OTLP_METRICS_ROWS, OTLP_RESOURCE_INFO_WRITE_ERRORS};
64
65fn trace_permission_targets(
66    table_name: &str,
67    groups: &[TraceSpanGroup],
68    ctx: &QueryContextRef,
69) -> PermissionTableTargets {
70    let catalog = ctx.current_catalog();
71    let schema = ctx.current_schema();
72    if catalog.is_empty() || schema.is_empty() || table_name.is_empty() {
73        return PermissionTableTargets::Unresolved;
74    }
75
76    let has_spans = groups.iter().any(|group| !group.spans.is_empty());
77    if !has_spans {
78        return PermissionTableTargets::resolved(Vec::new());
79    }
80
81    let mut targets = vec![PermissionTableTarget::new(catalog, &schema, table_name)];
82    if groups
83        .iter()
84        .flat_map(|group| &group.spans)
85        .any(|span| span.service_name.is_some())
86    {
87        targets.extend([
88            PermissionTableTarget::new(catalog, &schema, trace_services_table_name(table_name)),
89            PermissionTableTarget::new(catalog, &schema, trace_operations_table_name(table_name)),
90        ]);
91    }
92
93    PermissionTableTargets::resolved(targets)
94}
95
96#[async_trait]
97impl OpenTelemetryProtocolHandler for Instance {
98    #[tracing::instrument(skip_all)]
99    async fn metrics(
100        &self,
101        request: ExportMetricsServiceRequest,
102        ctx: QueryContextRef,
103        memory_limiter: Option<(ServerMemoryLimiter, u64)>,
104    ) -> ServerResult<MetricsIngestOutcome> {
105        self.plugins
106            .get::<PermissionCheckerRef>()
107            .as_ref()
108            .check_permission(ctx.current_user(), PermissionReq::Action(OTLP_WRITE))
109            .context(AuthSnafu)?;
110
111        let interceptor_ref = self
112            .plugins
113            .get::<OpenTelemetryProtocolInterceptorRef<servers::error::Error>>();
114        interceptor_ref.pre_execute(ctx.clone())?;
115        let ctx = Arc::new(ctx.fork());
116
117        let mut metric_ctx = ctx
118            .protocol_ctx()
119            .get_otlp_metric_ctx()
120            .cloned()
121            .unwrap_or_default();
122        metric_ctx.resource_info = self.otlp_resource_info;
123
124        let mut prepared = otlp::metrics::prepare_metrics(request, &mut metric_ctx)?;
125        // Authorize source tables before preflight can expose schemas or discard points.
126        let targets = {
127            let catalog = ctx.current_catalog();
128            let schema = ctx.current_schema();
129            let mut table_names = HashSet::new();
130            prepared
131                .requests
132                .iter()
133                .filter(|request| table_names.insert(request.table_name.as_str()))
134                .map(|request| PermissionTableTarget::new(catalog, &schema, &request.table_name))
135                .collect::<Vec<_>>()
136        };
137        self.check_table_permission(
138            &ctx,
139            PermissionReq::Action(OTLP_WRITE),
140            PermissionTableTargets::resolved(targets),
141        )
142        .context(AuthSnafu)?;
143
144        let preflight = self
145            .inserter
146            .validate_and_normalize_otlp_metrics(
147                &mut prepared.requests,
148                &ctx,
149                metric_ctx.with_metric_engine,
150                ctx.extension(PHYSICAL_TABLE_PARAM)
151                    .unwrap_or(GREPTIME_PHYSICAL_TABLE),
152            )
153            .await
154            .map_err(BoxedError::new)
155            .context(error::ExecuteGrpcQuerySnafu)?;
156        let rejections = prepared.point_rejections(preflight.rejections)?;
157        let otlp::metrics::MetricsConversion {
158            requests,
159            rows,
160            mut exemplars,
161            semantic_index,
162            resource_info,
163            mut outcome,
164        } = prepared.finish(rejections, &preflight.physical_tables)?;
165        if outcome.rejected_data_points > 0 {
166            warn!(
167                "Rejected {} OTLP metrics data points: {}",
168                outcome.rejected_data_points,
169                outcome.error_message.as_deref().unwrap_or_default()
170            );
171        }
172        if outcome.accepted_data_points == 0 {
173            return Ok(outcome);
174        }
175
176        // The engine option only controls new tables; existing ordinary sources cannot bind exemplars.
177        let before = exemplars.len();
178        exemplars.retain(|exemplar| {
179            preflight
180                .physical_tables
181                .contains_key(exemplar.metric.as_ref())
182        });
183        servers::metrics::EXEMPLARS_DROPPED_TOTAL
184            .with_label_values(&["otlp"])
185            .inc_by((before - exemplars.len()) as u64);
186        if !exemplars.is_empty() {
187            self.check_table_permission(
188                &ctx,
189                PermissionReq::Action(OTLP_WRITE),
190                PermissionTableTargets::resolved(vec![PermissionTableTarget::new(
191                    ctx.current_catalog(),
192                    ctx.current_schema(),
193                    METRICS_EXEMPLARS_TABLE,
194                )]),
195            )
196            .context(AuthSnafu)?;
197        }
198
199        let ctx = if !exemplars.is_empty()
200            && let Some((limiter, held_bytes)) = memory_limiter
201        {
202            let bytes =
203                servers::exemplar::estimated_row_bytes(std::iter::once(exemplars.as_slice()))
204                    .and_then(|bytes| u64::try_from(bytes).ok())
205                    .ok_or_else(|| {
206                        error::MemoryLimitExceededSnafu.into_error(
207                            common_memory_manager::Error::MemoryLimitExceeded {
208                                requested_bytes: u64::MAX,
209                                limit_bytes: limiter.limit_bytes(),
210                            },
211                        )
212                    })?;
213            limiter.check_request_fits(bytes, held_bytes)?;
214            // Holding body permits while waiting for expansion can stall concurrent requests.
215            let reservation = Arc::new(if held_bytes > 0 {
216                limiter.try_acquire(bytes).ok_or_else(|| {
217                    error::MemoryLimitExceededSnafu.into_error(
218                        common_memory_manager::Error::MemoryLimitExceeded {
219                            requested_bytes: bytes,
220                            limit_bytes: limiter.limit_bytes(),
221                        },
222                    )
223                })?
224            } else {
225                limiter.acquire(bytes).await?
226            });
227            let mut ctx = ctx.fork();
228            // Queued and detached writes retain the reservation after caller cancellation.
229            ctx.set_write_progress(Arc::new(WriteProgress::with_reservation(reservation)));
230            Arc::new(ctx)
231        } else {
232            ctx
233        };
234        let exemplar_rows = exemplars.len();
235        let (exemplars, _) = servers::exemplar::into_rows(exemplars)?;
236        let total_rows = rows.checked_add(exemplar_rows).ok_or_else(|| {
237            error::InvalidOtlpMetricInputSnafu {
238                reason: "combined metric and exemplar row count overflow",
239            }
240            .build()
241        })?;
242        let ctx = admit_write(total_rows as u64, &ctx)
243            .await
244            .map_err(BoxedError::new)
245            .context(error::ExecuteGrpcQuerySnafu)?;
246        OTLP_METRICS_ROWS.inc_by(rows as u64);
247
248        let ctx = {
249            let mut c = (*ctx).clone();
250            c.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_METRIC);
251            c.set_extension(SEMANTIC_SOURCE, SOURCE_OPENTELEMETRY);
252            // Per-table metric specifics + resource/scope lineage ride this
253            // internal channel; the auto-create path folds them per schema and
254            // table name.
255            if let Some(index) = semantic_index.encode(&c.current_schema()) {
256                c.set_extension(SEMANTIC_PER_TABLE_INDEX_KEY, index);
257            }
258            c.set_extension(OTLP_METRIC_COMPAT_KEY, OTLP_METRIC_COMPAT_PROM.to_string());
259            Arc::new(c)
260        };
261
262        if exemplar_rows > 0 {
263            let confirmed = Arc::new(ctx.with_confirmed_writes());
264            let source_names = exemplars
265                .inserts
266                .iter()
267                .filter_map(|request| request.rows.as_ref())
268                .flat_map(|rows| {
269                    let index = rows
270                        .schema
271                        .iter()
272                        .position(|c| c.column_name == EXEMPLAR_METRIC_COLUMN);
273                    rows.rows.iter().filter_map(move |row| {
274                        match &row.values.get(index?)?.value_data {
275                            Some(ValueData::StringValue(name)) => Some(name.as_str()),
276                            _ => None,
277                        }
278                    })
279                })
280                .collect::<HashSet<_>>();
281            let (source_requests, other): (Vec<_>, Vec<_>) = requests
282                .inserts
283                .into_iter()
284                .partition(|request| source_names.contains(request.table_name.as_str()));
285            let prepared = self
286                .inserter
287                .prepare_metric_row_inserts(
288                    RowInsertRequests {
289                        inserts: source_requests,
290                    },
291                    confirmed.clone(),
292                    &self.statement_executor,
293                    ctx.extension(PHYSICAL_TABLE_PARAM)
294                        .unwrap_or(GREPTIME_PHYSICAL_TABLE)
295                        .to_string(),
296                )
297                .await
298                .map_err(BoxedError::new)
299                .context(error::ExecuteGrpcQuerySnafu);
300            let source_ready = prepared.is_ok();
301            let sources = prepared
302                .as_ref()
303                .map(|request| request.source_tables().cloned().collect::<Vec<_>>())
304                .unwrap_or_default();
305            let (metric_output, other_output, exemplar_output) = tokio::join!(
306                async {
307                    self.inserter
308                        .handle_prepared_metric_inserts(prepared?, confirmed.clone())
309                        .await
310                        .map_err(BoxedError::new)
311                        .context(error::ExecuteGrpcQuerySnafu)
312                },
313                async {
314                    if other.is_empty() {
315                        Ok(Output::new_with_affected_rows(0))
316                    } else {
317                        self.handle_otlp_metric_row_inserts(
318                            RowInsertRequests { inserts: other },
319                            confirmed.clone(),
320                            &metric_ctx,
321                        )
322                        .await
323                    }
324                },
325                async {
326                    // Failed source preparation must not bind exemplars to a later replacement table.
327                    if !source_ready {
328                        return Ok(Output::new_with_affected_rows(0));
329                    }
330                    self.inserter
331                        .handle_exemplar_inserts(
332                            exemplars,
333                            confirmed.clone(),
334                            &self.statement_executor,
335                            &sources,
336                        )
337                        .await
338                        .map_err(BoxedError::new)
339                        .context(error::ExecuteGrpcQuerySnafu)
340                },
341            );
342            let mut progress = common_query::WriteProgress::default();
343            for output in [metric_output, other_output, exemplar_output] {
344                match output {
345                    Ok(output) => {
346                        outcome.write_cost += output.meta.cost;
347                        progress.merge(output.write_progress().await);
348                    }
349                    Err(error) => {
350                        progress.merge(common_query::WriteProgress::failed(BoxedError::new(error)))
351                    }
352                }
353            }
354            progress
355                .into_result()
356                .context(error::ExecuteGrpcQuerySnafu)?;
357        } else {
358            let output = self
359                .handle_otlp_metric_row_inserts(requests, ctx.clone(), &metric_ctx)
360                .await?;
361            outcome.write_cost = output.meta.cost;
362        }
363
364        // Derived enrichment follows the accepted metric submission, which may
365        // still be queued in asynchronous mode. Failures remain warning-only
366        // to avoid retrying metric data the server already accepted.
367        if let Some(resource_info) = resource_info {
368            let written = match self.check_row_insert_permission(
369                &resource_info,
370                &ctx,
371                PermissionReq::Action(OTLP_WRITE),
372            ) {
373                Ok(_) => self
374                    .handle_row_inserts(resource_info, ctx, false, false)
375                    .await
376                    .map_err(BoxedError::new)
377                    .map_err(|e| e.to_string()),
378                Err(e) => Err(e.to_string()),
379            };
380            match written {
381                Ok(descriptor_output) => outcome.write_cost += descriptor_output.meta.cost,
382                Err(e) => {
383                    OTLP_RESOURCE_INFO_WRITE_ERRORS.inc();
384                    warn!("Failed to write the OTLP resource descriptor table: {e}");
385                    outcome.error_message.get_or_insert(format!(
386                        "metric data was accepted, but writing the resource \
387                         descriptor table `{}` failed: {e}",
388                        otlp::metrics::OTEL_RESOURCE_INFO_TABLE_NAME
389                    ));
390                }
391            }
392        }
393
394        Ok(outcome)
395    }
396
397    #[tracing::instrument(skip_all)]
398    async fn traces(
399        &self,
400        _pipeline_handler: PipelineHandlerRef,
401        request: ExportTraceServiceRequest,
402        pipeline: PipelineWay,
403        _pipeline_params: GreptimePipelineParams,
404        table_name: String,
405        ctx: QueryContextRef,
406    ) -> ServerResult<TraceIngestOutcome> {
407        self.plugins
408            .get::<PermissionCheckerRef>()
409            .as_ref()
410            .check_permission(ctx.current_user(), PermissionReq::Action(OTLP_WRITE))
411            .context(AuthSnafu)?;
412
413        let interceptor_ref = self
414            .plugins
415            .get::<OpenTelemetryProtocolInterceptorRef<servers::error::Error>>();
416        interceptor_ref.pre_execute(ctx.clone())?;
417        let ctx = Arc::new(ctx.fork());
418
419        // `schema_url` is consumed by `parse`, so derive conventions first.
420        let conventions = trace_conventions(&request);
421        let spans = otlp::trace::span::parse(request);
422        let targets = trace_permission_targets(&table_name, &spans, &ctx);
423        self.check_table_permission(&ctx, PermissionReq::Action(OTLP_WRITE), targets)
424            .context(AuthSnafu)?;
425        // Count the external spans once, before chunking, retries, or derived tables.
426        let rows = spans.iter().map(|group| group.spans.len() as u64).sum();
427        let ctx = admit_write(rows, &ctx)
428            .await
429            .map_err(BoxedError::new)
430            .context(error::ExecuteGrpcQuerySnafu)?;
431        self.ingest_trace_spans(&pipeline, table_name, spans, &conventions, ctx)
432            .await
433    }
434
435    #[tracing::instrument(skip_all)]
436    async fn logs(
437        &self,
438        pipeline_handler: PipelineHandlerRef,
439        request: ExportLogsServiceRequest,
440        pipeline: PipelineWay,
441        pipeline_params: GreptimePipelineParams,
442        table_name: String,
443        ctx: QueryContextRef,
444    ) -> ServerResult<Vec<Output>> {
445        self.plugins
446            .get::<PermissionCheckerRef>()
447            .as_ref()
448            .check_permission(ctx.current_user(), PermissionReq::Action(OTLP_WRITE))
449            .context(AuthSnafu)?;
450
451        let interceptor_ref = self
452            .plugins
453            .get::<OpenTelemetryProtocolInterceptorRef<servers::error::Error>>();
454        interceptor_ref.pre_execute(ctx.clone())?;
455        let ctx = Arc::new(ctx.fork());
456
457        // `as_req_iter` clones this ctx into each `temp_ctx`, so identity set here
458        // reaches the context that drives table auto-create.
459        let ctx = {
460            let mut c = (*ctx).clone();
461            c.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_LOG);
462            c.set_extension(SEMANTIC_SOURCE, SOURCE_OPENTELEMETRY);
463            // Only applied when the table is auto-created. The v1 rows are built only
464            // when the table didn't exist at read time; pipeline-defined tables have no
465            // built-in schema.
466            if matches!(pipeline, PipelineWay::OtlpLogDirect(_)) {
467                c.set_extension(TABLE_DATA_MODEL, TABLE_DATA_MODEL_LOG_V1);
468                c.set_extension(SEMANTIC_PIPELINE, TABLE_DATA_MODEL_LOG_V1);
469                // TODO: declare the `service` entity on `service_name` once the cost of
470                // scanning log tables during entity-graph derivation is measured.
471            }
472            Arc::new(c)
473        };
474
475        let opt_req = otlp::logs::to_grpc_insert_requests(
476            request,
477            pipeline,
478            pipeline_params,
479            table_name,
480            &ctx,
481            pipeline_handler,
482        )
483        .await?;
484
485        let mut batches = opt_req.as_req_iter(ctx).collect::<Vec<_>>();
486        for (temp_ctx, requests) in &batches {
487            self.check_row_insert_permission(requests, temp_ctx, PermissionReq::Action(OTLP_WRITE))
488                .context(AuthSnafu)?;
489        }
490        admit_row_insert_batches(&mut batches)
491            .await
492            .map_err(BoxedError::new)
493            .context(error::ExecuteGrpcQuerySnafu)?;
494
495        let mut outputs = Vec::with_capacity(batches.len());
496        for (temp_ctx, requests) in batches {
497            let cnt = requests
498                .inserts
499                .iter()
500                .filter_map(|r| r.rows.as_ref().map(|r| r.rows.len()))
501                .sum::<usize>();
502
503            let o = self
504                .handle_log_inserts(requests, temp_ctx)
505                .await
506                .inspect(|_| OTLP_LOGS_ROWS.inc_by(cnt as u64))
507                .map_err(BoxedError::new)
508                .context(error::ExecuteGrpcQuerySnafu)?;
509            outputs.push(o);
510        }
511
512        Ok(outputs)
513    }
514}
515
516impl Instance {
517    /// Inserts converted OTLP metrics through the eligible batching or ordinary write path.
518    /// The caller must check table permissions and admit the complete request first.
519    pub async fn handle_otlp_metric_row_inserts(
520        &self,
521        requests: RowInsertRequests,
522        ctx: QueryContextRef,
523        metric_ctx: &OtlpMetricCtx,
524    ) -> ServerResult<Output> {
525        let physical_table = ctx
526            .extension(PHYSICAL_TABLE_PARAM)
527            .unwrap_or(GREPTIME_PHYSICAL_TABLE)
528            .to_string();
529        // The bulk path converts each request's time index unit to the
530        // destination table's unit during batch alignment, so no pre-gate
531        // alignment is needed here.
532        let batcher = self
533            .logical_batcher()
534            .filter(|_| ctx.logical_batching_enabled() && metric_ctx.with_metric_engine);
535        let batcher = if batcher.is_some()
536            && self
537                .inserter
538                .can_batch_metric_rows(&requests, &ctx, &physical_table)
539                .await
540                .map_err(BoxedError::new)
541                .context(error::ExecuteGrpcQuerySnafu)?
542        {
543            batcher
544        } else {
545            None
546        };
547
548        // OTLP tables have one sample field in both the ordinary and physical paths.
549        if let Some(batcher) = batcher {
550            let (mut output, cost) = batcher
551                .submit_output_with(requests, ctx.clone(), |mut requests| {
552                    let ctx = ctx.clone();
553                    async move {
554                        Inserter::meter_row_inserts(&mut requests, &ctx)
555                            .await
556                            .map_err(BoxedError::new)
557                            .context(error::ExecuteGrpcQuerySnafu)
558                    }
559                })
560                .await?;
561            output.meta.cost = cost as _;
562            Ok(output)
563        } else if !metric_ctx.with_metric_engine {
564            self.handle_row_inserts(requests, ctx.clone(), false, true)
565                .await
566                .map_err(BoxedError::new)
567                .context(error::ExecuteGrpcQuerySnafu)
568        } else {
569            self.handle_metric_row_inserts(requests, ctx.clone(), physical_table)
570                .await
571                .map_err(BoxedError::new)
572                .context(error::ExecuteGrpcQuerySnafu)
573        }
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use std::sync::Arc;
580
581    use auth::{PermissionTableTarget, PermissionTableTargets};
582    use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
583    use opentelemetry_proto::tonic::common::v1::{AnyValue, KeyValue, any_value};
584    use opentelemetry_proto::tonic::resource::v1::Resource;
585    use opentelemetry_proto::tonic::trace::v1::{ResourceSpans, ScopeSpans, Span};
586    use session::context::QueryContext;
587
588    use super::trace_permission_targets;
589
590    #[test]
591    fn test_trace_permission_targets() {
592        let request = ExportTraceServiceRequest {
593            resource_spans: vec![ResourceSpans {
594                resource: Some(Resource {
595                    attributes: vec![KeyValue {
596                        key: "service.name".to_string(),
597                        value: Some(AnyValue {
598                            value: Some(any_value::Value::StringValue("frontend".to_string())),
599                        }),
600                        ..Default::default()
601                    }],
602                    ..Default::default()
603                }),
604                scope_spans: vec![ScopeSpans {
605                    spans: vec![Span::default()],
606                    ..Default::default()
607                }],
608                ..Default::default()
609            }],
610        };
611        let groups = servers::otlp::trace::span::parse(request);
612        let ctx = Arc::new(QueryContext::with("greptime", "public"));
613
614        assert_eq!(
615            PermissionTableTargets::Resolved(vec![
616                PermissionTableTarget::new("greptime", "public", "traces"),
617                PermissionTableTarget::new("greptime", "public", "traces_services"),
618                PermissionTableTarget::new("greptime", "public", "traces_operations"),
619            ]),
620            trace_permission_targets("traces", &groups, &ctx)
621        );
622        assert_eq!(
623            PermissionTableTargets::Unresolved,
624            trace_permission_targets("", &groups, &ctx)
625        );
626    }
627}