Skip to content

Commit

Permalink
fix(datadog sinks): Compute proper validate endpoint (vectordotdev#20644
Browse files Browse the repository at this point in the history
)

* fix(datadog sinks): Compute proper validate endpoint

When no `endpoint` is configured in the `datadog_*` sinks, they use the `site`
instead to compute separate endpoints for the API key validation and for the
actual service. However, when an `endpoint` _is_ configured, that URI is used
for both, leading to a HTTP 404 Not Found error when trying to run the health
check.

This change pulls in the API endpoint computation used by the Datadog Agent to
rewrite the `endpoint` URI when it is detected as a valid Datadog domain.

* Add spelling allow for "ddog"

* Fix changelog grammar
  • Loading branch information
bruceg authored Jun 11, 2024
1 parent 36abc45 commit 9be2eeb
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 34 deletions.
1 change: 1 addition & 0 deletions .github/actions/spelling/allow.txt
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ datacenter
datadog
datadoghq
datanode
ddog
debian
demuxing
dfs
Expand Down
3 changes: 3 additions & 0 deletions changelog.d/datadog-api-endpoint.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The `endpoint` in the Datadog sinks is rewritten the same as the Datadog Agent
does to produce the API validation endpoint to avoid a HTTP 404 Not Found error
when running the health check.
27 changes: 23 additions & 4 deletions src/common/datadog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
// Datadog component type, whether it's used in integration tests, etc.
#![allow(dead_code)]
#![allow(unreachable_pub)]

use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use vector_lib::{
event::DatadogMetricOriginMetadata, schema::meaning, sensitive_string::SensitiveString,
Expand Down Expand Up @@ -79,10 +82,26 @@ pub struct DatadogPoint<T>(pub i64, pub T);
/// Gets the base API endpoint to use for any calls to Datadog.
///
/// If `endpoint` is not specified, we fallback to `site`.
pub(crate) fn get_api_base_endpoint(endpoint: Option<&String>, site: &str) -> String {
endpoint
.cloned()
.unwrap_or_else(|| format!("https://api.{}", site))
pub(crate) fn get_api_base_endpoint(endpoint: Option<&str>, site: &str) -> String {
endpoint.map_or_else(|| format!("https://api.{}", site), compute_api_endpoint)
}

fn compute_api_endpoint(endpoint: &str) -> String {
// This mechanism is derived from the forwarder health check in the Datadog Agent:
// https://github.com/DataDog/datadog-agent/blob/cdcf0fc809b9ac1cd6e08057b4971c7dbb8dbe30/comp/forwarder/defaultforwarder/forwarder_health.go#L45-L47
// https://github.com/DataDog/datadog-agent/blob/cdcf0fc809b9ac1cd6e08057b4971c7dbb8dbe30/comp/forwarder/defaultforwarder/forwarder_health.go#L188-L190
static DOMAIN_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(:?[a-z]{2}\d\.)?(datadoghq\.[a-z]+|ddog-gov\.com)/*$")
.expect("Could not build Datadog domain regex")
});

// If the endpoint domain matches one of the known Datadog domains, prefix that domain with
// `api.` to produce the API endpoint; otherwise, just use the given endpoint as-is.
if let Some(caps) = DOMAIN_REGEX.captures(endpoint) {
format!("https://api.{}", &caps[1])
} else {
endpoint.into()
}
}

/// Default settings to use for Datadog components.
Expand Down
22 changes: 6 additions & 16 deletions src/sinks/datadog/events/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,16 @@ use vector_lib::configurable::configurable_component;
use vector_lib::schema;
use vrl::value::Kind;

use super::{
service::{DatadogEventsResponse, DatadogEventsService},
sink::DatadogEventsSink,
};
use crate::{
common::datadog,
config::{AcknowledgementsConfig, GenerateConfig, Input, SinkConfig, SinkContext},
http::HttpClient,
sinks::{
datadog::{
events::{
service::{DatadogEventsResponse, DatadogEventsService},
sink::DatadogEventsSink,
},
get_api_base_endpoint, DatadogCommonConfig, LocalDatadogCommonConfig,
},
datadog::{DatadogCommonConfig, LocalDatadogCommonConfig},
util::{http::HttpStatusRetryLogic, ServiceBuilderExt, TowerRequestConfig},
Healthcheck, VectorSink,
},
Expand Down Expand Up @@ -49,14 +47,6 @@ impl GenerateConfig for DatadogEventsConfig {
}

impl DatadogEventsConfig {
fn get_api_events_endpoint(&self, dd_common: &DatadogCommonConfig) -> http::Uri {
let api_base_endpoint =
get_api_base_endpoint(dd_common.endpoint.as_ref(), dd_common.site.as_str());

// We know this URI will be valid since we have just built it up ourselves.
http::Uri::try_from(format!("{}/api/v1/events", api_base_endpoint)).expect("URI not valid")
}

fn build_client(&self, proxy: &ProxyConfig) -> crate::Result<HttpClient> {
let tls = MaybeTlsSettings::from_config(&self.dd_common.tls, false)?;
let client = HttpClient::new(tls, proxy)?;
Expand All @@ -69,7 +59,7 @@ impl DatadogEventsConfig {
client: HttpClient,
) -> crate::Result<VectorSink> {
let service = DatadogEventsService::new(
self.get_api_events_endpoint(dd_common),
dd_common.get_api_endpoint("/api/v1/events")?,
dd_common.default_api_key.clone(),
client,
);
Expand Down
25 changes: 11 additions & 14 deletions src/sinks/datadog/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ use vector_lib::{
sensitive_string::SensitiveString, tls::TlsEnableableConfig,
};

use super::Healthcheck;
use crate::{
common::datadog::{self, get_api_base_endpoint},
common::datadog,
http::{HttpClient, HttpError},
sinks::HealthcheckError,
};

use super::Healthcheck;

#[cfg(feature = "sinks-datadog_events")]
pub mod events;
#[cfg(feature = "sinks-datadog_logs")]
Expand Down Expand Up @@ -139,13 +138,20 @@ impl DatadogCommonConfig {
/// Returns a `Healthcheck` which is a future that will be used to ensure the
/// `<site>/api/v1/validate` endpoint is reachable.
pub fn build_healthcheck(&self, client: HttpClient) -> crate::Result<Healthcheck> {
let validate_endpoint =
get_api_validate_endpoint(self.endpoint.as_ref(), self.site.as_str())?;
let validate_endpoint = self.get_api_endpoint("/api/v1/validate")?;

let api_key: String = self.default_api_key.clone().into();

Ok(build_healthcheck_future(client, validate_endpoint, api_key).boxed())
}

/// Gets the API endpoint with a given suffix path.
///
/// If `endpoint` is not specified, we fallback to `site`.
fn get_api_endpoint(&self, path: &str) -> crate::Result<Uri> {
let base = datadog::get_api_base_endpoint(self.endpoint.as_deref(), self.site.as_str());
[&base, path].join("").parse().map_err(Into::into)
}
}

/// Makes a GET HTTP request to `<site>/api/v1/validate` using the provided client and API key.
Expand All @@ -167,15 +173,6 @@ async fn build_healthcheck_future(
}
}

/// Gets the API endpoint for validating credentials.
///
/// If `endpoint` is not specified, we fallback to `site`.
fn get_api_validate_endpoint(endpoint: Option<&String>, site: &str) -> crate::Result<Uri> {
let base = get_api_base_endpoint(endpoint, site);
let validate = format!("{}{}", base, "/api/v1/validate");
validate.parse::<Uri>().map_err(Into::into)
}

#[derive(Debug, Snafu)]
pub enum DatadogApiError {
#[snafu(display("Failed to make HTTP(S) request: {}", error))]
Expand Down

0 comments on commit 9be2eeb

Please sign in to comment.