From b0cdd7136ec9fab8f5df0ebe0e684b5b4619038e Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:42:48 -0400 Subject: [PATCH 1/9] feat(config): add LambdaExtension targeting datadog-agent-config crate Introduces a `LambdaExtension` struct that implements the upstream `ConfigExtension` trait from `datadog-agent-config`, plus a `LambdaSource` deserialization shape used for both env-var and YAML loading via figment's dual-extraction. This is the first step of migrating bottlecap's in-tree config module onto the shared serverless-components `datadog-agent-config` crate. The extension carries the 19 Lambda-specific fields with no upstream equivalent (api_key_secret_arn, kms_api_key, api_key_ssm_arn, serverless_logs_enabled, serverless_flush_strategy, enhanced_metrics, lambda_proc_enhanced_metrics, capture_lambda_payload, capture_lambda_payload_max_depth, compute_trace_stats_on_extension, span_dedup_timeout, api_key_secret_reload_interval, dd_org_uuid, serverless_appsec_enabled, appsec_rules, appsec_waf_timeout, api_security_enabled, api_security_sample_delay, custom_metrics_exclude_tags). Behavior preserved end-to-end with 33 tests covering each field from both DD_* env vars and datadog.yaml, plus: - DD_LOGS_ENABLED <-> DD_SERVERLESS_LOGS_ENABLED OR-merge - FlushStrategy ("end", "periodically,N", invalid -> Default) - Duration parsing (seconds, microseconds, ignore-zero) - org_uuid -> dd_org_uuid and lambda_customer_metrics_exclude_tags -> custom_metrics_exclude_tags source-to-config field mappings - env precedence over YAML - Forgiving fallback when a single field is malformed The dep is pinned to the pre-merge SHA of DataDog/serverless-components#135 (libdatadog rev alignment) for development; will be re-pinned to the merged SHA before opening the migration PR. Follow-ups (in subsequent commits): - Replace bottlecap::config::Config with datadog_agent_config::Config - Delete duplicated env.rs / yaml.rs / deserializer modules --- bottlecap/Cargo.lock | 45 +- bottlecap/Cargo.toml | 2 + bottlecap/src/config/lambda_extension.rs | 551 +++++++++++++++++++++++ bottlecap/src/config/mod.rs | 1 + 4 files changed, 598 insertions(+), 1 deletion(-) create mode 100644 bottlecap/src/config/lambda_extension.rs diff --git a/bottlecap/Cargo.lock b/bottlecap/Cargo.lock index c360692fe..9fa7eff6c 100644 --- a/bottlecap/Cargo.lock +++ b/bottlecap/Cargo.lock @@ -484,11 +484,12 @@ dependencies = [ "bytes", "chrono", "cookie", + "datadog-agent-config", "datadog-fips", "datadog-opentelemetry", "datadog-protos", "ddsketch-agent", - "dogstatsd", + "dogstatsd 0.1.0 (git+https://github.com/DataDog/serverless-components?rev=5b68f50f49c9defbfed4d25bd621e2a86405a972)", "figment", "flate2", "fnv", @@ -778,6 +779,24 @@ dependencies = [ "typenum", ] +[[package]] +name = "datadog-agent-config" +version = "0.1.0" +source = "git+https://github.com/DataDog/serverless-components?rev=14520949fb15c1dbf80b02006a974f75b57ec376#14520949fb15c1dbf80b02006a974f75b57ec376" +dependencies = [ + "datadog-opentelemetry", + "dogstatsd 0.1.0 (git+https://github.com/DataDog/serverless-components?rev=14520949fb15c1dbf80b02006a974f75b57ec376)", + "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" @@ -920,6 +939,30 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dogstatsd" +version = "0.1.0" +source = "git+https://github.com/DataDog/serverless-components?rev=14520949fb15c1dbf80b02006a974f75b57ec376#14520949fb15c1dbf80b02006a974f75b57ec376" +dependencies = [ + "datadog-protos", + "ddsketch-agent", + "derive_more", + "fnv", + "hashbrown 0.15.5", + "protobuf", + "regex", + "reqwest", + "serde", + "serde_json", + "socket2 0.6.3", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "tracing", + "ustr", + "zstd", +] + [[package]] name = "dogstatsd" version = "0.1.0" diff --git a/bottlecap/Cargo.toml b/bottlecap/Cargo.toml index 446bab161..d8772b231 100644 --- a/bottlecap/Cargo.toml +++ b/bottlecap/Cargo.toml @@ -84,6 +84,8 @@ libdd-trace-stats = { git = "https://github.com/DataDog/libdatadog", rev = "48da 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 } +# TODO: re-pin to merged SHA once DataDog/serverless-components#135 lands. +datadog-agent-config = { git = "https://github.com/DataDog/serverless-components", rev = "14520949fb15c1dbf80b02006a974f75b57ec376", 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/config/lambda_extension.rs b/bottlecap/src/config/lambda_extension.rs new file mode 100644 index 000000000..28b566b8b --- /dev/null +++ b/bottlecap/src/config/lambda_extension.rs @@ -0,0 +1,551 @@ +use std::time::Duration; + +use datadog_agent_config::{ + ConfigExtension, deserialize_array_from_comma_separated_string, + 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, flush_strategy::FlushStrategy, merge_fields, merge_string, +}; +use serde::Deserialize; + +/// Lambda-specific configuration that lives alongside the shared +/// `datadog_agent_config::Config` core fields under `config.ext`. +#[derive(Debug, PartialEq, Clone)] +#[allow(clippy::struct_excessive_bools)] +pub struct LambdaExtension { + 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: FlushStrategy, + 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 LambdaExtension { + 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: FlushStrategy::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)] +pub struct LambdaSource { + #[serde(deserialize_with = "deserialize_optional_string")] + pub api_key_secret_arn: Option, + #[serde(deserialize_with = "deserialize_optional_string")] + pub kms_api_key: Option, + #[serde(deserialize_with = "deserialize_optional_string")] + pub api_key_ssm_arn: Option, + + /// `DD_SERVERLESS_LOGS_ENABLED` — primary toggle for Lambda log shipping. + #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] + 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 = "deserialize_optional_bool_from_anything")] + pub logs_enabled: Option, + + pub serverless_flush_strategy: Option, + + #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] + pub enhanced_metrics: Option, + #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] + pub lambda_proc_enhanced_metrics: Option, + #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] + pub capture_lambda_payload: Option, + pub capture_lambda_payload_max_depth: Option, + #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] + pub compute_trace_stats_on_extension: Option, + + #[serde(deserialize_with = "deserialize_optional_duration_from_seconds_ignore_zero")] + pub span_dedup_timeout: Option, + #[serde(deserialize_with = "deserialize_optional_duration_from_seconds_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 = "deserialize_string_or_int")] + pub org_uuid: Option, + + #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] + pub serverless_appsec_enabled: Option, + #[serde(deserialize_with = "deserialize_optional_string")] + pub appsec_rules: Option, + #[serde(deserialize_with = "deserialize_optional_duration_from_microseconds")] + pub appsec_waf_timeout: Option, + #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] + pub api_security_enabled: Option, + #[serde(deserialize_with = "deserialize_optional_duration_from_seconds")] + 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 = "deserialize_array_from_comma_separated_string")] + pub lambda_customer_metrics_exclude_tags: Vec, +} + +impl ConfigExtension for LambdaExtension { + type Source = LambdaSource; + + fn merge_from(&mut self, source: &Self::Source) { + 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) + 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(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use std::path::Path; + + use datadog_agent_config::{ + Config, flush_strategy::PeriodicStrategy, get_config_with_extension, + }; + use figment::Jail; + + use super::*; + + fn load(jail_setup: impl FnOnce(&mut Jail) -> figment::Result<()>) -> Config { + 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_extension_default() { + let config = load(|_| Ok(())); + assert_eq!(config.ext, LambdaExtension::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() { + // YAML support is new in the extension; previously env-only in bottlecap. + 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, FlushStrategy::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, + FlushStrategy::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, + FlushStrategy::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, FlushStrategy::Default); + } + + // ---- 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 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_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); + } +} diff --git a/bottlecap/src/config/mod.rs b/bottlecap/src/config/mod.rs index 7f5d1aed8..27fd12c2b 100644 --- a/bottlecap/src/config/mod.rs +++ b/bottlecap/src/config/mod.rs @@ -3,6 +3,7 @@ pub mod apm_replace_rule; pub mod aws; pub mod env; pub mod flush_strategy; +pub mod lambda_extension; pub mod log_level; pub mod logs_additional_endpoints; pub mod processing_rule; From ade85e7ad38583d35d4cd31b34401be86e3324b8 Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:00:09 -0400 Subject: [PATCH 2/9] chore(deps): re-pin datadog-agent-config to merged SHA DataDog/serverless-components#135 merged at aaac1a5d63faf664750a3bf51b50b79449a31625. The previous pin was the pre-merge branch SHA used during development. --- bottlecap/Cargo.lock | 8 ++++---- bottlecap/Cargo.toml | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/bottlecap/Cargo.lock b/bottlecap/Cargo.lock index 9fa7eff6c..af66d243b 100644 --- a/bottlecap/Cargo.lock +++ b/bottlecap/Cargo.lock @@ -782,10 +782,10 @@ dependencies = [ [[package]] name = "datadog-agent-config" version = "0.1.0" -source = "git+https://github.com/DataDog/serverless-components?rev=14520949fb15c1dbf80b02006a974f75b57ec376#14520949fb15c1dbf80b02006a974f75b57ec376" +source = "git+https://github.com/DataDog/serverless-components?rev=aaac1a5d63faf664750a3bf51b50b79449a31625#aaac1a5d63faf664750a3bf51b50b79449a31625" dependencies = [ "datadog-opentelemetry", - "dogstatsd 0.1.0 (git+https://github.com/DataDog/serverless-components?rev=14520949fb15c1dbf80b02006a974f75b57ec376)", + "dogstatsd 0.1.0 (git+https://github.com/DataDog/serverless-components?rev=aaac1a5d63faf664750a3bf51b50b79449a31625)", "figment", "libdd-trace-obfuscation", "libdd-trace-utils 6.0.1", @@ -942,7 +942,7 @@ dependencies = [ [[package]] name = "dogstatsd" version = "0.1.0" -source = "git+https://github.com/DataDog/serverless-components?rev=14520949fb15c1dbf80b02006a974f75b57ec376#14520949fb15c1dbf80b02006a974f75b57ec376" +source = "git+https://github.com/DataDog/serverless-components?rev=5b68f50f49c9defbfed4d25bd621e2a86405a972#5b68f50f49c9defbfed4d25bd621e2a86405a972" dependencies = [ "datadog-protos", "ddsketch-agent", @@ -966,7 +966,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=aaac1a5d63faf664750a3bf51b50b79449a31625#aaac1a5d63faf664750a3bf51b50b79449a31625" dependencies = [ "datadog-protos", "ddsketch-agent", diff --git a/bottlecap/Cargo.toml b/bottlecap/Cargo.toml index d8772b231..ccfba3c53 100644 --- a/bottlecap/Cargo.toml +++ b/bottlecap/Cargo.toml @@ -84,8 +84,7 @@ libdd-trace-stats = { git = "https://github.com/DataDog/libdatadog", rev = "48da 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 } -# TODO: re-pin to merged SHA once DataDog/serverless-components#135 lands. -datadog-agent-config = { git = "https://github.com/DataDog/serverless-components", rev = "14520949fb15c1dbf80b02006a974f75b57ec376", default-features = false } +datadog-agent-config = { git = "https://github.com/DataDog/serverless-components", rev = "aaac1a5d63faf664750a3bf51b50b79449a31625", default-features = false } libddwaf = { version = "1.28.1", git = "https://github.com/DataDog/libddwaf-rust", rev = "d1534a158d976bd4f747bf9fcc58e0712d2d17fc", default-features = false, features = ["serde"] } [dev-dependencies] From fca8e69cd54f45b72917c3dc730d7f9b291d6023 Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:27:13 -0400 Subject: [PATCH 3/9] fix(deps): align dogstatsd/datadog-fips pins with datadog-agent-config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Datadog-agent-config transitively pulls dogstatsd@aaac1a5d (uses parse_metric_namespace from it); bottlecap's existing dogstatsd pin was still at 5b68f50f. Cargo treated the two as separate packages, which broke the FIPS clippy job — the new transitive copy didn't inherit bottlecap's rustls/fips feature plumbing and ring ended up in the dependency graph. Bumping dogstatsd and datadog-fips to the same SHA collapses the duplicate, restoring a clean FIPS dependency tree. --- bottlecap/Cargo.lock | 30 +++--------------------------- bottlecap/Cargo.toml | 4 ++-- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/bottlecap/Cargo.lock b/bottlecap/Cargo.lock index af66d243b..ed5a4c8c1 100644 --- a/bottlecap/Cargo.lock +++ b/bottlecap/Cargo.lock @@ -489,7 +489,7 @@ dependencies = [ "datadog-opentelemetry", "datadog-protos", "ddsketch-agent", - "dogstatsd 0.1.0 (git+https://github.com/DataDog/serverless-components?rev=5b68f50f49c9defbfed4d25bd621e2a86405a972)", + "dogstatsd", "figment", "flate2", "fnv", @@ -785,7 +785,7 @@ version = "0.1.0" source = "git+https://github.com/DataDog/serverless-components?rev=aaac1a5d63faf664750a3bf51b50b79449a31625#aaac1a5d63faf664750a3bf51b50b79449a31625" dependencies = [ "datadog-opentelemetry", - "dogstatsd 0.1.0 (git+https://github.com/DataDog/serverless-components?rev=aaac1a5d63faf664750a3bf51b50b79449a31625)", + "dogstatsd", "figment", "libdd-trace-obfuscation", "libdd-trace-utils 6.0.1", @@ -800,7 +800,7 @@ dependencies = [ [[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=aaac1a5d63faf664750a3bf51b50b79449a31625#aaac1a5d63faf664750a3bf51b50b79449a31625" dependencies = [ "reqwest", "rustls", @@ -939,30 +939,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "dogstatsd" -version = "0.1.0" -source = "git+https://github.com/DataDog/serverless-components?rev=5b68f50f49c9defbfed4d25bd621e2a86405a972#5b68f50f49c9defbfed4d25bd621e2a86405a972" -dependencies = [ - "datadog-protos", - "ddsketch-agent", - "derive_more", - "fnv", - "hashbrown 0.15.5", - "protobuf", - "regex", - "reqwest", - "serde", - "serde_json", - "socket2 0.6.3", - "thiserror 1.0.69", - "tokio", - "tokio-util", - "tracing", - "ustr", - "zstd", -] - [[package]] name = "dogstatsd" version = "0.1.0" diff --git a/bottlecap/Cargo.toml b/bottlecap/Cargo.toml index ccfba3c53..3e4fc83c9 100644 --- a/bottlecap/Cargo.toml +++ b/bottlecap/Cargo.toml @@ -82,8 +82,8 @@ 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 = "aaac1a5d63faf664750a3bf51b50b79449a31625", default-features = false } +datadog-fips = { git = "https://github.com/DataDog/serverless-components", rev = "aaac1a5d63faf664750a3bf51b50b79449a31625", default-features = false } datadog-agent-config = { git = "https://github.com/DataDog/serverless-components", rev = "aaac1a5d63faf664750a3bf51b50b79449a31625", default-features = false } libddwaf = { version = "1.28.1", git = "https://github.com/DataDog/libddwaf-rust", rev = "d1534a158d976bd4f747bf9fcc58e0712d2d17fc", default-features = false, features = ["serde"] } From cab7e5ef91e1057ba166891c82aff82bf5d86ff9 Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:32:38 -0400 Subject: [PATCH 4/9] refactor(config): rename LambdaExtension -> LambdaConfig, inline into mod.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `LambdaExtension` collides nominally with "the Datadog Lambda Extension" (this entire project). `LambdaConfig` reads more naturally for the extension type that holds Lambda-specific configuration fields. While here, fold the standalone lambda_extension.rs into config/mod.rs so consumers don't need a separate module hop. The inlined code uses fully-qualified `datadog_agent_config::merge_fields!` / `datadog_agent_config::merge_string!` invocations to coexist with the legacy `#[macro_export]` macros still at the top of mod.rs — both go away together once the migration onto the upstream Config lands. Renames carried through: LambdaExtension -> LambdaConfig LambdaSource -> LambdaConfigSource mod lambda_extension -> (gone; inlined) All 33 LambdaConfig tests still pass. --- bottlecap/src/config/lambda_extension.rs | 551 ---------------------- bottlecap/src/config/mod.rs | 572 ++++++++++++++++++++++- 2 files changed, 571 insertions(+), 552 deletions(-) delete mode 100644 bottlecap/src/config/lambda_extension.rs diff --git a/bottlecap/src/config/lambda_extension.rs b/bottlecap/src/config/lambda_extension.rs deleted file mode 100644 index 28b566b8b..000000000 --- a/bottlecap/src/config/lambda_extension.rs +++ /dev/null @@ -1,551 +0,0 @@ -use std::time::Duration; - -use datadog_agent_config::{ - ConfigExtension, deserialize_array_from_comma_separated_string, - 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, flush_strategy::FlushStrategy, merge_fields, merge_string, -}; -use serde::Deserialize; - -/// Lambda-specific configuration that lives alongside the shared -/// `datadog_agent_config::Config` core fields under `config.ext`. -#[derive(Debug, PartialEq, Clone)] -#[allow(clippy::struct_excessive_bools)] -pub struct LambdaExtension { - 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: FlushStrategy, - 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 LambdaExtension { - 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: FlushStrategy::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)] -pub struct LambdaSource { - #[serde(deserialize_with = "deserialize_optional_string")] - pub api_key_secret_arn: Option, - #[serde(deserialize_with = "deserialize_optional_string")] - pub kms_api_key: Option, - #[serde(deserialize_with = "deserialize_optional_string")] - pub api_key_ssm_arn: Option, - - /// `DD_SERVERLESS_LOGS_ENABLED` — primary toggle for Lambda log shipping. - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - 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 = "deserialize_optional_bool_from_anything")] - pub logs_enabled: Option, - - pub serverless_flush_strategy: Option, - - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub enhanced_metrics: Option, - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub lambda_proc_enhanced_metrics: Option, - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub capture_lambda_payload: Option, - pub capture_lambda_payload_max_depth: Option, - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub compute_trace_stats_on_extension: Option, - - #[serde(deserialize_with = "deserialize_optional_duration_from_seconds_ignore_zero")] - pub span_dedup_timeout: Option, - #[serde(deserialize_with = "deserialize_optional_duration_from_seconds_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 = "deserialize_string_or_int")] - pub org_uuid: Option, - - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub serverless_appsec_enabled: Option, - #[serde(deserialize_with = "deserialize_optional_string")] - pub appsec_rules: Option, - #[serde(deserialize_with = "deserialize_optional_duration_from_microseconds")] - pub appsec_waf_timeout: Option, - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub api_security_enabled: Option, - #[serde(deserialize_with = "deserialize_optional_duration_from_seconds")] - 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 = "deserialize_array_from_comma_separated_string")] - pub lambda_customer_metrics_exclude_tags: Vec, -} - -impl ConfigExtension for LambdaExtension { - type Source = LambdaSource; - - fn merge_from(&mut self, source: &Self::Source) { - 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) - 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(test)] -#[allow(clippy::unwrap_used)] -mod tests { - use std::path::Path; - - use datadog_agent_config::{ - Config, flush_strategy::PeriodicStrategy, get_config_with_extension, - }; - use figment::Jail; - - use super::*; - - fn load(jail_setup: impl FnOnce(&mut Jail) -> figment::Result<()>) -> Config { - 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_extension_default() { - let config = load(|_| Ok(())); - assert_eq!(config.ext, LambdaExtension::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() { - // YAML support is new in the extension; previously env-only in bottlecap. - 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, FlushStrategy::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, - FlushStrategy::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, - FlushStrategy::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, FlushStrategy::Default); - } - - // ---- 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 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_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); - } -} diff --git a/bottlecap/src/config/mod.rs b/bottlecap/src/config/mod.rs index 27fd12c2b..7dafede55 100644 --- a/bottlecap/src/config/mod.rs +++ b/bottlecap/src/config/mod.rs @@ -3,7 +3,6 @@ pub mod apm_replace_rule; pub mod aws; pub mod env; pub mod flush_strategy; -pub mod lambda_extension; pub mod log_level; pub mod logs_additional_endpoints; pub mod processing_rule; @@ -1671,3 +1670,574 @@ 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_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, + 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(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 + ); + } + + // ---- 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 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_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); + } +} From 1f944793c42d8fd513ca7568ce3b8b12bdda54de Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:56:52 -0400 Subject: [PATCH 5/9] fix(deps): wire datadog-agent-config TLS feature + regen license CSV Bumps datadog-agent-config to the upstream PR branch SHA carrying DataDog/serverless-components#136 (which switches the libdd deps to default-features = false and exposes new https/fips features), and plumbs them through bottlecap's default/fips feature spec so the right TLS provider is selected per build. Without this, datadog-agent-config's libdd transitive dependencies implicitly enabled `https` (ring-backed hyper-rustls) on top of the fips path that bottlecap activates, leaving both providers in the dependency graph. The datadog-fips build script then rejected the build because ring v0.17 was reachable via: ring v0.17 -> libdd-common feature "https" -> hyper-rustls feature "ring" -> rustls-webpki feature "ring" With agent-config now opting in via consumer features instead of defaults, the FIPS dep tree is clean again. Also regenerated LICENSE-3rdparty.csv to include the new datadog-agent-config package, per the dd-rust-license-tool check. TODO: re-pin to the merge SHA once DataDog/serverless-components#136 lands. --- bottlecap/Cargo.lock | 30 +++++++++++++++++++++++++++--- bottlecap/Cargo.toml | 5 ++++- bottlecap/LICENSE-3rdparty.csv | 1 + 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/bottlecap/Cargo.lock b/bottlecap/Cargo.lock index ed5a4c8c1..5a4d3c1ea 100644 --- a/bottlecap/Cargo.lock +++ b/bottlecap/Cargo.lock @@ -489,7 +489,7 @@ dependencies = [ "datadog-opentelemetry", "datadog-protos", "ddsketch-agent", - "dogstatsd", + "dogstatsd 0.1.0 (git+https://github.com/DataDog/serverless-components?rev=aaac1a5d63faf664750a3bf51b50b79449a31625)", "figment", "flate2", "fnv", @@ -782,10 +782,10 @@ dependencies = [ [[package]] name = "datadog-agent-config" version = "0.1.0" -source = "git+https://github.com/DataDog/serverless-components?rev=aaac1a5d63faf664750a3bf51b50b79449a31625#aaac1a5d63faf664750a3bf51b50b79449a31625" +source = "git+https://github.com/DataDog/serverless-components?rev=d43efc37327e07a7fe6a6d434d3dc229d186da9b#d43efc37327e07a7fe6a6d434d3dc229d186da9b" dependencies = [ "datadog-opentelemetry", - "dogstatsd", + "dogstatsd 0.1.0 (git+https://github.com/DataDog/serverless-components?rev=d43efc37327e07a7fe6a6d434d3dc229d186da9b)", "figment", "libdd-trace-obfuscation", "libdd-trace-utils 6.0.1", @@ -963,6 +963,30 @@ dependencies = [ "zstd", ] +[[package]] +name = "dogstatsd" +version = "0.1.0" +source = "git+https://github.com/DataDog/serverless-components?rev=d43efc37327e07a7fe6a6d434d3dc229d186da9b#d43efc37327e07a7fe6a6d434d3dc229d186da9b" +dependencies = [ + "datadog-protos", + "ddsketch-agent", + "derive_more", + "fnv", + "hashbrown 0.15.5", + "protobuf", + "regex", + "reqwest", + "serde", + "serde_json", + "socket2 0.6.3", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "tracing", + "ustr", + "zstd", +] + [[package]] name = "dunce" version = "1.0.5" diff --git a/bottlecap/Cargo.toml b/bottlecap/Cargo.toml index 3e4fc83c9..e14e09f2b 100644 --- a/bottlecap/Cargo.toml +++ b/bottlecap/Cargo.toml @@ -84,7 +84,8 @@ libdd-trace-stats = { git = "https://github.com/DataDog/libdatadog", rev = "48da datadog-opentelemetry = { git = "https://github.com/DataDog/dd-trace-rs", rev = "f51cefc4ad24bec81b38fb2f36b1ed93f21ae913", default-features = false } dogstatsd = { git = "https://github.com/DataDog/serverless-components", rev = "aaac1a5d63faf664750a3bf51b50b79449a31625", default-features = false } datadog-fips = { git = "https://github.com/DataDog/serverless-components", rev = "aaac1a5d63faf664750a3bf51b50b79449a31625", default-features = false } -datadog-agent-config = { git = "https://github.com/DataDog/serverless-components", rev = "aaac1a5d63faf664750a3bf51b50b79449a31625", default-features = false } +# TODO: re-pin to the merge SHA once DataDog/serverless-components#136 lands. +datadog-agent-config = { git = "https://github.com/DataDog/serverless-components", rev = "d43efc37327e07a7fe6a6d434d3dc229d186da9b", default-features = false } libddwaf = { version = "1.28.1", git = "https://github.com/DataDog/libddwaf-rust", rev = "d1534a158d976bd4f747bf9fcc58e0712d2d17fc", default-features = false, features = ["serde"] } [dev-dependencies] @@ -131,12 +132,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 From ec0d1e3979bba0f528e895c45092494712644a3b Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:00:12 -0400 Subject: [PATCH 6/9] chore(deps): re-pin datadog-agent-config to address PR #136 review Picks up the doc-comment clarification from the copilot review on DataDog/serverless-components#136 (https/fips features aren't Cargo-enforced). --- bottlecap/Cargo.lock | 8 ++++---- bottlecap/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bottlecap/Cargo.lock b/bottlecap/Cargo.lock index 5a4d3c1ea..c46b3fd3f 100644 --- a/bottlecap/Cargo.lock +++ b/bottlecap/Cargo.lock @@ -782,10 +782,10 @@ dependencies = [ [[package]] name = "datadog-agent-config" version = "0.1.0" -source = "git+https://github.com/DataDog/serverless-components?rev=d43efc37327e07a7fe6a6d434d3dc229d186da9b#d43efc37327e07a7fe6a6d434d3dc229d186da9b" +source = "git+https://github.com/DataDog/serverless-components?rev=429578db1c87d7304f368751c216db59dc8fd59a#429578db1c87d7304f368751c216db59dc8fd59a" dependencies = [ "datadog-opentelemetry", - "dogstatsd 0.1.0 (git+https://github.com/DataDog/serverless-components?rev=d43efc37327e07a7fe6a6d434d3dc229d186da9b)", + "dogstatsd 0.1.0 (git+https://github.com/DataDog/serverless-components?rev=429578db1c87d7304f368751c216db59dc8fd59a)", "figment", "libdd-trace-obfuscation", "libdd-trace-utils 6.0.1", @@ -942,7 +942,7 @@ dependencies = [ [[package]] name = "dogstatsd" version = "0.1.0" -source = "git+https://github.com/DataDog/serverless-components?rev=aaac1a5d63faf664750a3bf51b50b79449a31625#aaac1a5d63faf664750a3bf51b50b79449a31625" +source = "git+https://github.com/DataDog/serverless-components?rev=429578db1c87d7304f368751c216db59dc8fd59a#429578db1c87d7304f368751c216db59dc8fd59a" dependencies = [ "datadog-protos", "ddsketch-agent", @@ -966,7 +966,7 @@ dependencies = [ [[package]] name = "dogstatsd" version = "0.1.0" -source = "git+https://github.com/DataDog/serverless-components?rev=d43efc37327e07a7fe6a6d434d3dc229d186da9b#d43efc37327e07a7fe6a6d434d3dc229d186da9b" +source = "git+https://github.com/DataDog/serverless-components?rev=aaac1a5d63faf664750a3bf51b50b79449a31625#aaac1a5d63faf664750a3bf51b50b79449a31625" dependencies = [ "datadog-protos", "ddsketch-agent", diff --git a/bottlecap/Cargo.toml b/bottlecap/Cargo.toml index e14e09f2b..a43c67a97 100644 --- a/bottlecap/Cargo.toml +++ b/bottlecap/Cargo.toml @@ -85,7 +85,7 @@ datadog-opentelemetry = { git = "https://github.com/DataDog/dd-trace-rs", rev = dogstatsd = { git = "https://github.com/DataDog/serverless-components", rev = "aaac1a5d63faf664750a3bf51b50b79449a31625", default-features = false } datadog-fips = { git = "https://github.com/DataDog/serverless-components", rev = "aaac1a5d63faf664750a3bf51b50b79449a31625", default-features = false } # TODO: re-pin to the merge SHA once DataDog/serverless-components#136 lands. -datadog-agent-config = { git = "https://github.com/DataDog/serverless-components", rev = "d43efc37327e07a7fe6a6d434d3dc229d186da9b", default-features = false } +datadog-agent-config = { git = "https://github.com/DataDog/serverless-components", rev = "429578db1c87d7304f368751c216db59dc8fd59a", default-features = false } libddwaf = { version = "1.28.1", git = "https://github.com/DataDog/libddwaf-rust", rev = "d1534a158d976bd4f747bf9fcc58e0712d2d17fc", default-features = false, features = ["serde"] } [dev-dependencies] From f6abcb4f52da8e0ab7d2aeb91e78b549528b736a Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:05:01 -0400 Subject: [PATCH 7/9] chore(deps): re-pin datadog-agent-config to #136 merge SHA DataDog/serverless-components#136 merged at bb4dedeee20b949db3143c05e5a779b843a8a484. The previous pin was the pre-merge branch SHA used during development. --- bottlecap/Cargo.lock | 8 ++++---- bottlecap/Cargo.toml | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/bottlecap/Cargo.lock b/bottlecap/Cargo.lock index c46b3fd3f..b356edd11 100644 --- a/bottlecap/Cargo.lock +++ b/bottlecap/Cargo.lock @@ -782,10 +782,10 @@ dependencies = [ [[package]] name = "datadog-agent-config" version = "0.1.0" -source = "git+https://github.com/DataDog/serverless-components?rev=429578db1c87d7304f368751c216db59dc8fd59a#429578db1c87d7304f368751c216db59dc8fd59a" +source = "git+https://github.com/DataDog/serverless-components?rev=bb4dedeee20b949db3143c05e5a779b843a8a484#bb4dedeee20b949db3143c05e5a779b843a8a484" dependencies = [ "datadog-opentelemetry", - "dogstatsd 0.1.0 (git+https://github.com/DataDog/serverless-components?rev=429578db1c87d7304f368751c216db59dc8fd59a)", + "dogstatsd 0.1.0 (git+https://github.com/DataDog/serverless-components?rev=bb4dedeee20b949db3143c05e5a779b843a8a484)", "figment", "libdd-trace-obfuscation", "libdd-trace-utils 6.0.1", @@ -942,7 +942,7 @@ dependencies = [ [[package]] name = "dogstatsd" version = "0.1.0" -source = "git+https://github.com/DataDog/serverless-components?rev=429578db1c87d7304f368751c216db59dc8fd59a#429578db1c87d7304f368751c216db59dc8fd59a" +source = "git+https://github.com/DataDog/serverless-components?rev=aaac1a5d63faf664750a3bf51b50b79449a31625#aaac1a5d63faf664750a3bf51b50b79449a31625" dependencies = [ "datadog-protos", "ddsketch-agent", @@ -966,7 +966,7 @@ dependencies = [ [[package]] name = "dogstatsd" version = "0.1.0" -source = "git+https://github.com/DataDog/serverless-components?rev=aaac1a5d63faf664750a3bf51b50b79449a31625#aaac1a5d63faf664750a3bf51b50b79449a31625" +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 a43c67a97..c9c7e7d64 100644 --- a/bottlecap/Cargo.toml +++ b/bottlecap/Cargo.toml @@ -84,8 +84,7 @@ libdd-trace-stats = { git = "https://github.com/DataDog/libdatadog", rev = "48da datadog-opentelemetry = { git = "https://github.com/DataDog/dd-trace-rs", rev = "f51cefc4ad24bec81b38fb2f36b1ed93f21ae913", default-features = false } dogstatsd = { git = "https://github.com/DataDog/serverless-components", rev = "aaac1a5d63faf664750a3bf51b50b79449a31625", default-features = false } datadog-fips = { git = "https://github.com/DataDog/serverless-components", rev = "aaac1a5d63faf664750a3bf51b50b79449a31625", default-features = false } -# TODO: re-pin to the merge SHA once DataDog/serverless-components#136 lands. -datadog-agent-config = { git = "https://github.com/DataDog/serverless-components", rev = "429578db1c87d7304f368751c216db59dc8fd59a", 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] From f22897437992c6be5568d1e72b0f966290563fe7 Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:24:54 -0400 Subject: [PATCH 8/9] fix(config): address review feedback on #1249 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three real fixes from independent review: 1. **Cargo.lock dedup** — bump dogstatsd and datadog-fips from `aaac1a5d` to `bb4dedee` so they share the same serverless-components SHA as datadog-agent-config. Otherwise cargo resolves two distinct `dogstatsd` source entries (the dep transitively from agent-config pulled in a second copy at the newer rev). Source trees for both crates are byte-identical between the two SHAs; only #136 changed between them. PR description matches reality now. 2. **`capture_lambda_payload_max_depth` graceful deserializer** — the field was missing `#[serde(deserialize_with = ...)]`, violating the upstream `ConfigExtension::Source` contract. Without it, a malformed env value would silently reset *all* extension fields to their defaults (api_key_secret_arn, kms_api_key, …) and emit only a `tracing::warn!`. Adds `deser_opt_lossless` which returns `None` on bad input so the rest of the extension keeps its values. 3. **Test coverage gaps**: - YAML round-trip for the two source-to-config renames (`org_uuid → dd_org_uuid` and `lambda_customer_metrics_exclude_tags → custom_metrics_exclude_tags`). These were the exact cases where YAML behavior matters most and they only had env tests. - `FlushStrategy::EndPeriodically` (`"end,N"`) and `FlushStrategy::Continuously` (`"continuously,N"`) — the upstream enum has all four variants and bottlecap will rely on them behaviorally; previously only `End` and `Periodically` had coverage. Test count: 33 → 37. --- bottlecap/Cargo.lock | 30 ++----------------- bottlecap/Cargo.toml | 4 +-- bottlecap/src/config/mod.rs | 60 +++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/bottlecap/Cargo.lock b/bottlecap/Cargo.lock index b356edd11..3eacd5c28 100644 --- a/bottlecap/Cargo.lock +++ b/bottlecap/Cargo.lock @@ -489,7 +489,7 @@ dependencies = [ "datadog-opentelemetry", "datadog-protos", "ddsketch-agent", - "dogstatsd 0.1.0 (git+https://github.com/DataDog/serverless-components?rev=aaac1a5d63faf664750a3bf51b50b79449a31625)", + "dogstatsd", "figment", "flate2", "fnv", @@ -785,7 +785,7 @@ version = "0.1.0" source = "git+https://github.com/DataDog/serverless-components?rev=bb4dedeee20b949db3143c05e5a779b843a8a484#bb4dedeee20b949db3143c05e5a779b843a8a484" dependencies = [ "datadog-opentelemetry", - "dogstatsd 0.1.0 (git+https://github.com/DataDog/serverless-components?rev=bb4dedeee20b949db3143c05e5a779b843a8a484)", + "dogstatsd", "figment", "libdd-trace-obfuscation", "libdd-trace-utils 6.0.1", @@ -800,7 +800,7 @@ dependencies = [ [[package]] name = "datadog-fips" version = "0.1.0" -source = "git+https://github.com/DataDog/serverless-components?rev=aaac1a5d63faf664750a3bf51b50b79449a31625#aaac1a5d63faf664750a3bf51b50b79449a31625" +source = "git+https://github.com/DataDog/serverless-components?rev=bb4dedeee20b949db3143c05e5a779b843a8a484#bb4dedeee20b949db3143c05e5a779b843a8a484" dependencies = [ "reqwest", "rustls", @@ -939,30 +939,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "dogstatsd" -version = "0.1.0" -source = "git+https://github.com/DataDog/serverless-components?rev=aaac1a5d63faf664750a3bf51b50b79449a31625#aaac1a5d63faf664750a3bf51b50b79449a31625" -dependencies = [ - "datadog-protos", - "ddsketch-agent", - "derive_more", - "fnv", - "hashbrown 0.15.5", - "protobuf", - "regex", - "reqwest", - "serde", - "serde_json", - "socket2 0.6.3", - "thiserror 1.0.69", - "tokio", - "tokio-util", - "tracing", - "ustr", - "zstd", -] - [[package]] name = "dogstatsd" version = "0.1.0" diff --git a/bottlecap/Cargo.toml b/bottlecap/Cargo.toml index c9c7e7d64..b3ef5c58d 100644 --- a/bottlecap/Cargo.toml +++ b/bottlecap/Cargo.toml @@ -82,8 +82,8 @@ 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 = "aaac1a5d63faf664750a3bf51b50b79449a31625", default-features = false } -datadog-fips = { git = "https://github.com/DataDog/serverless-components", rev = "aaac1a5d63faf664750a3bf51b50b79449a31625", 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"] } diff --git a/bottlecap/src/config/mod.rs b/bottlecap/src/config/mod.rs index 7dafede55..72b88fdee 100644 --- a/bottlecap/src/config/mod.rs +++ b/bottlecap/src/config/mod.rs @@ -1681,6 +1681,7 @@ pub mod tests { 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, @@ -1775,6 +1776,7 @@ pub struct LambdaConfigSource { 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, @@ -2045,6 +2047,30 @@ mod lambda_config_tests { ); } + #[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] @@ -2196,6 +2222,23 @@ mod lambda_config_tests { ); } + #[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| { @@ -2211,6 +2254,23 @@ mod lambda_config_tests { ); } + #[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(())); From f6364933a9304ea000b9165f72a7c8fa62fc3ef3 Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:34:49 -0400 Subject: [PATCH 9/9] fix(config): skip coverage instrumentation on lambda_config_tests Per Copilot review on #1249. The existing `tests` module above already has `#[cfg_attr(coverage_nightly, coverage(off))]` to keep test code out of coverage metrics on nightly coverage builds; the new `lambda_config_tests` module should match. --- bottlecap/src/config/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/bottlecap/src/config/mod.rs b/bottlecap/src/config/mod.rs index 72b88fdee..c05551a4b 100644 --- a/bottlecap/src/config/mod.rs +++ b/bottlecap/src/config/mod.rs @@ -1854,6 +1854,7 @@ impl DatadogConfigExtension for LambdaConfig { } } +#[cfg_attr(coverage_nightly, coverage(off))] // Test modules skew coverage metrics #[cfg(test)] #[allow(clippy::unwrap_used)] mod lambda_config_tests {