vector/sinks/datadog/logs/
config.rs1use std::{convert::TryFrom, sync::Arc};
2
3use indoc::indoc;
4use tower::ServiceBuilder;
5use vector_lib::{
6 config::proxy::ProxyConfig, configurable::configurable_component, schema::meaning,
7};
8use vrl::value::Kind;
9
10use hyper::{Body, client::connect::Connect};
11
12use super::{service::LogApiRetry, sink::LogSinkBuilder};
13use crate::{
14 common::datadog,
15 http::HttpClient,
16 schema,
17 sinks::{
18 datadog::{DatadogCommonConfig, LocalDatadogCommonConfig, logs::service::LogApiService},
19 prelude::*,
20 util::http::RequestConfig,
21 },
22 tls::{MaybeTlsSettings, TlsEnableableConfig},
23};
24
25pub const MAX_PAYLOAD_BYTES: usize = 5_000_000;
33pub const BATCH_GOAL_BYTES: usize = 4_250_000;
34pub const BATCH_MAX_EVENTS: usize = 1_000;
35pub const BATCH_DEFAULT_TIMEOUT_SECS: f64 = 5.0;
36
37#[derive(Clone, Copy, Debug, Default)]
38pub struct DatadogLogsDefaultBatchSettings;
39
40impl SinkBatchSettings for DatadogLogsDefaultBatchSettings {
41 const MAX_EVENTS: Option<usize> = Some(BATCH_MAX_EVENTS);
42 const MAX_BYTES: Option<usize> = Some(BATCH_GOAL_BYTES);
43 const TIMEOUT_SECS: f64 = BATCH_DEFAULT_TIMEOUT_SECS;
44}
45
46#[configurable_component(sink("datadog_logs", "Publish log events to Datadog."))]
48#[derive(Clone, Debug, Derivative)]
49#[derivative(Default)]
50#[serde(deny_unknown_fields)]
51pub struct DatadogLogsConfig {
52 #[serde(flatten)]
53 pub local_dd_common: LocalDatadogCommonConfig,
54
55 #[configurable(derived)]
56 #[derivative(Default(value = "default_compression()"))]
57 #[serde(default = "default_compression")]
58 pub compression: Option<Compression>,
59
60 #[configurable(derived)]
61 #[serde(default, skip_serializing_if = "crate::serde::is_default")]
62 pub encoding: Transformer,
63
64 #[configurable(derived)]
65 #[serde(default)]
66 pub batch: BatchConfig<DatadogLogsDefaultBatchSettings>,
67
68 #[configurable(derived)]
69 #[serde(default)]
70 pub request: RequestConfig,
71
72 #[serde(default)]
77 pub conforms_as_agent: bool,
78}
79
80const fn default_compression() -> Option<Compression> {
81 Some(Compression::zstd_default())
82}
83
84impl GenerateConfig for DatadogLogsConfig {
85 fn generate_config() -> toml::Value {
86 toml::from_str(indoc! {r#"
87 default_api_key = "${DATADOG_API_KEY_ENV_VAR}"
88 "#})
89 .unwrap()
90 }
91}
92
93impl DatadogLogsConfig {
94 fn get_uri(&self, dd_common: &DatadogCommonConfig) -> http::Uri {
97 let base_url = dd_common
98 .endpoint
99 .clone()
100 .unwrap_or_else(|| format!("https://http-intake.logs.{}", dd_common.site));
101
102 http::Uri::try_from(format!("{base_url}/api/v2/logs")).expect("URI not valid")
103 }
104
105 pub fn get_protocol(&self, dd_common: &DatadogCommonConfig) -> String {
106 self.get_uri(dd_common)
107 .scheme_str()
108 .unwrap_or("http")
109 .to_string()
110 }
111
112 pub fn build_processor<C>(
113 &self,
114 dd_common: &DatadogCommonConfig,
115 client: HttpClient<Body, C>,
116 dd_evp_origin: String,
117 ) -> crate::Result<VectorSink>
118 where
119 C: Connect + Clone + Send + Sync + 'static,
120 {
121 let default_api_key: Arc<str> = Arc::from(dd_common.default_api_key.inner());
122 let request_limits = self.request.tower.into_settings();
123
124 let batch = self
127 .batch
128 .validate()?
129 .limit_max_bytes(BATCH_GOAL_BYTES)?
130 .limit_max_events(BATCH_MAX_EVENTS)?
131 .into_batcher_settings()?;
132
133 let headers = {
134 let mut request_headers = self.request.headers.clone();
135 if self.conforms_as_agent {
136 request_headers.insert(String::from("DD-PROTOCOL"), String::from("agent-json"));
137 }
138 request_headers
139 };
140
141 let conforms_as_agent = if let Some(value) = headers.get("DD-PROTOCOL") {
144 value == "agent-json"
145 } else {
146 false
147 };
148
149 let service = ServiceBuilder::new()
150 .settings(request_limits, LogApiRetry)
151 .service(LogApiService::new(
152 client,
153 self.get_uri(dd_common),
154 headers,
155 dd_evp_origin,
156 )?);
157
158 let encoding = self.encoding.clone();
159 let protocol = self.get_protocol(dd_common);
160
161 let sink = LogSinkBuilder::new(
162 encoding,
163 service,
164 default_api_key,
165 batch,
166 protocol,
167 conforms_as_agent,
168 )
169 .compression(self.compression.or_else(default_compression).unwrap())
170 .build();
171
172 Ok(VectorSink::from_event_streamsink(sink))
173 }
174
175 pub fn create_client(&self, proxy: &ProxyConfig) -> crate::Result<HttpClient> {
176 let default_tls_config;
177
178 let tls_settings = MaybeTlsSettings::from_config(
179 Some(match self.local_dd_common.tls.as_ref() {
180 Some(config) => config,
181 None => {
182 default_tls_config = TlsEnableableConfig::enabled();
183 &default_tls_config
184 }
185 }),
186 false,
187 )?;
188 Ok(HttpClient::new(tls_settings, proxy)?)
189 }
190}
191
192#[async_trait::async_trait]
193#[typetag::serde(name = "datadog_logs")]
194impl SinkConfig for DatadogLogsConfig {
195 async fn build(&self, cx: SinkContext) -> crate::Result<(VectorSink, Healthcheck)> {
196 let client = self.create_client(&cx.proxy)?;
197 let global = cx.extra_context.get_or_default::<datadog::Options>();
198 let dd_common = self.local_dd_common.with_globals(global)?;
199
200 let healthcheck = dd_common.build_healthcheck(client.clone())?;
201
202 let sink = self.build_processor(&dd_common, client, cx.app_name_slug)?;
203
204 Ok((sink, healthcheck))
205 }
206
207 fn input(&self) -> Input {
208 let requirement = schema::Requirement::empty()
209 .optional_meaning(meaning::MESSAGE, Kind::bytes())
210 .optional_meaning(meaning::TIMESTAMP, Kind::timestamp())
211 .optional_meaning(meaning::HOST, Kind::bytes())
212 .optional_meaning(meaning::SOURCE, Kind::bytes())
213 .optional_meaning(meaning::SEVERITY, Kind::bytes())
214 .optional_meaning(meaning::SERVICE, Kind::bytes())
215 .optional_meaning(meaning::TRACE_ID, Kind::bytes());
216
217 Input::log().with_schema_requirement(requirement)
218 }
219
220 fn acknowledgements(&self) -> &AcknowledgementsConfig {
221 &self.local_dd_common.acknowledgements
222 }
223}
224
225#[cfg(test)]
226mod test {
227 use vector_lib::{
228 codecs::{JsonSerializerConfig, MetricTagValues, encoding::format::JsonSerializerOptions},
229 config::LogNamespace,
230 };
231
232 use super::*;
233 use crate::{codecs::EncodingConfigWithFraming, components::validation::prelude::*};
234
235 #[test]
236 fn generate_config() {
237 crate::test_util::test_generate_config::<DatadogLogsConfig>();
238 }
239
240 #[test]
241 fn compression_config_field() {
242 assert_eq!(default_compression(), Some(Compression::zstd_default()));
244
245 let config_yaml = indoc! {r#"
248 default_api_key: "test_key"
249 "#};
250
251 let config: DatadogLogsConfig = serde_yaml::from_str(config_yaml).unwrap();
252 assert!(matches!(config.compression, Some(Compression::Zstd(_))));
254
255 let config_yaml_with_none = indoc! {r#"
257 default_api_key: "test_key"
258 compression: "none"
259 "#};
260
261 let config_no_compression: DatadogLogsConfig =
262 serde_yaml::from_str(config_yaml_with_none).unwrap();
263 assert_eq!(config_no_compression.compression, Some(Compression::None));
264
265 let config_yaml_with_zstd = indoc! {r#"
267 default_api_key: "test_key"
268 compression: "zstd"
269 "#};
270
271 let config_zstd: DatadogLogsConfig = serde_yaml::from_str(config_yaml_with_zstd).unwrap();
272 assert!(matches!(
273 config_zstd.compression,
274 Some(Compression::Zstd(_))
275 ));
276
277 let config_yaml_with_gzip = indoc! {r#"
279 default_api_key: "test_key"
280 compression: "gzip"
281 "#};
282
283 let config_gzip: DatadogLogsConfig = serde_yaml::from_str(config_yaml_with_gzip).unwrap();
284 assert!(matches!(
285 config_gzip.compression,
286 Some(Compression::Gzip(_))
287 ));
288 }
289
290 impl ValidatableComponent for DatadogLogsConfig {
291 fn validation_configuration() -> ValidationConfiguration {
292 let endpoint = "http://127.0.0.1:9005".to_string();
293 let config = Self {
294 local_dd_common: LocalDatadogCommonConfig {
295 endpoint: Some(endpoint.clone()),
296 default_api_key: Some("unused".to_string().into()),
297 ..Default::default()
298 },
299 compression: Some(Compression::None),
301 ..Default::default()
302 };
303
304 let encoding = EncodingConfigWithFraming::new(
305 None,
306 JsonSerializerConfig::new(MetricTagValues::Full, JsonSerializerOptions::default())
307 .into(),
308 config.encoding.clone(),
309 );
310
311 let logs_endpoint = format!("{endpoint}/api/v2/logs");
312
313 let external_resource = ExternalResource::new(
314 ResourceDirection::Push,
315 HttpResourceConfig::from_parts(
316 http::Uri::try_from(&logs_endpoint).expect("should not fail to parse URI"),
317 None,
318 ),
319 encoding,
320 );
321
322 ValidationConfiguration::from_sink(
323 Self::NAME,
324 LogNamespace::Legacy,
325 vec![ComponentTestCaseConfig::from_sink(
326 config,
327 None,
328 Some(external_resource),
329 )],
330 )
331 }
332 }
333
334 register_validatable_component!(DatadogLogsConfig);
335}