diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index fb711d1ed0637..5eaed35ee9c30 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -984,6 +984,7 @@ skywalking slashin slf slideover +slugified smallvec smartphone Smol diff --git a/src/app.rs b/src/app.rs index e8ac0a6061233..75893718347e1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -531,6 +531,18 @@ fn build_enterprise( config: &mut Config, config_paths: Vec, ) -> Result>>, ExitCode> { + use crate::ENTERPRISE_ENABLED; + + ENTERPRISE_ENABLED + .set( + config + .enterprise + .as_ref() + .map(|e| e.enabled) + .unwrap_or_default(), + ) + .expect("double initialization of enterprise enabled flag"); + match EnterpriseMetadata::try_from(&*config) { Ok(metadata) => { let enterprise = EnterpriseReporter::new(); diff --git a/src/config/sink.rs b/src/config/sink.rs index bf865eec40463..050e437f87a77 100644 --- a/src/config/sink.rs +++ b/src/config/sink.rs @@ -235,12 +235,27 @@ pub trait SinkConfig: DynClone + NamedComponent + core::fmt::Debug + Send + Sync dyn_clone::clone_trait_object!(SinkConfig); -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct SinkContext { pub healthcheck: SinkHealthcheckOptions, pub globals: GlobalOptions, pub proxy: ProxyConfig, pub schema: schema::Options, + pub app_name: String, + pub app_name_slug: String, +} + +impl Default for SinkContext { + fn default() -> Self { + Self { + healthcheck: Default::default(), + globals: Default::default(), + proxy: Default::default(), + schema: Default::default(), + app_name: crate::get_app_name().to_string(), + app_name_slug: crate::get_slugified_app_name(), + } + } } impl SinkContext { diff --git a/src/http.rs b/src/http.rs index 7eee5646f0ae3..84e0364c00515 100644 --- a/src/http.rs +++ b/src/http.rs @@ -82,9 +82,10 @@ where let proxy_connector = build_proxy_connector(tls_settings.into(), proxy_config)?; let client = client_builder.build(proxy_connector.clone()); + let app_name = crate::get_app_name(); let version = crate::get_version(); - let user_agent = HeaderValue::from_str(&format!("Vector/{}", version)) - .expect("Invalid header value for version!"); + let user_agent = HeaderValue::from_str(&format!("{}/{}", app_name, version)) + .expect("Invalid header value for user-agent!"); Ok(HttpClient { client, diff --git a/src/lib.rs b/src/lib.rs index c5ab3f1692955..a6568cf3523a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,6 +124,38 @@ pub use source_sender::SourceSender; pub use vector_common::{shutdown, Error, Result}; pub use vector_core::{event, metrics, schema, tcp, tls}; +static APP_NAME_SLUG: std::sync::OnceLock = std::sync::OnceLock::new(); + +/// Flag denoting whether or not enterprise features are enabled. +#[cfg(feature = "enterprise")] +pub static ENTERPRISE_ENABLED: std::sync::OnceLock = std::sync::OnceLock::new(); + +/// The name used to identify this Vector application. +/// +/// This can be set at compile-time through the VECTOR_APP_NAME env variable. +/// Defaults to "Vector". +pub fn get_app_name() -> &'static str { + #[cfg(not(feature = "enterprise"))] + let app_name = "Vector"; + #[cfg(feature = "enterprise")] + let app_name = if *ENTERPRISE_ENABLED.get().unwrap_or(&false) { + "Vector Enterprise" + } else { + "Vector" + }; + + option_env!("VECTOR_APP_NAME").unwrap_or(app_name) +} + +/// Returns a slugified version of the name used to identify this Vector application. +/// +/// Defaults to "vector". +pub fn get_slugified_app_name() -> String { + APP_NAME_SLUG + .get_or_init(|| get_app_name().to_lowercase().replace(' ', "-")) + .clone() +} + /// The current version of Vector in simplified format. /// `-nightly`. pub fn vector_version() -> impl std::fmt::Display { diff --git a/src/sinks/datadog/logs/config.rs b/src/sinks/datadog/logs/config.rs index 37f729b5d8d63..13050d3a87da8 100644 --- a/src/sinks/datadog/logs/config.rs +++ b/src/sinks/datadog/logs/config.rs @@ -109,7 +109,11 @@ impl DatadogLogsConfig { self.get_uri().scheme_str().unwrap_or("http").to_string() } - pub fn build_processor(&self, client: HttpClient) -> crate::Result { + pub fn build_processor( + &self, + client: HttpClient, + dd_evp_origin: String, + ) -> crate::Result { let default_api_key: Arc = Arc::from(self.dd_common.default_api_key.inner()); let request_limits = self.request.tower.unwrap_with(&Default::default()); @@ -128,6 +132,7 @@ impl DatadogLogsConfig { client, self.get_uri(), self.request.headers.clone(), + dd_evp_origin, )?); let encoding = self.encoding.clone(); @@ -164,7 +169,7 @@ impl SinkConfig for DatadogLogsConfig { .dd_common .build_healthcheck(client.clone(), self.region.as_ref())?; - let sink = self.build_processor(client)?; + let sink = self.build_processor(client, cx.app_name_slug)?; Ok((sink, healthcheck)) } diff --git a/src/sinks/datadog/logs/service.rs b/src/sinks/datadog/logs/service.rs index 47effa754df5e..09e9a81490370 100644 --- a/src/sinks/datadog/logs/service.rs +++ b/src/sinks/datadog/logs/service.rs @@ -95,6 +95,7 @@ pub struct LogApiService { client: HttpClient, uri: Uri, user_provided_headers: IndexMap, + dd_evp_headers: IndexMap, } impl LogApiService { @@ -102,13 +103,23 @@ impl LogApiService { client: HttpClient, uri: Uri, headers: IndexMap, + dd_evp_origin: String, ) -> crate::Result { - let headers = validate_headers(&headers)?; + let user_provided_headers = validate_headers(&headers)?; + + let dd_evp_headers = &[ + ("DD-EVP-ORIGIN".to_string(), dd_evp_origin), + ("DD-EVP-ORIGIN-VERSION".to_string(), crate::get_version()), + ] + .into_iter() + .collect(); + let dd_evp_headers = validate_headers(dd_evp_headers)?; Ok(Self { client, uri, - user_provided_headers: headers, + user_provided_headers, + dd_evp_headers, }) } } @@ -128,8 +139,6 @@ impl Service for LogApiService { let mut client = self.client.clone(); let http_request = Request::post(&self.uri) .header(CONTENT_TYPE, "application/json") - .header("DD-EVP-ORIGIN", "vector") - .header("DD-EVP-ORIGIN-VERSION", crate::get_version()) .header("DD-API-KEY", request.api_key.to_string()); let http_request = if let Some(ce) = request.compression.content_encoding() { @@ -149,6 +158,10 @@ impl Service for LogApiService { // Replace rather than append to any existing header values headers.insert(name, value.clone()); } + // Set DD EVP headers last so that they cannot be overridden. + for (name, value) in &self.dd_evp_headers { + headers.insert(name, value.clone()); + } } let http_request = http_request diff --git a/src/sinks/datadog/logs/tests.rs b/src/sinks/datadog/logs/tests.rs index c8ef154280e4f..ce2a828bd84f9 100644 --- a/src/sinks/datadog/logs/tests.rs +++ b/src/sinks/datadog/logs/tests.rs @@ -402,14 +402,13 @@ async fn enterprise_headers_v1() { } async fn enterprise_headers_inner(api_status: ApiStatus) { - let (mut config, cx) = load_sink::(indoc! {r#" + let (mut config, mut cx) = load_sink::(indoc! {r#" default_api_key = "atoken" compression = "none" - - [request] - headers.DD-EVP-ORIGIN = "vector-enterprise" "#}) .unwrap(); + cx.app_name = "Vector Enterprise".to_string(); + cx.app_name_slug = "vector-enterprise".to_string(); let addr = next_addr(); // Swap out the endpoint so we can force send it to our local server diff --git a/src/topology/builder.rs b/src/topology/builder.rs index a23f625334e0d..9215848d6c224 100644 --- a/src/topology/builder.rs +++ b/src/topology/builder.rs @@ -570,6 +570,8 @@ impl<'a> Builder<'a> { globals: self.config.global.clone(), proxy: ProxyConfig::merge_with_env(&self.config.global.proxy, sink.proxy()), schema: self.config.schema, + app_name: crate::get_app_name().to_string(), + app_name_slug: crate::get_slugified_app_name(), }; let (sink, healthcheck) = match sink.inner.build(cx).await {