diff --git a/bottlecap/Cargo.lock b/bottlecap/Cargo.lock index c360692fe..3eacd5c28 100644 --- a/bottlecap/Cargo.lock +++ b/bottlecap/Cargo.lock @@ -484,6 +484,7 @@ dependencies = [ "bytes", "chrono", "cookie", + "datadog-agent-config", "datadog-fips", "datadog-opentelemetry", "datadog-protos", @@ -778,10 +779,28 @@ dependencies = [ "typenum", ] +[[package]] +name = "datadog-agent-config" +version = "0.1.0" +source = "git+https://github.com/DataDog/serverless-components?rev=bb4dedeee20b949db3143c05e5a779b843a8a484#bb4dedeee20b949db3143c05e5a779b843a8a484" +dependencies = [ + "datadog-opentelemetry", + "dogstatsd", + "figment", + "libdd-trace-obfuscation", + "libdd-trace-utils 6.0.1", + "log", + "serde", + "serde-aux", + "serde_json", + "tokio", + "tracing", +] + [[package]] name = "datadog-fips" version = "0.1.0" -source = "git+https://github.com/DataDog/serverless-components?rev=5b68f50f49c9defbfed4d25bd621e2a86405a972#5b68f50f49c9defbfed4d25bd621e2a86405a972" +source = "git+https://github.com/DataDog/serverless-components?rev=bb4dedeee20b949db3143c05e5a779b843a8a484#bb4dedeee20b949db3143c05e5a779b843a8a484" dependencies = [ "reqwest", "rustls", @@ -923,7 +942,7 @@ dependencies = [ [[package]] name = "dogstatsd" version = "0.1.0" -source = "git+https://github.com/DataDog/serverless-components?rev=5b68f50f49c9defbfed4d25bd621e2a86405a972#5b68f50f49c9defbfed4d25bd621e2a86405a972" +source = "git+https://github.com/DataDog/serverless-components?rev=bb4dedeee20b949db3143c05e5a779b843a8a484#bb4dedeee20b949db3143c05e5a779b843a8a484" dependencies = [ "datadog-protos", "ddsketch-agent", diff --git a/bottlecap/Cargo.toml b/bottlecap/Cargo.toml index 446bab161..b3ef5c58d 100644 --- a/bottlecap/Cargo.toml +++ b/bottlecap/Cargo.toml @@ -82,8 +82,9 @@ libdd-trace-normalization = { git = "https://github.com/DataDog/libdatadog", rev libdd-trace-obfuscation = { git = "https://github.com/DataDog/libdatadog", rev = "48da0d82cb32b43d4cdece35b794c9bcbc275a03", default-features = false } libdd-trace-stats = { git = "https://github.com/DataDog/libdatadog", rev = "48da0d82cb32b43d4cdece35b794c9bcbc275a03", default-features = false } datadog-opentelemetry = { git = "https://github.com/DataDog/dd-trace-rs", rev = "f51cefc4ad24bec81b38fb2f36b1ed93f21ae913", default-features = false } -dogstatsd = { git = "https://github.com/DataDog/serverless-components", rev = "5b68f50f49c9defbfed4d25bd621e2a86405a972", default-features = false } -datadog-fips = { git = "https://github.com/DataDog/serverless-components", rev = "5b68f50f49c9defbfed4d25bd621e2a86405a972", default-features = false } +dogstatsd = { git = "https://github.com/DataDog/serverless-components", rev = "bb4dedeee20b949db3143c05e5a779b843a8a484", default-features = false } +datadog-fips = { git = "https://github.com/DataDog/serverless-components", rev = "bb4dedeee20b949db3143c05e5a779b843a8a484", default-features = false } +datadog-agent-config = { git = "https://github.com/DataDog/serverless-components", rev = "bb4dedeee20b949db3143c05e5a779b843a8a484", default-features = false } libddwaf = { version = "1.28.1", git = "https://github.com/DataDog/libddwaf-rust", rev = "d1534a158d976bd4f747bf9fcc58e0712d2d17fc", default-features = false, features = ["serde"] } [dev-dependencies] @@ -130,12 +131,14 @@ tikv-jemallocator = "0.5" default = [ "reqwest/rustls-tls-native-roots", "datadog-fips/default", + "datadog-agent-config/https", "libdd-common/https", "libdd-trace-utils/https", "libdd-trace-obfuscation/https", "libdd-trace-stats/https", ] fips = [ + "datadog-agent-config/fips", "libdd-common/fips", "libdd-trace-utils/fips", "libdd-trace-obfuscation/fips", diff --git a/bottlecap/LICENSE-3rdparty.csv b/bottlecap/LICENSE-3rdparty.csv index 25eed6bf1..93860d55f 100644 --- a/bottlecap/LICENSE-3rdparty.csv +++ b/bottlecap/LICENSE-3rdparty.csv @@ -40,6 +40,7 @@ crc32fast,https://github.com/srijs/rust-crc32fast,MIT OR Apache-2.0,"Sam Rijs datadog-protos,https://github.com/DataDog/saluki,Apache-2.0,The datadog-protos Authors diff --git a/bottlecap/src/config/mod.rs b/bottlecap/src/config/mod.rs index 7f5d1aed8..c05551a4b 100644 --- a/bottlecap/src/config/mod.rs +++ b/bottlecap/src/config/mod.rs @@ -1670,3 +1670,635 @@ pub mod tests { assert_eq!(result.tags, HashMap::new()); } } + +// --------------------------------------------------------------------------- +// LambdaConfig — bottlecap's `ConfigExtension` for the shared +// `datadog-agent-config` crate. Lives alongside the core config under +// `Config::ext` once the migration onto upstream lands; see the migration PR +// description for the full plan. +// --------------------------------------------------------------------------- + +use datadog_agent_config::{ + ConfigExtension as DatadogConfigExtension, + deserialize_array_from_comma_separated_string as deser_csv, + deserialize_option_lossless as deser_opt_lossless, + deserialize_optional_bool_from_anything as deser_opt_bool, + deserialize_optional_duration_from_microseconds as deser_dur_micros, + deserialize_optional_duration_from_seconds as deser_dur_secs, + deserialize_optional_duration_from_seconds_ignore_zero as deser_dur_secs_ignore_zero, + deserialize_optional_string as deser_opt_str, deserialize_string_or_int as deser_str_or_int, + flush_strategy::FlushStrategy as UpstreamFlushStrategy, +}; + +/// Lambda-specific configuration that lives alongside the shared +/// `datadog_agent_config::Config` core fields under `config.ext` once the +/// migration onto upstream lands. +#[derive(Debug, PartialEq, Clone)] +#[allow(clippy::module_name_repetitions)] +#[allow(clippy::struct_excessive_bools)] +pub struct LambdaConfig { + pub api_key_secret_arn: String, + pub kms_api_key: String, + pub api_key_ssm_arn: String, + pub serverless_logs_enabled: bool, + pub serverless_flush_strategy: UpstreamFlushStrategy, + pub enhanced_metrics: bool, + pub lambda_proc_enhanced_metrics: bool, + pub capture_lambda_payload: bool, + pub capture_lambda_payload_max_depth: u32, + pub compute_trace_stats_on_extension: bool, + pub span_dedup_timeout: Option, + pub api_key_secret_reload_interval: Option, + pub dd_org_uuid: String, + pub serverless_appsec_enabled: bool, + pub appsec_rules: Option, + pub appsec_waf_timeout: Duration, + pub api_security_enabled: bool, + pub api_security_sample_delay: Duration, + pub custom_metrics_exclude_tags: Vec, +} + +impl Default for LambdaConfig { + fn default() -> Self { + Self { + api_key_secret_arn: String::new(), + kms_api_key: String::new(), + api_key_ssm_arn: String::new(), + serverless_logs_enabled: true, + serverless_flush_strategy: UpstreamFlushStrategy::Default, + enhanced_metrics: true, + lambda_proc_enhanced_metrics: true, + capture_lambda_payload: false, + capture_lambda_payload_max_depth: 10, + compute_trace_stats_on_extension: false, + span_dedup_timeout: None, + api_key_secret_reload_interval: None, + dd_org_uuid: String::new(), + serverless_appsec_enabled: false, + appsec_rules: None, + appsec_waf_timeout: Duration::from_millis(5), + api_security_enabled: true, + api_security_sample_delay: Duration::from_secs(30), + custom_metrics_exclude_tags: Vec::new(), + } + } +} + +/// Intermediate deserialization type shared by env-var and YAML loading. +/// +/// `#[serde(default)]` and the forgiving per-field deserializers are required +/// by the `ConfigExtension` contract: one malformed field must not fail the +/// whole extraction. +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(default)] +#[allow(clippy::module_name_repetitions)] +pub struct LambdaConfigSource { + #[serde(deserialize_with = "deser_opt_str")] + pub api_key_secret_arn: Option, + #[serde(deserialize_with = "deser_opt_str")] + pub kms_api_key: Option, + #[serde(deserialize_with = "deser_opt_str")] + pub api_key_ssm_arn: Option, + + /// `DD_SERVERLESS_LOGS_ENABLED` — primary toggle for Lambda log shipping. + #[serde(deserialize_with = "deser_opt_bool")] + pub serverless_logs_enabled: Option, + /// `DD_LOGS_ENABLED` — alias for `serverless_logs_enabled`; OR-merged so + /// either being `true` turns logs on. See `merge_from` below. + #[serde(deserialize_with = "deser_opt_bool")] + pub logs_enabled: Option, + + pub serverless_flush_strategy: Option, + + #[serde(deserialize_with = "deser_opt_bool")] + pub enhanced_metrics: Option, + #[serde(deserialize_with = "deser_opt_bool")] + pub lambda_proc_enhanced_metrics: Option, + #[serde(deserialize_with = "deser_opt_bool")] + pub capture_lambda_payload: Option, + #[serde(deserialize_with = "deser_opt_lossless")] + pub capture_lambda_payload_max_depth: Option, + #[serde(deserialize_with = "deser_opt_bool")] + pub compute_trace_stats_on_extension: Option, + + #[serde(deserialize_with = "deser_dur_secs_ignore_zero")] + pub span_dedup_timeout: Option, + #[serde(deserialize_with = "deser_dur_secs_ignore_zero")] + pub api_key_secret_reload_interval: Option, + + /// `DD_ORG_UUID` — when set, delegated auth is auto-enabled. The source + /// field is `org_uuid` (matching the env var) and merges into the + /// `dd_org_uuid` config field. + #[serde(deserialize_with = "deser_str_or_int")] + pub org_uuid: Option, + + #[serde(deserialize_with = "deser_opt_bool")] + pub serverless_appsec_enabled: Option, + #[serde(deserialize_with = "deser_opt_str")] + pub appsec_rules: Option, + #[serde(deserialize_with = "deser_dur_micros")] + pub appsec_waf_timeout: Option, + #[serde(deserialize_with = "deser_opt_bool")] + pub api_security_enabled: Option, + #[serde(deserialize_with = "deser_dur_secs")] + pub api_security_sample_delay: Option, + + /// `DD_LAMBDA_CUSTOMER_METRICS_EXCLUDE_TAGS` — comma-separated list of tag + /// names to drop from customer `DogStatsD` metrics. Source field name + /// matches the env var; merges into `custom_metrics_exclude_tags`. + #[serde(deserialize_with = "deser_csv")] + pub lambda_customer_metrics_exclude_tags: Vec, +} + +impl DatadogConfigExtension for LambdaConfig { + type Source = LambdaConfigSource; + + fn merge_from(&mut self, source: &Self::Source) { + // Fully-qualified macro paths avoid colliding with the legacy + // `merge_*` macros declared with `#[macro_export]` at the top of this + // file, which will be removed once the migration onto upstream is + // complete. + datadog_agent_config::merge_fields!(self, source, + string: [api_key_secret_arn, kms_api_key, api_key_ssm_arn], + value: [ + serverless_flush_strategy, + enhanced_metrics, + lambda_proc_enhanced_metrics, + capture_lambda_payload, + capture_lambda_payload_max_depth, + compute_trace_stats_on_extension, + serverless_appsec_enabled, + appsec_waf_timeout, + api_security_enabled, + api_security_sample_delay, + ], + option: [span_dedup_timeout, api_key_secret_reload_interval, appsec_rules], + ); + + // OR-merge serverless_logs_enabled with the logs_enabled alias. Either + // env var set to `true` enables logs; if both are absent the default + // (true) is preserved. + if source.serverless_logs_enabled.is_some() || source.logs_enabled.is_some() { + self.serverless_logs_enabled = source.serverless_logs_enabled.unwrap_or(false) + || source.logs_enabled.unwrap_or(false); + } + + // org_uuid (source) → dd_org_uuid (config) + datadog_agent_config::merge_string!(self, dd_org_uuid, source, org_uuid); + + // lambda_customer_metrics_exclude_tags (source) → custom_metrics_exclude_tags (config) + if !source.lambda_customer_metrics_exclude_tags.is_empty() { + self.custom_metrics_exclude_tags + .clone_from(&source.lambda_customer_metrics_exclude_tags); + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] // Test modules skew coverage metrics +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod lambda_config_tests { + use datadog_agent_config::{ + Config as UpstreamConfig, flush_strategy::PeriodicStrategy, get_config_with_extension, + }; + use figment::Jail; + + use super::*; + + fn load( + jail_setup: impl FnOnce(&mut Jail) -> figment::Result<()>, + ) -> UpstreamConfig { + let mut result: Option> = None; + Jail::expect_with(|jail| { + jail.clear_env(); + jail_setup(jail)?; + result = Some(get_config_with_extension::(Path::new(""))); + Ok(()) + }); + result.unwrap() + } + + #[test] + fn defaults_match_lambda_config_default() { + let config = load(|_| Ok(())); + assert_eq!(config.ext, LambdaConfig::default()); + } + + // ---- string fields from env / yaml ---- + + #[test] + fn api_key_secret_arn_from_env() { + let config = load(|jail| { + jail.set_env("DD_API_KEY_SECRET_ARN", "arn:aws:secretsmanager:foo"); + Ok(()) + }); + assert_eq!(config.ext.api_key_secret_arn, "arn:aws:secretsmanager:foo"); + } + + #[test] + fn api_key_secret_arn_from_yaml() { + let config = load(|jail| { + jail.create_file( + "datadog.yaml", + "api_key_secret_arn: arn:aws:secretsmanager:foo\n", + )?; + Ok(()) + }); + assert_eq!(config.ext.api_key_secret_arn, "arn:aws:secretsmanager:foo"); + } + + #[test] + fn kms_api_key_from_env_and_yaml() { + let env = load(|jail| { + jail.set_env("DD_KMS_API_KEY", "kms-key-env"); + Ok(()) + }); + assert_eq!(env.ext.kms_api_key, "kms-key-env"); + + let yaml = load(|jail| { + jail.create_file("datadog.yaml", "kms_api_key: kms-key-yaml\n")?; + Ok(()) + }); + assert_eq!(yaml.ext.kms_api_key, "kms-key-yaml"); + } + + #[test] + fn api_key_ssm_arn_from_env() { + let config = load(|jail| { + jail.set_env("DD_API_KEY_SSM_ARN", "ssm-arn"); + Ok(()) + }); + assert_eq!(config.ext.api_key_ssm_arn, "ssm-arn"); + } + + #[test] + fn api_key_ssm_arn_from_yaml() { + let config = load(|jail| { + jail.create_file("datadog.yaml", "api_key_ssm_arn: ssm-yaml\n")?; + Ok(()) + }); + assert_eq!(config.ext.api_key_ssm_arn, "ssm-yaml"); + } + + // ---- serverless_logs_enabled with OR-merge alias ---- + + #[test] + fn serverless_logs_enabled_defaults_true() { + let config = load(|_| Ok(())); + assert!(config.ext.serverless_logs_enabled); + } + + #[test] + fn serverless_logs_enabled_false_explicit() { + let config = load(|jail| { + jail.set_env("DD_SERVERLESS_LOGS_ENABLED", "false"); + Ok(()) + }); + assert!(!config.ext.serverless_logs_enabled); + } + + #[test] + fn logs_enabled_alias_turns_on_when_serverless_is_off() { + let config = load(|jail| { + jail.set_env("DD_SERVERLESS_LOGS_ENABLED", "false"); + jail.set_env("DD_LOGS_ENABLED", "true"); + Ok(()) + }); + assert!(config.ext.serverless_logs_enabled); + } + + #[test] + fn logs_enabled_alias_only() { + let config = load(|jail| { + jail.set_env("DD_LOGS_ENABLED", "true"); + Ok(()) + }); + assert!(config.ext.serverless_logs_enabled); + } + + #[test] + fn serverless_logs_disabled_when_both_false() { + let config = load(|jail| { + jail.set_env("DD_SERVERLESS_LOGS_ENABLED", "false"); + jail.set_env("DD_LOGS_ENABLED", "false"); + Ok(()) + }); + assert!(!config.ext.serverless_logs_enabled); + } + + #[test] + fn serverless_logs_enabled_from_yaml() { + let config = load(|jail| { + jail.create_file("datadog.yaml", "serverless_logs_enabled: false\n")?; + Ok(()) + }); + assert!(!config.ext.serverless_logs_enabled); + } + + // ---- FlushStrategy ---- + + #[test] + fn flush_strategy_end_from_env() { + let config = load(|jail| { + jail.set_env("DD_SERVERLESS_FLUSH_STRATEGY", "end"); + Ok(()) + }); + assert_eq!( + config.ext.serverless_flush_strategy, + UpstreamFlushStrategy::End + ); + } + + #[test] + fn flush_strategy_periodically_from_env() { + let config = load(|jail| { + jail.set_env("DD_SERVERLESS_FLUSH_STRATEGY", "periodically,60000"); + Ok(()) + }); + assert_eq!( + config.ext.serverless_flush_strategy, + UpstreamFlushStrategy::Periodically(PeriodicStrategy { interval: 60000 }) + ); + } + + #[test] + fn flush_strategy_periodically_from_yaml() { + let config = load(|jail| { + jail.create_file( + "datadog.yaml", + "serverless_flush_strategy: \"periodically,5000\"\n", + )?; + Ok(()) + }); + assert_eq!( + config.ext.serverless_flush_strategy, + UpstreamFlushStrategy::Periodically(PeriodicStrategy { interval: 5000 }) + ); + } + + #[test] + fn flush_strategy_invalid_falls_back_to_default() { + let config = load(|jail| { + jail.set_env("DD_SERVERLESS_FLUSH_STRATEGY", "garbage"); + Ok(()) + }); + assert_eq!( + config.ext.serverless_flush_strategy, + UpstreamFlushStrategy::Default + ); + } + + #[test] + fn flush_strategy_end_periodically_from_env() { + let config = load(|jail| { + jail.set_env("DD_SERVERLESS_FLUSH_STRATEGY", "end,1000"); + Ok(()) + }); + assert_eq!( + config.ext.serverless_flush_strategy, + UpstreamFlushStrategy::EndPeriodically(PeriodicStrategy { interval: 1000 }) + ); + } + + #[test] + fn flush_strategy_continuously_from_env() { + let config = load(|jail| { + jail.set_env("DD_SERVERLESS_FLUSH_STRATEGY", "continuously,2000"); + Ok(()) + }); + assert_eq!( + config.ext.serverless_flush_strategy, + UpstreamFlushStrategy::Continuously(PeriodicStrategy { interval: 2000 }) + ); + } + + // ---- bool fields ---- + + #[test] + fn enhanced_metrics_disabled_from_env() { + let config = load(|jail| { + jail.set_env("DD_ENHANCED_METRICS", "false"); + Ok(()) + }); + assert!(!config.ext.enhanced_metrics); + } + + #[test] + fn lambda_proc_enhanced_metrics_disabled_from_env() { + let config = load(|jail| { + jail.set_env("DD_LAMBDA_PROC_ENHANCED_METRICS", "false"); + Ok(()) + }); + assert!(!config.ext.lambda_proc_enhanced_metrics); + } + + #[test] + fn capture_lambda_payload_from_env_and_yaml() { + let env = load(|jail| { + jail.set_env("DD_CAPTURE_LAMBDA_PAYLOAD", "true"); + jail.set_env("DD_CAPTURE_LAMBDA_PAYLOAD_MAX_DEPTH", "5"); + Ok(()) + }); + assert!(env.ext.capture_lambda_payload); + assert_eq!(env.ext.capture_lambda_payload_max_depth, 5); + + let yaml = load(|jail| { + jail.create_file( + "datadog.yaml", + "capture_lambda_payload: true\ncapture_lambda_payload_max_depth: 3\n", + )?; + Ok(()) + }); + assert!(yaml.ext.capture_lambda_payload); + assert_eq!(yaml.ext.capture_lambda_payload_max_depth, 3); + } + + #[test] + fn compute_trace_stats_on_extension_from_env() { + let config = load(|jail| { + jail.set_env("DD_COMPUTE_TRACE_STATS_ON_EXTENSION", "true"); + Ok(()) + }); + assert!(config.ext.compute_trace_stats_on_extension); + } + + // ---- Duration fields ---- + + #[test] + fn span_dedup_timeout_from_env_seconds() { + let config = load(|jail| { + jail.set_env("DD_SPAN_DEDUP_TIMEOUT", "5"); + Ok(()) + }); + assert_eq!(config.ext.span_dedup_timeout, Some(Duration::from_secs(5))); + } + + #[test] + fn span_dedup_timeout_zero_treated_as_none() { + let config = load(|jail| { + jail.set_env("DD_SPAN_DEDUP_TIMEOUT", "0"); + Ok(()) + }); + assert_eq!(config.ext.span_dedup_timeout, None); + } + + #[test] + fn api_key_secret_reload_interval_from_env() { + let config = load(|jail| { + jail.set_env("DD_API_KEY_SECRET_RELOAD_INTERVAL", "10"); + Ok(()) + }); + assert_eq!( + config.ext.api_key_secret_reload_interval, + Some(Duration::from_secs(10)) + ); + } + + #[test] + fn appsec_waf_timeout_from_env_microseconds() { + let config = load(|jail| { + jail.set_env("DD_APPSEC_WAF_TIMEOUT", "1000000"); + Ok(()) + }); + assert_eq!(config.ext.appsec_waf_timeout, Duration::from_secs(1)); + } + + #[test] + fn appsec_waf_timeout_from_yaml() { + let config = load(|jail| { + jail.create_file("datadog.yaml", "appsec_waf_timeout: 1000000\n")?; + Ok(()) + }); + assert_eq!(config.ext.appsec_waf_timeout, Duration::from_secs(1)); + } + + #[test] + fn api_security_sample_delay_from_env() { + let config = load(|jail| { + jail.set_env("DD_API_SECURITY_SAMPLE_DELAY", "60"); + Ok(()) + }); + assert_eq!( + config.ext.api_security_sample_delay, + Duration::from_secs(60) + ); + } + + // ---- AppSec / API Security ---- + + #[test] + fn appsec_block_from_env() { + let config = load(|jail| { + jail.set_env("DD_SERVERLESS_APPSEC_ENABLED", "true"); + jail.set_env("DD_APPSEC_RULES", "/etc/dd/rules.json"); + Ok(()) + }); + assert!(config.ext.serverless_appsec_enabled); + assert_eq!( + config.ext.appsec_rules.as_deref(), + Some("/etc/dd/rules.json") + ); + } + + #[test] + fn api_security_disabled_from_env() { + let config = load(|jail| { + jail.set_env("DD_API_SECURITY_ENABLED", "false"); + Ok(()) + }); + assert!(!config.ext.api_security_enabled); + } + + // ---- aliased name mappings ---- + + #[test] + fn org_uuid_env_maps_to_dd_org_uuid_field() { + let config = load(|jail| { + jail.set_env("DD_ORG_UUID", "00000000-1111-2222-3333-444444444444"); + Ok(()) + }); + assert_eq!( + config.ext.dd_org_uuid, + "00000000-1111-2222-3333-444444444444" + ); + } + + #[test] + fn org_uuid_yaml_maps_to_dd_org_uuid_field() { + // The yaml key matches the env-var name minus the DD_ prefix + // (`org_uuid:`), not the config field name (`dd_org_uuid:`). + let config = load(|jail| { + jail.create_file( + "datadog.yaml", + "org_uuid: 00000000-1111-2222-3333-444444444444\n", + )?; + Ok(()) + }); + assert_eq!( + config.ext.dd_org_uuid, + "00000000-1111-2222-3333-444444444444" + ); + } + + #[test] + fn custom_metrics_exclude_tags_from_env() { + let config = load(|jail| { + jail.set_env( + "DD_LAMBDA_CUSTOMER_METRICS_EXCLUDE_TAGS", + "function_arn,region", + ); + Ok(()) + }); + assert_eq!( + config.ext.custom_metrics_exclude_tags, + vec!["function_arn".to_string(), "region".to_string()] + ); + } + + #[test] + fn custom_metrics_exclude_tags_from_yaml() { + // YAML key matches the env var name; merges into the + // `custom_metrics_exclude_tags` config field. + let config = load(|jail| { + jail.create_file( + "datadog.yaml", + "lambda_customer_metrics_exclude_tags: \"function_arn,region\"\n", + )?; + Ok(()) + }); + assert_eq!( + config.ext.custom_metrics_exclude_tags, + vec!["function_arn".to_string(), "region".to_string()] + ); + } + + #[test] + fn custom_metrics_exclude_tags_defaults_to_empty() { + let config = load(|_| Ok(())); + assert!(config.ext.custom_metrics_exclude_tags.is_empty()); + } + + // ---- precedence: env wins over yaml for the same field ---- + + #[test] + fn env_overrides_yaml_for_extension_field() { + let config = load(|jail| { + jail.create_file("datadog.yaml", "capture_lambda_payload: false\n")?; + jail.set_env("DD_CAPTURE_LAMBDA_PAYLOAD", "true"); + Ok(()) + }); + assert!(config.ext.capture_lambda_payload); + } + + // ---- malformed input falls back to default (forgiving deserializers) ---- + + #[test] + fn malformed_bool_falls_back_to_default() { + let config = load(|jail| { + jail.set_env("DD_ENHANCED_METRICS", "not-a-bool"); + Ok(()) + }); + // Default is true. + assert!(config.ext.enhanced_metrics); + } +}