Skip to main content

codecs/decoding/format/
otlp.rs

1use bytes::Bytes;
2use opentelemetry_proto::proto::{
3    DESCRIPTOR_BYTES, LOGS_REQUEST_MESSAGE_TYPE, METRICS_REQUEST_MESSAGE_TYPE,
4    RESOURCE_LOGS_JSON_FIELD, RESOURCE_METRICS_JSON_FIELD, RESOURCE_SPANS_JSON_FIELD,
5    TRACES_REQUEST_MESSAGE_TYPE,
6};
7use smallvec::{SmallVec, smallvec};
8use vector_config::{configurable_component, indexmap::IndexSet};
9use vector_core::{
10    config::{DataType, LogNamespace},
11    event::Event,
12    schema,
13};
14use vrl::{event_path, protobuf::parse::Options, value::Kind};
15
16use super::{Deserializer, ProtobufDeserializer};
17
18/// OTLP signal type for prioritized parsing.
19#[configurable_component]
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[serde(rename_all = "snake_case")]
22pub enum OtlpSignalType {
23    /// OTLP logs signal (ExportLogsServiceRequest)
24    Logs,
25    /// OTLP metrics signal (ExportMetricsServiceRequest)
26    Metrics,
27    /// OTLP traces signal (ExportTraceServiceRequest)
28    Traces,
29}
30
31/// Config used to build an `OtlpDeserializer`.
32#[configurable_component]
33#[derive(Debug, Clone)]
34pub struct OtlpDeserializerConfig {
35    /// Signal types to attempt parsing, in priority order.
36    ///
37    /// The deserializer tries to parse signals in the specified order. This allows you to optimize
38    /// performance when you know the expected signal types. For example, if you only receive
39    /// traces, set this to `["traces"]` to avoid attempting to parse as logs or metrics first.
40    ///
41    /// If not specified, defaults to trying all types in this order: logs, metrics, traces.
42    /// Duplicate signal types are automatically removed while preserving order.
43    #[serde(default = "default_signal_types")]
44    pub signal_types: IndexSet<OtlpSignalType>,
45}
46
47fn default_signal_types() -> IndexSet<OtlpSignalType> {
48    IndexSet::from([
49        OtlpSignalType::Logs,
50        OtlpSignalType::Metrics,
51        OtlpSignalType::Traces,
52    ])
53}
54
55impl Default for OtlpDeserializerConfig {
56    fn default() -> Self {
57        Self {
58            signal_types: default_signal_types(),
59        }
60    }
61}
62
63impl OtlpDeserializerConfig {
64    /// Build the `OtlpDeserializer` from this configuration.
65    pub fn build(&self) -> OtlpDeserializer {
66        OtlpDeserializer::new_with_signals(self.signal_types.clone())
67    }
68
69    /// Return the type of event build by this deserializer.
70    pub fn output_type(&self) -> DataType {
71        DataType::Log | DataType::Trace
72    }
73
74    /// The schema produced by the deserializer.
75    pub fn schema_definition(&self, log_namespace: LogNamespace) -> schema::Definition {
76        match log_namespace {
77            LogNamespace::Legacy => {
78                schema::Definition::empty_legacy_namespace().unknown_fields(Kind::any())
79            }
80            LogNamespace::Vector => {
81                schema::Definition::new_with_default_metadata(Kind::any(), [log_namespace])
82            }
83        }
84    }
85}
86
87/// Deserializer that builds `Event`s from a byte frame containing [OTLP](https://opentelemetry.io/docs/specs/otlp/) protobuf data.
88///
89/// This deserializer decodes events using the OTLP protobuf specification. It handles the three
90/// OTLP signal types: logs, metrics, and traces.
91///
92/// The implementation supports three OTLP message types:
93/// - `ExportLogsServiceRequest` → Log events with `resourceLogs` field
94/// - `ExportMetricsServiceRequest` → Log events with `resourceMetrics` field
95/// - `ExportTraceServiceRequest` → Trace events with `resourceSpans` field
96///
97/// One major caveat here is that the incoming metrics will be parsed as logs but they will preserve the OTLP format.
98/// This means that components that work on metrics, will not be compatible with this output.
99/// However, these events can be forwarded directly to a downstream OTEL collector.
100///
101/// This is the inverse of what the OTLP encoder does, ensuring round-trip compatibility
102/// with the `opentelemetry` source when `use_otlp_decoding` is enabled.
103#[derive(Debug, Clone)]
104pub struct OtlpDeserializer {
105    logs_deserializer: ProtobufDeserializer,
106    metrics_deserializer: ProtobufDeserializer,
107    traces_deserializer: ProtobufDeserializer,
108    /// Signal types to parse, in priority order
109    signals: IndexSet<OtlpSignalType>,
110}
111
112impl Default for OtlpDeserializer {
113    fn default() -> Self {
114        Self::new_with_signals(default_signal_types())
115    }
116}
117
118impl OtlpDeserializer {
119    /// Creates a new OTLP deserializer with custom signal support.
120    /// During parsing, each signal type is tried in order until one succeeds.
121    pub fn new_with_signals(signals: IndexSet<OtlpSignalType>) -> Self {
122        let options = Options {
123            use_json_names: true,
124        };
125
126        let logs_deserializer = ProtobufDeserializer::new_from_bytes(
127            DESCRIPTOR_BYTES,
128            LOGS_REQUEST_MESSAGE_TYPE,
129            options.clone(),
130        )
131        .expect("Failed to create logs deserializer");
132
133        let metrics_deserializer = ProtobufDeserializer::new_from_bytes(
134            DESCRIPTOR_BYTES,
135            METRICS_REQUEST_MESSAGE_TYPE,
136            options.clone(),
137        )
138        .expect("Failed to create metrics deserializer");
139
140        let traces_deserializer = ProtobufDeserializer::new_from_bytes(
141            DESCRIPTOR_BYTES,
142            TRACES_REQUEST_MESSAGE_TYPE,
143            options,
144        )
145        .expect("Failed to create traces deserializer");
146
147        Self {
148            logs_deserializer,
149            metrics_deserializer,
150            traces_deserializer,
151            signals,
152        }
153    }
154}
155
156impl Deserializer for OtlpDeserializer {
157    fn parse(
158        &self,
159        bytes: Bytes,
160        log_namespace: LogNamespace,
161    ) -> vector_common::Result<SmallVec<[Event; 1]>> {
162        // Try parsing in the priority order specified
163        for signal_type in &self.signals {
164            match signal_type {
165                OtlpSignalType::Logs => {
166                    if let Ok(events) = self.logs_deserializer.parse(bytes.clone(), log_namespace)
167                        && let Some(Event::Log(log)) = events.first()
168                        && log.get(event_path!(RESOURCE_LOGS_JSON_FIELD)).is_some()
169                    {
170                        return Ok(events);
171                    }
172                }
173                OtlpSignalType::Metrics => {
174                    if let Ok(events) = self
175                        .metrics_deserializer
176                        .parse(bytes.clone(), log_namespace)
177                        && let Some(Event::Log(log)) = events.first()
178                        && log.get(event_path!(RESOURCE_METRICS_JSON_FIELD)).is_some()
179                    {
180                        return Ok(events);
181                    }
182                }
183                OtlpSignalType::Traces => {
184                    // Always use LogNamespace::Vector for traces to avoid spurious timestamp injection.
185                    // The log_namespace concept is logs-specific and doesn't apply to trace events.
186                    // See: https://github.com/vectordotdev/vector/issues/25045
187                    if let Ok(mut events) = self
188                        .traces_deserializer
189                        .parse(bytes.clone(), LogNamespace::Vector)
190                        && let Some(Event::Log(log)) = events.first()
191                        && log.get(event_path!(RESOURCE_SPANS_JSON_FIELD)).is_some()
192                    {
193                        // Convert the log event to a trace event by taking ownership
194                        if let Some(Event::Log(log)) = events.pop() {
195                            let trace_event = Event::Trace(log.into());
196                            return Ok(smallvec![trace_event]);
197                        }
198                    }
199                }
200            }
201        }
202
203        Err(format!("Invalid OTLP data: expected one of {:?}", self.signals).into())
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use opentelemetry_proto::proto::{
210        collector::{
211            logs::v1::ExportLogsServiceRequest, metrics::v1::ExportMetricsServiceRequest,
212            trace::v1::ExportTraceServiceRequest,
213        },
214        logs::v1::{LogRecord, ResourceLogs, ScopeLogs},
215        metrics::v1::{Metric, ResourceMetrics, ScopeMetrics},
216        resource::v1::Resource,
217        trace::v1::{ResourceSpans, ScopeSpans, Span},
218    };
219    use prost::Message;
220    use vrl::path;
221
222    use super::*;
223
224    // trace_id: 0102030405060708090a0b0c0d0e0f10 (16 bytes)
225    const TEST_TRACE_ID: [u8; 16] = [
226        0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
227        0x10,
228    ];
229    // span_id: 0102030405060708 (8 bytes)
230    const TEST_SPAN_ID: [u8; 8] = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
231
232    fn create_logs_request_bytes() -> Bytes {
233        let request = ExportLogsServiceRequest {
234            resource_logs: vec![ResourceLogs {
235                resource: Some(Resource {
236                    attributes: vec![],
237                    dropped_attributes_count: 0,
238                }),
239                scope_logs: vec![ScopeLogs {
240                    scope: None,
241                    log_records: vec![LogRecord {
242                        time_unix_nano: 1234567890,
243                        severity_number: 9,
244                        severity_text: "INFO".to_string(),
245                        body: None,
246                        attributes: vec![],
247                        dropped_attributes_count: 0,
248                        flags: 0,
249                        trace_id: vec![],
250                        span_id: vec![],
251                        observed_time_unix_nano: 0,
252                    }],
253                    schema_url: String::new(),
254                }],
255                schema_url: String::new(),
256            }],
257        };
258
259        Bytes::from(request.encode_to_vec())
260    }
261
262    fn create_metrics_request_bytes() -> Bytes {
263        let request = ExportMetricsServiceRequest {
264            resource_metrics: vec![ResourceMetrics {
265                resource: Some(Resource {
266                    attributes: vec![],
267                    dropped_attributes_count: 0,
268                }),
269                scope_metrics: vec![ScopeMetrics {
270                    scope: None,
271                    metrics: vec![Metric {
272                        name: "test_metric".to_string(),
273                        description: String::new(),
274                        unit: String::new(),
275                        data: None,
276                    }],
277                    schema_url: String::new(),
278                }],
279                schema_url: String::new(),
280            }],
281        };
282
283        Bytes::from(request.encode_to_vec())
284    }
285
286    fn create_traces_request_bytes() -> Bytes {
287        let request = ExportTraceServiceRequest {
288            resource_spans: vec![ResourceSpans {
289                resource: Some(Resource {
290                    attributes: vec![],
291                    dropped_attributes_count: 0,
292                }),
293                scope_spans: vec![ScopeSpans {
294                    scope: None,
295                    spans: vec![Span {
296                        trace_id: TEST_TRACE_ID.to_vec(),
297                        span_id: TEST_SPAN_ID.to_vec(),
298                        trace_state: String::new(),
299                        parent_span_id: vec![],
300                        name: "test_span".to_string(),
301                        kind: 0,
302                        start_time_unix_nano: 1234567890,
303                        end_time_unix_nano: 1234567900,
304                        attributes: vec![],
305                        dropped_attributes_count: 0,
306                        events: vec![],
307                        dropped_events_count: 0,
308                        links: vec![],
309                        dropped_links_count: 0,
310                        status: None,
311                    }],
312                    schema_url: String::new(),
313                }],
314                schema_url: String::new(),
315            }],
316        };
317
318        Bytes::from(request.encode_to_vec())
319    }
320
321    fn validate_trace_ids(trace: &vrl::value::Value) {
322        // Navigate to the span and check traceId and spanId
323        let resource_spans = trace
324            .get(path!("resourceSpans"))
325            .and_then(|v| v.as_array())
326            .expect("resourceSpans should be an array");
327
328        let first_rs = resource_spans
329            .first()
330            .expect("should have at least one resource span");
331
332        let scope_spans = first_rs
333            .get(path!("scopeSpans"))
334            .and_then(|v| v.as_array())
335            .expect("scopeSpans should be an array");
336
337        let first_ss = scope_spans
338            .first()
339            .expect("should have at least one scope span");
340
341        let spans = first_ss
342            .get(path!("spans"))
343            .and_then(|v| v.as_array())
344            .expect("spans should be an array");
345
346        let span = spans.first().expect("should have at least one span");
347
348        // Verify traceId - should be raw bytes (16 bytes for trace_id)
349        let trace_id = span
350            .get(path!("traceId"))
351            .and_then(|v| v.as_bytes())
352            .expect("traceId should exist and be bytes");
353
354        assert_eq!(
355            trace_id.as_ref(),
356            &TEST_TRACE_ID,
357            "traceId should match the expected 16 bytes (0102030405060708090a0b0c0d0e0f10)"
358        );
359
360        // Verify spanId - should be raw bytes (8 bytes for span_id)
361        let span_id = span
362            .get(path!("spanId"))
363            .and_then(|v| v.as_bytes())
364            .expect("spanId should exist and be bytes");
365
366        assert_eq!(
367            span_id.as_ref(),
368            &TEST_SPAN_ID,
369            "spanId should match the expected 8 bytes (0102030405060708)"
370        );
371    }
372
373    fn assert_otlp_event(bytes: Bytes, field: &str, is_trace: bool) {
374        let deserializer = OtlpDeserializer::default();
375        let events = deserializer.parse(bytes, LogNamespace::Legacy).unwrap();
376
377        assert_eq!(events.len(), 1);
378        if is_trace {
379            assert!(matches!(events[0], Event::Trace(_)));
380            let trace = events[0].as_trace();
381            assert!(trace.get(event_path!(field)).is_some());
382            validate_trace_ids(trace.value());
383        } else {
384            assert!(events[0].as_log().get(event_path!(field)).is_some());
385        }
386    }
387
388    #[test]
389    fn deserialize_otlp_logs() {
390        assert_otlp_event(create_logs_request_bytes(), RESOURCE_LOGS_JSON_FIELD, false);
391    }
392
393    #[test]
394    fn deserialize_otlp_metrics() {
395        assert_otlp_event(
396            create_metrics_request_bytes(),
397            RESOURCE_METRICS_JSON_FIELD,
398            false,
399        );
400    }
401
402    #[test]
403    fn deserialize_otlp_traces() {
404        assert_otlp_event(
405            create_traces_request_bytes(),
406            RESOURCE_SPANS_JSON_FIELD,
407            true,
408        );
409    }
410
411    #[test]
412    fn deserialize_invalid_otlp() {
413        let deserializer = OtlpDeserializer::default();
414        let bytes = Bytes::from("invalid protobuf data");
415        let result = deserializer.parse(bytes, LogNamespace::Legacy);
416
417        assert!(result.is_err());
418        assert!(
419            result
420                .unwrap_err()
421                .to_string()
422                .contains("Invalid OTLP data")
423        );
424    }
425
426    #[test]
427    fn deserialize_with_custom_priority_traces_only() {
428        // Configure to only try traces - should succeed for traces, fail for others
429        let deserializer =
430            OtlpDeserializer::new_with_signals(IndexSet::from([OtlpSignalType::Traces]));
431
432        // Traces should work
433        let trace_bytes = create_traces_request_bytes();
434        let result = deserializer.parse(trace_bytes, LogNamespace::Legacy);
435        assert!(result.is_ok());
436        assert!(matches!(result.unwrap()[0], Event::Trace(_)));
437
438        // Logs should fail since we're not trying to parse logs
439        let log_bytes = create_logs_request_bytes();
440        let result = deserializer.parse(log_bytes, LogNamespace::Legacy);
441        assert!(result.is_err());
442    }
443
444    #[test]
445    fn deserialize_traces_with_legacy_namespace_should_not_inject_timestamp() {
446        use vector_core::config::log_schema;
447
448        // This test verifies the fix for issue #25045
449        // When log_namespace is Legacy, the ProtobufDeserializer injects a timestamp
450        // into log events, but this behavior should NOT apply to trace events.
451        let deserializer = OtlpDeserializer::default();
452        let trace_bytes = create_traces_request_bytes();
453
454        // Parse with Legacy namespace
455        let events = deserializer
456            .parse(trace_bytes, LogNamespace::Legacy)
457            .unwrap();
458
459        assert_eq!(events.len(), 1);
460        assert!(matches!(events[0], Event::Trace(_)));
461
462        let trace = events[0].as_trace();
463
464        // Verify that no spurious timestamp was injected
465        // The timestamp field should not exist at the root level
466        if let Some(timestamp_key) = log_schema().timestamp_key_target_path() {
467            assert!(
468                trace.get(timestamp_key).is_none(),
469                "Trace event should not have spurious timestamp field '{}' injected when using LogNamespace::Legacy",
470                timestamp_key
471            );
472        }
473
474        // The trace should still have the OTLP trace data
475        assert!(trace.get(event_path!(RESOURCE_SPANS_JSON_FIELD)).is_some());
476        validate_trace_ids(trace.value());
477    }
478}