vector/sources/datadog_agent/
logs.rs1use std::sync::Arc;
2
3use bytes::{BufMut, Bytes, BytesMut};
4use chrono::Utc;
5use http::StatusCode;
6use tokio_util::codec::Decoder;
7use vector_lib::{
8 EstimatedJsonEncodedSizeOf,
9 codecs::StreamDecodingError,
10 config::LegacyKey,
11 internal_event::{CountByteSize, InternalEventHandle as _},
12 json_size::JsonSize,
13 lookup::path,
14};
15use vrl::core::Value;
16use warp::{Filter, filters::BoxedFilter, path as warp_path, path::FullPath, reply::Response};
17
18use super::{ApiKeyQueryParams, DatadogAgentConfig, DatadogAgentSource, LogMsg, RequestHandler};
19use crate::{
20 common::{datadog::DDTAGS, http::ErrorMessage},
21 event::Event,
22 internal_events::DatadogAgentJsonParseError,
23 sources::util::http::capped_body,
24};
25
26pub(super) fn build_warp_filter(
27 handler: RequestHandler,
28 source: DatadogAgentSource,
29) -> BoxedFilter<(Response,)> {
30 warp::post()
31 .and(warp_path!("v1" / "input" / ..).or(warp_path!("api" / "v2" / "logs" / ..)))
32 .and(warp::path::full())
33 .and(warp::header::optional::<String>("content-encoding"))
34 .and(warp::header::optional::<String>("dd-api-key"))
35 .and(warp::query::<ApiKeyQueryParams>())
36 .and(capped_body())
37 .and_then(
38 move |_,
39 path: FullPath,
40 encoding_header: Option<String>,
41 api_token: Option<String>,
42 query_params: ApiKeyQueryParams,
43 body: Bytes| {
44 let events = source
45 .decode(&encoding_header, body, path.as_str())
46 .and_then(|body| {
47 decode_log_body(
48 body,
49 source.api_key_extractor.extract(
50 path.as_str(),
51 api_token,
52 query_params.dd_api_key,
53 ),
54 &source,
55 )
56 });
57 handler.clone().handle_request(events, super::LOGS)
58 },
59 )
60 .boxed()
61}
62
63pub(crate) fn decode_log_body(
64 body: Bytes,
65 api_key: Option<Arc<str>>,
66 source: &DatadogAgentSource,
67) -> Result<Vec<Event>, ErrorMessage> {
68 if body.is_empty() || body.as_ref() == b"{}" {
69 debug!(message = "Empty payload ignored.");
72 return Ok(Vec::new());
73 }
74
75 let messages: Vec<LogMsg> = serde_json::from_slice(&body).map_err(|error| {
76 emit!(DatadogAgentJsonParseError { error: &error });
77
78 ErrorMessage::new(
79 StatusCode::BAD_REQUEST,
80 format!("Error parsing JSON: {error:?}"),
81 )
82 })?;
83
84 let now = Utc::now();
85 let mut decoded = Vec::new();
86 let mut event_bytes_received = JsonSize::zero();
87
88 for LogMsg {
89 message,
90 status,
91 timestamp,
92 hostname,
93 service,
94 ddsource,
95 ddtags,
96 } in messages
97 {
98 let mut decoder = source.decoder.clone();
99 let mut buffer = BytesMut::new();
100 buffer.put(message);
101
102 loop {
103 match decoder.decode_eof(&mut buffer) {
104 Ok(Some((events, _byte_size))) => {
105 for mut event in events {
106 if let Event::Log(ref mut log) = event {
107 let namespace = &source.log_namespace;
108 let source_name = "datadog_agent";
109
110 namespace.insert_source_metadata(
111 source_name,
112 log,
113 Some(LegacyKey::InsertIfEmpty(path!("status"))),
114 path!("status"),
115 status.clone(),
116 );
117 namespace.insert_source_metadata(
118 source_name,
119 log,
120 Some(LegacyKey::InsertIfEmpty(path!("timestamp"))),
121 path!("timestamp"),
122 timestamp,
123 );
124 namespace.insert_source_metadata(
125 source_name,
126 log,
127 Some(LegacyKey::InsertIfEmpty(path!("hostname"))),
128 path!("hostname"),
129 hostname.clone(),
130 );
131 namespace.insert_source_metadata(
132 source_name,
133 log,
134 Some(LegacyKey::InsertIfEmpty(path!("service"))),
135 path!("service"),
136 service.clone(),
137 );
138 namespace.insert_source_metadata(
139 source_name,
140 log,
141 Some(LegacyKey::InsertIfEmpty(path!("ddsource"))),
142 path!("ddsource"),
143 ddsource.clone(),
144 );
145
146 let ddtags: Value = if source.parse_ddtags {
147 parse_ddtags(&ddtags)
148 } else {
149 ddtags.clone().into()
150 };
151
152 namespace.insert_source_metadata(
153 source_name,
154 log,
155 Some(LegacyKey::InsertIfEmpty(path!(DDTAGS))),
156 path!(DDTAGS),
157 ddtags,
158 );
159
160 event_bytes_received += log.estimated_json_encoded_size_of();
162
163 namespace.insert_standard_vector_source_metadata(
164 log,
165 DatadogAgentConfig::NAME,
166 now,
167 );
168
169 if let Some(k) = &api_key {
170 log.metadata_mut().set_datadog_api_key(Arc::clone(k));
171 }
172
173 let logs_schema_definition = source
174 .logs_schema_definition
175 .as_ref()
176 .unwrap_or_else(|| panic!("registered log schema required"));
177
178 log.metadata_mut()
179 .set_schema_definition(logs_schema_definition);
180 }
181
182 decoded.push(event);
183 }
184 }
185 Ok(None) => break,
186 Err(error) => {
187 if !error.can_continue() {
190 break;
191 }
192 }
193 }
194 }
195 }
196
197 source
198 .events_received
199 .emit(CountByteSize(decoded.len(), event_bytes_received));
200
201 Ok(decoded)
202}
203
204fn parse_ddtags(ddtags_raw: &Bytes) -> Value {
211 if ddtags_raw.is_empty() {
212 return Vec::<Value>::new().into();
213 }
214
215 let ddtags_str = String::from_utf8_lossy(ddtags_raw);
216
217 let ddtags: Vec<Value> = ddtags_str
219 .split(',')
220 .filter(|kv| !kv.is_empty())
221 .map(|kv| Value::Bytes(Bytes::from(kv.trim().to_string())))
222 .collect();
223
224 if ddtags.is_empty() && !ddtags_str.is_empty() {
225 warn!(
226 message = "`parse_ddtags` set to true and Agent log contains non-empty ddtags string, but no tag-value pairs were parsed."
227 )
228 }
229
230 ddtags.into()
231}
232
233#[cfg(test)]
234mod tests {
235 use similar_asserts::assert_eq;
236 use vrl::value;
237
238 use super::*;
239
240 #[test]
241 fn ddtags_parse_empty() {
242 let raw = Bytes::from(String::from(""));
243 let val = parse_ddtags(&raw);
244
245 assert_eq!(val, value!([]));
246 }
247
248 #[test]
249 fn ddtags_parse_bare() {
250 let raw = Bytes::from(String::from("bare"));
251 let val = parse_ddtags(&raw);
252
253 assert_eq!(val, value!(["bare"]));
254 }
255
256 #[test]
257 fn ddtags_parse_kv_one() {
258 let raw = Bytes::from(String::from("filename:driver.log"));
259 let val = parse_ddtags(&raw);
260
261 assert_eq!(val, value!(["filename:driver.log"]));
262 }
263
264 #[test]
265 fn ddtags_parse_kv_multi() {
266 let raw = Bytes::from(String::from("filename:driver.log,wizard:the_grey"));
267 let val = parse_ddtags(&raw);
268
269 assert_eq!(val, value!(["filename:driver.log", "wizard:the_grey"]));
270 }
271
272 #[test]
273 fn ddtags_parse_kv_bare_combo() {
274 let raw = Bytes::from(String::from("filename:driver.log,debug,wizard:the_grey"));
275 let val = parse_ddtags(&raw);
276
277 assert_eq!(
278 val,
279 value!(["filename:driver.log", "debug", "wizard:the_grey"])
280 );
281 }
282}