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