Skip to main content

vector/sources/datadog_agent/
mod.rs

1#[cfg(all(test, feature = "datadog-agent-integration-tests"))]
2mod integration_tests;
3#[cfg(test)]
4mod tests;
5
6pub mod llmobs;
7pub mod logs;
8pub mod metrics;
9pub mod traces;
10
11#[allow(warnings, clippy::pedantic, clippy::nursery)]
12pub(crate) mod ddmetric_proto {
13    include!(concat!(env!("OUT_DIR"), "/datadog.agentpayload.rs"));
14}
15
16#[allow(warnings)]
17pub(crate) mod ddtrace_proto {
18    include!(concat!(env!("OUT_DIR"), "/dd_trace.rs"));
19}
20
21use std::{convert::Infallible, fmt::Debug, net::SocketAddr, sync::Arc, time::Duration};
22
23use bytes::{Buf, Bytes};
24use chrono::{DateTime, Utc, serde::ts_milliseconds};
25use futures::FutureExt;
26use http::StatusCode;
27use hyper::{Server, service::make_service_fn};
28use regex::Regex;
29use serde::{Deserialize, Serialize};
30use serde_with::serde_as;
31use snafu::Snafu;
32use tokio::net::TcpStream;
33use tower::ServiceBuilder;
34use tracing::Span;
35use vector_lib::{
36    codecs::decoding::{DeserializerConfig, FramingConfig},
37    config::{LegacyKey, LogNamespace},
38    configurable::configurable_component,
39    event::{BatchNotifier, BatchStatus},
40    internal_event::{EventsReceived, Registered},
41    lookup::owned_value_path,
42    schema::meaning,
43    source_sender::SendError,
44    tls::MaybeTlsIncomingStream,
45};
46use vrl::{
47    path::OwnedTargetPath,
48    value::{Kind, kind::Collection},
49};
50use warp::{Filter, Reply, filters::BoxedFilter, reject::Rejection, reply::Response};
51
52use crate::{
53    SourceSender,
54    codecs::{Decoder, DecodingConfig},
55    common::http::ErrorMessage,
56    config::{
57        DataType, GenerateConfig, Resource, SourceAcknowledgementsConfig, SourceConfig,
58        SourceContext, SourceOutput, log_schema,
59    },
60    event::Event,
61    http::{KeepaliveConfig, MaxConnectionAgeLayer, build_http_trace_layer},
62    internal_events::{HttpBytesReceived, StreamClosedError},
63    schema,
64    serde::{bool_or_struct, default_decoding, default_framing_message_based},
65    sources::{
66        self,
67        util::{
68            decompression::{CappedDecoder, max_decompressed_size_bytes},
69            http::emit_decompress_error,
70        },
71    },
72    tls::{MaybeTlsSettings, TlsEnableableConfig},
73};
74
75pub const LOGS: &str = "logs";
76pub const METRICS: &str = "metrics";
77pub const TRACES: &str = "traces";
78pub const LLMOBS: &str = "llmobs";
79
80/// Configuration for the `datadog_agent` source.
81#[configurable_component(source(
82    "datadog_agent",
83    "Receive logs, metrics, and traces collected by a Datadog Agent."
84))]
85#[serde_as]
86#[derive(Clone, Debug)]
87pub struct DatadogAgentConfig {
88    /// The socket address to accept connections on.
89    ///
90    /// It _must_ include a port.
91    #[configurable(metadata(docs::examples = "0.0.0.0:80"))]
92    #[configurable(metadata(docs::examples = "localhost:80"))]
93    address: SocketAddr,
94
95    /// If this is set to `true`, when incoming events contain a Datadog API key, it is
96    /// stored in the event metadata and used if the event is sent to a Datadog sink.
97    #[serde(default = "crate::serde::default_true")]
98    store_api_key: bool,
99
100    /// If this is set to `true`, logs are not accepted by the component.
101    #[serde(default = "crate::serde::default_false")]
102    disable_logs: bool,
103
104    /// If this is set to `true`, metrics (beta) are not accepted by the component.
105    #[serde(default = "crate::serde::default_false")]
106    disable_metrics: bool,
107
108    /// If this is set to `true`, traces (alpha) are not accepted by the component.
109    #[serde(default = "crate::serde::default_false")]
110    disable_traces: bool,
111
112    /// If this is set to `true`, LLM Observability events are not accepted by the component.
113    #[configurable(metadata(docs::advanced))]
114    #[serde(default = "crate::serde::default_false")]
115    disable_llmobs: bool,
116
117    /// If this is set to `true`, logs, metrics (beta), and traces (alpha) are sent to different outputs.
118    ///
119    ///
120    /// For a source component named `agent`, the received logs, metrics (beta), and traces (alpha) can then be
121    /// configured as input to other components by specifying `agent.logs`, `agent.metrics`, and
122    /// `agent.traces`, respectively.
123    #[serde(default = "crate::serde::default_false")]
124    multiple_outputs: bool,
125
126    /// If this is set to `true`, when log events contain the field `ddtags`, the string value that
127    /// contains a list of key:value pairs set by the Agent is parsed and expanded into an array.
128    #[serde(default = "crate::serde::default_false")]
129    parse_ddtags: bool,
130
131    /// If this is set to `true`, metric names are split at the first '.' into a namespace and name.
132    /// For example, `system.cpu.usage` would be split into namespace `system` and name `cpu.usage`.
133    /// If `false`, the full metric name is used without splitting. This may be useful if you are using a
134    /// default namespace for metrics in sinks connected to this source.
135    #[serde(default = "crate::serde::default_true")]
136    split_metric_namespace: bool,
137
138    /// The namespace to use for logs. This overrides the global setting.
139    #[serde(default)]
140    #[configurable(metadata(docs::hidden))]
141    log_namespace: Option<bool>,
142
143    #[configurable(derived)]
144    tls: Option<TlsEnableableConfig>,
145
146    #[configurable(derived)]
147    #[serde(default = "default_framing_message_based")]
148    framing: FramingConfig,
149
150    #[configurable(derived)]
151    #[serde(default = "default_decoding")]
152    decoding: DeserializerConfig,
153
154    #[configurable(derived)]
155    #[serde(default, deserialize_with = "bool_or_struct")]
156    acknowledgements: SourceAcknowledgementsConfig,
157
158    #[configurable(derived)]
159    #[serde(default)]
160    keepalive: KeepaliveConfig,
161
162    /// The timeout before responding to requests with a HTTP 503 Service Unavailable error.
163    ///
164    /// If not set, responses to completed requests will block indefinitely until connected
165    /// transforms or sinks are ready to receive the events. When this happens, the sending Datadog
166    /// Agent will eventually time out the request and drop the connection, resulting Vector
167    /// generating an "Events dropped." error and incrementing the `component_discarded_events_total`
168    /// internal metric. By setting this option to a value less than the Agent's timeout, Vector
169    /// will instead respond to the Agent with a HTTP 503 Service Unavailable error, emit a warning,
170    /// and increment the `component_timed_out_events_total` internal metric instead.
171    #[serde_as(as = "Option<serde_with::DurationSecondsWithFrac<f64>>")]
172    send_timeout_secs: Option<f64>,
173}
174
175impl GenerateConfig for DatadogAgentConfig {
176    fn generate_config() -> serde_json::Value {
177        serde_json::to_value(Self {
178            address: "0.0.0.0:8080".parse().unwrap(),
179            tls: None,
180            store_api_key: true,
181            framing: default_framing_message_based(),
182            decoding: default_decoding(),
183            acknowledgements: SourceAcknowledgementsConfig::default(),
184            disable_logs: false,
185            disable_metrics: false,
186            disable_traces: false,
187            disable_llmobs: false,
188            multiple_outputs: false,
189            parse_ddtags: false,
190            split_metric_namespace: true,
191            log_namespace: Some(false),
192            keepalive: KeepaliveConfig::default(),
193            send_timeout_secs: None,
194        })
195        .unwrap()
196    }
197}
198
199#[async_trait::async_trait]
200#[typetag::serde(name = "datadog_agent")]
201impl SourceConfig for DatadogAgentConfig {
202    async fn build(&self, cx: SourceContext) -> crate::Result<sources::Source> {
203        let log_namespace = cx.log_namespace(self.log_namespace);
204
205        let logs_schema_definition = cx
206            .schema_definitions
207            .get(&Some(LOGS.to_owned()))
208            .or_else(|| cx.schema_definitions.get(&None))
209            .cloned();
210
211        let decoder =
212            DecodingConfig::new(self.framing.clone(), self.decoding.clone(), log_namespace)
213                .build()?;
214
215        let tls = MaybeTlsSettings::from_config(self.tls.as_ref(), true)?;
216        let source = DatadogAgentSource::new(
217            self.store_api_key,
218            decoder,
219            tls.http_protocol_name(),
220            logs_schema_definition,
221            log_namespace,
222            self.parse_ddtags,
223            self.split_metric_namespace,
224        );
225        let listener = tls.bind(&self.address).await?;
226        let handler = RequestHandler {
227            acknowledgements: cx.do_acknowledgements(self.acknowledgements),
228            multiple_outputs: self.multiple_outputs,
229            out: cx.out,
230        };
231        let filters = source.build_warp_filters(handler, self)?;
232        let shutdown = cx.shutdown;
233        let keepalive_settings = self.keepalive.clone();
234
235        info!(message = "Building HTTP server.", address = %self.address);
236
237        Ok(Box::pin(async move {
238            let routes = filters.recover(|r: Rejection| async move {
239                if let Some(e_msg) = r.find::<ErrorMessage>() {
240                    let json = warp::reply::json(e_msg);
241                    Ok(warp::reply::with_status(json, e_msg.status_code()))
242                } else {
243                    // other internal error - will return 500 internal server error
244                    Err(r)
245                }
246            });
247
248            let span = Span::current();
249            let make_svc = make_service_fn(move |conn: &MaybeTlsIncomingStream<TcpStream>| {
250                let svc = ServiceBuilder::new()
251                    .layer(build_http_trace_layer(span.clone()))
252                    .option_layer(keepalive_settings.max_connection_age_secs.map(|secs| {
253                        MaxConnectionAgeLayer::new(
254                            Duration::from_secs(secs),
255                            keepalive_settings.max_connection_age_jitter_factor,
256                            conn.peer_addr(),
257                        )
258                    }))
259                    .service(warp::service(routes.clone()));
260                futures_util::future::ok::<_, Infallible>(svc)
261            });
262
263            Server::builder(hyper::server::accept::from_stream(listener.accept_stream()))
264                .serve(make_svc)
265                .with_graceful_shutdown(shutdown.map(|_| ()))
266                .await
267                .map_err(|err| {
268                    error!("An error occurred: {:?}.", err);
269                })?;
270
271            Ok(())
272        }))
273    }
274
275    fn outputs(&self, global_log_namespace: LogNamespace) -> Vec<SourceOutput> {
276        let definition = self
277            .decoding
278            .schema_definition(global_log_namespace.merge(self.log_namespace))
279            // NOTE: "status" is intentionally semantically mapped to "severity",
280            //       since that is what DD designates as the semantic meaning of status
281            // https://docs.datadoghq.com/logs/log_configuration/attributes_naming_convention/?s=severity#reserved-attributes
282            .with_source_metadata(
283                Self::NAME,
284                Some(LegacyKey::InsertIfEmpty(owned_value_path!("status"))),
285                &owned_value_path!("status"),
286                Kind::bytes(),
287                Some(meaning::SEVERITY),
288            )
289            .with_source_metadata(
290                Self::NAME,
291                Some(LegacyKey::InsertIfEmpty(owned_value_path!("timestamp"))),
292                &owned_value_path!("timestamp"),
293                Kind::timestamp(),
294                Some(meaning::TIMESTAMP),
295            )
296            .with_source_metadata(
297                Self::NAME,
298                Some(LegacyKey::InsertIfEmpty(owned_value_path!("hostname"))),
299                &owned_value_path!("hostname"),
300                Kind::bytes(),
301                Some(meaning::HOST),
302            )
303            .with_source_metadata(
304                Self::NAME,
305                Some(LegacyKey::InsertIfEmpty(owned_value_path!("service"))),
306                &owned_value_path!("service"),
307                Kind::bytes(),
308                Some(meaning::SERVICE),
309            )
310            .with_source_metadata(
311                Self::NAME,
312                Some(LegacyKey::InsertIfEmpty(owned_value_path!("ddsource"))),
313                &owned_value_path!("ddsource"),
314                Kind::bytes(),
315                Some(meaning::SOURCE),
316            )
317            .with_source_metadata(
318                Self::NAME,
319                Some(LegacyKey::InsertIfEmpty(owned_value_path!("ddtags"))),
320                &owned_value_path!("ddtags"),
321                if self.parse_ddtags {
322                    Kind::array(Collection::empty().with_unknown(Kind::bytes())).or_undefined()
323                } else {
324                    Kind::bytes()
325                },
326                Some(meaning::TAGS),
327            )
328            .with_standard_vector_source_metadata();
329
330        let log_namespace = global_log_namespace.merge(self.log_namespace);
331        let llmobs_definition = schema::Definition::new_with_default_metadata(
332            Kind::object(
333                Collection::empty()
334                    .with_known("span_id", Kind::bytes())
335                    .with_known("trace_id", Kind::bytes())
336                    .with_known("parent_id", Kind::bytes().or_undefined())
337                    .with_known("name", Kind::bytes().or_undefined())
338                    .with_known("session_id", Kind::bytes().or_undefined())
339                    .with_known("service", Kind::bytes().or_undefined())
340                    .with_known("start_ns", Kind::integer().or_undefined())
341                    .with_known("duration", Kind::integer().or_undefined())
342                    .with_known("status", Kind::bytes().or_undefined())
343                    .with_known("status_message", Kind::bytes().or_undefined())
344                    .with_known("ml_app", Kind::bytes().or_undefined())
345                    .with_known("meta", Kind::object(Collection::any()).or_undefined())
346                    .with_known("metrics", Kind::object(Collection::any()).or_undefined())
347                    .with_known(
348                        "tags",
349                        Kind::array(Collection::empty().with_unknown(Kind::bytes())).or_undefined(),
350                    )
351                    .with_known("span_links", Kind::any().or_undefined())
352                    .with_known("config", Kind::any().or_undefined())
353                    .with_known("collection_errors", Kind::any().or_undefined())
354                    .with_known(
355                        "_dd",
356                        Kind::object(
357                            Collection::empty()
358                                .with_known("tracer_version", Kind::bytes().or_undefined()),
359                        )
360                        .or_undefined(),
361                    ),
362            ),
363            [log_namespace],
364        )
365        .with_source_metadata(
366            Self::NAME,
367            Some(LegacyKey::InsertIfEmpty(owned_value_path!("timestamp"))),
368            &owned_value_path!("timestamp"),
369            Kind::timestamp(),
370            Some(meaning::TIMESTAMP),
371        )
372        .with_standard_vector_source_metadata();
373
374        let mut output = Vec::with_capacity(1);
375
376        if self.multiple_outputs {
377            if !self.disable_logs {
378                output.push(SourceOutput::new_maybe_logs(DataType::Log, definition).with_port(LOGS))
379            }
380            if !self.disable_metrics {
381                output.push(SourceOutput::new_metrics().with_port(METRICS))
382            }
383            if !self.disable_traces {
384                output.push(SourceOutput::new_traces().with_port(TRACES))
385            }
386            if !self.disable_llmobs {
387                output.push(
388                    SourceOutput::new_maybe_logs(DataType::Log, llmobs_definition)
389                        .with_port(LLMOBS),
390                )
391            }
392        } else {
393            output.push(SourceOutput::new_maybe_logs(
394                DataType::all_bits(),
395                definition,
396            ))
397        }
398        output
399    }
400
401    fn resources(&self) -> Vec<Resource> {
402        vec![Resource::tcp(self.address)]
403    }
404
405    fn can_acknowledge(&self) -> bool {
406        true
407    }
408
409    fn send_timeout(&self) -> Option<Duration> {
410        self.send_timeout_secs.map(Duration::from_secs_f64)
411    }
412}
413
414#[derive(Clone, Copy, Debug, Snafu)]
415pub(crate) enum ApiError {
416    ServerShutdown,
417}
418
419impl warp::reject::Reject for ApiError {}
420
421#[derive(Deserialize)]
422pub struct ApiKeyQueryParams {
423    #[serde(rename = "dd-api-key")]
424    pub dd_api_key: Option<String>,
425}
426
427#[derive(Clone)]
428pub(crate) struct DatadogAgentSource {
429    pub(crate) api_key_extractor: ApiKeyExtractor,
430    pub(crate) log_schema_host_key: OwnedTargetPath,
431    pub(crate) log_schema_source_type_key: OwnedTargetPath,
432    pub(crate) log_namespace: LogNamespace,
433    pub(crate) decoder: Decoder,
434    protocol: &'static str,
435    logs_schema_definition: Option<Arc<schema::Definition>>,
436    events_received: Registered<EventsReceived>,
437    parse_ddtags: bool,
438    split_metric_namespace: bool,
439}
440
441#[derive(Clone)]
442pub struct ApiKeyExtractor {
443    matcher: Regex,
444    store_api_key: bool,
445}
446
447impl ApiKeyExtractor {
448    pub fn extract(
449        &self,
450        path: &str,
451        header: Option<String>,
452        query_params: Option<String>,
453    ) -> Option<Arc<str>> {
454        if !self.store_api_key {
455            return None;
456        }
457        // Grab from URL first
458        self.matcher
459            .captures(path)
460            .and_then(|cap| cap.name("api_key").map(|key| key.as_str()).map(Arc::from))
461            // Try from query params
462            .or_else(|| query_params.map(Arc::from))
463            // Try from header next
464            .or_else(|| header.map(Arc::from))
465    }
466}
467
468impl DatadogAgentSource {
469    pub(crate) fn new(
470        store_api_key: bool,
471        decoder: Decoder,
472        protocol: &'static str,
473        logs_schema_definition: Option<schema::Definition>,
474        log_namespace: LogNamespace,
475        parse_ddtags: bool,
476        split_metric_namespace: bool,
477    ) -> Self {
478        Self {
479            api_key_extractor: ApiKeyExtractor {
480                store_api_key,
481                matcher: Regex::new(r"^/v1/input/(?P<api_key>[[:alnum:]]{32})/??")
482                    .expect("static regex always compiles"),
483            },
484            log_schema_host_key: log_schema()
485                .host_key_target_path()
486                .expect("global log_schema.host_key to be valid path")
487                .clone(),
488            log_schema_source_type_key: log_schema()
489                .source_type_key_target_path()
490                .expect("global log_schema.source_type_key to be valid path")
491                .clone(),
492            decoder,
493            protocol,
494            logs_schema_definition: logs_schema_definition.map(Arc::new),
495            log_namespace,
496            events_received: register!(EventsReceived),
497            parse_ddtags,
498            split_metric_namespace,
499        }
500    }
501
502    fn build_warp_filters(
503        &self,
504        handler: RequestHandler,
505        config: &DatadogAgentConfig,
506    ) -> crate::Result<BoxedFilter<(Response,)>> {
507        let mut filters =
508            (!config.disable_logs).then(|| logs::build_warp_filter(handler.clone(), self.clone()));
509
510        if !config.disable_traces {
511            let trace_filter = traces::build_warp_filter(handler.clone(), self.clone());
512            filters = filters
513                .map(|f| f.or(trace_filter.clone()).unify().boxed())
514                .or(Some(trace_filter));
515        }
516
517        if !config.disable_metrics {
518            let metrics_filter = metrics::build_warp_filter(handler.clone(), self.clone());
519            filters = filters
520                .map(|f| f.or(metrics_filter.clone()).unify().boxed())
521                .or(Some(metrics_filter));
522        }
523
524        if !config.disable_llmobs {
525            let llmobs_filter = llmobs::build_warp_filter(handler.clone(), self.clone());
526            filters = filters
527                .map(|f| f.or(llmobs_filter.clone()).unify().boxed())
528                .or(Some(llmobs_filter));
529        }
530
531        filters.ok_or_else(|| "At least one of the supported data type shall be enabled".into())
532    }
533
534    pub(crate) fn decode(
535        &self,
536        header: &Option<String>,
537        mut body: Bytes,
538        path: &str,
539    ) -> Result<Bytes, ErrorMessage> {
540        if let Some(encodings) = header {
541            for encoding in encodings.rsplit(',').map(str::trim) {
542                body = match encoding {
543                    "identity" => body,
544                    // Cap each decompressed payload so a compression bomb cannot drive
545                    // unbounded allocation on this unauthenticated HTTP listener.
546                    "gzip" | "x-gzip" => CappedDecoder::gzip(body.reader())
547                        .decompress()
548                        .map_err(|error| {
549                            emit_decompress_error(encoding, error, max_decompressed_size_bytes())
550                        })?
551                        .into(),
552                    "zstd" => CappedDecoder::zstd_http(body.reader())
553                        .map_err(|error| {
554                            emit_decompress_error(encoding, error, max_decompressed_size_bytes())
555                        })?
556                        .decompress()
557                        .map_err(|error| {
558                            emit_decompress_error(encoding, error, max_decompressed_size_bytes())
559                        })?
560                        .into(),
561                    "deflate" | "x-deflate" => CappedDecoder::zlib(body.reader())
562                        .decompress()
563                        .map_err(|error| {
564                            emit_decompress_error(encoding, error, max_decompressed_size_bytes())
565                        })?
566                        .into(),
567                    encoding => {
568                        return Err(ErrorMessage::new(
569                            StatusCode::UNSUPPORTED_MEDIA_TYPE,
570                            format!("Unsupported encoding {encoding}"),
571                        ));
572                    }
573                }
574            }
575        }
576        emit!(HttpBytesReceived {
577            byte_size: body.len(),
578            http_path: path,
579            protocol: self.protocol,
580        });
581        Ok(body)
582    }
583}
584
585#[derive(Clone)]
586struct RequestHandler {
587    acknowledgements: bool,
588    multiple_outputs: bool,
589    out: SourceSender,
590}
591
592impl RequestHandler {
593    async fn handle_request(
594        mut self,
595        events: Result<Vec<Event>, ErrorMessage>,
596        output: &'static str,
597    ) -> Result<Response, Rejection> {
598        match events {
599            Ok(events) => self.handle_events(events, output).await,
600            Err(err) => Err(warp::reject::custom(err)),
601        }
602    }
603
604    async fn handle_events(
605        &mut self,
606        mut events: Vec<Event>,
607        output: &'static str,
608    ) -> Result<Response, Rejection> {
609        let receiver = BatchNotifier::maybe_apply_to(self.acknowledgements, &mut events);
610        let count = events.len();
611        let output = self.multiple_outputs.then_some(output);
612
613        let result = if let Some(name) = output {
614            self.out.send_batch_named(name, events).await
615        } else {
616            self.out.send_batch(events).await
617        };
618        match result {
619            Ok(()) => {}
620            Err(SendError::Closed) => {
621                emit!(StreamClosedError { count });
622                return Err(warp::reject::custom(ApiError::ServerShutdown));
623            }
624            Err(SendError::Timeout) => {
625                return Ok(warp::reply::with_status(
626                    "Service unavailable",
627                    StatusCode::SERVICE_UNAVAILABLE,
628                )
629                .into_response());
630            }
631        }
632        match receiver {
633            None => Ok(warp::reply().into_response()),
634            Some(receiver) => match receiver.await {
635                BatchStatus::Delivered => Ok(warp::reply().into_response()),
636                BatchStatus::Errored => Err(warp::reject::custom(ErrorMessage::new(
637                    StatusCode::INTERNAL_SERVER_ERROR,
638                    "Error delivering contents to sink".into(),
639                ))),
640                BatchStatus::Rejected => Err(warp::reject::custom(ErrorMessage::new(
641                    StatusCode::BAD_REQUEST,
642                    "Contents failed to deliver to sink".into(),
643                ))),
644            },
645        }
646    }
647}
648
649// https://github.com/DataDog/datadog-agent/blob/a33248c2bc125920a9577af1e16f12298875a4ad/pkg/logs/processor/json.go#L23-L49
650#[derive(Clone, Debug, Deserialize, Serialize)]
651#[serde(deny_unknown_fields)]
652struct LogMsg {
653    pub message: Bytes,
654    pub status: Bytes,
655    #[serde(
656        deserialize_with = "ts_milliseconds::deserialize",
657        serialize_with = "ts_milliseconds::serialize"
658    )]
659    pub timestamp: DateTime<Utc>,
660    pub hostname: Bytes,
661    pub service: Bytes,
662    pub ddsource: Bytes,
663    pub ddtags: Bytes,
664}