1#[cfg(all(test, feature = "datadog-agent-integration-tests"))]
2mod integration_tests;
3#[cfg(test)]
4mod tests;
5
6pub mod logs;
7pub mod metrics;
8pub mod traces;
9
10#[allow(warnings, clippy::pedantic, clippy::nursery)]
11pub(crate) mod ddmetric_proto {
12 include!(concat!(env!("OUT_DIR"), "/datadog.agentpayload.rs"));
13}
14
15#[allow(warnings)]
16pub(crate) mod ddtrace_proto {
17 include!(concat!(env!("OUT_DIR"), "/dd_trace.rs"));
18}
19
20use std::{convert::Infallible, fmt::Debug, net::SocketAddr, sync::Arc, time::Duration};
21
22use bytes::{Buf, Bytes};
23use chrono::{DateTime, Utc, serde::ts_milliseconds};
24use futures::FutureExt;
25use http::StatusCode;
26use hyper::{Server, service::make_service_fn};
27use regex::Regex;
28use serde::{Deserialize, Serialize};
29use serde_with::serde_as;
30use snafu::Snafu;
31use tokio::net::TcpStream;
32use tower::ServiceBuilder;
33use tracing::Span;
34use vector_lib::{
35 codecs::decoding::{DeserializerConfig, FramingConfig},
36 config::{LegacyKey, LogNamespace},
37 configurable::configurable_component,
38 event::{BatchNotifier, BatchStatus},
39 internal_event::{EventsReceived, Registered},
40 lookup::owned_value_path,
41 schema::meaning,
42 source_sender::SendError,
43 tls::MaybeTlsIncomingStream,
44};
45use vrl::{
46 path::OwnedTargetPath,
47 value::{Kind, kind::Collection},
48};
49use warp::{Filter, Reply, filters::BoxedFilter, reject::Rejection, reply::Response};
50
51use crate::{
52 SourceSender,
53 codecs::{Decoder, DecodingConfig},
54 common::http::ErrorMessage,
55 config::{
56 DataType, GenerateConfig, Resource, SourceAcknowledgementsConfig, SourceConfig,
57 SourceContext, SourceOutput, log_schema,
58 },
59 event::Event,
60 http::{KeepaliveConfig, MaxConnectionAgeLayer, build_http_trace_layer},
61 internal_events::{HttpBytesReceived, StreamClosedError},
62 schema,
63 serde::{bool_or_struct, default_decoding, default_framing_message_based},
64 sources::{
65 self,
66 util::{
67 decompression::{CappedDecoder, max_decompressed_size_bytes},
68 http::emit_decompress_error,
69 },
70 },
71 tls::{MaybeTlsSettings, TlsEnableableConfig},
72};
73
74pub const LOGS: &str = "logs";
75pub const METRICS: &str = "metrics";
76pub const TRACES: &str = "traces";
77
78#[configurable_component(source(
80 "datadog_agent",
81 "Receive logs, metrics, and traces collected by a Datadog Agent."
82))]
83#[serde_as]
84#[derive(Clone, Debug)]
85pub struct DatadogAgentConfig {
86 #[configurable(metadata(docs::examples = "0.0.0.0:80"))]
90 #[configurable(metadata(docs::examples = "localhost:80"))]
91 address: SocketAddr,
92
93 #[configurable(metadata(docs::advanced))]
96 #[serde(default = "crate::serde::default_true")]
97 store_api_key: bool,
98
99 #[configurable(metadata(docs::advanced))]
101 #[serde(default = "crate::serde::default_false")]
102 disable_logs: bool,
103
104 #[configurable(metadata(docs::advanced))]
106 #[serde(default = "crate::serde::default_false")]
107 disable_metrics: bool,
108
109 #[configurable(metadata(docs::advanced))]
111 #[serde(default = "crate::serde::default_false")]
112 disable_traces: bool,
113
114 #[configurable(metadata(docs::advanced))]
121 #[serde(default = "crate::serde::default_false")]
122 multiple_outputs: bool,
123
124 #[configurable(metadata(docs::advanced))]
127 #[serde(default = "crate::serde::default_false")]
128 parse_ddtags: bool,
129
130 #[configurable(metadata(docs::advanced))]
135 #[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() -> toml::Value {
177 toml::Value::try_from(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 multiple_outputs: false,
188 parse_ddtags: false,
189 split_metric_namespace: true,
190 log_namespace: Some(false),
191 keepalive: KeepaliveConfig::default(),
192 send_timeout_secs: None,
193 })
194 .unwrap()
195 }
196}
197
198#[async_trait::async_trait]
199#[typetag::serde(name = "datadog_agent")]
200impl SourceConfig for DatadogAgentConfig {
201 async fn build(&self, cx: SourceContext) -> crate::Result<sources::Source> {
202 let log_namespace = cx.log_namespace(self.log_namespace);
203
204 let logs_schema_definition = cx
205 .schema_definitions
206 .get(&Some(LOGS.to_owned()))
207 .or_else(|| cx.schema_definitions.get(&None))
208 .cloned();
209
210 let decoder =
211 DecodingConfig::new(self.framing.clone(), self.decoding.clone(), log_namespace)
212 .build()?;
213
214 let tls = MaybeTlsSettings::from_config(self.tls.as_ref(), true)?;
215 let source = DatadogAgentSource::new(
216 self.store_api_key,
217 decoder,
218 tls.http_protocol_name(),
219 logs_schema_definition,
220 log_namespace,
221 self.parse_ddtags,
222 self.split_metric_namespace,
223 );
224 let listener = tls.bind(&self.address).await?;
225 let handler = RequestHandler {
226 acknowledgements: cx.do_acknowledgements(self.acknowledgements),
227 multiple_outputs: self.multiple_outputs,
228 out: cx.out,
229 };
230 let filters = source.build_warp_filters(handler, self)?;
231 let shutdown = cx.shutdown;
232 let keepalive_settings = self.keepalive.clone();
233
234 info!(message = "Building HTTP server.", address = %self.address);
235
236 Ok(Box::pin(async move {
237 let routes = filters.recover(|r: Rejection| async move {
238 if let Some(e_msg) = r.find::<ErrorMessage>() {
239 let json = warp::reply::json(e_msg);
240 Ok(warp::reply::with_status(json, e_msg.status_code()))
241 } else {
242 Err(r)
244 }
245 });
246
247 let span = Span::current();
248 let make_svc = make_service_fn(move |conn: &MaybeTlsIncomingStream<TcpStream>| {
249 let svc = ServiceBuilder::new()
250 .layer(build_http_trace_layer(span.clone()))
251 .option_layer(keepalive_settings.max_connection_age_secs.map(|secs| {
252 MaxConnectionAgeLayer::new(
253 Duration::from_secs(secs),
254 keepalive_settings.max_connection_age_jitter_factor,
255 conn.peer_addr(),
256 )
257 }))
258 .service(warp::service(routes.clone()));
259 futures_util::future::ok::<_, Infallible>(svc)
260 });
261
262 Server::builder(hyper::server::accept::from_stream(listener.accept_stream()))
263 .serve(make_svc)
264 .with_graceful_shutdown(shutdown.map(|_| ()))
265 .await
266 .map_err(|err| {
267 error!("An error occurred: {:?}.", err);
268 })?;
269
270 Ok(())
271 }))
272 }
273
274 fn outputs(&self, global_log_namespace: LogNamespace) -> Vec<SourceOutput> {
275 let definition = self
276 .decoding
277 .schema_definition(global_log_namespace.merge(self.log_namespace))
278 .with_source_metadata(
282 Self::NAME,
283 Some(LegacyKey::InsertIfEmpty(owned_value_path!("status"))),
284 &owned_value_path!("status"),
285 Kind::bytes(),
286 Some(meaning::SEVERITY),
287 )
288 .with_source_metadata(
289 Self::NAME,
290 Some(LegacyKey::InsertIfEmpty(owned_value_path!("timestamp"))),
291 &owned_value_path!("timestamp"),
292 Kind::timestamp(),
293 Some(meaning::TIMESTAMP),
294 )
295 .with_source_metadata(
296 Self::NAME,
297 Some(LegacyKey::InsertIfEmpty(owned_value_path!("hostname"))),
298 &owned_value_path!("hostname"),
299 Kind::bytes(),
300 Some(meaning::HOST),
301 )
302 .with_source_metadata(
303 Self::NAME,
304 Some(LegacyKey::InsertIfEmpty(owned_value_path!("service"))),
305 &owned_value_path!("service"),
306 Kind::bytes(),
307 Some(meaning::SERVICE),
308 )
309 .with_source_metadata(
310 Self::NAME,
311 Some(LegacyKey::InsertIfEmpty(owned_value_path!("ddsource"))),
312 &owned_value_path!("ddsource"),
313 Kind::bytes(),
314 Some(meaning::SOURCE),
315 )
316 .with_source_metadata(
317 Self::NAME,
318 Some(LegacyKey::InsertIfEmpty(owned_value_path!("ddtags"))),
319 &owned_value_path!("ddtags"),
320 if self.parse_ddtags {
321 Kind::array(Collection::empty().with_unknown(Kind::bytes())).or_undefined()
322 } else {
323 Kind::bytes()
324 },
325 Some(meaning::TAGS),
326 )
327 .with_standard_vector_source_metadata();
328
329 let mut output = Vec::with_capacity(1);
330
331 if self.multiple_outputs {
332 if !self.disable_logs {
333 output.push(SourceOutput::new_maybe_logs(DataType::Log, definition).with_port(LOGS))
334 }
335 if !self.disable_metrics {
336 output.push(SourceOutput::new_metrics().with_port(METRICS))
337 }
338 if !self.disable_traces {
339 output.push(SourceOutput::new_traces().with_port(TRACES))
340 }
341 } else {
342 output.push(SourceOutput::new_maybe_logs(
343 DataType::all_bits(),
344 definition,
345 ))
346 }
347 output
348 }
349
350 fn resources(&self) -> Vec<Resource> {
351 vec![Resource::tcp(self.address)]
352 }
353
354 fn can_acknowledge(&self) -> bool {
355 true
356 }
357
358 fn send_timeout(&self) -> Option<Duration> {
359 self.send_timeout_secs.map(Duration::from_secs_f64)
360 }
361}
362
363#[derive(Clone, Copy, Debug, Snafu)]
364pub(crate) enum ApiError {
365 ServerShutdown,
366}
367
368impl warp::reject::Reject for ApiError {}
369
370#[derive(Deserialize)]
371pub struct ApiKeyQueryParams {
372 #[serde(rename = "dd-api-key")]
373 pub dd_api_key: Option<String>,
374}
375
376#[derive(Clone)]
377pub(crate) struct DatadogAgentSource {
378 pub(crate) api_key_extractor: ApiKeyExtractor,
379 pub(crate) log_schema_host_key: OwnedTargetPath,
380 pub(crate) log_schema_source_type_key: OwnedTargetPath,
381 pub(crate) log_namespace: LogNamespace,
382 pub(crate) decoder: Decoder,
383 protocol: &'static str,
384 logs_schema_definition: Option<Arc<schema::Definition>>,
385 events_received: Registered<EventsReceived>,
386 parse_ddtags: bool,
387 split_metric_namespace: bool,
388}
389
390#[derive(Clone)]
391pub struct ApiKeyExtractor {
392 matcher: Regex,
393 store_api_key: bool,
394}
395
396impl ApiKeyExtractor {
397 pub fn extract(
398 &self,
399 path: &str,
400 header: Option<String>,
401 query_params: Option<String>,
402 ) -> Option<Arc<str>> {
403 if !self.store_api_key {
404 return None;
405 }
406 self.matcher
408 .captures(path)
409 .and_then(|cap| cap.name("api_key").map(|key| key.as_str()).map(Arc::from))
410 .or_else(|| query_params.map(Arc::from))
412 .or_else(|| header.map(Arc::from))
414 }
415}
416
417impl DatadogAgentSource {
418 pub(crate) fn new(
419 store_api_key: bool,
420 decoder: Decoder,
421 protocol: &'static str,
422 logs_schema_definition: Option<schema::Definition>,
423 log_namespace: LogNamespace,
424 parse_ddtags: bool,
425 split_metric_namespace: bool,
426 ) -> Self {
427 Self {
428 api_key_extractor: ApiKeyExtractor {
429 store_api_key,
430 matcher: Regex::new(r"^/v1/input/(?P<api_key>[[:alnum:]]{32})/??")
431 .expect("static regex always compiles"),
432 },
433 log_schema_host_key: log_schema()
434 .host_key_target_path()
435 .expect("global log_schema.host_key to be valid path")
436 .clone(),
437 log_schema_source_type_key: log_schema()
438 .source_type_key_target_path()
439 .expect("global log_schema.source_type_key to be valid path")
440 .clone(),
441 decoder,
442 protocol,
443 logs_schema_definition: logs_schema_definition.map(Arc::new),
444 log_namespace,
445 events_received: register!(EventsReceived),
446 parse_ddtags,
447 split_metric_namespace,
448 }
449 }
450
451 fn build_warp_filters(
452 &self,
453 handler: RequestHandler,
454 config: &DatadogAgentConfig,
455 ) -> crate::Result<BoxedFilter<(Response,)>> {
456 let mut filters =
457 (!config.disable_logs).then(|| logs::build_warp_filter(handler.clone(), self.clone()));
458
459 if !config.disable_traces {
460 let trace_filter = traces::build_warp_filter(handler.clone(), self.clone());
461 filters = filters
462 .map(|f| f.or(trace_filter.clone()).unify().boxed())
463 .or(Some(trace_filter));
464 }
465
466 if !config.disable_metrics {
467 let metrics_filter = metrics::build_warp_filter(handler, self.clone());
468 filters = filters
469 .map(|f| f.or(metrics_filter.clone()).unify().boxed())
470 .or(Some(metrics_filter));
471 }
472
473 filters.ok_or_else(|| "At least one of the supported data type shall be enabled".into())
474 }
475
476 pub(crate) fn decode(
477 &self,
478 header: &Option<String>,
479 mut body: Bytes,
480 path: &str,
481 ) -> Result<Bytes, ErrorMessage> {
482 if let Some(encodings) = header {
483 for encoding in encodings.rsplit(',').map(str::trim) {
484 body = match encoding {
485 "identity" => body,
486 "gzip" | "x-gzip" => CappedDecoder::gzip(body.reader())
489 .decompress()
490 .map_err(|error| {
491 emit_decompress_error(encoding, error, max_decompressed_size_bytes())
492 })?
493 .into(),
494 "zstd" => CappedDecoder::zstd_http(body.reader())
495 .map_err(|error| {
496 emit_decompress_error(encoding, error, max_decompressed_size_bytes())
497 })?
498 .decompress()
499 .map_err(|error| {
500 emit_decompress_error(encoding, error, max_decompressed_size_bytes())
501 })?
502 .into(),
503 "deflate" | "x-deflate" => CappedDecoder::zlib(body.reader())
504 .decompress()
505 .map_err(|error| {
506 emit_decompress_error(encoding, error, max_decompressed_size_bytes())
507 })?
508 .into(),
509 encoding => {
510 return Err(ErrorMessage::new(
511 StatusCode::UNSUPPORTED_MEDIA_TYPE,
512 format!("Unsupported encoding {encoding}"),
513 ));
514 }
515 }
516 }
517 }
518 emit!(HttpBytesReceived {
519 byte_size: body.len(),
520 http_path: path,
521 protocol: self.protocol,
522 });
523 Ok(body)
524 }
525}
526
527#[derive(Clone)]
528struct RequestHandler {
529 acknowledgements: bool,
530 multiple_outputs: bool,
531 out: SourceSender,
532}
533
534impl RequestHandler {
535 async fn handle_request(
536 mut self,
537 events: Result<Vec<Event>, ErrorMessage>,
538 output: &'static str,
539 ) -> Result<Response, Rejection> {
540 match events {
541 Ok(events) => self.handle_events(events, output).await,
542 Err(err) => Err(warp::reject::custom(err)),
543 }
544 }
545
546 async fn handle_events(
547 &mut self,
548 mut events: Vec<Event>,
549 output: &'static str,
550 ) -> Result<Response, Rejection> {
551 let receiver = BatchNotifier::maybe_apply_to(self.acknowledgements, &mut events);
552 let count = events.len();
553 let output = self.multiple_outputs.then_some(output);
554
555 let result = if let Some(name) = output {
556 self.out.send_batch_named(name, events).await
557 } else {
558 self.out.send_batch(events).await
559 };
560 match result {
561 Ok(()) => {}
562 Err(SendError::Closed) => {
563 emit!(StreamClosedError { count });
564 return Err(warp::reject::custom(ApiError::ServerShutdown));
565 }
566 Err(SendError::Timeout) => {
567 return Ok(warp::reply::with_status(
568 "Service unavailable",
569 StatusCode::SERVICE_UNAVAILABLE,
570 )
571 .into_response());
572 }
573 }
574 match receiver {
575 None => Ok(warp::reply().into_response()),
576 Some(receiver) => match receiver.await {
577 BatchStatus::Delivered => Ok(warp::reply().into_response()),
578 BatchStatus::Errored => Err(warp::reject::custom(ErrorMessage::new(
579 StatusCode::INTERNAL_SERVER_ERROR,
580 "Error delivering contents to sink".into(),
581 ))),
582 BatchStatus::Rejected => Err(warp::reject::custom(ErrorMessage::new(
583 StatusCode::BAD_REQUEST,
584 "Contents failed to deliver to sink".into(),
585 ))),
586 },
587 }
588 }
589}
590
591#[derive(Clone, Debug, Deserialize, Serialize)]
593#[serde(deny_unknown_fields)]
594struct LogMsg {
595 pub message: Bytes,
596 pub status: Bytes,
597 #[serde(
598 deserialize_with = "ts_milliseconds::deserialize",
599 serialize_with = "ts_milliseconds::serialize"
600 )]
601 pub timestamp: DateTime<Utc>,
602 pub hostname: Bytes,
603 pub service: Bytes,
604 pub ddsource: Bytes,
605 pub ddtags: Bytes,
606}