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#[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 #[configurable(metadata(docs::examples = "0.0.0.0:80"))]
92 #[configurable(metadata(docs::examples = "localhost:80"))]
93 address: SocketAddr,
94
95 #[serde(default = "crate::serde::default_true")]
98 store_api_key: bool,
99
100 #[serde(default = "crate::serde::default_false")]
102 disable_logs: bool,
103
104 #[serde(default = "crate::serde::default_false")]
106 disable_metrics: bool,
107
108 #[serde(default = "crate::serde::default_false")]
110 disable_traces: bool,
111
112 #[configurable(metadata(docs::advanced))]
114 #[serde(default = "crate::serde::default_false")]
115 disable_llmobs: bool,
116
117 #[serde(default = "crate::serde::default_false")]
124 multiple_outputs: bool,
125
126 #[serde(default = "crate::serde::default_false")]
129 parse_ddtags: bool,
130
131 #[serde(default = "crate::serde::default_true")]
136 split_metric_namespace: bool,
137
138 #[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 #[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 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 .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 self.matcher
459 .captures(path)
460 .and_then(|cap| cap.name("api_key").map(|key| key.as_str()).map(Arc::from))
461 .or_else(|| query_params.map(Arc::from))
463 .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 "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#[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}