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 async_trait::async_trait;
24use auth::{
25    OTLP_WRITE, PermissionChecker, PermissionCheckerRef, PermissionReq, PermissionTableTarget,
26    PermissionTableTargets,
27};
28use client::Output;
29use common_catalog::consts::{trace_operations_table_name, trace_services_table_name};
30use common_error::ext::BoxedError;
31use common_query::prelude::GREPTIME_PHYSICAL_TABLE;
32use common_query::{OutputData, OutputMeta};
33use common_telemetry::{tracing, warn};
34use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest;
35use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
36use operator::insert::{Inserter, admit_row_insert_batches, admit_write};
37use otel_arrow_rust::proto::opentelemetry::collector::metrics::v1::ExportMetricsServiceRequest;
38use pipeline::{GreptimePipelineParams, PipelineWay};
39use servers::error::{self, AuthSnafu, Result as ServerResult};
40use servers::http::prom_store::PHYSICAL_TABLE_PARAM;
41use servers::interceptor::{OpenTelemetryProtocolInterceptor, OpenTelemetryProtocolInterceptorRef};
42use servers::otlp;
43use servers::otlp::trace::span::TraceSpanGroup;
44use servers::query_handler::{
45    MetricsIngestOutcome, OpenTelemetryProtocolHandler, PipelineHandlerRef, TraceIngestOutcome,
46};
47use session::context::QueryContextRef;
48use session::protocol_ctx::OtlpMetricCtx;
49use snafu::ResultExt;
50use table::requests::{
51    OTLP_METRIC_COMPAT_KEY, OTLP_METRIC_COMPAT_PROM, SEMANTIC_PER_TABLE_INDEX_KEY,
52    SEMANTIC_SIGNAL_TYPE, SEMANTIC_SOURCE, SIGNAL_TYPE_LOG, SIGNAL_TYPE_METRIC,
53    SOURCE_OPENTELEMETRY,
54};
55
56pub(crate) use self::trace_ingest::TraceAuxCache;
57use self::trace_ingest::trace_conventions;
58use crate::instance::Instance;
59use crate::metrics::{OTLP_LOGS_ROWS, OTLP_METRICS_ROWS, OTLP_RESOURCE_INFO_WRITE_ERRORS};
60
61fn trace_permission_targets(
62    table_name: &str,
63    groups: &[TraceSpanGroup],
64    ctx: &QueryContextRef,
65) -> PermissionTableTargets {
66    let catalog = ctx.current_catalog();
67    let schema = ctx.current_schema();
68    if catalog.is_empty() || schema.is_empty() || table_name.is_empty() {
69        return PermissionTableTargets::Unresolved;
70    }
71
72    let has_spans = groups.iter().any(|group| !group.spans.is_empty());
73    if !has_spans {
74        return PermissionTableTargets::resolved(Vec::new());
75    }
76
77    let mut targets = vec![PermissionTableTarget::new(catalog, &schema, table_name)];
78    if groups
79        .iter()
80        .flat_map(|group| &group.spans)
81        .any(|span| span.service_name.is_some())
82    {
83        targets.extend([
84            PermissionTableTarget::new(catalog, &schema, trace_services_table_name(table_name)),
85            PermissionTableTarget::new(catalog, &schema, trace_operations_table_name(table_name)),
86        ]);
87    }
88
89    PermissionTableTargets::resolved(targets)
90}
91
92#[async_trait]
93impl OpenTelemetryProtocolHandler for Instance {
94    #[tracing::instrument(skip_all)]
95    async fn metrics(
96        &self,
97        request: ExportMetricsServiceRequest,
98        ctx: QueryContextRef,
99    ) -> ServerResult<MetricsIngestOutcome> {
100        self.plugins
101            .get::<PermissionCheckerRef>()
102            .as_ref()
103            .check_permission(ctx.current_user(), PermissionReq::Action(OTLP_WRITE))
104            .context(AuthSnafu)?;
105
106        let interceptor_ref = self
107            .plugins
108            .get::<OpenTelemetryProtocolInterceptorRef<servers::error::Error>>();
109        interceptor_ref.pre_execute(ctx.clone())?;
110        let ctx = Arc::new(ctx.fork());
111
112        let mut metric_ctx = ctx
113            .protocol_ctx()
114            .get_otlp_metric_ctx()
115            .cloned()
116            .unwrap_or_default();
117        metric_ctx.resource_info = self.otlp_resource_info;
118
119        let mut prepared = otlp::metrics::prepare_metrics(request, &mut metric_ctx)?;
120        // Check all destinations before preflight can expose schemas or discard points.
121        let targets = {
122            let catalog = ctx.current_catalog();
123            let schema = ctx.current_schema();
124            let mut table_names = HashSet::new();
125            prepared
126                .requests
127                .iter()
128                .filter(|request| table_names.insert(request.table_name.as_str()))
129                .map(|request| PermissionTableTarget::new(catalog, &schema, &request.table_name))
130                .collect()
131        };
132        self.check_table_permission(
133            &ctx,
134            PermissionReq::Action(OTLP_WRITE),
135            PermissionTableTargets::resolved(targets),
136        )
137        .context(AuthSnafu)?;
138
139        let preflight = self
140            .inserter
141            .validate_and_normalize_otlp_metrics(
142                &mut prepared.requests,
143                &ctx,
144                metric_ctx.with_metric_engine,
145                ctx.extension(PHYSICAL_TABLE_PARAM)
146                    .unwrap_or(GREPTIME_PHYSICAL_TABLE),
147            )
148            .await
149            .map_err(BoxedError::new)
150            .context(error::ExecuteGrpcQuerySnafu)?;
151        let rejections = prepared.point_rejections(preflight.rejections)?;
152        let otlp::metrics::MetricsConversion {
153            requests,
154            rows,
155            semantic_index,
156            resource_info,
157            mut outcome,
158        } = prepared.finish(rejections, &preflight.physical_tables)?;
159        if outcome.rejected_data_points > 0 {
160            warn!(
161                "Rejected {} OTLP metrics data points: {}",
162                outcome.rejected_data_points,
163                outcome.error_message.as_deref().unwrap_or_default()
164            );
165        }
166        if outcome.accepted_data_points == 0 {
167            return Ok(outcome);
168        }
169
170        let ctx = admit_write(rows as u64, &ctx)
171            .await
172            .map_err(BoxedError::new)
173            .context(error::ExecuteGrpcQuerySnafu)?;
174        OTLP_METRICS_ROWS.inc_by(rows as u64);
175
176        let ctx = {
177            let mut c = (*ctx).clone();
178            c.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_METRIC);
179            c.set_extension(SEMANTIC_SOURCE, SOURCE_OPENTELEMETRY);
180            // Per-table metric specifics + resource/scope lineage ride this
181            // internal channel; the auto-create path folds them per schema and
182            // table name.
183            if let Some(index) = semantic_index.encode(&c.current_schema()) {
184                c.set_extension(SEMANTIC_PER_TABLE_INDEX_KEY, index);
185            }
186            c.set_extension(OTLP_METRIC_COMPAT_KEY, OTLP_METRIC_COMPAT_PROM.to_string());
187            Arc::new(c)
188        };
189
190        let output = self
191            .handle_otlp_metric_row_inserts(requests, ctx.clone(), &metric_ctx)
192            .await?;
193        outcome.write_cost = output.meta.cost;
194
195        // Derived enrichment follows the accepted metric submission, which may
196        // still be queued in asynchronous mode. Failures remain warning-only
197        // to avoid retrying metric data the server already accepted.
198        if let Some(resource_info) = resource_info {
199            let written = match self.check_row_insert_permission(
200                &resource_info,
201                &ctx,
202                PermissionReq::Action(OTLP_WRITE),
203            ) {
204                Ok(_) => self
205                    .handle_row_inserts(resource_info, ctx, false, false)
206                    .await
207                    .map_err(BoxedError::new)
208                    .map_err(|e| e.to_string()),
209                Err(e) => Err(e.to_string()),
210            };
211            match written {
212                Ok(descriptor_output) => outcome.write_cost += descriptor_output.meta.cost,
213                Err(e) => {
214                    OTLP_RESOURCE_INFO_WRITE_ERRORS.inc();
215                    warn!("Failed to write the OTLP resource descriptor table: {e}");
216                    outcome.error_message.get_or_insert(format!(
217                        "metric data was accepted, but writing the resource \
218                         descriptor table `{}` failed: {e}",
219                        otlp::metrics::OTEL_RESOURCE_INFO_TABLE_NAME
220                    ));
221                }
222            }
223        }
224
225        Ok(outcome)
226    }
227
228    #[tracing::instrument(skip_all)]
229    async fn traces(
230        &self,
231        _pipeline_handler: PipelineHandlerRef,
232        request: ExportTraceServiceRequest,
233        pipeline: PipelineWay,
234        _pipeline_params: GreptimePipelineParams,
235        table_name: String,
236        ctx: QueryContextRef,
237    ) -> ServerResult<TraceIngestOutcome> {
238        self.plugins
239            .get::<PermissionCheckerRef>()
240            .as_ref()
241            .check_permission(ctx.current_user(), PermissionReq::Action(OTLP_WRITE))
242            .context(AuthSnafu)?;
243
244        let interceptor_ref = self
245            .plugins
246            .get::<OpenTelemetryProtocolInterceptorRef<servers::error::Error>>();
247        interceptor_ref.pre_execute(ctx.clone())?;
248        let ctx = Arc::new(ctx.fork());
249
250        // `schema_url` is consumed by `parse`, so derive conventions first.
251        let conventions = trace_conventions(&request);
252        let spans = otlp::trace::span::parse(request);
253        let targets = trace_permission_targets(&table_name, &spans, &ctx);
254        self.check_table_permission(&ctx, PermissionReq::Action(OTLP_WRITE), targets)
255            .context(AuthSnafu)?;
256        // Count the external spans once, before chunking, retries, or derived tables.
257        let rows = spans.iter().map(|group| group.spans.len() as u64).sum();
258        let ctx = admit_write(rows, &ctx)
259            .await
260            .map_err(BoxedError::new)
261            .context(error::ExecuteGrpcQuerySnafu)?;
262        self.ingest_trace_spans(&pipeline, table_name, spans, &conventions, ctx)
263            .await
264    }
265
266    #[tracing::instrument(skip_all)]
267    async fn logs(
268        &self,
269        pipeline_handler: PipelineHandlerRef,
270        request: ExportLogsServiceRequest,
271        pipeline: PipelineWay,
272        pipeline_params: GreptimePipelineParams,
273        table_name: String,
274        ctx: QueryContextRef,
275    ) -> ServerResult<Vec<Output>> {
276        self.plugins
277            .get::<PermissionCheckerRef>()
278            .as_ref()
279            .check_permission(ctx.current_user(), PermissionReq::Action(OTLP_WRITE))
280            .context(AuthSnafu)?;
281
282        let interceptor_ref = self
283            .plugins
284            .get::<OpenTelemetryProtocolInterceptorRef<servers::error::Error>>();
285        interceptor_ref.pre_execute(ctx.clone())?;
286        let ctx = Arc::new(ctx.fork());
287
288        // `as_req_iter` clones this ctx into each `temp_ctx`, so identity set here
289        // reaches the context that drives table auto-create.
290        let ctx = {
291            let mut c = (*ctx).clone();
292            c.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_LOG);
293            c.set_extension(SEMANTIC_SOURCE, SOURCE_OPENTELEMETRY);
294            Arc::new(c)
295        };
296
297        let opt_req = otlp::logs::to_grpc_insert_requests(
298            request,
299            pipeline,
300            pipeline_params,
301            table_name,
302            &ctx,
303            pipeline_handler,
304        )
305        .await?;
306
307        let mut batches = opt_req.as_req_iter(ctx).collect::<Vec<_>>();
308        for (temp_ctx, requests) in &batches {
309            self.check_row_insert_permission(requests, temp_ctx, PermissionReq::Action(OTLP_WRITE))
310                .context(AuthSnafu)?;
311        }
312        admit_row_insert_batches(&mut batches)
313            .await
314            .map_err(BoxedError::new)
315            .context(error::ExecuteGrpcQuerySnafu)?;
316
317        let mut outputs = Vec::with_capacity(batches.len());
318        for (temp_ctx, requests) in batches {
319            let cnt = requests
320                .inserts
321                .iter()
322                .filter_map(|r| r.rows.as_ref().map(|r| r.rows.len()))
323                .sum::<usize>();
324
325            let o = self
326                .handle_log_inserts(requests, temp_ctx)
327                .await
328                .inspect(|_| OTLP_LOGS_ROWS.inc_by(cnt as u64))
329                .map_err(BoxedError::new)
330                .context(error::ExecuteGrpcQuerySnafu)?;
331            outputs.push(o);
332        }
333
334        Ok(outputs)
335    }
336}
337
338impl Instance {
339    /// Inserts converted OTLP metrics through the eligible batching or ordinary write path.
340    /// The caller must check table permissions and admit the complete request first.
341    pub async fn handle_otlp_metric_row_inserts(
342        &self,
343        requests: RowInsertRequests,
344        ctx: QueryContextRef,
345        metric_ctx: &OtlpMetricCtx,
346    ) -> ServerResult<Output> {
347        let physical_table = ctx
348            .extension(PHYSICAL_TABLE_PARAM)
349            .unwrap_or(GREPTIME_PHYSICAL_TABLE)
350            .to_string();
351        // The bulk path converts each request's time index unit to the
352        // destination table's unit during batch alignment, so no pre-gate
353        // alignment is needed here.
354        let batcher = self
355            .logical_batcher()
356            .filter(|_| ctx.logical_batching_enabled() && metric_ctx.with_metric_engine);
357        let batcher = if batcher.is_some()
358            && self
359                .inserter
360                .can_batch_metric_rows(&requests, &ctx, &physical_table)
361                .await
362                .map_err(BoxedError::new)
363                .context(error::ExecuteGrpcQuerySnafu)?
364        {
365            batcher
366        } else {
367            None
368        };
369
370        // OTLP tables have one sample field in both the ordinary and physical paths.
371        if let Some(batcher) = batcher {
372            let (rows, cost) = batcher
373                .submit_with(requests, ctx.clone(), |mut requests| {
374                    let ctx = ctx.clone();
375                    async move {
376                        Inserter::meter_row_inserts(&mut requests, &ctx)
377                            .await
378                            .map_err(BoxedError::new)
379                            .context(error::ExecuteGrpcQuerySnafu)
380                    }
381                })
382                .await?;
383            Ok(Output::new(
384                OutputData::AffectedRows(rows as usize),
385                OutputMeta::new_with_cost(cost as _),
386            ))
387        } else if !metric_ctx.with_metric_engine {
388            self.handle_row_inserts(requests, ctx.clone(), false, true)
389                .await
390                .map_err(BoxedError::new)
391                .context(error::ExecuteGrpcQuerySnafu)
392        } else {
393            self.handle_metric_row_inserts(requests, ctx.clone(), physical_table)
394                .await
395                .map_err(BoxedError::new)
396                .context(error::ExecuteGrpcQuerySnafu)
397        }
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use std::sync::Arc;
404
405    use auth::{PermissionTableTarget, PermissionTableTargets};
406    use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
407    use opentelemetry_proto::tonic::common::v1::{AnyValue, KeyValue, any_value};
408    use opentelemetry_proto::tonic::resource::v1::Resource;
409    use opentelemetry_proto::tonic::trace::v1::{ResourceSpans, ScopeSpans, Span};
410    use session::context::QueryContext;
411
412    use super::trace_permission_targets;
413
414    #[test]
415    fn test_trace_permission_targets() {
416        let request = ExportTraceServiceRequest {
417            resource_spans: vec![ResourceSpans {
418                resource: Some(Resource {
419                    attributes: vec![KeyValue {
420                        key: "service.name".to_string(),
421                        value: Some(AnyValue {
422                            value: Some(any_value::Value::StringValue("frontend".to_string())),
423                        }),
424                        ..Default::default()
425                    }],
426                    ..Default::default()
427                }),
428                scope_spans: vec![ScopeSpans {
429                    spans: vec![Span::default()],
430                    ..Default::default()
431                }],
432                ..Default::default()
433            }],
434        };
435        let groups = servers::otlp::trace::span::parse(request);
436        let ctx = Arc::new(QueryContext::with("greptime", "public"));
437
438        assert_eq!(
439            PermissionTableTargets::Resolved(vec![
440                PermissionTableTarget::new("greptime", "public", "traces"),
441                PermissionTableTarget::new("greptime", "public", "traces_services"),
442                PermissionTableTarget::new("greptime", "public", "traces_operations"),
443            ]),
444            trace_permission_targets("traces", &groups, &ctx)
445        );
446        assert_eq!(
447            PermissionTableTargets::Unresolved,
448            trace_permission_targets("", &groups, &ctx)
449        );
450    }
451}