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#[configurable_component]
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[serde(rename_all = "snake_case")]
22pub enum OtlpSignalType {
23 Logs,
25 Metrics,
27 Traces,
29}
30
31#[configurable_component]
33#[derive(Debug, Clone)]
34pub struct OtlpDeserializerConfig {
35 #[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 pub fn build(&self) -> OtlpDeserializer {
66 OtlpDeserializer::new_with_signals(self.signal_types.clone())
67 }
68
69 pub fn output_type(&self) -> DataType {
71 DataType::Log | DataType::Trace
72 }
73
74 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#[derive(Debug, Clone)]
104pub struct OtlpDeserializer {
105 logs_deserializer: ProtobufDeserializer,
106 metrics_deserializer: ProtobufDeserializer,
107 traces_deserializer: ProtobufDeserializer,
108 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 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 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 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 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 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 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 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 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 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 let deserializer =
430 OtlpDeserializer::new_with_signals(IndexSet::from([OtlpSignalType::Traces]));
431
432 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 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 let deserializer = OtlpDeserializer::default();
452 let trace_bytes = create_traces_request_bytes();
453
454 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 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 assert!(trace.get(event_path!(RESOURCE_SPANS_JSON_FIELD)).is_some());
476 validate_trace_ids(trace.value());
477 }
478}