diff --git a/crates/datadog-agent-config/apm_replace_rule.rs b/crates/datadog-agent-config/apm_replace_rule.rs index 41b1359..363446a 100644 --- a/crates/datadog-agent-config/apm_replace_rule.rs +++ b/crates/datadog-agent-config/apm_replace_rule.rs @@ -59,7 +59,13 @@ pub fn deserialize_apm_replace_rules<'de, D>( where D: Deserializer<'de>, { - let json_string = deserializer.deserialize_any(StringOrReplaceRulesVisitor)?; + let json_string = match deserializer.deserialize_any(StringOrReplaceRulesVisitor) { + Ok(s) => s, + Err(e) => { + tracing::error!("Failed to deserialize APM replace rules: {e}, ignoring"); + return Ok(None); + } + }; match parse_rules_from_string(&json_string) { Ok(rules) => Ok(Some(rules)), diff --git a/crates/datadog-agent-config/env.rs b/crates/datadog-agent-config/env.rs index 8068593..bb77e94 100644 --- a/crates/datadog-agent-config/env.rs +++ b/crates/datadog-agent-config/env.rs @@ -15,7 +15,7 @@ use crate::{ deserialize_optional_bool_from_anything, deserialize_optional_duration_from_microseconds, deserialize_optional_duration_from_seconds, deserialize_optional_duration_from_seconds_ignore_zero, deserialize_optional_string, - deserialize_string_or_int, deserialize_trace_propagation_style, + deserialize_string_or_int, deserialize_trace_propagation_style, deserialize_with_default, flush_strategy::FlushStrategy, log_level::LogLevel, logs_additional_endpoints::{LogsAdditionalEndpoint, deserialize_logs_additional_endpoints}, @@ -43,6 +43,7 @@ pub struct EnvConfig { /// /// Minimum log level of the Datadog Agent. /// Valid log levels are: trace, debug, info, warn, and error. + #[serde(deserialize_with = "deserialize_with_default")] pub log_level: Option, /// @env `DD_FLUSH_TIMEOUT` @@ -398,6 +399,7 @@ pub struct EnvConfig { /// @env `DD_SERVERLESS_FLUSH_STRATEGY` /// /// The flush strategy to use for AWS Lambda. + #[serde(deserialize_with = "deserialize_with_default")] pub serverless_flush_strategy: Option, /// @env `DD_ENHANCED_METRICS` /// @@ -716,6 +718,130 @@ mod tests { processing_rule::{Kind, ProcessingRule}, }; + /// Comprehensive test: every non-string env var set to an invalid value. + /// The load MUST succeed, and invalid fields must fall back to defaults. + /// + /// Note: string-typed fields (DD_SITE, DD_API_KEY, etc.) can't fail from env + /// because all env values are strings. This test covers all non-string types. + /// + /// When adding a new non-string field to EnvConfig, add an entry here with + /// an invalid value to ensure graceful deserialization is in place. + #[test] + fn test_all_env_fields_wrong_type_fallback_to_default() { + figment::Jail::expect_with(|jail| { + jail.clear_env(); + + // Numeric fields → invalid string + jail.set_env("DD_FLUSH_TIMEOUT", "not_a_number"); + jail.set_env("DD_COMPRESSION_LEVEL", "not_a_number"); + jail.set_env("DD_LOGS_CONFIG_COMPRESSION_LEVEL", "not_a_number"); + jail.set_env("DD_APM_CONFIG_COMPRESSION_LEVEL", "not_a_number"); + jail.set_env("DD_METRICS_CONFIG_COMPRESSION_LEVEL", "not_a_number"); + jail.set_env("DD_CAPTURE_LAMBDA_PAYLOAD_MAX_DEPTH", "not_a_number"); + jail.set_env("DD_DOGSTATSD_SO_RCVBUF", "not_a_number"); + jail.set_env("DD_DOGSTATSD_BUFFER_SIZE", "not_a_number"); + jail.set_env("DD_DOGSTATSD_QUEUE_SIZE", "not_a_number"); + jail.set_env( + "DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_MAX_RECV_MSG_SIZE_MIB", + "not_a_number", + ); + jail.set_env("DD_OTLP_CONFIG_METRICS_DELTA_TTL", "not_a_number"); + jail.set_env( + "DD_OTLP_CONFIG_TRACES_PROBABILISTIC_SAMPLER_SAMPLING_PERCENTAGE", + "not_a_number", + ); + + // Bool fields → invalid string + jail.set_env("DD_SKIP_SSL_VALIDATION", "not_a_bool"); + jail.set_env("DD_LOGS_CONFIG_USE_COMPRESSION", "not_a_bool"); + jail.set_env( + "DD_APM_CONFIG_OBFUSCATION_HTTP_REMOVE_QUERY_STRING", + "not_a_bool", + ); + jail.set_env( + "DD_APM_CONFIG_OBFUSCATION_HTTP_REMOVE_PATHS_WITH_DIGITS", + "not_a_bool", + ); + jail.set_env("DD_TRACE_PROPAGATION_EXTRACT_FIRST", "not_a_bool"); + jail.set_env("DD_TRACE_PROPAGATION_HTTP_BAGGAGE_ENABLED", "not_a_bool"); + jail.set_env("DD_TRACE_AWS_SERVICE_REPRESENTATION_ENABLED", "not_a_bool"); + jail.set_env("DD_ENHANCED_METRICS", "not_a_bool"); + jail.set_env("DD_LAMBDA_PROC_ENHANCED_METRICS", "not_a_bool"); + jail.set_env("DD_CAPTURE_LAMBDA_PAYLOAD", "not_a_bool"); + jail.set_env("DD_COMPUTE_TRACE_STATS_ON_EXTENSION", "not_a_bool"); + jail.set_env("DD_SERVERLESS_APPSEC_ENABLED", "not_a_bool"); + jail.set_env("DD_API_SECURITY_ENABLED", "not_a_bool"); + jail.set_env("DD_OTLP_CONFIG_TRACES_ENABLED", "not_a_bool"); + jail.set_env( + "DD_OTLP_CONFIG_TRACES_SPAN_NAME_AS_RESOURCE_NAME", + "not_a_bool", + ); + jail.set_env("DD_OTLP_CONFIG_IGNORE_MISSING_DATADOG_FIELDS", "not_a_bool"); + jail.set_env("DD_OTLP_CONFIG_METRICS_ENABLED", "not_a_bool"); + jail.set_env( + "DD_OTLP_CONFIG_METRICS_RESOURCE_ATTRIBUTES_AS_TAGS", + "not_a_bool", + ); + jail.set_env( + "DD_OTLP_CONFIG_METRICS_INSTRUMENTATION_SCOPE_METADATA_AS_TAGS", + "not_a_bool", + ); + jail.set_env( + "DD_OTLP_CONFIG_METRICS_HISTOGRAMS_SEND_COUNT_SUM_METRICS", + "not_a_bool", + ); + jail.set_env( + "DD_OTLP_CONFIG_METRICS_HISTOGRAMS_SEND_AGGREGATION_METRICS", + "not_a_bool", + ); + jail.set_env("DD_OTLP_CONFIG_LOGS_ENABLED", "not_a_bool"); + jail.set_env( + "DD_OBSERVABILITY_PIPELINES_WORKER_LOGS_ENABLED", + "not_a_bool", + ); + jail.set_env("DD_SERVERLESS_LOGS_ENABLED", "not_a_bool"); + jail.set_env("DD_LOGS_ENABLED", "not_a_bool"); + + // Enum fields → invalid string + jail.set_env("DD_LOG_LEVEL", "invalid_level_999"); + jail.set_env("DD_SERVERLESS_FLUSH_STRATEGY", "[[[invalid"); + + // Duration fields → invalid string + jail.set_env("DD_SPAN_DEDUP_TIMEOUT", "not_a_number"); + jail.set_env("DD_API_KEY_SECRET_RELOAD_INTERVAL", "not_a_number"); + jail.set_env("DD_APPSEC_WAF_TIMEOUT", "not_a_number"); + jail.set_env("DD_API_SECURITY_SAMPLE_DELAY", "not_a_number"); + + // JSON fields → invalid JSON + jail.set_env("DD_ADDITIONAL_ENDPOINTS", "not_json{{"); + jail.set_env("DD_APM_ADDITIONAL_ENDPOINTS", "not_json{{"); + jail.set_env("DD_LOGS_CONFIG_PROCESSING_RULES", "not_json{{"); + jail.set_env("DD_LOGS_CONFIG_ADDITIONAL_ENDPOINTS", "not_json{{"); + jail.set_env("DD_APM_REPLACE_TAGS", "not_json{{"); + + // Comma/space-separated fields → invalid (these are lenient but include for completeness) + jail.set_env("DD_SERVICE_MAPPING", "no-colon-here"); + jail.set_env("DD_APM_FEATURES", ""); // empty + jail.set_env("DD_APM_FILTER_TAGS_REQUIRE", ""); + jail.set_env("DD_APM_FILTER_TAGS_REJECT", ""); + jail.set_env("DD_APM_FILTER_TAGS_REGEX_REQUIRE", ""); + jail.set_env("DD_APM_FILTER_TAGS_REGEX_REJECT", ""); + jail.set_env("DD_OTLP_CONFIG_TRACES_SPAN_NAME_REMAPPINGS", "no-colon"); + + let mut config = Config::default(); + // This MUST succeed — no single field should crash the whole config + EnvConfigSource + .load(&mut config) + .expect("load must not fail when env vars have wrong types"); + + // No string env vars were set, so string fields stay at default. + // All non-string env vars were set to invalid values, so they also stay at default. + // The entire config should equal the default. + assert_eq!(config, Config::default()); + Ok(()) + }); + } + #[test] #[allow(clippy::too_many_lines)] fn test_merge_config_overrides_with_environment_variables() { diff --git a/crates/datadog-agent-config/mod.rs b/crates/datadog-agent-config/mod.rs index 2071af9..90105da 100644 --- a/crates/datadog-agent-config/mod.rs +++ b/crates/datadog-agent-config/mod.rs @@ -20,7 +20,7 @@ use serde_json::Value; use std::path::Path; use std::time::Duration; use std::{collections::HashMap, fmt}; -use tracing::{debug, error}; +use tracing::{debug, error, warn}; use crate::{ apm_replace_rule::deserialize_apm_replace_rules, @@ -689,7 +689,13 @@ pub fn deserialize_key_value_pair_array_to_hashmap<'de, D>( where D: Deserializer<'de>, { - let array: Vec = Vec::deserialize(deserializer)?; + let array: Vec = match Vec::deserialize(deserializer) { + Ok(v) => v, + Err(e) => { + error!("Failed to deserialize tags array: {e}, ignoring"); + return Ok(HashMap::new()); + } + }; let mut map = HashMap::new(); for s in array { if let Some((key, val)) = parse_key_value_tag(&s) { @@ -760,40 +766,82 @@ where } } +/// Gracefully deserialize any field, falling back to `T::default()` on error. +/// +/// This ensures that a single field with the wrong type never fails the entire +/// struct extraction. Works for any `T` that implements `Deserialize + Default`: +/// - `Option` defaults to `None` +/// - `Vec` defaults to `[]` +/// - `HashMap` defaults to `{}` +/// - Structs with `#[derive(Default)]` use their default +pub fn deserialize_with_default<'de, D, T>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: Deserialize<'de> + Default, +{ + match T::deserialize(deserializer) { + Ok(value) => Ok(value), + Err(e) => { + warn!("Failed to deserialize field: {}, using default", e); + Ok(T::default()) + } + } +} + pub fn deserialize_optional_duration_from_microseconds<'de, D: Deserializer<'de>>( deserializer: D, ) -> Result, D::Error> { - Ok(Option::::deserialize(deserializer)?.map(Duration::from_micros)) + match Option::::deserialize(deserializer) { + Ok(opt) => Ok(opt.map(Duration::from_micros)), + Err(e) => { + error!("Failed to deserialize duration (microseconds): {e}, ignoring"); + Ok(None) + } + } } pub fn deserialize_optional_duration_from_seconds<'de, D: Deserializer<'de>>( deserializer: D, ) -> Result, D::Error> { - struct DurationVisitor; - impl serde::de::Visitor<'_> for DurationVisitor { - type Value = Option; - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "a duration in seconds (integer or float)") - } - fn visit_u64(self, v: u64) -> Result { - Ok(Some(Duration::from_secs(v))) - } - fn visit_i64(self, v: i64) -> Result { - if v < 0 { - error!("Failed to parse duration: negative durations are not allowed, ignoring"); - return Ok(None); + // Deserialize into a generic Value first to avoid propagating type errors, + // then try to extract a duration from it. + match Value::deserialize(deserializer) { + Ok(Value::Number(n)) => { + if let Some(u) = n.as_u64() { + Ok(Some(Duration::from_secs(u))) + } else if let Some(i) = n.as_i64() { + if i < 0 { + error!( + "Failed to parse duration: negative durations are not allowed, ignoring" + ); + Ok(None) + } else { + Ok(Some(Duration::from_secs(i as u64))) + } + } else if let Some(f) = n.as_f64() { + if f < 0.0 { + error!( + "Failed to parse duration: negative durations are not allowed, ignoring" + ); + Ok(None) + } else { + Ok(Some(Duration::from_secs_f64(f))) + } + } else { + error!("Failed to parse duration: unsupported number format, ignoring"); + Ok(None) } - self.visit_u64(u64::try_from(v).expect("positive i64 to u64 conversion never fails")) } - fn visit_f64(self, v: f64) -> Result { - if v < 0f64 { - error!("Failed to parse duration: negative durations are not allowed, ignoring"); - return Ok(None); - } - Ok(Some(Duration::from_secs_f64(v))) + Ok(Value::Null) => Ok(None), + Ok(other) => { + error!("Failed to parse duration: expected number, got {other}, ignoring"); + Ok(None) + } + Err(e) => { + error!("Failed to deserialize duration: {e}, ignoring"); + Ok(None) } } - deserializer.deserialize_any(DurationVisitor) } // Like deserialize_optional_duration_from_seconds(), but return None if the value is 0 @@ -814,7 +862,13 @@ where D: Deserializer<'de>, { use std::str::FromStr; - let s: String = String::deserialize(deserializer)?; + let s: String = match String::deserialize(deserializer) { + Ok(s) => s, + Err(e) => { + error!("Failed to deserialize trace propagation style: {e}, ignoring"); + return Ok(Vec::new()); + } + }; Ok(s.split(',') .filter_map( @@ -1477,18 +1531,25 @@ pub mod tests { serde_json::from_str::("{}").expect("failed to parse JSON"), Value { duration: None } ); - serde_json::from_str::(r#"{"duration":-1}"#) - .expect_err("should have failed parsing"); + // Negative and non-integer values gracefully fall back to None + assert_eq!( + serde_json::from_str::(r#"{"duration":-1}"#).expect("should not fail"), + Value { duration: None } + ); assert_eq!( serde_json::from_str::(r#"{"duration":1000000}"#).expect("failed to parse JSON"), Value { duration: Some(Duration::from_secs(1)) } ); - serde_json::from_str::(r#"{"duration":-1.5}"#) - .expect_err("should have failed parsing"); - serde_json::from_str::(r#"{"duration":1.5}"#) - .expect_err("should have failed parsing"); + assert_eq!( + serde_json::from_str::(r#"{"duration":-1.5}"#).expect("should not fail"), + Value { duration: None } + ); + assert_eq!( + serde_json::from_str::(r#"{"duration":1.5}"#).expect("should not fail"), + Value { duration: None } + ); } #[test] diff --git a/crates/datadog-agent-config/service_mapping.rs b/crates/datadog-agent-config/service_mapping.rs index 5b13398..f50c79b 100644 --- a/crates/datadog-agent-config/service_mapping.rs +++ b/crates/datadog-agent-config/service_mapping.rs @@ -9,7 +9,13 @@ pub fn deserialize_service_mapping<'de, D>( where D: Deserializer<'de>, { - let s: String = String::deserialize(deserializer)?; + let s: String = match String::deserialize(deserializer) { + Ok(s) => s, + Err(e) => { + tracing::error!("Failed to deserialize service mapping: {e}, ignoring"); + return Ok(HashMap::new()); + } + }; let map = s .split(',') diff --git a/crates/datadog-agent-config/yaml.rs b/crates/datadog-agent-config/yaml.rs index 1be32bd..dbb831c 100644 --- a/crates/datadog-agent-config/yaml.rs +++ b/crates/datadog-agent-config/yaml.rs @@ -9,7 +9,7 @@ use crate::{ deserialize_optional_duration_from_seconds, deserialize_optional_duration_from_seconds_ignore_zero, deserialize_optional_string, deserialize_processing_rules, deserialize_string_or_int, deserialize_trace_propagation_style, - flush_strategy::FlushStrategy, log_level::LogLevel, + deserialize_with_default, flush_strategy::FlushStrategy, log_level::LogLevel, logs_additional_endpoints::LogsAdditionalEndpoint, merge_hashmap, merge_option, merge_option_to_value, merge_string, merge_vec, service_mapping::deserialize_service_mapping, }; @@ -32,6 +32,7 @@ pub struct YamlConfig { pub site: Option, #[serde(deserialize_with = "deserialize_optional_string")] pub api_key: Option, + #[serde(deserialize_with = "deserialize_with_default")] pub log_level: Option, #[serde(deserialize_with = "deserialize_option_lossless")] @@ -41,6 +42,7 @@ pub struct YamlConfig { pub compression_level: Option, // Proxy + #[serde(deserialize_with = "deserialize_with_default")] pub proxy: ProxyConfig, // nit: this should probably be in the endpoints section #[serde(deserialize_with = "deserialize_optional_string")] @@ -68,9 +70,11 @@ pub struct YamlConfig { pub tags: HashMap, // Logs + #[serde(deserialize_with = "deserialize_with_default")] pub logs_config: LogsConfig, // APM + #[serde(deserialize_with = "deserialize_with_default")] pub apm_config: ApmConfig, #[serde(deserialize_with = "deserialize_service_mapping")] pub service_mapping: HashMap, @@ -87,6 +91,7 @@ pub struct YamlConfig { pub trace_propagation_http_baggage_enabled: Option, // Metrics + #[serde(deserialize_with = "deserialize_with_default")] pub metrics_config: MetricsConfig, // DogStatsD @@ -101,6 +106,7 @@ pub struct YamlConfig { pub dogstatsd_queue_size: Option, // OTLP + #[serde(deserialize_with = "deserialize_with_default")] pub otlp_config: Option, // AWS Lambda @@ -112,6 +118,7 @@ pub struct YamlConfig { pub serverless_logs_enabled: Option, #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] pub logs_enabled: Option, + #[serde(deserialize_with = "deserialize_with_default")] pub serverless_flush_strategy: Option, #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] pub enhanced_metrics: Option, @@ -144,7 +151,9 @@ pub struct YamlConfig { #[serde(default)] #[allow(clippy::module_name_repetitions)] pub struct ProxyConfig { + #[serde(deserialize_with = "deserialize_with_default")] pub https: Option, + #[serde(deserialize_with = "deserialize_with_default")] pub no_proxy: Option>, } @@ -155,6 +164,7 @@ pub struct ProxyConfig { #[serde(default)] #[allow(clippy::module_name_repetitions)] pub struct LogsConfig { + #[serde(deserialize_with = "deserialize_with_default")] pub logs_dd_url: Option, #[serde(deserialize_with = "deserialize_processing_rules")] pub processing_rules: Option>, @@ -162,6 +172,7 @@ pub struct LogsConfig { pub use_compression: Option, #[serde(deserialize_with = "deserialize_option_lossless")] pub compression_level: Option, + #[serde(deserialize_with = "deserialize_with_default")] pub additional_endpoints: Vec, } @@ -182,12 +193,15 @@ pub struct MetricsConfig { #[serde(default)] #[allow(clippy::module_name_repetitions)] pub struct ApmConfig { + #[serde(deserialize_with = "deserialize_with_default")] pub apm_dd_url: Option, #[serde(deserialize_with = "deserialize_apm_replace_rules")] pub replace_tags: Option>, + #[serde(deserialize_with = "deserialize_with_default")] pub obfuscation: Option, #[serde(deserialize_with = "deserialize_option_lossless")] pub compression_level: Option, + #[serde(deserialize_with = "deserialize_with_default")] pub features: Vec, #[serde(deserialize_with = "deserialize_additional_endpoints")] pub additional_endpoints: HashMap>, @@ -213,6 +227,7 @@ impl ApmConfig { #[serde(default)] #[allow(clippy::module_name_repetitions)] pub struct ApmObfuscation { + #[serde(deserialize_with = "deserialize_with_default")] pub http: ApmHttpObfuscation, } @@ -233,11 +248,15 @@ pub struct ApmHttpObfuscation { #[serde(default)] #[allow(clippy::module_name_repetitions)] pub struct OtlpConfig { + #[serde(deserialize_with = "deserialize_with_default")] pub receiver: Option, + #[serde(deserialize_with = "deserialize_with_default")] pub traces: Option, // NOT SUPPORTED + #[serde(deserialize_with = "deserialize_with_default")] pub metrics: Option, + #[serde(deserialize_with = "deserialize_with_default")] pub logs: Option, } @@ -245,6 +264,7 @@ pub struct OtlpConfig { #[serde(default)] #[allow(clippy::module_name_repetitions)] pub struct OtlpReceiverConfig { + #[serde(deserialize_with = "deserialize_with_default")] pub protocols: Option, } @@ -252,9 +272,11 @@ pub struct OtlpReceiverConfig { #[serde(default)] #[allow(clippy::module_name_repetitions)] pub struct OtlpReceiverProtocolsConfig { + #[serde(deserialize_with = "deserialize_with_default")] pub http: Option, // NOT SUPPORTED + #[serde(deserialize_with = "deserialize_with_default")] pub grpc: Option, } @@ -262,6 +284,7 @@ pub struct OtlpReceiverProtocolsConfig { #[serde(default)] #[allow(clippy::module_name_repetitions)] pub struct OtlpReceiverHttpConfig { + #[serde(deserialize_with = "deserialize_with_default")] pub endpoint: Option, } @@ -269,7 +292,9 @@ pub struct OtlpReceiverHttpConfig { #[serde(default)] #[allow(clippy::module_name_repetitions)] pub struct OtlpReceiverGrpcConfig { + #[serde(deserialize_with = "deserialize_with_default")] pub endpoint: Option, + #[serde(deserialize_with = "deserialize_with_default")] pub transport: Option, #[serde(deserialize_with = "deserialize_option_lossless")] pub max_recv_msg_size_mib: Option, @@ -283,11 +308,13 @@ pub struct OtlpTracesConfig { pub enabled: Option, #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] pub span_name_as_resource_name: Option, + #[serde(deserialize_with = "deserialize_with_default")] pub span_name_remappings: HashMap, #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] pub ignore_missing_datadog_fields: Option, // NOT SUPORTED + #[serde(deserialize_with = "deserialize_with_default")] pub probabilistic_sampler: Option, } @@ -306,17 +333,22 @@ pub struct OtlpMetricsConfig { pub resource_attributes_as_tags: Option, #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] pub instrumentation_scope_metadata_as_tags: Option, + #[serde(deserialize_with = "deserialize_with_default")] pub tag_cardinality: Option, #[serde(deserialize_with = "deserialize_option_lossless")] pub delta_ttl: Option, + #[serde(deserialize_with = "deserialize_with_default")] pub histograms: Option, + #[serde(deserialize_with = "deserialize_with_default")] pub sums: Option, + #[serde(deserialize_with = "deserialize_with_default")] pub summaries: Option, } #[derive(Debug, PartialEq, Clone, Deserialize, Default)] #[serde(default)] pub struct OtlpMetricsHistograms { + #[serde(deserialize_with = "deserialize_with_default")] pub mode: Option, #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] pub send_count_sum_metrics: Option, @@ -327,13 +359,16 @@ pub struct OtlpMetricsHistograms { #[derive(Debug, PartialEq, Clone, Deserialize, Default)] #[serde(default)] pub struct OtlpMetricsSums { + #[serde(deserialize_with = "deserialize_with_default")] pub cumulative_monotonic_mode: Option, + #[serde(deserialize_with = "deserialize_with_default")] pub initial_cumulative_monotonic_value: Option, } #[derive(Debug, PartialEq, Clone, Deserialize, Default)] #[serde(default)] pub struct OtlpMetricsSummaries { + #[serde(deserialize_with = "deserialize_with_default")] pub mode: Option, } @@ -739,10 +774,158 @@ mod tests { use std::path::Path; use std::time::Duration; - use crate::{flush_strategy::PeriodicStrategy, processing_rule::Kind}; + use crate::{flush_strategy::PeriodicStrategy, log_level::LogLevel, processing_rule::Kind}; use super::*; + /// Comprehensive test: every field in the YAML set to the wrong type. + /// The load MUST succeed, and every field must fall back to its default. + /// + /// When adding a new field to YamlConfig or any nested struct, add an entry + /// here with the wrong type to ensure graceful deserialization is in place. + #[test] + fn test_all_yaml_fields_wrong_type_fallback_to_default() { + figment::Jail::expect_with(|jail| { + jail.clear_env(); + // Every field is set to an array [1, 2, 3] which is the wrong type + // for strings, numbers, bools, and nested structs. This exercises + // every deserialize_with handler. + jail.create_file( + "datadog.yaml", + r#" +# Basic fields +site: [1, 2, 3] +api_key: [1, 2, 3] +log_level: [1, 2, 3] +flush_timeout: [1, 2, 3] +compression_level: [1, 2, 3] + +# Proxy (nested) +proxy: + https: [1, 2, 3] + no_proxy: 12345 + +# Endpoints +dd_url: [1, 2, 3] +http_protocol: [1, 2, 3] +tls_cert_file: [1, 2, 3] +skip_ssl_validation: [1, 2, 3] +additional_endpoints: [1, 2, 3] + +# Unified Service Tagging +env: [1, 2, 3] +service: [1, 2, 3] +version: [1, 2, 3] +tags: 12345 + +# Logs (nested) +logs_config: + logs_dd_url: [1, 2, 3] + processing_rules: 12345 + use_compression: [1, 2, 3] + compression_level: [1, 2, 3] + additional_endpoints: 12345 + +# APM (nested) +apm_config: + apm_dd_url: [1, 2, 3] + replace_tags: 12345 + obfuscation: + http: + remove_query_string: [1, 2, 3] + remove_paths_with_digits: [1, 2, 3] + compression_level: [1, 2, 3] + features: 12345 + additional_endpoints: [1, 2, 3] + +service_mapping: [1, 2, 3] +trace_aws_service_representation_enabled: [1, 2, 3] + +# Trace Propagation +trace_propagation_style: [1, 2, 3] +trace_propagation_style_extract: [1, 2, 3] +trace_propagation_extract_first: [1, 2, 3] +trace_propagation_http_baggage_enabled: [1, 2, 3] + +# Metrics (nested) +metrics_config: + compression_level: [1, 2, 3] + +# DogStatsD +dogstatsd_so_rcvbuf: [1, 2, 3] +dogstatsd_buffer_size: [1, 2, 3] +dogstatsd_queue_size: [1, 2, 3] + +# OTLP (deeply nested) +otlp_config: + receiver: + protocols: + http: + endpoint: [1, 2, 3] + grpc: + endpoint: [1, 2, 3] + transport: [1, 2, 3] + max_recv_msg_size_mib: [1, 2, 3] + traces: + enabled: [1, 2, 3] + span_name_as_resource_name: [1, 2, 3] + span_name_remappings: [1, 2, 3] + ignore_missing_datadog_fields: [1, 2, 3] + probabilistic_sampler: + sampling_percentage: [1, 2, 3] + metrics: + enabled: [1, 2, 3] + resource_attributes_as_tags: [1, 2, 3] + instrumentation_scope_metadata_as_tags: [1, 2, 3] + tag_cardinality: [1, 2, 3] + delta_ttl: [1, 2, 3] + histograms: + mode: [1, 2, 3] + send_count_sum_metrics: [1, 2, 3] + send_aggregation_metrics: [1, 2, 3] + sums: + cumulative_monotonic_mode: [1, 2, 3] + initial_cumulative_monotonic_value: [1, 2, 3] + summaries: + mode: [1, 2, 3] + logs: + enabled: [1, 2, 3] + +# AWS Lambda +api_key_secret_arn: [1, 2, 3] +kms_api_key: [1, 2, 3] +serverless_logs_enabled: [1, 2, 3] +logs_enabled: [1, 2, 3] +serverless_flush_strategy: [1, 2, 3] +enhanced_metrics: [1, 2, 3] +lambda_proc_enhanced_metrics: [1, 2, 3] +capture_lambda_payload: [1, 2, 3] +capture_lambda_payload_max_depth: [1, 2, 3] +compute_trace_stats_on_extension: [1, 2, 3] +api_key_secret_reload_interval: [1, 2, 3] +serverless_appsec_enabled: [1, 2, 3] +appsec_rules: [1, 2, 3] +appsec_waf_timeout: [1, 2, 3] +api_security_enabled: [1, 2, 3] +api_security_sample_delay: [1, 2, 3] +"#, + )?; + + let mut config = Config::default(); + let source = YamlConfigSource { + path: PathBuf::from("datadog.yaml"), + }; + // This MUST succeed — no single field should crash the whole config + source + .load(&mut config) + .expect("load must not fail when fields have wrong types"); + + // Every field should be at its default since all values were invalid + assert_eq!(config, Config::default()); + Ok(()) + }); + } + #[test] #[allow(clippy::too_many_lines)] fn test_merge_config_overrides_with_yaml_file() {