Skip to main content

vector/sinks/datadog/
mod.rs

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/// Shared configuration for Datadog sinks.
34/// Contains the maximum set of common settings that applies to all DD sink components.
35#[configurable_component]
36#[derive(Clone, Debug, Default)]
37#[serde(deny_unknown_fields)]
38pub struct LocalDatadogCommonConfig {
39    /// The endpoint to send observability data to.
40    ///
41    /// The endpoint must contain an HTTP scheme, and may specify a hostname or IP
42    /// address and port. The API path should NOT be specified as this is handled by
43    /// the sink.
44    ///
45    /// If set, overrides the `site` option.
46    #[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    /// The Datadog [site][dd_site] to send observability data to.
53    ///
54    /// This value can also be set by specifying the `DD_SITE` environment variable.
55    /// The value specified here takes precedence over the environment variable.
56    ///
57    /// If not specified by the environment variable, a default value of
58    /// `datadoghq.com` is taken.
59    ///
60    /// [dd_site]: https://docs.datadoghq.com/getting_started/site
61    #[configurable(metadata(docs::examples = "us3.datadoghq.com"))]
62    #[configurable(metadata(docs::examples = "datadoghq.eu"))]
63    pub site: Option<String>,
64
65    /// The default Datadog [API key][api_key] to use in authentication of HTTP requests.
66    ///
67    /// If an event has a Datadog [API key][api_key] set explicitly in its metadata, it takes
68    /// precedence over this setting.
69    ///
70    /// This value can also be set by specifying the `DD_API_KEY` environment variable.
71    /// The value specified here takes precedence over the environment variable.
72    ///
73    /// [api_key]: https://docs.datadoghq.com/api/?lang=bash#authentication
74    /// [global_options]: /docs/reference/configuration/global-options/#datadog
75    #[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    /// Returns a `Healthcheck` which is a future that will be used to ensure the
139    /// `<site>/api/v1/validate` endpoint is reachable.
140    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    /// Gets the API endpoint with a given suffix path.
152    ///
153    /// If `endpoint` is not specified, we fallback to `site`.
154    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
160/// Makes a GET HTTP request to `<site>/api/v1/validate` using the provided client and API key.
161async 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    /// Common DatadogApiError handling for HTTP Responses.
206    /// Returns Ok(response) if the response was Ok/Accepted.
207    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                    // From https://docs.datadoghq.com/api/latest/logs/:
214                    //
215                    // The status codes answered by the HTTP API are:
216                    // 200: OK (v1)
217                    // 202: Accepted (v2)
218                    // 400: Bad request (likely an issue in the payload
219                    //      formatting)
220                    // 401: Unauthorized (likely a missing API Key))
221                    // 403: Permission issue (likely using an invalid API Key)
222                    // 408: Request Timeout, request should be retried after some
223                    // 413: Payload too large (batch is above 5MB uncompressed)
224                    // 429: Too Many Requests, request should be retried after some time
225                    // 500: Internal Server Error, the server encountered an unexpected condition
226                    //      that prevented it from fulfilling the request, request should be
227                    //      retried after some time
228                    // 503: Service Unavailable, the server is not ready to handle the request
229                    //      probably because it is overloaded, request should be retried after some time
230                    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            // This retry logic will be expanded further, but specifically retrying unauthorized
248            // requests and lower level HttpErrors for now.
249            // I verified using `curl` that `403` is the response code for this.
250            //
251            // https://github.com/vectordotdev/vector/issues/10870
252            // https://github.com/vectordotdev/vector/issues/12220
253            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}