From 775bd3895d445cdd5ed8ad77f7125e03ef97512b Mon Sep 17 00:00:00 2001 From: John Chrostek Date: Tue, 11 Nov 2025 13:16:59 -0500 Subject: [PATCH] [SLES-2547] add metric namespace for DogStatsD --- bottlecap/Cargo.lock | 32 +++-------- bottlecap/Cargo.toml | 4 +- bottlecap/src/bin/bottlecap/main.rs | 1 + bottlecap/src/config/env.rs | 10 ++++ bottlecap/src/config/mod.rs | 84 +++++++++++++++++++++++++++++ bottlecap/src/config/yaml.rs | 1 + 6 files changed, 106 insertions(+), 26 deletions(-) diff --git a/bottlecap/Cargo.lock b/bottlecap/Cargo.lock index f8ef42308..5edcb7995 100644 --- a/bottlecap/Cargo.lock +++ b/bottlecap/Cargo.lock @@ -778,7 +778,7 @@ dependencies = [ [[package]] name = "datadog-fips" version = "0.1.0" -source = "git+https://github.com/DataDog/serverless-components?rev=05b9dee1ac7c72d296ea12e85685c026cb6dbaaf#05b9dee1ac7c72d296ea12e85685c026cb6dbaaf" +source = "git+https://github.com/DataDog/serverless-components?rev=aa7961984f221ec3f1048ebf3379c4b94a33be0e#aa7961984f221ec3f1048ebf3379c4b94a33be0e" dependencies = [ "reqwest", "rustls", @@ -994,7 +994,7 @@ dependencies = [ [[package]] name = "dogstatsd" version = "0.1.0" -source = "git+https://github.com/DataDog/serverless-components?rev=05b9dee1ac7c72d296ea12e85685c026cb6dbaaf#05b9dee1ac7c72d296ea12e85685c026cb6dbaaf" +source = "git+https://github.com/DataDog/serverless-components?rev=aa7961984f221ec3f1048ebf3379c4b94a33be0e#aa7961984f221ec3f1048ebf3379c4b94a33be0e" dependencies = [ "datadog-fips", "datadog-protos", @@ -1119,12 +1119,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - [[package]] name = "flate2" version = "1.1.2" @@ -1648,7 +1642,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.0", "tokio", "tower-service", "tracing", @@ -1878,7 +1872,7 @@ dependencies = [ "ena", "itertools 0.11.0", "lalrpop-util", - "petgraph 0.6.5", + "petgraph", "pico-args", "regex", "regex-syntax 0.8.5", @@ -2328,17 +2322,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "fixedbitset 0.4.2", - "indexmap 2.10.0", -] - -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset 0.5.7", + "fixedbitset", "indexmap 2.10.0", ] @@ -2519,11 +2503,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck", - "itertools 0.11.0", + "itertools 0.14.0", "log", "multimap", "once_cell", - "petgraph 0.7.1", + "petgraph", "prettyplease", "prost", "prost-types", @@ -2539,7 +2523,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.104", diff --git a/bottlecap/Cargo.toml b/bottlecap/Cargo.toml index 7e95ba098..3f66aa0b1 100644 --- a/bottlecap/Cargo.toml +++ b/bottlecap/Cargo.toml @@ -63,8 +63,8 @@ datadog-trace-utils = { git = "https://github.com/DataDog/libdatadog", rev = "ba datadog-trace-normalization = { git = "https://github.com/DataDog/libdatadog", rev = "ba8955394cf35cf24a1a508fbe6264ad84702567" } datadog-trace-obfuscation = { git = "https://github.com/DataDog/libdatadog", rev = "ba8955394cf35cf24a1a508fbe6264ad84702567" } datadog-trace-stats = { git = "https://github.com/DataDog/libdatadog", rev = "ba8955394cf35cf24a1a508fbe6264ad84702567" } -dogstatsd = { git = "https://github.com/DataDog/serverless-components", rev = "05b9dee1ac7c72d296ea12e85685c026cb6dbaaf", default-features = false } -datadog-fips = { git = "https://github.com/DataDog/serverless-components", rev = "05b9dee1ac7c72d296ea12e85685c026cb6dbaaf", default-features = false } +dogstatsd = { git = "https://github.com/DataDog/serverless-components", rev = "aa7961984f221ec3f1048ebf3379c4b94a33be0e", default-features = false } +datadog-fips = { git = "https://github.com/DataDog/serverless-components", rev = "aa7961984f221ec3f1048ebf3379c4b94a33be0e", default-features = false } libddwaf = { version = "1.28.1", git = "https://github.com/DataDog/libddwaf-rust", rev = "d1534a158d976bd4f747bf9fcc58e0712d2d17fc", default-features = false, features = ["serde"] } [dev-dependencies] diff --git a/bottlecap/src/bin/bottlecap/main.rs b/bottlecap/src/bin/bottlecap/main.rs index b4eb69eda..2d7f5fb1b 100644 --- a/bottlecap/src/bin/bottlecap/main.rs +++ b/bottlecap/src/bin/bottlecap/main.rs @@ -1164,6 +1164,7 @@ async fn start_dogstatsd( let dogstatsd_config = DogStatsDConfig { host: EXTENSION_HOST.to_string(), port: DOGSTATSD_PORT, + metric_namespace: config.statsd_metric_namespace.clone(), }; let cancel_token = tokio_util::sync::CancellationToken::new(); let dogstatsd_agent = DogStatsD::new( diff --git a/bottlecap/src/config/env.rs b/bottlecap/src/config/env.rs index 1ba53dffa..407837379 100644 --- a/bottlecap/src/config/env.rs +++ b/bottlecap/src/config/env.rs @@ -266,6 +266,11 @@ pub struct EnvConfig { #[serde(deserialize_with = "deserialize_option_lossless")] pub metrics_config_compression_level: Option, + /// @env `DD_STATSD_METRIC_NAMESPACE` + /// Prefix all `StatsD` metrics with a namespace. + #[serde(deserialize_with = "deserialize_optional_string")] + pub statsd_metric_namespace: Option, + // OTLP // // - APM / Traces @@ -521,6 +526,10 @@ fn merge_config(config: &mut Config, env_config: &EnvConfig) { ); merge_option_to_value!(config, env_config, metrics_config_compression_level); + if let Some(namespace) = &env_config.statsd_metric_namespace { + config.statsd_metric_namespace = super::validate_metric_namespace(namespace); + } + // OTLP merge_option_to_value!(config, env_config, otlp_config_traces_enabled); merge_option_to_value!( @@ -938,6 +947,7 @@ mod tests { otlp_config_metrics_summaries_mode: Some("quantiles".to_string()), otlp_config_traces_probabilistic_sampler_sampling_percentage: Some(50), otlp_config_logs_enabled: true, + statsd_metric_namespace: None, api_key_secret_arn: "arn:aws:secretsmanager:region:account:secret:datadog-api-key" .to_string(), kms_api_key: "test-kms-key".to_string(), diff --git a/bottlecap/src/config/mod.rs b/bottlecap/src/config/mod.rs index 66fd00542..9e55cc07a 100644 --- a/bottlecap/src/config/mod.rs +++ b/bottlecap/src/config/mod.rs @@ -298,6 +298,7 @@ pub struct Config { // Metrics pub metrics_config_compression_level: i32, + pub statsd_metric_namespace: Option, // OTLP // @@ -411,6 +412,7 @@ impl Default for Config { // Metrics metrics_config_compression_level: 3, + statsd_metric_namespace: None, // OTLP otlp_config_traces_enabled: true, @@ -699,6 +701,39 @@ where } } +fn validate_metric_namespace(namespace: &str) -> Option { + let trimmed = namespace.trim(); + if trimmed.is_empty() { + return None; + } + + let mut chars = trimmed.chars(); + + if let Some(first_char) = chars.next() { + if !first_char.is_ascii_alphabetic() { + error!( + "DD_STATSD_METRIC_NAMESPACE must start with a letter, got: '{}'. Ignoring namespace.", + trimmed + ); + return None; + } + } else { + return None; + } + + if let Some(invalid_char) = + chars.find(|&ch| !ch.is_ascii_alphanumeric() && ch != '_' && ch != '.') + { + error!( + "DD_STATSD_METRIC_NAMESPACE contains invalid character '{}' in '{}'. Only ASCII alphanumerics, underscores, and periods are allowed. Ignoring namespace.", + invalid_char, trimmed + ); + return None; + } + + Some(trimmed.to_string()) +} + pub fn deserialize_option_lossless<'de, D, T>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, @@ -1476,4 +1511,53 @@ pub mod tests { expected.insert("valid".to_string(), "tag".to_string()); assert_eq!(result.tags, expected); } + + #[test] + fn test_validate_metric_namespace_valid() { + assert_eq!( + validate_metric_namespace("myapp"), + Some("myapp".to_string()) + ); + assert_eq!( + validate_metric_namespace("my_app"), + Some("my_app".to_string()) + ); + assert_eq!( + validate_metric_namespace("my.app"), + Some("my.app".to_string()) + ); + assert_eq!( + validate_metric_namespace("MyApp123"), + Some("MyApp123".to_string()) + ); + assert_eq!( + validate_metric_namespace(" myapp "), + Some("myapp".to_string()) + ); + } + + #[test] + fn test_validate_metric_namespace_empty() { + assert_eq!(validate_metric_namespace(""), None); + assert_eq!(validate_metric_namespace(" "), None); + assert_eq!(validate_metric_namespace("\t\n"), None); + } + + #[test] + fn test_validate_metric_namespace_invalid_first_char() { + assert_eq!(validate_metric_namespace("1myapp"), None); + assert_eq!(validate_metric_namespace("_myapp"), None); + assert_eq!(validate_metric_namespace(".myapp"), None); + assert_eq!(validate_metric_namespace("-myapp"), None); + } + + #[test] + fn test_validate_metric_namespace_invalid_chars() { + assert_eq!(validate_metric_namespace("my-app"), None); + assert_eq!(validate_metric_namespace("my app"), None); + assert_eq!(validate_metric_namespace("my@app"), None); + assert_eq!(validate_metric_namespace("my#app"), None); + assert_eq!(validate_metric_namespace("my$app"), None); + assert_eq!(validate_metric_namespace("my!app"), None); + } } diff --git a/bottlecap/src/config/yaml.rs b/bottlecap/src/config/yaml.rs index 4df1f85b2..c800455f7 100644 --- a/bottlecap/src/config/yaml.rs +++ b/bottlecap/src/config/yaml.rs @@ -993,6 +993,7 @@ api_security_sample_delay: 60 # Seconds apm_filter_tags_reject: None, apm_filter_tags_regex_require: None, apm_filter_tags_regex_reject: None, + statsd_metric_namespace: None, }; // Assert that