Skip to main content

vector/sources/opentelemetry/
config.rs

1use std::net::SocketAddr;
2
3use crate::{
4    config::{
5        DataType, GenerateConfig, Resource, SourceAcknowledgementsConfig, SourceConfig,
6        SourceContext, SourceOutput,
7    },
8    http::KeepaliveConfig,
9    serde::bool_or_struct,
10    sources::{
11        Source,
12        http_server::{build_param_matcher, remove_duplicates},
13        opentelemetry::{
14            grpc::Service,
15            http::{build_warp_filter, run_http_server},
16        },
17        util::{
18            decompression::max_decompressed_size_bytes,
19            grpc::{GrpcKeepaliveConfig, run_grpc_server_with_routes},
20        },
21    },
22};
23use futures::FutureExt;
24use futures_util::{TryFutureExt, future::join};
25use tonic::transport::server::RoutesBuilder;
26use vector_config::indexmap::IndexSet;
27use vector_lib::{
28    codecs::decoding::{OtlpDeserializer, OtlpSignalType},
29    config::{LegacyKey, LogNamespace, log_schema},
30    configurable::configurable_component,
31    internal_event::{BytesReceived, EventsReceived, Protocol},
32    lookup::{OwnedTargetPath, owned_value_path},
33    opentelemetry::{
34        logs::{
35            ATTRIBUTES_KEY, DROPPED_ATTRIBUTES_COUNT_KEY, FLAGS_KEY, OBSERVED_TIMESTAMP_KEY,
36            RESOURCE_KEY, SEVERITY_NUMBER_KEY, SEVERITY_TEXT_KEY, SPAN_ID_KEY, TRACE_ID_KEY,
37        },
38        proto::collector::{
39            logs::v1::logs_service_server::LogsServiceServer,
40            metrics::v1::metrics_service_server::MetricsServiceServer,
41            trace::v1::trace_service_server::TraceServiceServer,
42        },
43    },
44    schema::Definition,
45    tls::{MaybeTlsSettings, TlsEnableableConfig},
46};
47use vrl::value::{Kind, kind::Collection};
48
49pub const LOGS: &str = "logs";
50pub const METRICS: &str = "metrics";
51pub const TRACES: &str = "traces";
52
53/// Configuration for OTLP decoding behavior.
54#[configurable_component]
55#[derive(Clone, Debug, Default, PartialEq, Eq)]
56#[serde(deny_unknown_fields)]
57pub struct OtlpDecodingConfig {
58    /// Whether to use OTLP decoding for logs.
59    ///
60    /// When `true`, logs preserve their OTLP format.
61    /// When `false` (default), logs are converted to Vector's native format.
62    #[serde(default)]
63    pub logs: bool,
64
65    /// Whether to use OTLP decoding for metrics.
66    ///
67    /// When `true`, metrics preserve their OTLP format but are processed as logs.
68    /// When `false` (default), metrics are converted to Vector's native metric format.
69    #[serde(default)]
70    pub metrics: bool,
71
72    /// Whether to use OTLP decoding for traces.
73    ///
74    /// When `true`, traces preserve their OTLP format.
75    /// When `false` (default), traces are converted to Vector's native format.
76    #[serde(default)]
77    pub traces: bool,
78}
79
80impl From<bool> for OtlpDecodingConfig {
81    /// Converts a boolean value to an OtlpDecodingConfig.
82    ///
83    /// This provides backward compatibility with the previous boolean configuration.
84    /// - `true` enables OTLP decoding for all signals
85    /// - `false` disables OTLP decoding for all signals (uses Vector native format)
86    fn from(value: bool) -> Self {
87        Self {
88            logs: value,
89            metrics: value,
90            traces: value,
91        }
92    }
93}
94
95impl OtlpDecodingConfig {
96    /// Returns true if any signal is configured to use OTLP decoding.
97    pub const fn any_enabled(&self) -> bool {
98        self.logs || self.metrics || self.traces
99    }
100
101    /// Returns true if all signals are configured to use OTLP decoding.
102    pub const fn all_enabled(&self) -> bool {
103        self.logs && self.metrics && self.traces
104    }
105
106    /// Returns true if signals have mixed configuration (some enabled, some disabled).
107    pub const fn is_mixed(&self) -> bool {
108        self.any_enabled() && !self.all_enabled()
109    }
110}
111
112/// Configuration for the `opentelemetry` source.
113#[configurable_component(source("opentelemetry", "Receive OTLP data through gRPC or HTTP."))]
114#[derive(Clone, Debug)]
115#[serde(deny_unknown_fields)]
116pub struct OpentelemetryConfig {
117    #[configurable(derived)]
118    pub grpc: GrpcConfig,
119
120    #[configurable(derived)]
121    pub http: HttpConfig,
122
123    #[configurable(derived)]
124    #[serde(default, deserialize_with = "bool_or_struct")]
125    pub acknowledgements: SourceAcknowledgementsConfig,
126
127    /// The namespace to use for logs. This overrides the global setting.
128    #[configurable(metadata(docs::hidden))]
129    #[serde(default)]
130    pub log_namespace: Option<bool>,
131
132    /// Configuration for OTLP decoding behavior.
133    ///
134    /// This configuration controls how OpenTelemetry Protocol (OTLP) data is decoded for each
135    /// signal type (logs, metrics, traces). When a signal is configured to use OTLP decoding, the raw OTLP format is
136    /// preserved, allowing the data to be forwarded to downstream OTLP collectors without transformation.
137    /// Otherwise, the signal is converted to Vector's native event format.
138    ///
139    /// Simple boolean form:
140    ///
141    /// ```yaml
142    /// use_otlp_decoding: true  # All signals preserve OTLP format
143    /// # or
144    /// use_otlp_decoding: false # All signals use Vector native format (default)
145    /// ```
146    ///
147    /// Per-signal configuration:
148    ///
149    /// ```yaml
150    /// use_otlp_decoding:
151    ///   logs: false     # Convert to Vector native format
152    ///   metrics: false  # Convert to Vector native format
153    ///   traces: true    # Preserve OTLP format
154    /// ```
155    ///
156    /// **Note:** When OTLP decoding is enabled for metrics:
157    /// - Metrics are parsed as logs while preserving the OTLP format
158    /// - Vector's metric transforms will NOT be compatible with this output
159    /// - The events can be forwarded directly (passthrough) to a downstream OTLP collector
160    #[serde(default, deserialize_with = "bool_or_struct")]
161    pub use_otlp_decoding: OtlpDecodingConfig,
162}
163
164/// Configuration for the `opentelemetry` gRPC server.
165#[configurable_component]
166#[configurable(metadata(docs::examples = "example_grpc_config()"))]
167#[derive(Clone, Debug)]
168#[serde(deny_unknown_fields)]
169pub struct GrpcConfig {
170    /// The socket address to listen for connections on.
171    ///
172    /// It _must_ include a port.
173    #[configurable(metadata(docs::examples = "0.0.0.0:4317", docs::examples = "localhost:4317"))]
174    pub address: SocketAddr,
175
176    #[configurable(derived)]
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub tls: Option<TlsEnableableConfig>,
179
180    #[configurable(derived)]
181    #[serde(default)]
182    pub keepalive: GrpcKeepaliveConfig,
183}
184
185fn example_grpc_config() -> GrpcConfig {
186    GrpcConfig {
187        address: "0.0.0.0:4317".parse().unwrap(),
188        tls: None,
189        keepalive: GrpcKeepaliveConfig::default(),
190    }
191}
192
193/// Configuration for the `opentelemetry` HTTP server.
194#[configurable_component]
195#[configurable(metadata(docs::examples = "example_http_config()"))]
196#[derive(Clone, Debug)]
197#[serde(deny_unknown_fields)]
198pub struct HttpConfig {
199    /// The socket address to listen for connections on.
200    ///
201    /// It _must_ include a port.
202    #[configurable(metadata(docs::examples = "0.0.0.0:4318", docs::examples = "localhost:4318"))]
203    pub address: SocketAddr,
204
205    #[configurable(derived)]
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub tls: Option<TlsEnableableConfig>,
208
209    #[configurable(derived)]
210    #[serde(default)]
211    pub keepalive: KeepaliveConfig,
212
213    /// A list of HTTP headers to include in the event.
214    ///
215    /// Accepts the wildcard (`*`) character for headers matching a specified pattern.
216    ///
217    /// Specifying "*" results in all headers included in the event.
218    ///
219    /// For log events in legacy namespace mode, headers are not included if a field with a conflicting name exists.
220    /// For metrics and traces, headers are always added to event metadata.
221    #[serde(default)]
222    #[configurable(metadata(docs::examples = "User-Agent"))]
223    #[configurable(metadata(docs::examples = "X-My-Custom-Header"))]
224    #[configurable(metadata(docs::examples = "X-*"))]
225    #[configurable(metadata(docs::examples = "*"))]
226    pub headers: Vec<String>,
227}
228
229fn example_http_config() -> HttpConfig {
230    HttpConfig {
231        address: "0.0.0.0:4318".parse().unwrap(),
232        tls: None,
233        keepalive: KeepaliveConfig::default(),
234        headers: vec![],
235    }
236}
237
238impl GenerateConfig for OpentelemetryConfig {
239    fn generate_config() -> toml::Value {
240        toml::Value::try_from(Self {
241            grpc: example_grpc_config(),
242            http: example_http_config(),
243            acknowledgements: Default::default(),
244            log_namespace: None,
245            use_otlp_decoding: OtlpDecodingConfig::default(),
246        })
247        .unwrap()
248    }
249}
250
251impl OpentelemetryConfig {
252    pub(crate) fn get_signal_deserializer(
253        &self,
254        signal_type: OtlpSignalType,
255    ) -> vector_common::Result<Option<OtlpDeserializer>> {
256        let should_use_otlp = match signal_type {
257            OtlpSignalType::Logs => self.use_otlp_decoding.logs,
258            OtlpSignalType::Metrics => self.use_otlp_decoding.metrics,
259            OtlpSignalType::Traces => self.use_otlp_decoding.traces,
260        };
261
262        if should_use_otlp {
263            Ok(Some(OtlpDeserializer::new_with_signals(IndexSet::from([
264                signal_type,
265            ]))))
266        } else {
267            Ok(None)
268        }
269    }
270}
271
272#[async_trait::async_trait]
273#[typetag::serde(name = "opentelemetry")]
274impl SourceConfig for OpentelemetryConfig {
275    async fn build(&self, cx: SourceContext) -> crate::Result<Source> {
276        let acknowledgements = cx.do_acknowledgements(self.acknowledgements);
277        let events_received = register!(EventsReceived);
278        let log_namespace = cx.log_namespace(self.log_namespace);
279
280        let grpc_tls_settings = MaybeTlsSettings::from_config(self.grpc.tls.as_ref(), true)?;
281
282        // Log info message when using mixed OTLP decoding formats
283        if self.use_otlp_decoding.is_mixed() {
284            info!(
285                message = "Signals with OTLP decoding enabled will preserve raw format; others will use Vector native format.",
286                logs_otlp = self.use_otlp_decoding.logs,
287                metrics_otlp = self.use_otlp_decoding.metrics,
288                traces_otlp = self.use_otlp_decoding.traces,
289            );
290        }
291
292        let logs_deserializer = self.get_signal_deserializer(OtlpSignalType::Logs)?;
293        let metrics_deserializer = self.get_signal_deserializer(OtlpSignalType::Metrics)?;
294        let traces_deserializer = self.get_signal_deserializer(OtlpSignalType::Traces)?;
295
296        // Compression negotiation (gzip, zstd) is handled centrally by
297        // `DecompressionAndMetricsLayer` in `sources::util::grpc`, so these
298        // services deliberately do not call `.accept_compressed(..)`.
299        let log_service = LogsServiceServer::new(Service {
300            pipeline: cx.out.clone(),
301            acknowledgements,
302            log_namespace,
303            events_received: events_received.clone(),
304            deserializer: logs_deserializer.clone(),
305        })
306        .max_decoding_message_size(max_decompressed_size_bytes());
307
308        let metrics_service = MetricsServiceServer::new(Service {
309            pipeline: cx.out.clone(),
310            acknowledgements,
311            log_namespace,
312            events_received: events_received.clone(),
313            deserializer: metrics_deserializer.clone(),
314        })
315        .max_decoding_message_size(max_decompressed_size_bytes());
316
317        let trace_service = TraceServiceServer::new(Service {
318            pipeline: cx.out.clone(),
319            acknowledgements,
320            log_namespace,
321            events_received: events_received.clone(),
322            deserializer: traces_deserializer.clone(),
323        })
324        .max_decoding_message_size(max_decompressed_size_bytes());
325
326        let mut builder = RoutesBuilder::default();
327        builder
328            .add_service(log_service)
329            .add_service(metrics_service)
330            .add_service(trace_service);
331
332        let grpc_source = run_grpc_server_with_routes(
333            self.grpc.address,
334            grpc_tls_settings,
335            builder.routes(),
336            self.grpc.keepalive.clone(),
337            cx.shutdown.clone(),
338        )
339        .map_err(|error| {
340            error!(message = "OpenTelemetry source gRPC server failed.", %error);
341        });
342
343        let http_tls_settings = MaybeTlsSettings::from_config(self.http.tls.as_ref(), true)?;
344        let protocol = http_tls_settings.http_protocol_name();
345        let bytes_received = register!(BytesReceived::from(Protocol::from(protocol)));
346        let headers =
347            build_param_matcher(&remove_duplicates(self.http.headers.clone(), "headers"))?;
348
349        let filters = build_warp_filter(
350            acknowledgements,
351            log_namespace,
352            cx.out,
353            bytes_received,
354            events_received,
355            headers,
356            logs_deserializer,
357            metrics_deserializer,
358            traces_deserializer,
359        );
360
361        let http_source = run_http_server(
362            self.http.address,
363            http_tls_settings,
364            filters,
365            cx.shutdown,
366            self.http.keepalive.clone(),
367        )
368        .map_err(|error| {
369            error!(message = "OpenTelemetry source HTTP server failed.", %error);
370        });
371
372        Ok(join(grpc_source, http_source).map(|_| Ok(())).boxed())
373    }
374
375    // TODO: appropriately handle "severity" meaning across both "severity_text" and "severity_number",
376    // as both are optional and can be converted to/from.
377    fn outputs(&self, global_log_namespace: LogNamespace) -> Vec<SourceOutput> {
378        let log_namespace = global_log_namespace.merge(self.log_namespace);
379        let schema_definition = Definition::new_with_default_metadata(Kind::any(), [log_namespace])
380            .with_source_metadata(
381                Self::NAME,
382                Some(LegacyKey::Overwrite(owned_value_path!(RESOURCE_KEY))),
383                &owned_value_path!(RESOURCE_KEY),
384                Kind::object(Collection::from_unknown(Kind::any())).or_undefined(),
385                None,
386            )
387            .with_source_metadata(
388                Self::NAME,
389                Some(LegacyKey::Overwrite(owned_value_path!(ATTRIBUTES_KEY))),
390                &owned_value_path!(ATTRIBUTES_KEY),
391                Kind::object(Collection::from_unknown(Kind::any())).or_undefined(),
392                None,
393            )
394            .with_source_metadata(
395                Self::NAME,
396                Some(LegacyKey::Overwrite(owned_value_path!(TRACE_ID_KEY))),
397                &owned_value_path!(TRACE_ID_KEY),
398                Kind::bytes().or_undefined(),
399                None,
400            )
401            .with_source_metadata(
402                Self::NAME,
403                Some(LegacyKey::Overwrite(owned_value_path!(SPAN_ID_KEY))),
404                &owned_value_path!(SPAN_ID_KEY),
405                Kind::bytes().or_undefined(),
406                None,
407            )
408            .with_source_metadata(
409                Self::NAME,
410                Some(LegacyKey::Overwrite(owned_value_path!(SEVERITY_TEXT_KEY))),
411                &owned_value_path!(SEVERITY_TEXT_KEY),
412                Kind::bytes().or_undefined(),
413                Some("severity"),
414            )
415            .with_source_metadata(
416                Self::NAME,
417                Some(LegacyKey::Overwrite(owned_value_path!(SEVERITY_NUMBER_KEY))),
418                &owned_value_path!(SEVERITY_NUMBER_KEY),
419                Kind::integer().or_undefined(),
420                None,
421            )
422            .with_source_metadata(
423                Self::NAME,
424                Some(LegacyKey::Overwrite(owned_value_path!(FLAGS_KEY))),
425                &owned_value_path!(FLAGS_KEY),
426                Kind::integer().or_undefined(),
427                None,
428            )
429            .with_source_metadata(
430                Self::NAME,
431                Some(LegacyKey::Overwrite(owned_value_path!(
432                    DROPPED_ATTRIBUTES_COUNT_KEY
433                ))),
434                &owned_value_path!(DROPPED_ATTRIBUTES_COUNT_KEY),
435                Kind::integer(),
436                None,
437            )
438            .with_source_metadata(
439                Self::NAME,
440                Some(LegacyKey::Overwrite(owned_value_path!(
441                    OBSERVED_TIMESTAMP_KEY
442                ))),
443                &owned_value_path!(OBSERVED_TIMESTAMP_KEY),
444                Kind::timestamp(),
445                None,
446            )
447            .with_source_metadata(
448                Self::NAME,
449                None,
450                &owned_value_path!("timestamp"),
451                Kind::timestamp(),
452                Some("timestamp"),
453            )
454            .with_standard_vector_source_metadata();
455
456        let schema_definition = match log_namespace {
457            LogNamespace::Vector => {
458                schema_definition.with_meaning(OwnedTargetPath::event_root(), "message")
459            }
460            LogNamespace::Legacy => {
461                schema_definition.with_meaning(log_schema().owned_message_path(), "message")
462            }
463        };
464
465        let logs_output = if self.use_otlp_decoding.logs {
466            SourceOutput::new_maybe_logs(DataType::Log, Definition::any()).with_port(LOGS)
467        } else {
468            SourceOutput::new_maybe_logs(DataType::Log, schema_definition).with_port(LOGS)
469        };
470
471        let metrics_output = if self.use_otlp_decoding.metrics {
472            SourceOutput::new_maybe_logs(DataType::Log, Definition::any()).with_port(METRICS)
473        } else {
474            SourceOutput::new_metrics().with_port(METRICS)
475        };
476
477        vec![
478            logs_output,
479            metrics_output,
480            SourceOutput::new_traces().with_port(TRACES),
481        ]
482    }
483
484    fn resources(&self) -> Vec<Resource> {
485        vec![
486            Resource::tcp(self.grpc.address),
487            Resource::tcp(self.http.address),
488        ]
489    }
490
491    fn can_acknowledge(&self) -> bool {
492        true
493    }
494}