1use futures_util::FutureExt;
2use http::{Request, StatusCode, Uri};
3use hyper::{body::Body, client::connect::Connect};
4use snafu::Snafu;
5use vector_lib::{
6 config::AcknowledgementsConfig, configurable::configurable_component,
7 sensitive_string::SensitiveString, tls::TlsEnableableConfig,
8};
9
10use super::Healthcheck;
11use crate::{
12 common::datadog,
13 http::{HttpClient, HttpError},
14 sinks::{HealthcheckError, util::HttpEndpoint},
15};
16
17#[cfg(feature = "sinks-datadog_events")]
18pub mod events;
19#[cfg(feature = "sinks-datadog_logs")]
20pub mod logs;
21#[cfg(feature = "sinks-datadog_metrics")]
22pub mod metrics;
23#[cfg(any(
24 all(feature = "sinks-datadog_logs", feature = "test-utils"),
25 all(feature = "sinks-datadog_metrics", feature = "test-utils"),
26 all(feature = "sinks-datadog_logs", test),
27 all(feature = "sinks-datadog_metrics", test)
28))]
29pub mod test_utils;
30#[cfg(feature = "sinks-datadog_traces")]
31pub mod traces;
32
33#[configurable_component]
36#[derive(Clone, Debug, Default)]
37#[serde(deny_unknown_fields)]
38pub struct LocalDatadogCommonConfig {
39 #[configurable(metadata(docs::examples = "http://127.0.0.1:8080"))]
47 #[configurable(metadata(docs::examples = "http://example.com:12345"))]
48 #[serde(default)]
49 pub endpoint: Option<String>,
50
51 #[configurable(metadata(docs::examples = "us3.datadoghq.com"))]
61 #[configurable(metadata(docs::examples = "datadoghq.eu"))]
62 pub site: Option<String>,
63
64 #[configurable(metadata(docs::examples = "${DATADOG_API_KEY_ENV_VAR}"))]
75 #[configurable(metadata(docs::examples = "ef8d5de700e7989468166c40fc8a0ccd"))]
76 pub default_api_key: Option<SensitiveString>,
77
78 #[configurable(derived)]
79 #[serde(default)]
80 pub tls: Option<TlsEnableableConfig>,
81
82 #[configurable(derived)]
83 #[serde(
84 default,
85 deserialize_with = "crate::serde::bool_or_struct",
86 skip_serializing_if = "crate::serde::is_default"
87 )]
88 pub acknowledgements: AcknowledgementsConfig,
89}
90
91impl LocalDatadogCommonConfig {
92 pub fn new(
93 endpoint: Option<String>,
94 site: Option<String>,
95 default_api_key: Option<SensitiveString>,
96 ) -> Self {
97 Self {
98 endpoint,
99 site,
100 default_api_key,
101 ..Default::default()
102 }
103 }
104
105 pub fn with_globals(
106 &self,
107 config: datadog::Options,
108 ) -> Result<DatadogCommonConfig, ConfigurationError> {
109 Ok(DatadogCommonConfig {
110 endpoint: self.endpoint.clone(),
111 site: self.site.clone().unwrap_or(config.site),
112 default_api_key: self
113 .default_api_key
114 .clone()
115 .or(config.api_key)
116 .ok_or(ConfigurationError::ApiKeyRequired)?,
117 acknowledgements: self.acknowledgements,
118 })
119 }
120}
121
122#[derive(Debug, Snafu, PartialEq, Eq)]
123pub enum ConfigurationError {
124 #[snafu(display("API Key must be specified."))]
125 ApiKeyRequired,
126}
127
128#[derive(Clone, Debug, Default)]
129pub struct DatadogCommonConfig {
130 pub endpoint: Option<String>,
131 pub site: String,
132 pub default_api_key: SensitiveString,
133 pub acknowledgements: AcknowledgementsConfig,
134}
135
136impl DatadogCommonConfig {
137 pub fn build_healthcheck<C>(&self, client: HttpClient<Body, C>) -> crate::Result<Healthcheck>
140 where
141 C: Connect + Clone + Send + Sync + 'static,
142 {
143 let validate_endpoint = self.get_api_endpoint("/api/v1/validate")?;
144
145 let api_key: String = self.default_api_key.clone().into();
146
147 Ok(build_healthcheck_future(client, validate_endpoint, api_key).boxed())
148 }
149
150 fn get_api_endpoint(&self, path: &str) -> crate::Result<Uri> {
156 let base = datadog::get_api_base_endpoint(self.endpoint.as_deref(), self.site.as_str());
157 let endpoint = HttpEndpoint::parse(&base)?.append_path(path)?;
158 Ok(endpoint.into_uri())
159 }
160}
161
162async fn build_healthcheck_future<C>(
164 client: HttpClient<Body, C>,
165 validate_endpoint: Uri,
166 api_key: String,
167) -> crate::Result<()>
168where
169 C: Connect + Clone + Send + Sync + 'static,
170{
171 let request = Request::get(validate_endpoint)
172 .header("DD-API-KEY", api_key)
173 .body(hyper::Body::empty())
174 .map_err(|e| format!("Failed to make HTTP(S) request: {e:?}"))?;
175
176 let response = client.send(request).await?;
177
178 match response.status() {
179 StatusCode::OK => Ok(()),
180 other => Err(HealthcheckError::UnexpectedStatus { status: other }.into()),
181 }
182}
183
184#[derive(Debug, Snafu)]
185pub enum DatadogApiError {
186 #[snafu(display("Failed to make HTTP(S) request: {}", error))]
187 HttpError { error: HttpError },
188 #[snafu(display("Client request was not valid for unknown reasons."))]
189 BadRequest,
190 #[snafu(display("Client request was unauthorized."))]
191 Unauthorized,
192 #[snafu(display("Client request was forbidden."))]
193 Forbidden,
194 #[snafu(display("Client request timed out."))]
195 RequestTimeout,
196 #[snafu(display("Client sent a payload that is too large."))]
197 PayloadTooLarge,
198 #[snafu(display("Client sent too many requests (rate limiting)."))]
199 TooManyRequests,
200 #[snafu(display("Client request was invalid."))]
201 ClientError,
202 #[snafu(display("Server responded with an error."))]
203 ServerError,
204}
205
206impl DatadogApiError {
207 pub fn from_result(
210 result: Result<http::Response<Body>, HttpError>,
211 ) -> Result<http::Response<Body>, DatadogApiError> {
212 match result {
213 Ok(response) => {
214 match response.status() {
215 s if s.is_success() => Ok(response),
233 StatusCode::BAD_REQUEST => Err(DatadogApiError::BadRequest),
234 StatusCode::UNAUTHORIZED => Err(DatadogApiError::Unauthorized),
235 StatusCode::FORBIDDEN => Err(DatadogApiError::Forbidden),
236 StatusCode::REQUEST_TIMEOUT => Err(DatadogApiError::RequestTimeout),
237 StatusCode::PAYLOAD_TOO_LARGE => Err(DatadogApiError::PayloadTooLarge),
238 StatusCode::TOO_MANY_REQUESTS => Err(DatadogApiError::TooManyRequests),
239 s if s.is_client_error() => Err(DatadogApiError::ClientError),
240 _ => Err(DatadogApiError::ServerError),
241 }
242 }
243 Err(error) => Err(DatadogApiError::HttpError { error }),
244 }
245 }
246
247 pub const fn is_retriable(&self) -> bool {
248 match self {
249 DatadogApiError::HttpError { error } => error.is_retriable(),
256 DatadogApiError::BadRequest | DatadogApiError::PayloadTooLarge => false,
257 DatadogApiError::ServerError
258 | DatadogApiError::ClientError
259 | DatadogApiError::Unauthorized
260 | DatadogApiError::Forbidden
261 | DatadogApiError::RequestTimeout
262 | DatadogApiError::TooManyRequests => true,
263 }
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn local_config_with_no_overrides() {
273 let local = LocalDatadogCommonConfig::new(
274 None,
275 Some("potato.com".into()),
276 Some("key".to_string().into()),
277 );
278 let global = datadog::Options {
279 api_key: Some("more key".to_string().into()),
280 site: "tomato.com".into(),
281 };
282
283 let overridden = local.with_globals(global).unwrap();
284
285 assert_eq!(None, overridden.endpoint);
286 assert_eq!("potato.com".to_string(), overridden.site);
287 assert_eq!(
288 SensitiveString::from("key".to_string()),
289 overridden.default_api_key
290 );
291 }
292
293 #[test]
294 fn local_config_with_overrides() {
295 let local = LocalDatadogCommonConfig::new(None, None, None);
296 let global = datadog::Options {
297 api_key: Some("more key".to_string().into()),
298 site: "tomato.com".into(),
299 };
300
301 let overridden = local.with_globals(global).unwrap();
302
303 assert_eq!(None, overridden.endpoint);
304 assert_eq!("tomato.com".to_string(), overridden.site);
305 assert_eq!(
306 SensitiveString::from("more key".to_string()),
307 overridden.default_api_key
308 );
309 }
310
311 #[test]
312 fn no_api_key() {
313 let local = LocalDatadogCommonConfig::new(None, None, None);
314 let global = datadog::Options {
315 api_key: None,
316 site: "tomato.com".into(),
317 };
318
319 let error = local.with_globals(global).unwrap_err();
320 assert_eq!(ConfigurationError::ApiKeyRequired, error);
321 }
322
323 #[test]
324 fn get_api_endpoint_defaults_missing_scheme_to_https() {
325 let config = DatadogCommonConfig {
326 endpoint: Some("localhost:8080".to_string()),
327 site: "datadoghq.com".to_string(),
328 default_api_key: SensitiveString::from("key".to_string()),
329 acknowledgements: Default::default(),
330 };
331 assert_eq!(
332 config
333 .get_api_endpoint("/api/v1/validate")
334 .unwrap()
335 .to_string(),
336 "https://localhost:8080/api/v1/validate"
337 );
338 let default = DatadogCommonConfig {
340 endpoint: None,
341 site: "datadoghq.com".to_string(),
342 default_api_key: SensitiveString::from("key".to_string()),
343 acknowledgements: Default::default(),
344 };
345 assert_eq!(
346 default
347 .get_api_endpoint("/api/v1/validate")
348 .unwrap()
349 .to_string(),
350 "https://api.datadoghq.com/api/v1/validate"
351 );
352 }
353}