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,
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::advanced))]
47 #[configurable(metadata(docs::examples = "http://127.0.0.1:8080"))]
48 #[configurable(metadata(docs::examples = "http://example.com:12345"))]
49 #[serde(default)]
50 pub endpoint: Option<String>,
51
52 #[configurable(metadata(docs::examples = "us3.datadoghq.com"))]
62 #[configurable(metadata(docs::examples = "datadoghq.eu"))]
63 pub site: Option<String>,
64
65 #[configurable(metadata(docs::examples = "${DATADOG_API_KEY_ENV_VAR}"))]
76 #[configurable(metadata(docs::examples = "ef8d5de700e7989468166c40fc8a0ccd"))]
77 pub default_api_key: Option<SensitiveString>,
78
79 #[configurable(derived)]
80 #[serde(default)]
81 pub tls: Option<TlsEnableableConfig>,
82
83 #[configurable(derived)]
84 #[serde(
85 default,
86 deserialize_with = "crate::serde::bool_or_struct",
87 skip_serializing_if = "crate::serde::is_default"
88 )]
89 pub acknowledgements: AcknowledgementsConfig,
90}
91
92impl LocalDatadogCommonConfig {
93 pub fn new(
94 endpoint: Option<String>,
95 site: Option<String>,
96 default_api_key: Option<SensitiveString>,
97 ) -> Self {
98 Self {
99 endpoint,
100 site,
101 default_api_key,
102 ..Default::default()
103 }
104 }
105
106 pub fn with_globals(
107 &self,
108 config: datadog::Options,
109 ) -> Result<DatadogCommonConfig, ConfigurationError> {
110 Ok(DatadogCommonConfig {
111 endpoint: self.endpoint.clone(),
112 site: self.site.clone().unwrap_or(config.site),
113 default_api_key: self
114 .default_api_key
115 .clone()
116 .or(config.api_key)
117 .ok_or(ConfigurationError::ApiKeyRequired)?,
118 acknowledgements: self.acknowledgements,
119 })
120 }
121}
122
123#[derive(Debug, Snafu, PartialEq, Eq)]
124pub enum ConfigurationError {
125 #[snafu(display("API Key must be specified."))]
126 ApiKeyRequired,
127}
128
129#[derive(Clone, Debug, Default)]
130pub struct DatadogCommonConfig {
131 pub endpoint: Option<String>,
132 pub site: String,
133 pub default_api_key: SensitiveString,
134 pub acknowledgements: AcknowledgementsConfig,
135}
136
137impl DatadogCommonConfig {
138 pub fn build_healthcheck<C>(&self, client: HttpClient<Body, C>) -> crate::Result<Healthcheck>
141 where
142 C: Connect + Clone + Send + Sync + 'static,
143 {
144 let validate_endpoint = self.get_api_endpoint("/api/v1/validate")?;
145
146 let api_key: String = self.default_api_key.clone().into();
147
148 Ok(build_healthcheck_future(client, validate_endpoint, api_key).boxed())
149 }
150
151 fn get_api_endpoint(&self, path: &str) -> crate::Result<Uri> {
155 let base = datadog::get_api_base_endpoint(self.endpoint.as_deref(), self.site.as_str());
156 [&base, path].join("").parse().map_err(Into::into)
157 }
158}
159
160async fn build_healthcheck_future<C>(
162 client: HttpClient<Body, C>,
163 validate_endpoint: Uri,
164 api_key: String,
165) -> crate::Result<()>
166where
167 C: Connect + Clone + Send + Sync + 'static,
168{
169 let request = Request::get(validate_endpoint)
170 .header("DD-API-KEY", api_key)
171 .body(hyper::Body::empty())
172 .map_err(|e| format!("Failed to make HTTP(S) request: {e:?}"))?;
173
174 let response = client.send(request).await?;
175
176 match response.status() {
177 StatusCode::OK => Ok(()),
178 other => Err(HealthcheckError::UnexpectedStatus { status: other }.into()),
179 }
180}
181
182#[derive(Debug, Snafu)]
183pub enum DatadogApiError {
184 #[snafu(display("Failed to make HTTP(S) request: {}", error))]
185 HttpError { error: HttpError },
186 #[snafu(display("Client request was not valid for unknown reasons."))]
187 BadRequest,
188 #[snafu(display("Client request was unauthorized."))]
189 Unauthorized,
190 #[snafu(display("Client request was forbidden."))]
191 Forbidden,
192 #[snafu(display("Client request timed out."))]
193 RequestTimeout,
194 #[snafu(display("Client sent a payload that is too large."))]
195 PayloadTooLarge,
196 #[snafu(display("Client sent too many requests (rate limiting)."))]
197 TooManyRequests,
198 #[snafu(display("Client request was invalid."))]
199 ClientError,
200 #[snafu(display("Server responded with an error."))]
201 ServerError,
202}
203
204impl DatadogApiError {
205 pub fn from_result(
208 result: Result<http::Response<Body>, HttpError>,
209 ) -> Result<http::Response<Body>, DatadogApiError> {
210 match result {
211 Ok(response) => {
212 match response.status() {
213 s if s.is_success() => Ok(response),
231 StatusCode::BAD_REQUEST => Err(DatadogApiError::BadRequest),
232 StatusCode::UNAUTHORIZED => Err(DatadogApiError::Unauthorized),
233 StatusCode::FORBIDDEN => Err(DatadogApiError::Forbidden),
234 StatusCode::REQUEST_TIMEOUT => Err(DatadogApiError::RequestTimeout),
235 StatusCode::PAYLOAD_TOO_LARGE => Err(DatadogApiError::PayloadTooLarge),
236 StatusCode::TOO_MANY_REQUESTS => Err(DatadogApiError::TooManyRequests),
237 s if s.is_client_error() => Err(DatadogApiError::ClientError),
238 _ => Err(DatadogApiError::ServerError),
239 }
240 }
241 Err(error) => Err(DatadogApiError::HttpError { error }),
242 }
243 }
244
245 pub const fn is_retriable(&self) -> bool {
246 match self {
247 DatadogApiError::HttpError { error } => error.is_retriable(),
254 DatadogApiError::BadRequest | DatadogApiError::PayloadTooLarge => false,
255 DatadogApiError::ServerError
256 | DatadogApiError::ClientError
257 | DatadogApiError::Unauthorized
258 | DatadogApiError::Forbidden
259 | DatadogApiError::RequestTimeout
260 | DatadogApiError::TooManyRequests => true,
261 }
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn local_config_with_no_overrides() {
271 let local = LocalDatadogCommonConfig::new(
272 None,
273 Some("potato.com".into()),
274 Some("key".to_string().into()),
275 );
276 let global = datadog::Options {
277 api_key: Some("more key".to_string().into()),
278 site: "tomato.com".into(),
279 };
280
281 let overridden = local.with_globals(global).unwrap();
282
283 assert_eq!(None, overridden.endpoint);
284 assert_eq!("potato.com".to_string(), overridden.site);
285 assert_eq!(
286 SensitiveString::from("key".to_string()),
287 overridden.default_api_key
288 );
289 }
290
291 #[test]
292 fn local_config_with_overrides() {
293 let local = LocalDatadogCommonConfig::new(None, None, None);
294 let global = datadog::Options {
295 api_key: Some("more key".to_string().into()),
296 site: "tomato.com".into(),
297 };
298
299 let overridden = local.with_globals(global).unwrap();
300
301 assert_eq!(None, overridden.endpoint);
302 assert_eq!("tomato.com".to_string(), overridden.site);
303 assert_eq!(
304 SensitiveString::from("more key".to_string()),
305 overridden.default_api_key
306 );
307 }
308
309 #[test]
310 fn no_api_key() {
311 let local = LocalDatadogCommonConfig::new(None, None, None);
312 let global = datadog::Options {
313 api_key: None,
314 site: "tomato.com".into(),
315 };
316
317 let error = local.with_globals(global).unwrap_err();
318 assert_eq!(ConfigurationError::ApiKeyRequired, error);
319 }
320}