From 502f005c56b8d51dee95424a9c1404df46e2aae4 Mon Sep 17 00:00:00 2001 From: Tianning Li Date: Tue, 11 Nov 2025 15:50:10 -0500 Subject: [PATCH] refactor(dogstatsd): move parse_metric_namespace to shared util module --- crates/datadog-serverless-compat/src/main.rs | 104 +--------------- crates/dogstatsd/src/lib.rs | 1 + crates/dogstatsd/src/util.rs | 124 +++++++++++++++++++ 3 files changed, 127 insertions(+), 102 deletions(-) create mode 100644 crates/dogstatsd/src/util.rs diff --git a/crates/datadog-serverless-compat/src/main.rs b/crates/datadog-serverless-compat/src/main.rs index e62305b8..4354609b 100644 --- a/crates/datadog-serverless-compat/src/main.rs +++ b/crates/datadog-serverless-compat/src/main.rs @@ -32,6 +32,7 @@ use dogstatsd::{ datadog::{MetricsIntakeUrlPrefix, RetryStrategy, Site}, dogstatsd::{DogStatsD, DogStatsDConfig}, flusher::{Flusher, FlusherConfig}, + util::parse_metric_namespace, }; use dogstatsd::metric::{SortedTags, EMPTY_TAGS}; @@ -74,7 +75,7 @@ pub async fn main() { .unwrap_or(true); let dd_statsd_metric_namespace: Option = env::var("DD_STATSD_METRIC_NAMESPACE") .ok() - .and_then(|val| validate_metric_namespace(&val)); + .and_then(|val| parse_metric_namespace(&val)); let https_proxy = env::var("DD_PROXY_HTTPS") .or_else(|_| env::var("HTTPS_PROXY")) @@ -234,104 +235,3 @@ async fn start_dogstatsd( (dogstatsd_cancel_token, metrics_flusher, handle) } - -fn validate_metric_namespace(namespace: &str) -> Option { - let trimmed = namespace.trim(); - if trimmed.is_empty() { - return None; - } - - let mut chars = trimmed.chars(); - - // Check first character is a letter - 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; - } - - // Check remaining characters are valid (alphanumeric, underscore, or period) - 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()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[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("a1.b2_c3"), - Some("a1.b2_c3".to_string()) - ); - } - - #[test] - fn test_validate_metric_namespace_with_whitespace() { - assert_eq!( - validate_metric_namespace(" myapp "), - Some("myapp".to_string()) - ); - assert_eq!( - validate_metric_namespace("\tmyapp\n"), - 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_start() { - 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_characters() { - 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/crates/dogstatsd/src/lib.rs b/crates/dogstatsd/src/lib.rs index e30bb861..9af0e43f 100644 --- a/crates/dogstatsd/src/lib.rs +++ b/crates/dogstatsd/src/lib.rs @@ -17,3 +17,4 @@ pub mod errors; pub mod flusher; pub mod metric; pub mod origin; +pub mod util; diff --git a/crates/dogstatsd/src/util.rs b/crates/dogstatsd/src/util.rs new file mode 100644 index 00000000..c0137383 --- /dev/null +++ b/crates/dogstatsd/src/util.rs @@ -0,0 +1,124 @@ +// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Utility functions for DogStatsD operations. + +/// Parses and validates a metric namespace string according to Datadog naming conventions. +/// +/// A valid namespace must: +/// - Start with an ASCII letter +/// - Contain only ASCII alphanumerics, underscores, or periods +/// - Not be empty or contain only whitespace +/// +/// Whitespace is automatically trimmed from the input. +/// +/// # Arguments +/// +/// * `namespace` - The namespace string to parse +/// +/// # Returns +/// +/// * `Some(String)` - The trimmed namespace if valid +/// * `None` - If the namespace is invalid or empty +/// +/// # Examples +/// +/// ``` +/// use dogstatsd::util::parse_metric_namespace; +/// +/// assert_eq!(parse_metric_namespace("myapp"), Some("myapp".to_string())); +/// assert_eq!(parse_metric_namespace("my_app.metrics"), Some("my_app.metrics".to_string())); +/// assert_eq!(parse_metric_namespace("1invalid"), None); +/// assert_eq!(parse_metric_namespace("my-app"), None); +/// ``` +pub fn parse_metric_namespace(namespace: &str) -> Option { + let trimmed = namespace.trim(); + if trimmed.is_empty() { + return None; + } + + let mut chars = trimmed.chars(); + + // Check first character is a letter + if let Some(first_char) = chars.next() { + if !first_char.is_ascii_alphabetic() { + tracing::error!( + "DD_STATSD_METRIC_NAMESPACE must start with a letter, got: '{}'. Ignoring namespace.", + trimmed + ); + return None; + } + } else { + return None; + } + + // Check remaining characters are valid (alphanumeric, underscore, or period) + if let Some(invalid_char) = + chars.find(|&ch| !ch.is_ascii_alphanumeric() && ch != '_' && ch != '.') + { + tracing::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()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_metric_namespace_valid() { + assert_eq!(parse_metric_namespace("myapp"), Some("myapp".to_string())); + assert_eq!(parse_metric_namespace("my_app"), Some("my_app".to_string())); + assert_eq!(parse_metric_namespace("my.app"), Some("my.app".to_string())); + assert_eq!( + parse_metric_namespace("myApp123"), + Some("myApp123".to_string()) + ); + assert_eq!( + parse_metric_namespace("a1.b2_c3"), + Some("a1.b2_c3".to_string()) + ); + } + + #[test] + fn test_parse_metric_namespace_with_whitespace() { + assert_eq!( + parse_metric_namespace(" myapp "), + Some("myapp".to_string()) + ); + assert_eq!( + parse_metric_namespace("\tmyapp\n"), + Some("myapp".to_string()) + ); + } + + #[test] + fn test_parse_metric_namespace_empty() { + assert_eq!(parse_metric_namespace(""), None); + assert_eq!(parse_metric_namespace(" "), None); + assert_eq!(parse_metric_namespace("\t\n"), None); + } + + #[test] + fn test_parse_metric_namespace_invalid_start() { + assert_eq!(parse_metric_namespace("1myapp"), None); + assert_eq!(parse_metric_namespace("_myapp"), None); + assert_eq!(parse_metric_namespace(".myapp"), None); + assert_eq!(parse_metric_namespace("-myapp"), None); + } + + #[test] + fn test_parse_metric_namespace_invalid_characters() { + assert_eq!(parse_metric_namespace("my-app"), None); + assert_eq!(parse_metric_namespace("my app"), None); + assert_eq!(parse_metric_namespace("my@app"), None); + assert_eq!(parse_metric_namespace("my#app"), None); + assert_eq!(parse_metric_namespace("my$app"), None); + assert_eq!(parse_metric_namespace("my!app"), None); + } +}