From 3d0fa3c845795e49684805fd192ab04e32db7085 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/6] 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/src/config/lambda_extension.rs | 551 +++++++++++++++++++++++ bottlecap/src/config/mod.rs | 1 + 2 files changed, 552 insertions(+) create mode 100644 bottlecap/src/config/lambda_extension.rs 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 3c49087ba..d270d4f19 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 cd8966c67ef4850a4067841b877c7a327be78b32 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 2/6] 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 ----------------------- 1 file changed, 551 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); - } -} From c095b8377b8a476da00096094847bc4c62233069 Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:53:34 -0400 Subject: [PATCH 3/6] refactor(config): replace bottlecap Config with upstream Config Completes the migration started in the previous PR onto the shared datadog-agent-config crate. bottlecap::config::Config is now a type alias for datadog_agent_config::Config; Lambda-specific fields live under .ext (per the upstream ConfigExtension trait), and the 10 in-tree config submodules that duplicated upstream implementations are removed entirely. What changes structurally: - bottlecap/src/config/mod.rs shrinks from 2243 lines to ~600. The legacy Config struct, ConfigBuilder, ConfigSource, ConfigError, the #[macro_export] merge_* macros, all the per-field deserializer helpers, and the entire test module that mirrored upstream's behavior are gone. What remains: a `type Config` alias, a `get_config(path)` wrapper, the LambdaConfig extension itself, and re-exports of upstream modules under the same `crate::config::*` paths so existing imports keep working. - New module bottlecap/src/config/propagation_wrapper.rs holds a newtype `PropConfig(Arc)` so we can implement the foreign PropagationConfig trait on the foreign Config type without running afoul of Rust's orphan rule. The wrapper is scoped to the dd-trace-rs propagator boundary; nothing else uses it. - bottlecap/src/traces/propagation/mod.rs wraps the inner propagator in PropConfig instead of passing Config directly. All call sites that previously handed `Arc` to the propagator are unchanged - the wrapping happens inside DatadogCompositePropagator::new. - Deleted files (each redundant with upstream): additional_endpoints.rs, apm_replace_rule.rs, env.rs, flush_strategy.rs, log_level.rs, logs_additional_endpoints.rs, processing_rule.rs, service_mapping.rs, trace_propagation_style.rs, yaml.rs - All ~70 field-access sites referencing Lambda-specific fields (config.api_key_secret_arn, config.serverless_logs_enabled, config.enhanced_metrics, etc.) updated to read through config.ext.X. Test struct literals that construct Config { ... } with Lambda fields now nest them under `ext: LambdaConfig { ..., ..Default::default() }`. What stays the same: - LambdaConfig itself (and its 33 tests) - already shipped in the parent PR; no behavior change in this commit. - All other tests pass: 501 lib + 5 integration tests green. - The .ext indirection is invisible to callers that hold an `Arc` thanks to Rust's field-access auto-deref - they just go from `config.foo` to `config.ext.foo` for the 19 Lambda fields. Stacked on top of the LambdaConfig foundation PR (#1249). --- bottlecap/src/appsec/mod.rs | 2 +- bottlecap/src/appsec/processor/mod.rs | 52 +- bottlecap/src/bin/bottlecap/main.rs | 17 +- bottlecap/src/config/additional_endpoints.rs | 129 -- bottlecap/src/config/apm_replace_rule.rs | 71 - bottlecap/src/config/env.rs | 1337 ------------- bottlecap/src/config/flush_strategy.rs | 168 -- bottlecap/src/config/log_level.rs | 85 - .../src/config/logs_additional_endpoints.rs | 72 - bottlecap/src/config/mod.rs | 1716 +---------------- bottlecap/src/config/processing_rule.rs | 56 - bottlecap/src/config/propagation_wrapper.rs | 52 + bottlecap/src/config/service_mapping.rs | 32 - .../src/config/trace_propagation_style.rs | 27 - bottlecap/src/config/yaml.rs | 1098 ----------- .../src/lifecycle/invocation/processor.rs | 22 +- .../src/lifecycle/invocation/span_inferrer.rs | 7 +- bottlecap/src/logs/lambda/processor.rs | 14 +- bottlecap/src/metrics/enhanced/lambda.rs | 42 +- bottlecap/src/otlp/agent.rs | 2 +- bottlecap/src/proxy/mod.rs | 23 +- bottlecap/src/secrets/decrypt.rs | 20 +- .../src/secrets/delegated_auth/client.rs | 2 +- bottlecap/src/tags/lambda/tags.rs | 7 +- bottlecap/src/traces/propagation/mod.rs | 8 +- bottlecap/src/traces/trace_agent.rs | 4 +- bottlecap/src/traces/trace_processor.rs | 31 +- bottlecap/tests/apm_integration_test.rs | 5 +- bottlecap/tests/appsec_processor_test.rs | 27 +- 29 files changed, 275 insertions(+), 4853 deletions(-) delete mode 100644 bottlecap/src/config/additional_endpoints.rs delete mode 100644 bottlecap/src/config/apm_replace_rule.rs delete mode 100644 bottlecap/src/config/env.rs delete mode 100644 bottlecap/src/config/flush_strategy.rs delete mode 100644 bottlecap/src/config/log_level.rs delete mode 100644 bottlecap/src/config/logs_additional_endpoints.rs delete mode 100644 bottlecap/src/config/processing_rule.rs create mode 100644 bottlecap/src/config/propagation_wrapper.rs delete mode 100644 bottlecap/src/config/service_mapping.rs delete mode 100644 bottlecap/src/config/trace_propagation_style.rs delete mode 100644 bottlecap/src/config/yaml.rs diff --git a/bottlecap/src/appsec/mod.rs b/bottlecap/src/appsec/mod.rs index c0af69870..18755a04c 100644 --- a/bottlecap/src/appsec/mod.rs +++ b/bottlecap/src/appsec/mod.rs @@ -7,7 +7,7 @@ pub mod processor; /// Determines whether the Serverless App & API Protection features are enabled. #[must_use] pub const fn is_enabled(cfg: &Config) -> bool { - cfg.serverless_appsec_enabled + cfg.ext.serverless_appsec_enabled } /// Determines whether APM is only used as a transport for App & API Protection, diff --git a/bottlecap/src/appsec/processor/mod.rs b/bottlecap/src/appsec/processor/mod.rs index 11d532a2d..d0acfee61 100644 --- a/bottlecap/src/appsec/processor/mod.rs +++ b/bottlecap/src/appsec/processor/mod.rs @@ -90,10 +90,10 @@ impl Processor { Ok(Self { handle, ruleset_version, - waf_timeout: cfg.appsec_waf_timeout, - api_sec_sampler: if cfg.api_security_enabled { + waf_timeout: cfg.ext.appsec_waf_timeout, + api_sec_sampler: if cfg.ext.api_security_enabled { Some(Arc::new(Mutex::new(apisec::Sampler::with_interval( - cfg.api_security_sample_delay, + cfg.ext.api_security_sample_delay, )))) } else { None @@ -215,7 +215,7 @@ impl Processor { /// the default built-in ruleset if the [`Config::appsec_rules`] field is /// [`None`]. fn get_rules(cfg: &Config) -> Result { - if let Some(ref rules) = cfg.appsec_rules { + if let Some(ref rules) = cfg.ext.appsec_rules { let file = File::open(rules).map_err(|e| Error::AppsecRulesError(rules.clone(), e))?; serde_json::from_reader(file) } else { @@ -716,7 +716,10 @@ mod tests { #[test] fn test_new_with_default_config() { let config = Config { - serverless_appsec_enabled: true, + ext: crate::config::LambdaConfig { + serverless_appsec_enabled: true, + ..Default::default() + }, ..Config::default() }; let _ = Processor::new(&config).expect("Should not fail"); @@ -725,7 +728,10 @@ mod tests { #[test] fn test_new_disabled() { let config = Config { - serverless_appsec_enabled: false, // Explicitly testing this condition + ext: crate::config::LambdaConfig { + serverless_appsec_enabled: false, // Explicitly testing this condition + ..Default::default() + }, ..Config::default() }; assert!(matches!( @@ -739,13 +745,16 @@ mod tests { let tmp = tempfile::NamedTempFile::new().expect("Failed to create tempfile"); let config = Config { - serverless_appsec_enabled: true, - appsec_rules: Some( - tmp.path() - .to_str() - .expect("Failed to get tempfile path") - .to_string(), - ), + ext: crate::config::LambdaConfig { + serverless_appsec_enabled: true, + appsec_rules: Some( + tmp.path() + .to_str() + .expect("Failed to get tempfile path") + .to_string(), + ), + ..Default::default() + }, ..Config::default() }; assert!(matches!( @@ -797,13 +806,16 @@ mod tests { tmp.flush().expect("Failed to flush temp file"); let config = Config { - serverless_appsec_enabled: true, - appsec_rules: Some( - tmp.path() - .to_str() - .expect("Failed to get tempfile path") - .to_string(), - ), + ext: crate::config::LambdaConfig { + serverless_appsec_enabled: true, + appsec_rules: Some( + tmp.path() + .to_str() + .expect("Failed to get tempfile path") + .to_string(), + ), + ..Default::default() + }, ..Config::default() }; let result = Processor::new(&config); diff --git a/bottlecap/src/bin/bottlecap/main.rs b/bottlecap/src/bin/bottlecap/main.rs index acfbf444c..39f352149 100644 --- a/bottlecap/src/bin/bottlecap/main.rs +++ b/bottlecap/src/bin/bottlecap/main.rs @@ -259,7 +259,7 @@ fn create_api_key_factory( let config = Arc::clone(config); let aws_config = Arc::clone(aws_config); let client = client.clone(); - let api_key_secret_reload_interval = config.api_key_secret_reload_interval; + let api_key_secret_reload_interval = config.ext.api_key_secret_reload_interval; Arc::new(ApiKeyFactory::new_from_resolver( Arc::new(move || { @@ -398,7 +398,7 @@ async fn extension_loop_active( &aws_config.runtime_api, logs_agent_channel, event_bus_tx.clone(), - config.serverless_logs_enabled, + config.ext.serverless_logs_enabled, aws_config.is_managed_instance_mode(), ) .await?; @@ -412,7 +412,8 @@ async fn extension_loop_active( ); // Validate and get the appropriate flush strategy for the current mode - let flush_strategy = get_flush_strategy_for_mode(&aws_config, config.serverless_flush_strategy); + let flush_strategy = + get_flush_strategy_for_mode(&aws_config, config.ext.serverless_flush_strategy); debug!("Flush strategy: {:?}", flush_strategy); let mut flush_control = FlushControl::new(flush_strategy, config.flush_timeout); @@ -1222,19 +1223,23 @@ async fn start_dogstatsd( ) { // Start aggregator service and handle let start_time = Instant::now(); - let enrichment_tags = if config.custom_metrics_exclude_tags.is_empty() { + let enrichment_tags = if config.ext.custom_metrics_exclude_tags.is_empty() { tags_provider.get_tags_string() } else { debug!( "Excluding tags from custom metrics: {:?}", - config.custom_metrics_exclude_tags + config.ext.custom_metrics_exclude_tags ); tags_provider .get_tags_vec() .into_iter() .filter(|tag| { let key = tag.split(':').next().unwrap_or(""); - !config.custom_metrics_exclude_tags.iter().any(|e| e == key) + !config + .ext + .custom_metrics_exclude_tags + .iter() + .any(|e| e == key) }) .collect::>() .join(",") diff --git a/bottlecap/src/config/additional_endpoints.rs b/bottlecap/src/config/additional_endpoints.rs deleted file mode 100644 index 166118331..000000000 --- a/bottlecap/src/config/additional_endpoints.rs +++ /dev/null @@ -1,129 +0,0 @@ -use serde::{Deserialize, Deserializer}; -use serde_json::Value; -use std::collections::HashMap; -use tracing::error; - -#[allow(clippy::module_name_repetitions)] -pub fn deserialize_additional_endpoints<'de, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - let value = Value::deserialize(deserializer)?; - - match value { - Value::Object(map) => { - // For YAML format (object) in datadog.yaml - let mut result = HashMap::new(); - for (key, value) in map { - match value { - Value::Array(arr) => { - let urls: Vec = arr - .into_iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect(); - result.insert(key, urls); - } - _ => { - error!( - "Failed to deserialize additional endpoints - Invalid YAML format: expected array for key {}", - key - ); - } - } - } - Ok(result) - } - Value::String(s) if !s.is_empty() => { - // For JSON format (string) in DD_ADDITIONAL_ENDPOINTS - if let Ok(map) = serde_json::from_str(&s) { - Ok(map) - } else { - error!("Failed to deserialize additional endpoints - Invalid JSON format"); - Ok(HashMap::new()) - } - } - _ => Ok(HashMap::new()), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn test_deserialize_additional_endpoints_yaml() { - // Test YAML format (object) - let input = json!({ - "https://app.datadoghq.com": ["key1", "key2"], - "https://app.datadoghq.eu": ["key3"] - }); - - let result = deserialize_additional_endpoints(input) - .expect("Failed to deserialize additional endpoints"); - - let mut expected = HashMap::new(); - expected.insert( - "https://app.datadoghq.com".to_string(), - vec!["key1".to_string(), "key2".to_string()], - ); - expected.insert( - "https://app.datadoghq.eu".to_string(), - vec!["key3".to_string()], - ); - - assert_eq!(result, expected); - } - - #[test] - fn test_deserialize_additional_endpoints_json() { - // Test JSON string format - let input = json!( - "{\"https://app.datadoghq.com\":[\"key1\",\"key2\"],\"https://app.datadoghq.eu\":[\"key3\"]}" - ); - - let result = deserialize_additional_endpoints(input) - .expect("Failed to deserialize additional endpoints"); - - let mut expected = HashMap::new(); - expected.insert( - "https://app.datadoghq.com".to_string(), - vec!["key1".to_string(), "key2".to_string()], - ); - expected.insert( - "https://app.datadoghq.eu".to_string(), - vec!["key3".to_string()], - ); - - assert_eq!(result, expected); - } - - #[test] - fn test_deserialize_additional_endpoints_invalid_or_empty() { - // Test empty YAML - let input = json!({}); - let result = deserialize_additional_endpoints(input) - .expect("Failed to deserialize additional endpoints"); - assert!(result.is_empty()); - - // Test empty JSON - let input = json!(""); - let result = deserialize_additional_endpoints(input) - .expect("Failed to deserialize additional endpoints"); - assert!(result.is_empty()); - - let input = json!({ - "https://app.datadoghq.com": "invalid-yaml" - }); - let result = deserialize_additional_endpoints(input) - .expect("Failed to deserialize additional endpoints"); - assert!(result.is_empty()); - - let input = json!("invalid-json"); - let result = deserialize_additional_endpoints(input) - .expect("Failed to deserialize additional endpoints"); - assert!(result.is_empty()); - } -} diff --git a/bottlecap/src/config/apm_replace_rule.rs b/bottlecap/src/config/apm_replace_rule.rs deleted file mode 100644 index 41b135949..000000000 --- a/bottlecap/src/config/apm_replace_rule.rs +++ /dev/null @@ -1,71 +0,0 @@ -use libdd_trace_obfuscation::replacer::{ReplaceRule, parse_rules_from_string}; -use serde::de::{Deserializer, SeqAccess, Visitor}; -use serde::{Deserialize, Serialize}; -use serde_json; -use std::fmt; - -#[derive(Deserialize, Serialize)] -struct ReplaceRuleYaml { - name: String, - pattern: String, - repl: String, -} - -struct StringOrReplaceRulesVisitor; - -impl<'de> Visitor<'de> for StringOrReplaceRulesVisitor { - type Value = String; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a JSON string or YAML sequence of replace rules") - } - - // Handle existing JSON strings - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - match serde_json::from_str::(value) { - Ok(_) => Ok(value.to_string()), - Err(e) => { - tracing::error!("Invalid JSON string for APM replace rules: {}", e); - Ok(String::new()) - } - } - } - - // Convert YAML sequences to JSON strings - fn visit_seq(self, mut seq: A) -> Result - where - A: SeqAccess<'de>, - { - let mut rules = Vec::new(); - while let Some(rule) = seq.next_element::()? { - rules.push(rule); - } - match serde_json::to_string(&rules) { - Ok(json) => Ok(json), - Err(e) => { - tracing::error!("Failed to convert YAML rules to JSON: {}", e); - Ok(String::new()) - } - } - } -} - -pub fn deserialize_apm_replace_rules<'de, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - let json_string = deserializer.deserialize_any(StringOrReplaceRulesVisitor)?; - - match parse_rules_from_string(&json_string) { - Ok(rules) => Ok(Some(rules)), - Err(e) => { - tracing::error!("Failed to parse APM replace rule, ignoring: {}", e); - Ok(None) - } - } -} diff --git a/bottlecap/src/config/env.rs b/bottlecap/src/config/env.rs deleted file mode 100644 index 96d4afee3..000000000 --- a/bottlecap/src/config/env.rs +++ /dev/null @@ -1,1337 +0,0 @@ -use figment::{Figment, providers::Env}; -use serde::Deserialize; -use std::collections::HashMap; -use std::time::Duration; - -use dogstatsd::util::parse_metric_namespace; -use libdd_trace_obfuscation::replacer::ReplaceRule; - -use crate::{ - config::{ - Config, ConfigError, ConfigSource, - additional_endpoints::deserialize_additional_endpoints, - apm_replace_rule::deserialize_apm_replace_rules, - deserialize_apm_filter_tags, deserialize_array_from_comma_separated_string, - deserialize_key_value_pairs, deserialize_option_lossless, - 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, - log_level::LogLevel, - logs_additional_endpoints::{ - LogsAdditionalEndpoint, deserialize_logs_additional_endpoints, - }, - processing_rule::{ProcessingRule, deserialize_processing_rules}, - service_mapping::deserialize_service_mapping, - trace_propagation_style::deserialize_trace_propagation_style, - }, - merge_hashmap, merge_option, merge_option_to_value, merge_string, merge_vec, -}; -use datadog_opentelemetry::propagation::TracePropagationStyle; - -#[derive(Debug, PartialEq, Deserialize, Clone, Default)] -#[serde(default)] -#[allow(clippy::struct_excessive_bools)] -#[allow(clippy::module_name_repetitions)] -pub struct EnvConfig { - /// @env `DD_SITE` - /// - /// The Datadog site to send telemetry to - #[serde(deserialize_with = "deserialize_optional_string")] - pub site: Option, - /// @env `DD_API_KEY` - /// - /// The Datadog API key used to submit telemetry to Datadog - #[serde(deserialize_with = "deserialize_optional_string")] - pub api_key: Option, - /// @env `DD_LOG_LEVEL` - /// - /// Minimum log level of the Datadog Agent. - /// Valid log levels are: trace, debug, info, warn, and error. - pub log_level: Option, - - /// @env `DD_FLUSH_TIMEOUT` - /// - /// Flush timeout in seconds - /// todo(duncanista): find out where this comes from - /// todo(?): go agent adds jitter too - #[serde(deserialize_with = "deserialize_option_lossless")] - pub flush_timeout: Option, - - // Proxy - /// @env `DD_PROXY_HTTPS` - /// - /// Proxy endpoint for HTTPS connections (most Datadog traffic) - #[serde(deserialize_with = "deserialize_optional_string")] - pub proxy_https: Option, - /// @env `DD_PROXY_NO_PROXY` - /// - /// Specify hosts the Agent should connect to directly, bypassing the proxy. - #[serde(deserialize_with = "deserialize_array_from_comma_separated_string")] - pub proxy_no_proxy: Vec, - /// @env `DD_HTTP_PROTOCOL` - /// - /// The HTTP protocol to use for the Datadog Agent. - /// The transport type to use for sending logs. Possible values are "auto" or "http1". - #[serde(deserialize_with = "deserialize_optional_string")] - pub http_protocol: Option, - /// @env `DD_TLS_CERT_FILE` - /// The path to a file of concatenated CA certificates in PEM format. - /// Example: `/opt/ca-cert.pem` - #[serde(deserialize_with = "deserialize_optional_string")] - pub tls_cert_file: Option, - /// @env `DD_SKIP_SSL_VALIDATION` - /// - /// If set to true, the Agent will skip TLS certificate validation for outgoing connections. - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub skip_ssl_validation: Option, - - // Metrics - /// @env `DD_DD_URL` - /// - /// @default `https://app.datadoghq.com` - /// - /// The host of the Datadog intake server to send **metrics** to, only set this option - /// if you need the Agent to send **metrics** to a custom URL, it overrides the site - /// setting defined in "site". It does not affect APM, Logs, Remote Configuration, - /// or Live Process intake which have their own "*_`dd_url`" settings. - /// - /// If `DD_DD_URL` and `DD_URL` are both set, `DD_DD_URL` is used in priority. - #[serde(deserialize_with = "deserialize_optional_string")] - pub dd_url: Option, - /// @env `DD_URL` - /// - /// @default `https://app.datadoghq.com` - #[serde(deserialize_with = "deserialize_optional_string")] - pub url: Option, - /// @env `DD_ADDITIONAL_ENDPOINTS` - /// - /// Additional endpoints to send metrics to. - /// - #[serde(deserialize_with = "deserialize_additional_endpoints")] - pub additional_endpoints: HashMap>, - - // Unified Service Tagging - /// @env `DD_ENV` - /// - /// The environment name where the agent is running. Attached in-app to every - /// metric, event, log, trace, and service check emitted by this Agent. - #[serde(deserialize_with = "deserialize_string_or_int")] - pub env: Option, - /// @env `DD_SERVICE` - #[serde(deserialize_with = "deserialize_string_or_int")] - pub service: Option, - /// @env `DD_VERSION` - #[serde(deserialize_with = "deserialize_string_or_int")] - pub version: Option, - /// @env `DD_TAGS` - #[serde(deserialize_with = "deserialize_key_value_pairs")] - pub tags: HashMap, - /// @env `DD_COMPRESSION_LEVEL` - /// - /// Global level `compression_level` parameter accepts values from 0 (no compression) - /// to 9 (maximum compression but higher resource usage). This value is effective only if - /// the individual component doesn't specify its own. - #[serde(deserialize_with = "deserialize_option_lossless")] - pub compression_level: Option, - - // Logs - /// @env `DD_LOGS_CONFIG_LOGS_DD_URL` - /// - /// Define the endpoint and port to hit when using a proxy for logs. - #[serde(deserialize_with = "deserialize_optional_string")] - pub logs_config_logs_dd_url: Option, - /// @env `DD_LOGS_CONFIG_PROCESSING_RULES` - /// - /// Global processing rules that are applied to all logs. The available rules are - /// "`exclude_at_match`", "`include_at_match`" and "`mask_sequences`". More information in Datadog documentation: - /// - #[serde(deserialize_with = "deserialize_processing_rules")] - pub logs_config_processing_rules: Option>, - /// @env `DD_LOGS_CONFIG_USE_COMPRESSION` - /// - /// If enabled, the Agent compresses logs before sending them. - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub logs_config_use_compression: Option, - /// @env `DD_LOGS_CONFIG_COMPRESSION_LEVEL` - /// - /// The `compression_level` parameter accepts values from 0 (no compression) - /// to 9 (maximum compression but higher resource usage). Only takes effect if - /// `use_compression` is set to `true`. - #[serde(deserialize_with = "deserialize_option_lossless")] - pub logs_config_compression_level: Option, - /// @env `DD_LOGS_CONFIG_ADDITIONAL_ENDPOINTS` - /// - /// Additional endpoints to send logs to. - /// - #[serde(deserialize_with = "deserialize_logs_additional_endpoints")] - pub logs_config_additional_endpoints: Vec, - - /// @env `DD_OBSERVABILITY_PIPELINES_WORKER_LOGS_ENABLED` - /// When true, emit plain json suitable for Observability Pipelines - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub observability_pipelines_worker_logs_enabled: Option, - /// @env `DD_OBSERVABILITY_PIPELINES_WORKER_LOGS_URL` - /// - /// The URL endpoint for sending logs to Observability Pipelines Worker - #[serde(deserialize_with = "deserialize_optional_string")] - pub observability_pipelines_worker_logs_url: Option, - - // APM - // - /// @env `DD_SERVICE_MAPPING` - #[serde(deserialize_with = "deserialize_service_mapping")] - pub service_mapping: HashMap, - // - /// @env `DD_APM_DD_URL` - /// - /// Define the endpoint and port to hit when using a proxy for APM. - #[serde(deserialize_with = "deserialize_optional_string")] - pub apm_dd_url: Option, - /// @env `DD_APM_REPLACE_TAGS` - /// - /// Defines a set of rules to replace or remove certain resources, tags containing - /// potentially sensitive information. - /// Each rule has to contain: - /// * name - string - The tag name to replace, for resources use "resource.name". - /// * pattern - string - The pattern to match the desired content to replace - /// * repl - string - what to inline if the pattern is matched - /// - /// - #[serde(deserialize_with = "deserialize_apm_replace_rules")] - pub apm_replace_tags: Option>, - /// @env `DD_APM_CONFIG_OBFUSCATION_HTTP_REMOVE_QUERY_STRING` - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub apm_config_obfuscation_http_remove_query_string: Option, - /// @env `DD_APM_CONFIG_OBFUSCATION_HTTP_REMOVE_PATHS_WITH_DIGITS` - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub apm_config_obfuscation_http_remove_paths_with_digits: Option, - /// @env `DD_APM_CONFIG_COMPRESSION_LEVEL` - /// - /// The Agent compresses traces before sending them. The `compression_level` parameter - /// accepts values from 0 (no compression) to 9 (maximum compression but - /// higher resource usage). - #[serde(deserialize_with = "deserialize_option_lossless")] - pub apm_config_compression_level: Option, - /// @env `DD_APM_FEATURES` - #[serde(deserialize_with = "deserialize_array_from_comma_separated_string")] - pub apm_features: Vec, - /// @env `DD_APM_ADDITIONAL_ENDPOINTS` - /// - /// Additional endpoints to send traces to. - /// - #[serde(deserialize_with = "deserialize_additional_endpoints")] - pub apm_additional_endpoints: HashMap>, - /// @env `DD_APM_FILTER_TAGS_REQUIRE` - /// - /// Space-separated list of key:value tag pairs that spans must match to be kept. - /// Only spans matching at least one of these tags will be sent to Datadog. - /// Example: "env:production service:api-gateway" - #[serde(deserialize_with = "deserialize_apm_filter_tags")] - pub apm_filter_tags_require: Option>, - /// @env `DD_APM_FILTER_TAGS_REJECT` - /// - /// Space-separated list of key:value tag pairs that will cause spans to be filtered out. - /// Spans matching any of these tags will be dropped. - /// Example: "env:development debug:true name:health.check" - #[serde(deserialize_with = "deserialize_apm_filter_tags")] - pub apm_filter_tags_reject: Option>, - /// @env `DD_APM_FILTER_TAGS_REGEX_REQUIRE` - /// - /// Space-separated list of key:value tag pairs with regex values that spans must match to be kept. - /// Only spans matching at least one of these regex patterns will be sent to Datadog. - /// Example: "env:^prod.*$ service:^api-.*$" - #[serde(deserialize_with = "deserialize_apm_filter_tags")] - pub apm_filter_tags_regex_require: Option>, - /// @env `DD_APM_FILTER_TAGS_REGEX_REJECT` - /// - /// Space-separated list of key:value tag pairs with regex values that will cause spans to be filtered out. - /// Spans matching any of these regex patterns will be dropped. - /// Example: "env:^test.*$ debug:^true$" - #[serde(deserialize_with = "deserialize_apm_filter_tags")] - pub apm_filter_tags_regex_reject: Option>, - /// @env `DD_TRACE_AWS_SERVICE_REPRESENTATION_ENABLED` - /// - /// Enable the new AWS-resource naming logic in the tracer. - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub trace_aws_service_representation_enabled: Option, - // - // Trace Propagation - /// @env `DD_TRACE_PROPAGATION_STYLE` - #[serde(deserialize_with = "deserialize_trace_propagation_style")] - pub trace_propagation_style: Vec, - /// @env `DD_TRACE_PROPAGATION_STYLE_EXTRACT` - #[serde(deserialize_with = "deserialize_trace_propagation_style")] - pub trace_propagation_style_extract: Vec, - /// @env `DD_TRACE_PROPAGATION_EXTRACT_FIRST` - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub trace_propagation_extract_first: Option, - /// @env `DD_TRACE_PROPAGATION_HTTP_BAGGAGE_ENABLED` - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub trace_propagation_http_baggage_enabled: Option, - - /// @env `DD_METRICS_CONFIG_COMPRESSION_LEVEL` - /// The metrics compresses traces before sending them. The `compression_level` parameter - /// accepts values from 0 (no compression) to 9 (maximum compression but - /// higher resource usage). - #[serde(deserialize_with = "deserialize_option_lossless")] - pub metrics_config_compression_level: Option, - - /// @env `DD_STATSD_METRIC_NAMESPACE` - /// Prefix all `StatsD` metrics with a namespace. - #[serde(deserialize_with = "deserialize_optional_string")] - pub statsd_metric_namespace: Option, - - /// @env `DD_LAMBDA_CUSTOMER_METRICS_EXCLUDE_TAGS` - /// - /// Comma-separated list of tag keys to exclude from custom `DogStatsD` metrics - /// enrichment. Use this to drop auto-injected tags (e.g. `function_arn,region`) - /// from custom metrics to reduce billing. - #[serde(deserialize_with = "deserialize_array_from_comma_separated_string")] - pub lambda_customer_metrics_exclude_tags: Vec, - - /// @env `DD_DOGSTATSD_SO_RCVBUF` - /// Size of the receive buffer for `DogStatsD` UDP packets, in bytes (`SO_RCVBUF`). - /// Increase to reduce packet loss under high-throughput metric bursts. - #[serde(deserialize_with = "deserialize_option_lossless")] - pub dogstatsd_so_rcvbuf: Option, - - /// @env `DD_DOGSTATSD_BUFFER_SIZE` - /// Maximum size of a single read from any transport (UDP or named pipe), in bytes. - /// Defaults to 8192. - #[serde(deserialize_with = "deserialize_option_lossless")] - pub dogstatsd_buffer_size: Option, - - /// @env `DD_DOGSTATSD_QUEUE_SIZE` - /// Internal queue capacity between the socket reader and metric processor. - /// Defaults to 1024. Increase if the processor can't keep up with burst traffic. - #[serde(deserialize_with = "deserialize_option_lossless")] - pub dogstatsd_queue_size: Option, - - // OTLP - // - // - APM / Traces - /// @env `DD_OTLP_CONFIG_TRACES_ENABLED` - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub otlp_config_traces_enabled: Option, - /// @env `DD_OTLP_CONFIG_TRACES_SPAN_NAME_AS_RESOURCE_NAME` - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub otlp_config_traces_span_name_as_resource_name: Option, - /// @env `DD_OTLP_CONFIG_TRACES_SPAN_NAME_REMAPPINGS` - #[serde(deserialize_with = "deserialize_key_value_pairs")] - pub otlp_config_traces_span_name_remappings: HashMap, - /// @env `DD_OTLP_CONFIG_IGNORE_MISSING_DATADOG_FIELDS` - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub otlp_config_ignore_missing_datadog_fields: Option, - // - // - Receiver / HTTP - /// @env `DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT` - #[serde(deserialize_with = "deserialize_optional_string")] - pub otlp_config_receiver_protocols_http_endpoint: Option, - // - Unsupported Configuration - // - // - Receiver / GRPC - /// @env `DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT` - #[serde(deserialize_with = "deserialize_optional_string")] - pub otlp_config_receiver_protocols_grpc_endpoint: Option, - /// @env `DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_TRANSPORT` - #[serde(deserialize_with = "deserialize_optional_string")] - pub otlp_config_receiver_protocols_grpc_transport: Option, - /// @env `DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_MAX_RECV_MSG_SIZE_MIB` - #[serde(deserialize_with = "deserialize_option_lossless")] - pub otlp_config_receiver_protocols_grpc_max_recv_msg_size_mib: Option, - // - Metrics - /// @env `DD_OTLP_CONFIG_METRICS_ENABLED` - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub otlp_config_metrics_enabled: Option, - /// @env `DD_OTLP_CONFIG_METRICS_RESOURCE_ATTRIBUTES_AS_TAGS` - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub otlp_config_metrics_resource_attributes_as_tags: Option, - /// @env `DD_OTLP_CONFIG_METRICS_INSTRUMENTATION_SCOPE_METADATA_AS_TAGS` - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub otlp_config_metrics_instrumentation_scope_metadata_as_tags: Option, - /// @env `DD_OTLP_CONFIG_METRICS_TAG_CARDINALITY` - #[serde(deserialize_with = "deserialize_optional_string")] - pub otlp_config_metrics_tag_cardinality: Option, - /// @env `DD_OTLP_CONFIG_METRICS_DELTA_TTL` - #[serde(deserialize_with = "deserialize_option_lossless")] - pub otlp_config_metrics_delta_ttl: Option, - /// @env `DD_OTLP_CONFIG_METRICS_HISTOGRAMS_MODE` - #[serde(deserialize_with = "deserialize_optional_string")] - pub otlp_config_metrics_histograms_mode: Option, - /// @env `DD_OTLP_CONFIG_METRICS_HISTOGRAMS_SEND_COUNT_SUM_METRICS` - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub otlp_config_metrics_histograms_send_count_sum_metrics: Option, - /// @env `DD_OTLP_CONFIG_METRICS_HISTOGRAMS_SEND_AGGREGATION_METRICS` - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub otlp_config_metrics_histograms_send_aggregation_metrics: Option, - #[serde(deserialize_with = "deserialize_optional_string")] - pub otlp_config_metrics_sums_cumulative_monotonic_mode: Option, - /// @env `DD_OTLP_CONFIG_METRICS_SUMS_INITIAL_CUMULATIVE_MONOTONIC_VALUE` - #[serde(deserialize_with = "deserialize_optional_string")] - pub otlp_config_metrics_sums_initial_cumulativ_monotonic_value: Option, - /// @env `DD_OTLP_CONFIG_METRICS_SUMMARIES_MODE` - #[serde(deserialize_with = "deserialize_optional_string")] - pub otlp_config_metrics_summaries_mode: Option, - // - Traces - /// @env `DD_OTLP_CONFIG_TRACES_PROBABILISTIC_SAMPLER_SAMPLING_PERCENTAGE` - #[serde(deserialize_with = "deserialize_option_lossless")] - pub otlp_config_traces_probabilistic_sampler_sampling_percentage: Option, - // - Logs - /// @env `DD_OTLP_CONFIG_LOGS_ENABLED` - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub otlp_config_logs_enabled: Option, - - // AWS Lambda - /// @env `DD_API_KEY_SECRET_ARN` - /// - /// The AWS ARN of the secret containing the Datadog API key. - #[serde(deserialize_with = "deserialize_optional_string")] - pub api_key_secret_arn: Option, - /// @env `DD_KMS_API_KEY` - /// - /// The AWS KMS API key to use for the Datadog Agent. - #[serde(deserialize_with = "deserialize_optional_string")] - pub kms_api_key: Option, - /// @env `DD_API_KEY_SSM_ARN` - /// - /// The AWS Systems Manager Parameter Store parameter ARN containing the Datadog API key. - #[serde(deserialize_with = "deserialize_optional_string")] - pub api_key_ssm_arn: Option, - /// @env `DD_SERVERLESS_LOGS_ENABLED` - /// - /// Enable logs for AWS Lambda. Default is `true`. - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub serverless_logs_enabled: Option, - /// @env `DD_LOGS_ENABLED` - /// - /// Enable logs for AWS Lambda. Alias for `DD_SERVERLESS_LOGS_ENABLED`. Default is `true`. - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub logs_enabled: Option, - /// @env `DD_SERVERLESS_FLUSH_STRATEGY` - /// - /// The flush strategy to use for AWS Lambda. - pub serverless_flush_strategy: Option, - /// @env `DD_ENHANCED_METRICS` - /// - /// Enable enhanced metrics for AWS Lambda. Default is `true`. - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub enhanced_metrics: Option, - /// @env `DD_LAMBDA_PROC_ENHANCED_METRICS` - /// - /// Enable Lambda process metrics for AWS Lambda. Default is `true`. - /// - /// This is for metrics like: - /// - CPU usage - /// - Network usage - /// - File descriptor count - /// - Thread count - /// - Temp directory usage - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub lambda_proc_enhanced_metrics: Option, - /// @env `DD_CAPTURE_LAMBDA_PAYLOAD` - /// - /// Enable capture of the Lambda request and response payloads. - /// Default is `false`. - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub capture_lambda_payload: Option, - /// @env `DD_CAPTURE_LAMBDA_PAYLOAD_MAX_DEPTH` - /// - /// The maximum depth of the Lambda payload to capture. - /// Default is `10`. Requires `capture_lambda_payload` to be `true`. - #[serde(deserialize_with = "deserialize_option_lossless")] - pub capture_lambda_payload_max_depth: Option, - /// @env `DD_COMPUTE_TRACE_STATS_ON_EXTENSION` - /// - /// If true, enable computation of trace stats on the extension side. - /// If false, trace stats will be computed on the backend side. - /// Default is `false`. - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub compute_trace_stats_on_extension: Option, - /// @env `DD_SPAN_DEDUP_TIMEOUT` - /// - /// The timeout for the span deduplication service to check if a span key exists, in seconds. - /// For now, this is a temporary field added to debug the failure of `check_and_add()` in span dedup service. - /// Do not use this field extensively in production. - #[serde(deserialize_with = "deserialize_optional_duration_from_seconds_ignore_zero")] - pub span_dedup_timeout: Option, - /// @env `DD_API_KEY_SECRET_RELOAD_INTERVAL` - /// - /// The interval at which the Datadog API key is reloaded, in seconds. - /// If None, the API key will not be reloaded. - /// Default is `None`. - #[serde(deserialize_with = "deserialize_optional_duration_from_seconds_ignore_zero")] - pub api_key_secret_reload_interval: Option, - /// @env `DD_SERVERLESS_APPSEC_ENABLED` - /// - /// Enable Application and API Protection (AAP), previously known as AppSec/ASM, for AWS Lambda. - /// Default is `false`. - /// - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub serverless_appsec_enabled: Option, - /// @env `DD_APPSEC_RULES` - /// - /// The path to a user-configured App & API Protection ruleset (in JSON format). - #[serde(deserialize_with = "deserialize_optional_string")] - pub appsec_rules: Option, - /// @env `DD_APPSEC_WAF_TIMEOUT` - /// - /// The timeout for the WAF to process a request, in microseconds. - #[serde(deserialize_with = "deserialize_optional_duration_from_microseconds")] - pub appsec_waf_timeout: Option, - /// @env `DD_API_SECURITY_ENABLED` - /// - /// Enable API Security for AWS Lambda. - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub api_security_enabled: Option, - /// @env `DD_API_SECURITY_SAMPLE_DELAY` - /// - /// The delay between two samples of the API Security schema collection, in seconds. - #[serde(deserialize_with = "deserialize_optional_duration_from_seconds")] - pub api_security_sample_delay: Option, - - /// @env `DD_ORG_UUID` - /// - /// The Datadog organization UUID. When set, delegated auth is auto-enabled. - #[serde(deserialize_with = "deserialize_string_or_int")] - pub org_uuid: Option, - - /// @env `DD_LAMBDA_DURABLE_FUNCTION_LOG_BUFFER_SIZE` - /// - /// Maximum number of request IDs whose logs are held waiting for durable execution - /// context. Set to 0 to disable log holding; logs will be sent immediately without - /// durable execution context enrichment. Useful when the tracer is not installed. - /// Default is `0`. - #[serde(deserialize_with = "deserialize_option_lossless")] - pub lambda_durable_function_log_buffer_size: Option, -} - -#[allow(clippy::too_many_lines)] -fn merge_config(config: &mut Config, env_config: &EnvConfig) { - // Basic fields - merge_string!(config, env_config, site); - merge_string!(config, env_config, api_key); - merge_option_to_value!(config, env_config, log_level); - merge_option_to_value!(config, env_config, flush_timeout); - - // Unified Service Tagging - merge_option!(config, env_config, env); - merge_option!(config, env_config, service); - merge_option!(config, env_config, version); - merge_hashmap!(config, env_config, tags); - - // Proxy - merge_option!(config, env_config, proxy_https); - merge_vec!(config, env_config, proxy_no_proxy); - merge_option!(config, env_config, http_protocol); - merge_option!(config, env_config, tls_cert_file); - merge_option_to_value!(config, env_config, skip_ssl_validation); - - // Endpoints - merge_string!(config, env_config, dd_url); - merge_string!(config, env_config, url); - merge_hashmap!(config, env_config, additional_endpoints); - - merge_option_to_value!(config, env_config, compression_level); - - // Logs - merge_string!(config, env_config, logs_config_logs_dd_url); - merge_option!(config, env_config, logs_config_processing_rules); - merge_option_to_value!(config, env_config, logs_config_use_compression); - merge_option_to_value!( - config, - logs_config_compression_level, - env_config, - compression_level - ); - merge_option_to_value!(config, env_config, logs_config_compression_level); - merge_vec!(config, env_config, logs_config_additional_endpoints); - merge_option_to_value!( - config, - env_config, - observability_pipelines_worker_logs_enabled - ); - merge_string!(config, env_config, observability_pipelines_worker_logs_url); - - // APM - merge_hashmap!(config, env_config, service_mapping); - merge_string!(config, env_config, apm_dd_url); - merge_option!(config, env_config, apm_replace_tags); - merge_option_to_value!( - config, - env_config, - apm_config_obfuscation_http_remove_query_string - ); - merge_option_to_value!( - config, - env_config, - apm_config_obfuscation_http_remove_paths_with_digits - ); - merge_option_to_value!( - config, - apm_config_compression_level, - env_config, - compression_level - ); - merge_option_to_value!(config, env_config, apm_config_compression_level); - merge_vec!(config, env_config, apm_features); - merge_hashmap!(config, env_config, apm_additional_endpoints); - merge_option!(config, env_config, apm_filter_tags_require); - merge_option!(config, env_config, apm_filter_tags_reject); - merge_option!(config, env_config, apm_filter_tags_regex_require); - merge_option!(config, env_config, apm_filter_tags_regex_reject); - merge_option_to_value!(config, env_config, trace_aws_service_representation_enabled); - - // Trace Propagation - merge_vec!(config, env_config, trace_propagation_style); - merge_vec!(config, env_config, trace_propagation_style_extract); - merge_option_to_value!(config, env_config, trace_propagation_extract_first); - merge_option_to_value!(config, env_config, trace_propagation_http_baggage_enabled); - - // Metrics - merge_option_to_value!( - config, - metrics_config_compression_level, - env_config, - compression_level - ); - merge_option_to_value!(config, env_config, metrics_config_compression_level); - - if let Some(namespace) = &env_config.statsd_metric_namespace { - config.statsd_metric_namespace = parse_metric_namespace(namespace); - } - - merge_vec!( - config, - custom_metrics_exclude_tags, - env_config, - lambda_customer_metrics_exclude_tags - ); - - // DogStatsD - merge_option!(config, env_config, dogstatsd_so_rcvbuf); - merge_option!(config, env_config, dogstatsd_buffer_size); - merge_option!(config, env_config, dogstatsd_queue_size); - - // OTLP - merge_option_to_value!(config, env_config, otlp_config_traces_enabled); - merge_option_to_value!( - config, - env_config, - otlp_config_traces_span_name_as_resource_name - ); - merge_hashmap!(config, env_config, otlp_config_traces_span_name_remappings); - merge_option_to_value!( - config, - env_config, - otlp_config_ignore_missing_datadog_fields - ); - merge_option!( - config, - env_config, - otlp_config_receiver_protocols_http_endpoint - ); - merge_option!( - config, - env_config, - otlp_config_receiver_protocols_grpc_endpoint - ); - merge_option!( - config, - env_config, - otlp_config_receiver_protocols_grpc_transport - ); - merge_option!( - config, - env_config, - otlp_config_receiver_protocols_grpc_max_recv_msg_size_mib - ); - merge_option_to_value!(config, env_config, otlp_config_metrics_enabled); - merge_option_to_value!( - config, - env_config, - otlp_config_metrics_resource_attributes_as_tags - ); - merge_option_to_value!( - config, - env_config, - otlp_config_metrics_instrumentation_scope_metadata_as_tags - ); - merge_option!(config, env_config, otlp_config_metrics_tag_cardinality); - merge_option!(config, env_config, otlp_config_metrics_delta_ttl); - merge_option!(config, env_config, otlp_config_metrics_histograms_mode); - merge_option_to_value!( - config, - env_config, - otlp_config_metrics_histograms_send_count_sum_metrics - ); - merge_option_to_value!( - config, - env_config, - otlp_config_metrics_histograms_send_aggregation_metrics - ); - merge_option!( - config, - env_config, - otlp_config_metrics_sums_cumulative_monotonic_mode - ); - merge_option!( - config, - env_config, - otlp_config_metrics_sums_initial_cumulativ_monotonic_value - ); - merge_option!(config, env_config, otlp_config_metrics_summaries_mode); - merge_option!( - config, - env_config, - otlp_config_traces_probabilistic_sampler_sampling_percentage - ); - merge_option_to_value!(config, env_config, otlp_config_logs_enabled); - - // AWS Lambda - merge_string!(config, env_config, api_key_secret_arn); - merge_string!(config, env_config, kms_api_key); - merge_string!(config, env_config, api_key_ssm_arn); - merge_option_to_value!(config, env_config, serverless_logs_enabled); - - // Handle serverless_logs_enabled with OR logic: if either DD_LOGS_ENABLED or DD_SERVERLESS_LOGS_ENABLED is true, enable logs - if env_config.serverless_logs_enabled.is_some() || env_config.logs_enabled.is_some() { - config.serverless_logs_enabled = env_config.serverless_logs_enabled.unwrap_or(false) - || env_config.logs_enabled.unwrap_or(false); - } - - merge_option_to_value!(config, env_config, serverless_flush_strategy); - merge_option_to_value!(config, env_config, enhanced_metrics); - merge_option_to_value!(config, env_config, lambda_proc_enhanced_metrics); - merge_option_to_value!(config, env_config, capture_lambda_payload); - merge_option_to_value!(config, env_config, capture_lambda_payload_max_depth); - merge_option_to_value!(config, env_config, compute_trace_stats_on_extension); - merge_option!(config, env_config, span_dedup_timeout); - merge_option!(config, env_config, api_key_secret_reload_interval); - merge_option_to_value!(config, env_config, serverless_appsec_enabled); - merge_option!(config, env_config, appsec_rules); - merge_option_to_value!(config, env_config, appsec_waf_timeout); - merge_option_to_value!(config, env_config, api_security_enabled); - merge_option_to_value!(config, env_config, api_security_sample_delay); - - merge_string!(config, dd_org_uuid, env_config, org_uuid); - merge_option_to_value!(config, env_config, lambda_durable_function_log_buffer_size); -} - -#[derive(Debug, PartialEq, Clone, Copy)] -#[allow(clippy::module_name_repetitions)] -pub struct EnvConfigSource; - -impl ConfigSource for EnvConfigSource { - fn load(&self, config: &mut Config) -> Result<(), ConfigError> { - let figment = Figment::new() - .merge(Env::prefixed("DATADOG_")) - .merge(Env::prefixed("DD_")); - - match figment.extract::() { - Ok(env_config) => merge_config(config, &env_config), - Err(e) => { - return Err(ConfigError::ParseError(format!( - "Failed to parse config from environment variables: {e}, using default config.", - ))); - } - } - - Ok(()) - } -} - -#[cfg_attr(coverage_nightly, coverage(off))] // Test modules skew coverage metrics -#[cfg(test)] -mod tests { - use std::time::Duration; - - use super::*; - use crate::config::{ - Config, - flush_strategy::{FlushStrategy, PeriodicStrategy}, - log_level::LogLevel, - processing_rule::{Kind, ProcessingRule}, - }; - use datadog_opentelemetry::propagation::TracePropagationStyle; - - #[test] - #[allow(clippy::too_many_lines)] - fn test_merge_config_overrides_with_environment_variables() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - - // Set environment variables here - jail.set_env("DD_SITE", "test-site"); - jail.set_env("DD_API_KEY", "test-api-key"); - jail.set_env("DD_LOG_LEVEL", "debug"); - jail.set_env("DD_FLUSH_TIMEOUT", "42"); - - // Proxy - jail.set_env("DD_PROXY_HTTPS", "https://proxy.example.com"); - jail.set_env("DD_PROXY_NO_PROXY", "localhost,127.0.0.1"); - jail.set_env("DD_HTTP_PROTOCOL", "http1"); - jail.set_env("DD_TLS_CERT_FILE", "/opt/ca-cert.pem"); - jail.set_env("DD_SKIP_SSL_VALIDATION", "true"); - - // Metrics - jail.set_env("DD_DD_URL", "https://metrics.datadoghq.com"); - jail.set_env("DD_URL", "https://app.datadoghq.com"); - jail.set_env( - "DD_ADDITIONAL_ENDPOINTS", - "{\"https://app.datadoghq.com\": [\"apikey2\", \"apikey3\"], \"https://app.datadoghq.eu\": [\"apikey4\"]}", - ); - - // Unified Service Tagging - jail.set_env("DD_ENV", "test-env"); - jail.set_env("DD_SERVICE", "test-service"); - jail.set_env("DD_VERSION", "1.0.0"); - jail.set_env("DD_TAGS", "team:test-team,project:test-project"); - jail.set_env("DD_COMPRESSION_LEVEL", "4"); - - // Logs - jail.set_env("DD_LOGS_CONFIG_LOGS_DD_URL", "https://logs.datadoghq.com"); - jail.set_env( - "DD_LOGS_CONFIG_PROCESSING_RULES", - r#"[{"type":"exclude_at_match","name":"exclude","pattern":"exclude"}]"#, - ); - jail.set_env("DD_LOGS_CONFIG_USE_COMPRESSION", "false"); - jail.set_env("DD_LOGS_CONFIG_COMPRESSION_LEVEL", "1"); - jail.set_env( - "DD_LOGS_CONFIG_ADDITIONAL_ENDPOINTS", - "[{\"api_key\": \"apikey2\", \"Host\": \"agent-http-intake.logs.datadoghq.com\", \"Port\": 443, \"is_reliable\": true}]", - ); - - // APM - jail.set_env("DD_SERVICE_MAPPING", "old-service:new-service"); - jail.set_env("DD_APPSEC_ENABLED", "true"); - jail.set_env("DD_APM_DD_URL", "https://apm.datadoghq.com"); - jail.set_env( - "DD_APM_REPLACE_TAGS", - r#"[{"name":"test-tag","pattern":"test-pattern","repl":"replacement"}]"#, - ); - jail.set_env("DD_APM_CONFIG_OBFUSCATION_HTTP_REMOVE_QUERY_STRING", "true"); - jail.set_env( - "DD_APM_CONFIG_OBFUSCATION_HTTP_REMOVE_PATHS_WITH_DIGITS", - "true", - ); - jail.set_env("DD_APM_CONFIG_COMPRESSION_LEVEL", "2"); - jail.set_env( - "DD_APM_FEATURES", - "enable_otlp_compute_top_level_by_span_kind,enable_stats_by_span_kind", - ); - jail.set_env("DD_APM_ADDITIONAL_ENDPOINTS", "{\"https://trace.agent.datadoghq.com\": [\"apikey2\", \"apikey3\"], \"https://trace.agent.datadoghq.eu\": [\"apikey4\"]}"); - jail.set_env("DD_APM_FILTER_TAGS_REQUIRE", "env:production service:api"); - jail.set_env("DD_APM_FILTER_TAGS_REJECT", "debug:true env:test"); - jail.set_env( - "DD_APM_FILTER_TAGS_REGEX_REQUIRE", - "env:^test.*$ debug:^true$", - ); - jail.set_env( - "DD_APM_FILTER_TAGS_REGEX_REJECT", - "env:^test.*$ debug:^true$", - ); - - jail.set_env("DD_METRICS_CONFIG_COMPRESSION_LEVEL", "3"); - // Trace Propagation - jail.set_env("DD_TRACE_PROPAGATION_STYLE", "datadog"); - jail.set_env("DD_TRACE_PROPAGATION_STYLE_EXTRACT", "tracecontext"); - jail.set_env("DD_TRACE_PROPAGATION_EXTRACT_FIRST", "true"); - jail.set_env("DD_TRACE_PROPAGATION_HTTP_BAGGAGE_ENABLED", "true"); - jail.set_env("DD_TRACE_AWS_SERVICE_REPRESENTATION_ENABLED", "true"); - - // OTLP - jail.set_env("DD_OTLP_CONFIG_TRACES_ENABLED", "false"); - jail.set_env("DD_OTLP_CONFIG_TRACES_SPAN_NAME_AS_RESOURCE_NAME", "true"); - jail.set_env( - "DD_OTLP_CONFIG_TRACES_SPAN_NAME_REMAPPINGS", - "old-span:new-span", - ); - jail.set_env("DD_OTLP_CONFIG_IGNORE_MISSING_DATADOG_FIELDS", "true"); - jail.set_env( - "DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT", - "http://localhost:4318", - ); - jail.set_env( - "DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT", - "http://localhost:4317", - ); - jail.set_env("DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_TRANSPORT", "tcp"); - jail.set_env( - "DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_MAX_RECV_MSG_SIZE_MIB", - "4", - ); - jail.set_env("DD_OTLP_CONFIG_METRICS_ENABLED", "true"); - jail.set_env("DD_OTLP_CONFIG_METRICS_RESOURCE_ATTRIBUTES_AS_TAGS", "true"); - jail.set_env( - "DD_OTLP_CONFIG_METRICS_INSTRUMENTATION_SCOPE_METADATA_AS_TAGS", - "true", - ); - jail.set_env("DD_OTLP_CONFIG_METRICS_TAG_CARDINALITY", "low"); - jail.set_env("DD_OTLP_CONFIG_METRICS_DELTA_TTL", "3600"); - jail.set_env("DD_OTLP_CONFIG_METRICS_HISTOGRAMS_MODE", "counters"); - jail.set_env( - "DD_OTLP_CONFIG_METRICS_HISTOGRAMS_SEND_COUNT_SUM_METRICS", - "true", - ); - jail.set_env( - "DD_OTLP_CONFIG_METRICS_HISTOGRAMS_SEND_AGGREGATION_METRICS", - "true", - ); - jail.set_env( - "DD_OTLP_CONFIG_METRICS_SUMS_CUMULATIVE_MONOTONIC_MODE", - "to_delta", - ); - jail.set_env( - "DD_OTLP_CONFIG_METRICS_SUMS_INITIAL_CUMULATIV_MONOTONIC_VALUE", - "auto", - ); - jail.set_env("DD_OTLP_CONFIG_METRICS_SUMMARIES_MODE", "quantiles"); - jail.set_env( - "DD_OTLP_CONFIG_TRACES_PROBABILISTIC_SAMPLER_SAMPLING_PERCENTAGE", - "50", - ); - jail.set_env("DD_OTLP_CONFIG_LOGS_ENABLED", "true"); - - jail.set_env( - "DD_LAMBDA_CUSTOMER_METRICS_EXCLUDE_TAGS", - "function_arn,region", - ); - - // DogStatsD - jail.set_env("DD_DOGSTATSD_SO_RCVBUF", "1048576"); - jail.set_env("DD_DOGSTATSD_BUFFER_SIZE", "65507"); - jail.set_env("DD_DOGSTATSD_QUEUE_SIZE", "2048"); - - // AWS Lambda - jail.set_env( - "DD_API_KEY_SECRET_ARN", - "arn:aws:secretsmanager:region:account:secret:datadog-api-key", - ); - jail.set_env("DD_KMS_API_KEY", "test-kms-key"); - jail.set_env("DD_SERVERLESS_LOGS_ENABLED", "false"); - jail.set_env("DD_SERVERLESS_FLUSH_STRATEGY", "periodically,60000"); - jail.set_env("DD_ENHANCED_METRICS", "false"); - jail.set_env("DD_LAMBDA_PROC_ENHANCED_METRICS", "false"); - jail.set_env("DD_CAPTURE_LAMBDA_PAYLOAD", "true"); - jail.set_env("DD_CAPTURE_LAMBDA_PAYLOAD_MAX_DEPTH", "5"); - jail.set_env("DD_COMPUTE_TRACE_STATS_ON_EXTENSION", "true"); - jail.set_env("DD_SPAN_DEDUP_TIMEOUT", "5"); - jail.set_env("DD_API_KEY_SECRET_RELOAD_INTERVAL", "10"); - jail.set_env("DD_SERVERLESS_APPSEC_ENABLED", "true"); - jail.set_env("DD_APPSEC_RULES", "/path/to/rules.json"); - jail.set_env("DD_APPSEC_WAF_TIMEOUT", "1000000"); // Microseconds - jail.set_env("DD_API_SECURITY_ENABLED", "0"); // Seconds - jail.set_env("DD_API_SECURITY_SAMPLE_DELAY", "60"); // Seconds - - let mut config = Config::default(); - let env_config_source = EnvConfigSource; - env_config_source - .load(&mut config) - .expect("Failed to load config"); - - let expected_config = Config { - site: "test-site".to_string(), - api_key: "test-api-key".to_string(), - log_level: LogLevel::Debug, - compression_level: 4, - flush_timeout: 42, - proxy_https: Some("https://proxy.example.com".to_string()), - proxy_no_proxy: vec!["localhost".to_string(), "127.0.0.1".to_string()], - http_protocol: Some("http1".to_string()), - tls_cert_file: Some("/opt/ca-cert.pem".to_string()), - skip_ssl_validation: true, - dd_url: "https://metrics.datadoghq.com".to_string(), - url: "https://app.datadoghq.com".to_string(), - additional_endpoints: HashMap::from([ - ( - "https://app.datadoghq.com".to_string(), - vec!["apikey2".to_string(), "apikey3".to_string()], - ), - ( - "https://app.datadoghq.eu".to_string(), - vec!["apikey4".to_string()], - ), - ]), - env: Some("test-env".to_string()), - service: Some("test-service".to_string()), - version: Some("1.0.0".to_string()), - tags: HashMap::from([ - ("team".to_string(), "test-team".to_string()), - ("project".to_string(), "test-project".to_string()), - ]), - logs_config_logs_dd_url: "https://logs.datadoghq.com".to_string(), - logs_config_processing_rules: Some(vec![ProcessingRule { - kind: Kind::ExcludeAtMatch, - name: "exclude".to_string(), - pattern: "exclude".to_string(), - replace_placeholder: None, - }]), - logs_config_use_compression: false, - logs_config_compression_level: 1, - logs_config_additional_endpoints: vec![LogsAdditionalEndpoint { - api_key: "apikey2".to_string(), - host: "agent-http-intake.logs.datadoghq.com".to_string(), - port: 443, - is_reliable: true, - }], - observability_pipelines_worker_logs_enabled: false, - observability_pipelines_worker_logs_url: String::default(), - service_mapping: HashMap::from([( - "old-service".to_string(), - "new-service".to_string(), - )]), - apm_dd_url: "https://apm.datadoghq.com".to_string(), - apm_replace_tags: Some( - libdd_trace_obfuscation::replacer::parse_rules_from_string( - r#"[{"name":"test-tag","pattern":"test-pattern","repl":"replacement"}]"#, - ) - .expect("Failed to parse replace rules"), - ), - apm_config_obfuscation_http_remove_query_string: true, - apm_config_obfuscation_http_remove_paths_with_digits: true, - apm_config_compression_level: 2, - apm_features: vec![ - "enable_otlp_compute_top_level_by_span_kind".to_string(), - "enable_stats_by_span_kind".to_string(), - ], - apm_additional_endpoints: HashMap::from([ - ( - "https://trace.agent.datadoghq.com".to_string(), - vec!["apikey2".to_string(), "apikey3".to_string()], - ), - ( - "https://trace.agent.datadoghq.eu".to_string(), - vec!["apikey4".to_string()], - ), - ]), - apm_filter_tags_require: Some(vec![ - "env:production".to_string(), - "service:api".to_string(), - ]), - apm_filter_tags_reject: Some(vec![ - "debug:true".to_string(), - "env:test".to_string(), - ]), - apm_filter_tags_regex_require: Some(vec![ - "env:^test.*$".to_string(), - "debug:^true$".to_string(), - ]), - apm_filter_tags_regex_reject: Some(vec![ - "env:^test.*$".to_string(), - "debug:^true$".to_string(), - ]), - trace_propagation_style: vec![TracePropagationStyle::Datadog], - trace_propagation_style_extract: vec![TracePropagationStyle::TraceContext], - trace_propagation_extract_first: true, - trace_propagation_http_baggage_enabled: true, - trace_aws_service_representation_enabled: true, - metrics_config_compression_level: 3, - otlp_config_traces_enabled: false, - otlp_config_traces_span_name_as_resource_name: true, - otlp_config_traces_span_name_remappings: HashMap::from([( - "old-span".to_string(), - "new-span".to_string(), - )]), - otlp_config_ignore_missing_datadog_fields: true, - otlp_config_receiver_protocols_http_endpoint: Some( - "http://localhost:4318".to_string(), - ), - otlp_config_receiver_protocols_grpc_endpoint: Some( - "http://localhost:4317".to_string(), - ), - otlp_config_receiver_protocols_grpc_transport: Some("tcp".to_string()), - otlp_config_receiver_protocols_grpc_max_recv_msg_size_mib: Some(4), - otlp_config_metrics_enabled: true, - otlp_config_metrics_resource_attributes_as_tags: true, - otlp_config_metrics_instrumentation_scope_metadata_as_tags: true, - otlp_config_metrics_tag_cardinality: Some("low".to_string()), - otlp_config_metrics_delta_ttl: Some(3600), - otlp_config_metrics_histograms_mode: Some("counters".to_string()), - otlp_config_metrics_histograms_send_count_sum_metrics: true, - otlp_config_metrics_histograms_send_aggregation_metrics: true, - otlp_config_metrics_sums_cumulative_monotonic_mode: Some("to_delta".to_string()), - otlp_config_metrics_sums_initial_cumulativ_monotonic_value: Some( - "auto".to_string(), - ), - otlp_config_metrics_summaries_mode: Some("quantiles".to_string()), - otlp_config_traces_probabilistic_sampler_sampling_percentage: Some(50), - otlp_config_logs_enabled: true, - statsd_metric_namespace: None, - custom_metrics_exclude_tags: vec!["function_arn".to_string(), "region".to_string()], - dogstatsd_so_rcvbuf: Some(1_048_576), - dogstatsd_buffer_size: Some(65507), - dogstatsd_queue_size: Some(2048), - api_key_secret_arn: "arn:aws:secretsmanager:region:account:secret:datadog-api-key" - .to_string(), - kms_api_key: "test-kms-key".to_string(), - api_key_ssm_arn: String::default(), - serverless_logs_enabled: false, - serverless_flush_strategy: FlushStrategy::Periodically(PeriodicStrategy { - interval: 60000, - }), - enhanced_metrics: false, - lambda_proc_enhanced_metrics: false, - capture_lambda_payload: true, - capture_lambda_payload_max_depth: 5, - compute_trace_stats_on_extension: true, - span_dedup_timeout: Some(Duration::from_secs(5)), - api_key_secret_reload_interval: Some(Duration::from_secs(10)), - serverless_appsec_enabled: true, - appsec_rules: Some("/path/to/rules.json".to_string()), - appsec_waf_timeout: Duration::from_secs(1), - api_security_enabled: false, - api_security_sample_delay: Duration::from_secs(60), - - dd_org_uuid: String::default(), - lambda_durable_function_log_buffer_size: 0, - }; - - assert_eq!(config, expected_config); - - Ok(()) - }); - } - - #[test] - fn test_dd_logs_enabled_true() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_LOGS_ENABLED", "true"); - - let mut config = Config::default(); - let env_config_source = EnvConfigSource; - env_config_source - .load(&mut config) - .expect("Failed to load config"); - - assert!(config.serverless_logs_enabled); - Ok(()) - }); - } - - #[test] - fn test_dd_logs_enabled_false() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_LOGS_ENABLED", "false"); - - let mut config = Config::default(); - let env_config_source = EnvConfigSource; - env_config_source - .load(&mut config) - .expect("Failed to load config"); - - assert!(!config.serverless_logs_enabled); - Ok(()) - }); - } - - #[test] - fn test_dd_serverless_logs_enabled_true() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_SERVERLESS_LOGS_ENABLED", "true"); - - let mut config = Config::default(); - let env_config_source = EnvConfigSource; - env_config_source - .load(&mut config) - .expect("Failed to load config"); - - assert!(config.serverless_logs_enabled); - Ok(()) - }); - } - - #[test] - fn test_dd_serverless_logs_enabled_false() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_SERVERLESS_LOGS_ENABLED", "false"); - - let mut config = Config::default(); - let env_config_source = EnvConfigSource; - env_config_source - .load(&mut config) - .expect("Failed to load config"); - - assert!(!config.serverless_logs_enabled); - Ok(()) - }); - } - - #[test] - fn test_both_logs_enabled_true() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_LOGS_ENABLED", "true"); - jail.set_env("DD_SERVERLESS_LOGS_ENABLED", "true"); - - let mut config = Config::default(); - let env_config_source = EnvConfigSource; - env_config_source - .load(&mut config) - .expect("Failed to load config"); - - assert!(config.serverless_logs_enabled); - Ok(()) - }); - } - - #[test] - fn test_both_logs_enabled_false() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_LOGS_ENABLED", "false"); - jail.set_env("DD_SERVERLESS_LOGS_ENABLED", "false"); - - let mut config = Config::default(); - let env_config_source = EnvConfigSource; - env_config_source - .load(&mut config) - .expect("Failed to load config"); - - assert!(!config.serverless_logs_enabled); - Ok(()) - }); - } - - #[test] - fn test_logs_enabled_true_serverless_logs_enabled_false() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_LOGS_ENABLED", "true"); - jail.set_env("DD_SERVERLESS_LOGS_ENABLED", "false"); - - let mut config = Config::default(); - let env_config_source = EnvConfigSource; - env_config_source - .load(&mut config) - .expect("Failed to load config"); - - // OR logic: if either is true, logs are enabled - assert!(config.serverless_logs_enabled); - Ok(()) - }); - } - - #[test] - fn test_logs_enabled_false_serverless_logs_enabled_true() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_LOGS_ENABLED", "false"); - jail.set_env("DD_SERVERLESS_LOGS_ENABLED", "true"); - - let mut config = Config::default(); - let env_config_source = EnvConfigSource; - env_config_source - .load(&mut config) - .expect("Failed to load config"); - - // OR logic: if either is true, logs are enabled - assert!(config.serverless_logs_enabled); - Ok(()) - }); - } - - #[test] - fn test_neither_logs_enabled_set_uses_default() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - - let mut config = Config::default(); - let env_config_source = EnvConfigSource; - env_config_source - .load(&mut config) - .expect("Failed to load config"); - - // Default value is true - assert!(config.serverless_logs_enabled); - Ok(()) - }); - } - - #[test] - fn test_dogstatsd_config_from_env() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_DOGSTATSD_SO_RCVBUF", "1048576"); - jail.set_env("DD_DOGSTATSD_BUFFER_SIZE", "65507"); - jail.set_env("DD_DOGSTATSD_QUEUE_SIZE", "2048"); - - let mut config = Config::default(); - let env_config_source = EnvConfigSource; - env_config_source - .load(&mut config) - .expect("Failed to load config"); - - assert_eq!(config.dogstatsd_so_rcvbuf, Some(1_048_576)); - assert_eq!(config.dogstatsd_buffer_size, Some(65507)); - assert_eq!(config.dogstatsd_queue_size, Some(2048)); - Ok(()) - }); - } - - #[test] - fn test_custom_metrics_exclude_tags_from_env() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env( - "DD_LAMBDA_CUSTOMER_METRICS_EXCLUDE_TAGS", - "function_arn,region,account_id", - ); - - let mut config = Config::default(); - let env_config_source = EnvConfigSource; - env_config_source - .load(&mut config) - .expect("Failed to load config"); - - assert_eq!( - config.custom_metrics_exclude_tags, - vec![ - "function_arn".to_string(), - "region".to_string(), - "account_id".to_string() - ] - ); - Ok(()) - }); - } - - #[test] - fn test_custom_metrics_exclude_tags_defaults_to_empty() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - - let mut config = Config::default(); - let env_config_source = EnvConfigSource; - env_config_source - .load(&mut config) - .expect("Failed to load config"); - - assert!(config.custom_metrics_exclude_tags.is_empty()); - Ok(()) - }); - } - - #[test] - fn test_dogstatsd_config_defaults_to_none() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - - let mut config = Config::default(); - let env_config_source = EnvConfigSource; - env_config_source - .load(&mut config) - .expect("Failed to load config"); - - assert_eq!(config.dogstatsd_so_rcvbuf, None); - assert_eq!(config.dogstatsd_buffer_size, None); - assert_eq!(config.dogstatsd_queue_size, None); - Ok(()) - }); - } -} diff --git a/bottlecap/src/config/flush_strategy.rs b/bottlecap/src/config/flush_strategy.rs deleted file mode 100644 index 0a09e8227..000000000 --- a/bottlecap/src/config/flush_strategy.rs +++ /dev/null @@ -1,168 +0,0 @@ -use serde::{Deserialize, Deserializer}; -use tracing::debug; - -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct PeriodicStrategy { - pub interval: u64, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum FlushStrategy { - // Flush every 1s and at the end of the invocation - Default, - // User specifies the interval in milliseconds, will not block on the runtimeDone event - Periodically(PeriodicStrategy), - // Always flush at the end of the invocation - End, - // Flush both (1) at the end of the invocation and (2) periodically with the specified interval - EndPeriodically(PeriodicStrategy), - // Flush in a non-blocking, asynchronous manner, so the next invocation can start without waiting - // for the flush to complete - Continuously(PeriodicStrategy), -} - -impl FlushStrategy { - /// Returns the name of the flush strategy as a string slice. - #[must_use] - pub const fn name(&self) -> &'static str { - match self { - FlushStrategy::Default => "default", - FlushStrategy::End => "end", - FlushStrategy::Periodically(_) => "periodically", - FlushStrategy::EndPeriodically(_) => "end-periodically", - FlushStrategy::Continuously(_) => "continuously", - } - } -} - -// A restricted subset of `FlushStrategy`. The Default strategy is now allowed, which is required to be -// translated into a concrete strategy. -#[allow(clippy::module_name_repetitions)] -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum ConcreteFlushStrategy { - Periodically(PeriodicStrategy), - End, - EndPeriodically(PeriodicStrategy), - Continuously(PeriodicStrategy), -} - -// Deserialize for FlushStrategy -// Flush Strategy can be either "end", "end,", or "periodically," -impl<'de> Deserialize<'de> for FlushStrategy { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - if value.as_str() == "end" { - Ok(FlushStrategy::End) - } else { - let mut split_value = value.as_str().split(','); - // "periodically,60000" - // "end,1000" - let strategy = split_value.next(); - let interval: Option = split_value.next().and_then(|v| v.parse().ok()); - - match (strategy, interval) { - (Some("periodically"), Some(interval)) => { - Ok(FlushStrategy::Periodically(PeriodicStrategy { interval })) - } - (Some("continuously"), Some(interval)) => { - Ok(FlushStrategy::Continuously(PeriodicStrategy { interval })) - } - (Some("end"), Some(interval)) => { - Ok(FlushStrategy::EndPeriodically(PeriodicStrategy { - interval, - })) - } - (Some(strategy), _) => { - debug!("Invalid flush interval: {}, using default", strategy); - Ok(FlushStrategy::Default) - } - _ => { - debug!("Invalid flush strategy: {}, using default", value); - Ok(FlushStrategy::Default) - } - } - } - } -} - -#[cfg(test)] -#[allow(clippy::unwrap_used)] -mod tests { - use super::*; - - #[test] - fn deserialize_end() { - let flush_strategy: FlushStrategy = serde_json::from_str("\"end\"").unwrap(); - assert_eq!(flush_strategy, FlushStrategy::End); - } - - #[test] - fn deserialize_periodically() { - let flush_strategy: FlushStrategy = serde_json::from_str("\"periodically,60000\"").unwrap(); - assert_eq!( - flush_strategy, - FlushStrategy::Periodically(PeriodicStrategy { interval: 60000 }) - ); - } - - #[test] - fn deserialize_end_periodically() { - let flush_strategy: FlushStrategy = serde_json::from_str("\"end,1000\"").unwrap(); - assert_eq!( - flush_strategy, - FlushStrategy::EndPeriodically(PeriodicStrategy { interval: 1000 }) - ); - } - - #[test] - fn deserialize_invalid() { - let flush_strategy: FlushStrategy = serde_json::from_str("\"invalid\"").unwrap(); - assert_eq!(flush_strategy, FlushStrategy::Default); - } - - #[test] - fn deserialize_invalid_interval() { - let flush_strategy: FlushStrategy = - serde_json::from_str("\"periodically,invalid\"").unwrap(); - assert_eq!(flush_strategy, FlushStrategy::Default); - } - - #[test] - fn deserialize_invalid_end_interval() { - let flush_strategy: FlushStrategy = serde_json::from_str("\"end,invalid\"").unwrap(); - assert_eq!(flush_strategy, FlushStrategy::Default); - } - - #[test] - fn test_flush_strategy_name_default() { - let strategy = FlushStrategy::Default; - assert_eq!(strategy.name(), "default"); - } - - #[test] - fn test_flush_strategy_name_end() { - let strategy = FlushStrategy::End; - assert_eq!(strategy.name(), "end"); - } - - #[test] - fn test_flush_strategy_name_periodically() { - let strategy = FlushStrategy::Periodically(PeriodicStrategy { interval: 1000 }); - assert_eq!(strategy.name(), "periodically"); - } - - #[test] - fn test_flush_strategy_name_end_periodically() { - let strategy = FlushStrategy::EndPeriodically(PeriodicStrategy { interval: 2000 }); - assert_eq!(strategy.name(), "end-periodically"); - } - - #[test] - fn test_flush_strategy_name_continuously() { - let strategy = FlushStrategy::Continuously(PeriodicStrategy { interval: 30000 }); - assert_eq!(strategy.name(), "continuously"); - } -} diff --git a/bottlecap/src/config/log_level.rs b/bottlecap/src/config/log_level.rs deleted file mode 100644 index 7443f3caa..000000000 --- a/bottlecap/src/config/log_level.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::str::FromStr; - -use serde::{Deserialize, Deserializer}; -use serde_json::Value; -use tracing::error; - -#[derive(Clone, Copy, Debug, PartialEq, Default)] -pub enum LogLevel { - /// Designates very serious errors. - Error, - /// Designates hazardous situations. - #[default] - Warn, - /// Designates useful information. - Info, - /// Designates lower priority information. - Debug, - /// Designates very low priority, often extremely verbose, information. - Trace, -} - -impl AsRef for LogLevel { - fn as_ref(&self) -> &str { - match self { - LogLevel::Error => "ERROR", - LogLevel::Warn => "WARN", - LogLevel::Info => "INFO", - LogLevel::Debug => "DEBUG", - LogLevel::Trace => "TRACE", - } - } -} - -impl LogLevel { - /// Construct a `log::LevelFilter` from a `LogLevel` - #[must_use] - pub fn as_level_filter(self) -> log::LevelFilter { - match self { - LogLevel::Error => log::LevelFilter::Error, - LogLevel::Warn => log::LevelFilter::Warn, - LogLevel::Info => log::LevelFilter::Info, - LogLevel::Debug => log::LevelFilter::Debug, - LogLevel::Trace => log::LevelFilter::Trace, - } - } -} - -impl FromStr for LogLevel { - type Err = String; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "error" => Ok(LogLevel::Error), - "warn" => Ok(LogLevel::Warn), - "info" => Ok(LogLevel::Info), - "debug" => Ok(LogLevel::Debug), - "trace" => Ok(LogLevel::Trace), - _ => Err(format!( - "Invalid log level: '{s}'. Valid levels are: error, warn, info, debug, trace", - )), - } - } -} - -impl<'de> Deserialize<'de> for LogLevel { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = Value::deserialize(deserializer)?; - - if let Value::String(s) = value { - match LogLevel::from_str(&s) { - Ok(level) => Ok(level), - Err(e) => { - error!("{}", e); - Ok(LogLevel::Warn) - } - } - } else { - error!("Expected a string for log level, got {:?}", value); - Ok(LogLevel::Warn) - } - } -} diff --git a/bottlecap/src/config/logs_additional_endpoints.rs b/bottlecap/src/config/logs_additional_endpoints.rs deleted file mode 100644 index f3d18c151..000000000 --- a/bottlecap/src/config/logs_additional_endpoints.rs +++ /dev/null @@ -1,72 +0,0 @@ -use serde::{Deserialize, Deserializer}; -use serde_json::Value; -use tracing::error; - -#[derive(Debug, PartialEq, Clone, Deserialize)] -pub struct LogsAdditionalEndpoint { - pub api_key: String, - #[serde(rename = "Host")] - pub host: String, - #[serde(rename = "Port")] - pub port: u32, - pub is_reliable: bool, -} - -#[allow(clippy::module_name_repetitions)] -pub fn deserialize_logs_additional_endpoints<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let value = Value::deserialize(deserializer)?; - - match value { - Value::String(s) if !s.is_empty() => { - // For JSON format (string) in DD_ADDITIONAL_ENDPOINTS - Ok(serde_json::from_str(&s).unwrap_or_else(|err| { - error!("Failed to deserialize DD_LOGS_CONFIG_ADDITIONAL_ENDPOINTS: {err}"); - vec![] - })) - } - _ => Ok(Vec::new()), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn test_deserialize_logs_additional_endpoints_valid() { - let input = json!( - "[{\"api_key\": \"apiKey2\", \"Host\": \"agent-http-intake.logs.datadoghq.com\", \"Port\": 443, \"is_reliable\": true}]" - ); - - let result = deserialize_logs_additional_endpoints(input) - .expect("Failed to deserialize logs additional endpoints"); - let expected = vec![LogsAdditionalEndpoint { - api_key: "apiKey2".to_string(), - host: "agent-http-intake.logs.datadoghq.com".to_string(), - port: 443, - is_reliable: true, - }]; - - assert_eq!(result, expected); - } - - #[test] - fn test_deserialize_logs_additional_endpoints_invalid() { - // input missing "Port" field - let input = json!( - "[{\"api_key\": \"apiKey2\", \"Host\": \"agent-http-intake.logs.datadoghq.com\", \"is_reliable\": true}]" - ); - - let result = deserialize_logs_additional_endpoints(input) - .expect("Failed to deserialize logs additional endpoints"); - let expected = Vec::new(); // expect empty list due to invalid input - - assert_eq!(result, expected); - } -} diff --git a/bottlecap/src/config/mod.rs b/bottlecap/src/config/mod.rs index d270d4f19..ce72fcf69 100644 --- a/bottlecap/src/config/mod.rs +++ b/bottlecap/src/config/mod.rs @@ -1,1685 +1,35 @@ -pub mod additional_endpoints; -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; -pub mod service_mapping; -pub mod trace_propagation_style; -pub mod yaml; - -use libdd_trace_obfuscation::replacer::ReplaceRule; -use libdd_trace_utils::config_utils::{trace_intake_url, trace_intake_url_prefixed}; - -use serde::{Deserialize, Deserializer}; -use serde_aux::prelude::deserialize_bool_from_anything; -use serde_json::Value; - -use std::path::Path; -use std::time::Duration; -use std::{collections::HashMap, fmt}; -use tracing::{debug, error}; - -use crate::config::{ - apm_replace_rule::deserialize_apm_replace_rules, - env::EnvConfigSource, - flush_strategy::FlushStrategy, - log_level::LogLevel, - logs_additional_endpoints::LogsAdditionalEndpoint, - processing_rule::{ProcessingRule, deserialize_processing_rules}, - yaml::YamlConfigSource, -}; -use datadog_opentelemetry::propagation::TracePropagationStyle; - -/// Helper macro to merge Option fields to String fields -/// -/// Providing one field argument will merge the value from the source config field into the config -/// field. -/// -/// Providing two field arguments will merge the value from the source config field into the config -/// field if the value is not empty. -#[macro_export] -macro_rules! merge_string { - ($config:expr, $config_field:ident, $source:expr, $source_field:ident) => { - if let Some(value) = &$source.$source_field { - $config.$config_field.clone_from(value); - } - }; - ($config:expr, $source:expr, $field:ident) => { - if let Some(value) = &$source.$field { - $config.$field.clone_from(value); - } - }; -} - -/// Helper macro to merge Option fields where T implements Clone -/// -/// Providing one field argument will merge the value from the source config field into the config -/// field. -/// -/// Providing two field arguments will merge the value from the source config field into the config -/// field if the value is not empty. -#[macro_export] -macro_rules! merge_option { - ($config:expr, $config_field:ident, $source:expr, $source_field:ident) => { - if $source.$source_field.is_some() { - $config.$config_field.clone_from(&$source.$source_field); - } - }; - ($config:expr, $source:expr, $field:ident) => { - if $source.$field.is_some() { - $config.$field.clone_from(&$source.$field); - } - }; -} - -/// Helper macro to merge Option fields to T fields when Option is Some -/// -/// Providing one field argument will merge the value from the source config field into the config -/// field. -/// -/// Providing two field arguments will merge the value from the source config field into the config -/// field if the value is not empty. -#[macro_export] -macro_rules! merge_option_to_value { - ($config:expr, $config_field:ident, $source:expr, $source_field:ident) => { - if let Some(value) = &$source.$source_field { - $config.$config_field = value.clone(); - } - }; - ($config:expr, $source:expr, $field:ident) => { - if let Some(value) = &$source.$field { - $config.$field = value.clone(); - } - }; -} - -/// Helper macro to merge `Vec` fields when `Vec` is not empty -/// -/// Providing one field argument will merge the value from the source config field into the config -/// field. -/// -/// Providing two field arguments will merge the value from the source config field into the config -/// field if the value is not empty. -#[macro_export] -macro_rules! merge_vec { - ($config:expr, $config_field:ident, $source:expr, $source_field:ident) => { - if !$source.$source_field.is_empty() { - $config.$config_field.clone_from(&$source.$source_field); - } - }; - ($config:expr, $source:expr, $field:ident) => { - if !$source.$field.is_empty() { - $config.$field.clone_from(&$source.$field); - } - }; -} - -// nit: these will replace one map with the other, not merge the maps togehter, right? -/// Helper macro to merge `HashMap` fields when `HashMap` is not empty -/// -/// Providing one field argument will merge the value from the source config field into the config -/// field. -/// -/// Providing two field arguments will merge the value from the source config field into the config -/// field if the value is not empty. -#[macro_export] -macro_rules! merge_hashmap { - ($config:expr, $config_field:ident, $source:expr, $source_field:ident) => { - if !$source.$source_field.is_empty() { - $config.$config_field.clone_from(&$source.$source_field); - } - }; - ($config:expr, $source:expr, $field:ident) => { - if !$source.$field.is_empty() { - $config.$field.clone_from(&$source.$field); - } - }; -} - -#[derive(Debug, PartialEq)] -#[allow(clippy::module_name_repetitions)] -pub enum ConfigError { - ParseError(String), - UnsupportedField(String), -} - -#[allow(clippy::module_name_repetitions)] -pub trait ConfigSource { - fn load(&self, config: &mut Config) -> Result<(), ConfigError>; -} - -#[derive(Default)] -#[allow(clippy::module_name_repetitions)] -pub struct ConfigBuilder { - sources: Vec>, - config: Config, -} - -#[allow(clippy::module_name_repetitions)] -impl ConfigBuilder { - #[must_use] - pub fn add_source(mut self, source: Box) -> Self { - self.sources.push(source); - self - } - - pub fn build(&mut self) -> Config { - let mut failed_sources = 0; - for source in &self.sources { - match source.load(&mut self.config) { - Ok(()) => (), - Err(e) => { - error!("Failed to load config: {:?}", e); - failed_sources += 1; - } - } - } - - if !self.sources.is_empty() && failed_sources == self.sources.len() { - debug!("All sources failed to load config, using default config."); - } - - if self.config.site.is_empty() { - self.config.site = "datadoghq.com".to_string(); - } - - // If `proxy_https` is not set, set it from `HTTPS_PROXY` environment variable - // if it exists - if let Ok(https_proxy) = std::env::var("HTTPS_PROXY") - && self.config.proxy_https.is_none() - { - self.config.proxy_https = Some(https_proxy); - } - - // If `proxy_https` is set, check if the site is in `NO_PROXY` environment variable - // or in the `proxy_no_proxy` config field. - if self.config.proxy_https.is_some() { - let site_in_no_proxy = std::env::var("NO_PROXY") - .is_ok_and(|no_proxy| no_proxy.contains(&self.config.site)) - || self - .config - .proxy_no_proxy - .iter() - .any(|no_proxy| no_proxy.contains(&self.config.site)); - if site_in_no_proxy { - self.config.proxy_https = None; - } - } - - // If extraction is not set, set it to the same as the propagation style - if self.config.trace_propagation_style_extract.is_empty() { - self.config - .trace_propagation_style_extract - .clone_from(&self.config.trace_propagation_style); - } - - // If Logs URL is not set, set it to the default - if self.config.logs_config_logs_dd_url.trim().is_empty() { - self.config.logs_config_logs_dd_url = build_fqdn_logs(self.config.site.clone()); - } else { - self.config.logs_config_logs_dd_url = - logs_intake_url(self.config.logs_config_logs_dd_url.as_str()); - } - - // If APM URL is not set, set it to the default - if self.config.apm_dd_url.is_empty() { - self.config.apm_dd_url = trace_intake_url(self.config.site.clone().as_str()); - } else { - // If APM URL is set, add the site to the URL - self.config.apm_dd_url = trace_intake_url_prefixed(self.config.apm_dd_url.as_str()); - } - - self.config.clone() - } -} - -#[derive(Debug, PartialEq, Clone)] -#[allow(clippy::module_name_repetitions)] -#[allow(clippy::struct_excessive_bools)] -pub struct Config { - pub site: String, - pub api_key: String, - pub log_level: LogLevel, - - // Timeout for the request to flush data to Datadog endpoint - pub flush_timeout: u64, - - // Global config of compression levels. - // It would be overridden by the setup for the individual component - pub compression_level: i32, - - // Proxy - pub proxy_https: Option, - pub proxy_no_proxy: Vec, - pub http_protocol: Option, - pub tls_cert_file: Option, - pub skip_ssl_validation: bool, - - // Endpoints - pub dd_url: String, - pub url: String, - pub additional_endpoints: HashMap>, - - // Unified Service Tagging - pub env: Option, - pub service: Option, - pub version: Option, - pub tags: HashMap, - - // Logs - pub logs_config_logs_dd_url: String, - pub logs_config_processing_rules: Option>, - pub logs_config_use_compression: bool, - pub logs_config_compression_level: i32, - pub logs_config_additional_endpoints: Vec, - pub observability_pipelines_worker_logs_enabled: bool, - pub observability_pipelines_worker_logs_url: String, - - // APM - // - pub service_mapping: HashMap, - // - pub apm_dd_url: String, - pub apm_replace_tags: Option>, - pub apm_config_obfuscation_http_remove_query_string: bool, - pub apm_config_obfuscation_http_remove_paths_with_digits: bool, - pub apm_config_compression_level: i32, - pub apm_features: Vec, - pub apm_additional_endpoints: HashMap>, - pub apm_filter_tags_require: Option>, - pub apm_filter_tags_reject: Option>, - pub apm_filter_tags_regex_require: Option>, - pub apm_filter_tags_regex_reject: Option>, - // - // Trace Propagation - pub trace_propagation_style: Vec, - pub trace_propagation_style_extract: Vec, - pub trace_propagation_extract_first: bool, - pub trace_propagation_http_baggage_enabled: bool, - pub trace_aws_service_representation_enabled: bool, - - // Metrics - pub metrics_config_compression_level: i32, - pub statsd_metric_namespace: Option, - pub custom_metrics_exclude_tags: Vec, - /// Size of the receive buffer for `DogStatsD` UDP packets, in bytes (`SO_RCVBUF`). - /// Increase to reduce packet loss under high-throughput metric bursts. - /// If None, uses the OS default. - pub dogstatsd_so_rcvbuf: Option, - /// Maximum size of a single read from any transport (UDP or named pipe), in bytes. - /// Defaults to 8192. For UDP, the client must batch metrics into packets of - /// this size for the increase to take effect. - pub dogstatsd_buffer_size: Option, - /// Internal queue capacity between the socket reader and metric processor. - /// Defaults to 1024. Increase if the processor can't keep up with burst traffic. - pub dogstatsd_queue_size: Option, - - // OTLP - // - // - APM / Traces - pub otlp_config_traces_enabled: bool, - pub otlp_config_traces_span_name_as_resource_name: bool, - pub otlp_config_traces_span_name_remappings: HashMap, - pub otlp_config_ignore_missing_datadog_fields: bool, - // - // - Receiver / HTTP - pub otlp_config_receiver_protocols_http_endpoint: Option, - // - Unsupported Configuration - // - // - Receiver / GRPC - pub otlp_config_receiver_protocols_grpc_endpoint: Option, - pub otlp_config_receiver_protocols_grpc_transport: Option, - pub otlp_config_receiver_protocols_grpc_max_recv_msg_size_mib: Option, - // - Metrics - pub otlp_config_metrics_enabled: bool, - pub otlp_config_metrics_resource_attributes_as_tags: bool, - pub otlp_config_metrics_instrumentation_scope_metadata_as_tags: bool, - pub otlp_config_metrics_tag_cardinality: Option, - pub otlp_config_metrics_delta_ttl: Option, - pub otlp_config_metrics_histograms_mode: Option, - pub otlp_config_metrics_histograms_send_count_sum_metrics: bool, - pub otlp_config_metrics_histograms_send_aggregation_metrics: bool, - pub otlp_config_metrics_sums_cumulative_monotonic_mode: Option, - // nit: is the e in cumulative missing intentionally? - pub otlp_config_metrics_sums_initial_cumulativ_monotonic_value: Option, - pub otlp_config_metrics_summaries_mode: Option, - // - Traces - pub otlp_config_traces_probabilistic_sampler_sampling_percentage: Option, - // - Logs - pub otlp_config_logs_enabled: bool, - - // AWS Lambda - 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, - - /// Maximum number of request IDs whose logs are held in `held_logs` waiting for durable - /// execution context. Set to 0 to disable log holding; logs will be flushed immediately - /// without durable execution context enrichment. Defaults to 0 until the tracer-side - /// durable execution support is released; set to 50 to re-enable enrichment. - pub lambda_durable_function_log_buffer_size: usize, -} - -impl Default for Config { - fn default() -> Self { - Self { - site: String::default(), - api_key: String::default(), - log_level: LogLevel::default(), - flush_timeout: 30, - - // Proxy - proxy_https: None, - proxy_no_proxy: vec![], - http_protocol: None, - tls_cert_file: None, - skip_ssl_validation: false, - - // Endpoints - dd_url: String::default(), - url: String::default(), - additional_endpoints: HashMap::new(), - - // Unified Service Tagging - env: None, - service: None, - version: None, - tags: HashMap::new(), - - compression_level: 3, - - // Logs - logs_config_logs_dd_url: String::default(), - logs_config_processing_rules: None, - logs_config_use_compression: true, - logs_config_compression_level: 3, - logs_config_additional_endpoints: Vec::new(), - observability_pipelines_worker_logs_enabled: false, - observability_pipelines_worker_logs_url: String::default(), - - // APM - service_mapping: HashMap::new(), - apm_dd_url: String::default(), - apm_replace_tags: None, - apm_config_obfuscation_http_remove_query_string: false, - apm_config_obfuscation_http_remove_paths_with_digits: false, - apm_config_compression_level: 3, - apm_features: vec![], - apm_additional_endpoints: HashMap::new(), - apm_filter_tags_require: None, - apm_filter_tags_reject: None, - apm_filter_tags_regex_require: None, - apm_filter_tags_regex_reject: None, - trace_aws_service_representation_enabled: true, - trace_propagation_style: vec![ - TracePropagationStyle::Datadog, - TracePropagationStyle::TraceContext, - ], - trace_propagation_style_extract: vec![], - trace_propagation_extract_first: false, - trace_propagation_http_baggage_enabled: false, - - // Metrics - metrics_config_compression_level: 3, - statsd_metric_namespace: None, - - custom_metrics_exclude_tags: vec![], - - // DogStatsD - // Defaults to None, which uses the OS default. - dogstatsd_so_rcvbuf: None, - // Defaults to 8192 internally. - dogstatsd_buffer_size: None, - // Defaults to 1024 internally. - dogstatsd_queue_size: None, - - // OTLP - otlp_config_traces_enabled: true, - otlp_config_traces_span_name_as_resource_name: false, - otlp_config_traces_span_name_remappings: HashMap::new(), - otlp_config_ignore_missing_datadog_fields: false, - otlp_config_receiver_protocols_http_endpoint: None, - otlp_config_receiver_protocols_grpc_endpoint: None, - otlp_config_receiver_protocols_grpc_transport: None, - otlp_config_receiver_protocols_grpc_max_recv_msg_size_mib: None, - otlp_config_metrics_enabled: false, // TODO(duncanista): Go Agent default is to true - otlp_config_metrics_resource_attributes_as_tags: false, - otlp_config_metrics_instrumentation_scope_metadata_as_tags: false, - otlp_config_metrics_tag_cardinality: None, - otlp_config_metrics_delta_ttl: None, - otlp_config_metrics_histograms_mode: None, - otlp_config_metrics_histograms_send_count_sum_metrics: false, - otlp_config_metrics_histograms_send_aggregation_metrics: false, - otlp_config_metrics_sums_cumulative_monotonic_mode: None, - otlp_config_metrics_sums_initial_cumulativ_monotonic_value: None, - otlp_config_metrics_summaries_mode: None, - otlp_config_traces_probabilistic_sampler_sampling_percentage: None, - otlp_config_logs_enabled: false, - - // AWS Lambda - api_key_secret_arn: String::default(), - kms_api_key: String::default(), - api_key_ssm_arn: String::default(), - 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::default(), - - 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), - - lambda_durable_function_log_buffer_size: 0, - } - } -} - -impl datadog_opentelemetry::propagation::PropagationConfig for Config { - fn trace_propagation_style(&self) -> Option<&[TracePropagationStyle]> { - if self.trace_propagation_style.is_empty() { - None - } else { - Some(&self.trace_propagation_style) - } - } - - fn trace_propagation_style_extract(&self) -> Option<&[TracePropagationStyle]> { - if self.trace_propagation_style_extract.is_empty() { - None - } else { - Some(&self.trace_propagation_style_extract) - } - } - - fn trace_propagation_style_inject(&self) -> Option<&[TracePropagationStyle]> { - // Bottlecap does not configure injection styles separately - None - } - - fn trace_propagation_extract_first(&self) -> bool { - self.trace_propagation_extract_first - } - - fn datadog_tags_max_length(&self) -> usize { - // Default max length matching dd-trace-rs - 512 - } -} - -#[allow(clippy::module_name_repetitions)] -#[inline] -#[must_use] -pub fn get_config(config_directory: &Path) -> Config { - let path: std::path::PathBuf = config_directory.join("datadog.yaml"); - ConfigBuilder::default() - .add_source(Box::new(YamlConfigSource { path })) - .add_source(Box::new(EnvConfigSource)) - .build() -} - -#[inline] -#[must_use] -fn build_fqdn_logs(site: String) -> String { - format!("https://http-intake.logs.{site}") -} - -#[inline] -#[must_use] -fn logs_intake_url(url: &str) -> String { - let url = url.trim(); - if url.is_empty() { - return url.to_string(); - } - if url.starts_with("https://") || url.starts_with("http://") { - return url.to_string(); - } - format!("https://{url}") -} - -pub fn deserialize_optional_string<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - match Value::deserialize(deserializer)? { - Value::String(s) => Ok(Some(s)), - other => { - error!( - "Failed to parse value, expected a string, got: {}, ignoring", - other - ); - Ok(None) - } - } -} - -pub fn deserialize_string_or_int<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let value = Value::deserialize(deserializer)?; - match value { - Value::String(s) => { - if s.trim().is_empty() { - Ok(None) - } else { - Ok(Some(s)) - } - } - Value::Number(n) => Ok(Some(n.to_string())), - _ => { - error!("Failed to parse value, expected a string or an integer, ignoring"); - Ok(None) - } - } -} - -pub fn deserialize_optional_bool_from_anything<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - // First try to deserialize as Option<_> to handle null/missing values - let opt: Option = Option::deserialize(deserializer)?; - - match opt { - None => Ok(None), - Some(value) => match deserialize_bool_from_anything(value) { - Ok(bool_result) => Ok(Some(bool_result)), - Err(e) => { - error!("Failed to parse bool value: {}, ignoring", e); - Ok(None) - } - }, - } -} - -/// Parse a single "key:value" string into a (key, value) tuple -/// Returns None if the string is invalid (e.g., missing colon, empty key/value) -fn parse_key_value_tag(tag: &str) -> Option<(String, String)> { - let parts: Vec<&str> = tag.splitn(2, ':').collect(); - if parts.len() == 2 && !parts[0].is_empty() && !parts[1].is_empty() { - Some((parts[0].to_string(), parts[1].to_string())) - } else { - error!( - "Failed to parse tag '{}', expected format 'key:value', ignoring", - tag - ); - None - } -} - -pub fn deserialize_key_value_pairs<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - struct KeyValueVisitor; - - impl serde::de::Visitor<'_> for KeyValueVisitor { - type Value = HashMap; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a string in format 'key1:value1,key2:value2' or 'key1:value1'") - } - - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - let mut map = HashMap::new(); - for tag in value.split(&[',', ' ']) { - if tag.is_empty() { - continue; - } - if let Some((key, val)) = parse_key_value_tag(tag) { - map.insert(key, val); - } - } - - Ok(map) - } - - fn visit_u64(self, value: u64) -> Result - where - E: serde::de::Error, - { - error!( - "Failed to parse tags: expected string in format 'key:value', got number {}, ignoring", - value - ); - Ok(HashMap::new()) - } - - fn visit_i64(self, value: i64) -> Result - where - E: serde::de::Error, - { - error!( - "Failed to parse tags: expected string in format 'key:value', got number {}, ignoring", - value - ); - Ok(HashMap::new()) - } - - fn visit_f64(self, value: f64) -> Result - where - E: serde::de::Error, - { - error!( - "Failed to parse tags: expected string in format 'key:value', got number {}, ignoring", - value - ); - Ok(HashMap::new()) - } - - fn visit_bool(self, value: bool) -> Result - where - E: serde::de::Error, - { - error!( - "Failed to parse tags: expected string in format 'key:value', got boolean {}, ignoring", - value - ); - Ok(HashMap::new()) - } - } - - deserializer.deserialize_any(KeyValueVisitor) -} - -pub fn deserialize_array_from_comma_separated_string<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let s: String = String::deserialize(deserializer)?; - Ok(s.split(',') - .map(|feature| feature.trim().to_string()) - .filter(|feature| !feature.is_empty()) - .collect()) -} - -pub fn deserialize_key_value_pair_array_to_hashmap<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let array: Vec = Vec::deserialize(deserializer)?; - let mut map = HashMap::new(); - for s in array { - if let Some((key, val)) = parse_key_value_tag(&s) { - map.insert(key, val); - } - } - Ok(map) -} - -/// Deserialize APM filter tags from space-separated "key:value" pairs, also support key-only tags -pub fn deserialize_apm_filter_tags<'de, D>(deserializer: D) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - let opt: Option = Option::deserialize(deserializer)?; - - match opt { - None => Ok(None), - Some(s) if s.trim().is_empty() => Ok(None), - Some(s) => { - let tags: Vec = s - .split_whitespace() - .filter_map(|pair| { - let parts: Vec<&str> = pair.splitn(2, ':').collect(); - if parts.len() == 2 { - let key = parts[0].trim(); - let value = parts[1].trim(); - if key.is_empty() { - None - } else if value.is_empty() { - Some(key.to_string()) - } else { - Some(format!("{key}:{value}")) - } - } else if parts.len() == 1 { - let key = parts[0].trim(); - if key.is_empty() { - None - } else { - Some(key.to_string()) - } - } else { - None - } - }) - .collect(); - - if tags.is_empty() { - Ok(None) - } else { - Ok(Some(tags)) - } - } - } -} - -pub fn deserialize_option_lossless<'de, D, T>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, - T: Deserialize<'de>, -{ - match Option::::deserialize(deserializer) { - Ok(value) => Ok(value), - Err(e) => { - error!("Failed to deserialize optional value: {}, ignoring", e); - Ok(None) - } - } -} - -pub fn deserialize_optional_duration_from_microseconds<'de, D: Deserializer<'de>>( - deserializer: D, -) -> Result, D::Error> { - Ok(Option::::deserialize(deserializer)?.map(Duration::from_micros)) -} - -pub fn deserialize_optional_duration_from_seconds<'de, D: Deserializer<'de>>( - deserializer: D, -) -> Result, D::Error> { - struct DurationVisitor; - impl serde::de::Visitor<'_> for DurationVisitor { - type Value = Option; - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "a duration in seconds (integer or float)") - } - fn visit_u64(self, v: u64) -> Result { - Ok(Some(Duration::from_secs(v))) - } - fn visit_i64(self, v: i64) -> Result { - if v < 0 { - error!("Failed to parse duration: negative durations are not allowed, ignoring"); - return Ok(None); - } - self.visit_u64(u64::try_from(v).expect("positive i64 to u64 conversion never fails")) - } - fn visit_f64(self, v: f64) -> Result { - if v < 0f64 { - error!("Failed to parse duration: negative durations are not allowed, ignoring"); - return Ok(None); - } - Ok(Some(Duration::from_secs_f64(v))) - } - } - deserializer.deserialize_any(DurationVisitor) -} - -// Like deserialize_optional_duration_from_seconds(), but return None if the value is 0 -pub fn deserialize_optional_duration_from_seconds_ignore_zero<'de, D: Deserializer<'de>>( - deserializer: D, -) -> Result, D::Error> { - let duration: Option = deserialize_optional_duration_from_seconds(deserializer)?; - if duration.is_some_and(|d| d.as_secs() == 0) { - return Ok(None); - } - Ok(duration) -} - -#[cfg_attr(coverage_nightly, coverage(off))] // Test modules skew coverage metrics -#[cfg(test)] -pub mod tests { - use libdd_trace_obfuscation::replacer::parse_rules_from_string; - - use super::*; - - use crate::config::{ - flush_strategy::{FlushStrategy, PeriodicStrategy}, - log_level::LogLevel, - processing_rule::ProcessingRule, - }; - use datadog_opentelemetry::propagation::TracePropagationStyle; - - #[test] - fn test_default_logs_intake_url() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - - let config = get_config(Path::new("")); - assert_eq!( - config.logs_config_logs_dd_url, - "https://http-intake.logs.datadoghq.com".to_string() - ); - Ok(()) - }); - } - - #[test] - fn test_support_pci_logs_intake_url() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env( - "DD_LOGS_CONFIG_LOGS_DD_URL", - "agent-http-intake-pci.logs.datadoghq.com:443", - ); - - let config = get_config(Path::new("")); - assert_eq!( - config.logs_config_logs_dd_url, - "https://agent-http-intake-pci.logs.datadoghq.com:443".to_string() - ); - Ok(()) - }); - } - - #[test] - fn test_logs_intake_url_adds_prefix() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env( - "DD_LOGS_CONFIG_LOGS_DD_URL", - "dr-test-failover-http-intake.logs.datadoghq.com:443", - ); - - let config = get_config(Path::new("")); - // ensure host:port URL is prefixed with https:// - assert_eq!( - config.logs_config_logs_dd_url, - "https://dr-test-failover-http-intake.logs.datadoghq.com:443".to_string() - ); - Ok(()) - }); - } - - #[test] - fn test_prefixed_logs_intake_url() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env( - "DD_LOGS_CONFIG_LOGS_DD_URL", - "https://custom-intake.logs.datadoghq.com:443", - ); - - let config = get_config(Path::new("")); - assert_eq!( - config.logs_config_logs_dd_url, - "https://custom-intake.logs.datadoghq.com:443".to_string() - ); - Ok(()) - }); - } - - #[test] - fn test_support_pci_traces_intake_url() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_APM_DD_URL", "https://trace-pci.agent.datadoghq.com"); - - let config = get_config(Path::new("")); - assert_eq!( - config.apm_dd_url, - "https://trace-pci.agent.datadoghq.com/api/v0.2/traces".to_string() - ); - Ok(()) - }); - } - - #[test] - fn test_support_dd_dd_url() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_DD_URL", "custom_proxy:3128"); - - let config = get_config(Path::new("")); - assert_eq!(config.dd_url, "custom_proxy:3128".to_string()); - Ok(()) - }); - } - - #[test] - fn test_support_dd_url() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_URL", "custom_proxy:3128"); - - let config = get_config(Path::new("")); - assert_eq!(config.url, "custom_proxy:3128".to_string()); - Ok(()) - }); - } - - #[test] - fn test_dd_dd_url_default() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - - let config = get_config(Path::new("")); - assert_eq!(config.dd_url, String::new()); - Ok(()) - }); - } - - #[test] - fn test_precedence() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.create_file( - "datadog.yaml", - r" - site: datadoghq.eu, - ", - )?; - jail.set_env("DD_SITE", "datad0g.com"); - let config = get_config(Path::new("")); - assert_eq!(config.site, "datad0g.com"); - Ok(()) - }); - } - - #[test] - fn test_parse_config_file() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - // nit: does parsing an empty file actually test "parse config file"? - jail.create_file( - "datadog.yaml", - r" - ", - )?; - let config = get_config(Path::new("")); - assert_eq!(config.site, "datadoghq.com"); - Ok(()) - }); - } - - #[test] - fn test_parse_env() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_SITE", "datadoghq.eu"); - let config = get_config(Path::new("")); - assert_eq!(config.site, "datadoghq.eu"); - Ok(()) - }); - } - - #[test] - fn test_parse_log_level() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_LOG_LEVEL", "TRACE"); - let config = get_config(Path::new("")); - assert_eq!(config.log_level, LogLevel::Trace); - Ok(()) - }); - } - - #[test] - fn test_parse_default() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - let config = get_config(Path::new("")); - assert_eq!( - config, - Config { - site: "datadoghq.com".to_string(), - trace_propagation_style_extract: vec![ - TracePropagationStyle::Datadog, - TracePropagationStyle::TraceContext - ], - logs_config_logs_dd_url: "https://http-intake.logs.datadoghq.com".to_string(), - apm_dd_url: trace_intake_url("datadoghq.com").clone(), - dd_url: String::new(), // We add the prefix in main.rs - ..Config::default() - } - ); - Ok(()) - }); - } - - #[test] - fn test_proxy_config() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_PROXY_HTTPS", "my-proxy:3128"); - let config = get_config(Path::new("")); - assert_eq!(config.proxy_https, Some("my-proxy:3128".to_string())); - Ok(()) - }); - } - - #[test] - fn test_noproxy_config() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_SITE", "datadoghq.eu"); - jail.set_env("DD_PROXY_HTTPS", "my-proxy:3128"); - jail.set_env( - "NO_PROXY", - "127.0.0.1,localhost,172.16.0.0/12,us-east-1.amazonaws.com,datadoghq.eu", - ); - let config = get_config(Path::new("")); - assert_eq!(config.proxy_https, None); - Ok(()) - }); - } - - #[test] - fn test_proxy_yaml() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.create_file( - "datadog.yaml", - r" - proxy: - https: my-proxy:3128 - ", - )?; - - let config = get_config(Path::new("")); - assert_eq!(config.proxy_https, Some("my-proxy:3128".to_string())); - Ok(()) - }); - } - - #[test] - fn test_no_proxy_yaml() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.create_file( - "datadog.yaml", - r" - proxy: - https: my-proxy:3128 - no_proxy: - - datadoghq.com - ", - )?; - - let config = get_config(Path::new("")); - assert_eq!(config.proxy_https, None); - // Assertion to ensure config.site runs before proxy - // because we chenck that noproxy contains the site - assert_eq!(config.site, "datadoghq.com"); - Ok(()) - }); - } - - #[test] - fn test_parse_flush_strategy_end() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_SERVERLESS_FLUSH_STRATEGY", "end"); - let config = get_config(Path::new("")); - assert_eq!(config.serverless_flush_strategy, FlushStrategy::End); - Ok(()) - }); - } - - #[test] - fn test_parse_flush_strategy_periodically() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_SERVERLESS_FLUSH_STRATEGY", "periodically,100000"); - let config = get_config(Path::new("")); - assert_eq!( - config.serverless_flush_strategy, - FlushStrategy::Periodically(PeriodicStrategy { interval: 100_000 }) - ); - Ok(()) - }); - } - - #[test] - fn test_parse_flush_strategy_invalid() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_SERVERLESS_FLUSH_STRATEGY", "invalid_strategy"); - let config = get_config(Path::new("")); - assert_eq!(config.serverless_flush_strategy, FlushStrategy::Default); - Ok(()) - }); - } - - #[test] - fn test_parse_flush_strategy_invalid_periodic() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env( - "DD_SERVERLESS_FLUSH_STRATEGY", - "periodically,invalid_interval", - ); - let config = get_config(Path::new("")); - assert_eq!(config.serverless_flush_strategy, FlushStrategy::Default); - Ok(()) - }); - } - - #[test] - fn parse_number_or_string_env_vars() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_VERSION", "123"); - jail.set_env("DD_ENV", "123456890"); - jail.set_env("DD_SERVICE", "123456"); - let config = get_config(Path::new("")); - assert_eq!(config.version.expect("failed to parse DD_VERSION"), "123"); - assert_eq!(config.env.expect("failed to parse DD_ENV"), "123456890"); - assert_eq!( - config.service.expect("failed to parse DD_SERVICE"), - "123456" - ); - Ok(()) - }); - } - - #[test] - fn test_parse_logs_config_processing_rules_from_env() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env( - "DD_LOGS_CONFIG_PROCESSING_RULES", - r#"[{"type":"exclude_at_match","name":"exclude","pattern":"exclude"}]"#, - ); - jail.create_file( - "datadog.yaml", - r" - logs_config: - processing_rules: - - type: exclude_at_match - name: exclude-me-yaml - pattern: exclude-me-yaml - ", - )?; - let config = get_config(Path::new("")); - assert_eq!( - config.logs_config_processing_rules, - Some(vec![ProcessingRule { - kind: processing_rule::Kind::ExcludeAtMatch, - name: "exclude".to_string(), - pattern: "exclude".to_string(), - replace_placeholder: None - }]) - ); - Ok(()) - }); - } - - #[test] - fn test_parse_logs_config_processing_rules_from_yaml() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.create_file( - "datadog.yaml", - r" - site: datadoghq.com - logs_config: - processing_rules: - - type: exclude_at_match - name: exclude - pattern: exclude - ", - )?; - let config = get_config(Path::new("")); - assert_eq!( - config.logs_config_processing_rules, - Some(vec![ProcessingRule { - kind: processing_rule::Kind::ExcludeAtMatch, - name: "exclude".to_string(), - pattern: "exclude".to_string(), - replace_placeholder: None - }]), - ); - Ok(()) - }); - } - - #[test] - fn test_parse_apm_replace_tags_from_yaml() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.create_file( - "datadog.yaml", - r" - site: datadoghq.com - apm_config: - replace_tags: - - name: '*' - pattern: 'foo' - repl: 'REDACTED' - ", - )?; - let config = get_config(Path::new("")); - let rule = parse_rules_from_string( - r#"[ - {"name": "*", "pattern": "foo", "repl": "REDACTED"} - ]"#, - ) - .expect("can't parse rules"); - assert_eq!(config.apm_replace_tags, Some(rule),); - Ok(()) - }); - } - - #[test] - fn test_apm_tags_env_overrides_yaml() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env( - "DD_APM_REPLACE_TAGS", - r#"[{"name":"*","pattern":"foo","repl":"REDACTED-ENV"}]"#, - ); - jail.create_file( - "datadog.yaml", - r" - site: datadoghq.com - apm_config: - replace_tags: - - name: '*' - pattern: 'foo' - repl: 'REDACTED-YAML' - ", - )?; - let config = get_config(Path::new("")); - let rule = parse_rules_from_string( - r#"[ - {"name": "*", "pattern": "foo", "repl": "REDACTED-ENV"} - ]"#, - ) - .expect("can't parse rules"); - assert_eq!(config.apm_replace_tags, Some(rule),); - Ok(()) - }); - } - - #[test] - fn test_parse_apm_http_obfuscation_from_yaml() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.create_file( - "datadog.yaml", - r" - site: datadoghq.com - apm_config: - obfuscation: - http: - remove_query_string: true - remove_paths_with_digits: true - ", - )?; - let config = get_config(Path::new("")); - assert!(config.apm_config_obfuscation_http_remove_query_string,); - assert!(config.apm_config_obfuscation_http_remove_paths_with_digits,); - Ok(()) - }); - } - #[test] - fn test_parse_trace_propagation_style() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_TRACE_PROPAGATION_STYLE", "datadog,tracecontext"); - let config = get_config(Path::new("")); - - let expected_styles = vec![ - TracePropagationStyle::Datadog, - TracePropagationStyle::TraceContext, - ]; - assert_eq!(config.trace_propagation_style, expected_styles); - assert_eq!(config.trace_propagation_style_extract, expected_styles); - Ok(()) - }); - } - - #[test] - fn test_parse_trace_propagation_style_extract() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_TRACE_PROPAGATION_STYLE_EXTRACT", "datadog"); - let config = get_config(Path::new("")); - - assert_eq!( - config.trace_propagation_style, - vec![ - TracePropagationStyle::Datadog, - TracePropagationStyle::TraceContext, - ] - ); - assert_eq!( - config.trace_propagation_style_extract, - vec![TracePropagationStyle::Datadog] - ); - Ok(()) - }); - } - - #[test] - fn test_bad_tags() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_TAGS", 123); - let config = get_config(Path::new("")); - assert_eq!(config.tags, HashMap::new()); - Ok(()) - }); - } - - #[test] - fn test_tags_comma_separated() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_TAGS", "team:serverless,env:prod,version:1.0"); - let config = get_config(Path::new("")); - assert_eq!(config.tags.get("team"), Some(&"serverless".to_string())); - assert_eq!(config.tags.get("env"), Some(&"prod".to_string())); - assert_eq!(config.tags.get("version"), Some(&"1.0".to_string())); - assert_eq!(config.tags.len(), 3); - Ok(()) - }); - } - - #[test] - fn test_tags_space_separated() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_TAGS", "team:serverless env:prod version:1.0"); - let config = get_config(Path::new("")); - assert_eq!(config.tags.get("team"), Some(&"serverless".to_string())); - assert_eq!(config.tags.get("env"), Some(&"prod".to_string())); - assert_eq!(config.tags.get("version"), Some(&"1.0".to_string())); - assert_eq!(config.tags.len(), 3); - Ok(()) - }); - } - - #[test] - fn test_tags_space_separated_with_extra_spaces() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_TAGS", "team:serverless env:prod version:1.0"); - let config = get_config(Path::new("")); - assert_eq!(config.tags.get("team"), Some(&"serverless".to_string())); - assert_eq!(config.tags.get("env"), Some(&"prod".to_string())); - assert_eq!(config.tags.get("version"), Some(&"1.0".to_string())); - assert_eq!(config.tags.len(), 3); - Ok(()) - }); - } - - #[test] - fn test_tags_mixed_separators() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_TAGS", "team:serverless,env:prod version:1.0"); - let config = get_config(Path::new("")); - assert_eq!(config.tags.get("team"), Some(&"serverless".to_string())); - assert_eq!(config.tags.get("env"), Some(&"prod".to_string())); - assert_eq!(config.tags.get("version"), Some(&"1.0".to_string())); - assert_eq!(config.tags.len(), 3); - Ok(()) - }); - } - - #[test] - fn test_parse_bool_from_anything() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.set_env("DD_SERVERLESS_LOGS_ENABLED", "true"); - jail.set_env("DD_ENHANCED_METRICS", "1"); - jail.set_env("DD_LOGS_CONFIG_USE_COMPRESSION", "TRUE"); - jail.set_env("DD_CAPTURE_LAMBDA_PAYLOAD", "0"); - let config = get_config(Path::new("")); - assert!(config.serverless_logs_enabled); - assert!(config.enhanced_metrics); - assert!(config.logs_config_use_compression); - assert!(!config.capture_lambda_payload); - Ok(()) - }); - } - - #[test] - fn test_overrides_config_based_on_priority() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.create_file( - "datadog.yaml", - r#" - site: us3.datadoghq.com - api_key: "yaml-api-key" - log_level: "debug" - "#, - )?; - jail.set_env("DD_SITE", "us5.datadoghq.com"); - jail.set_env("DD_API_KEY", "env-api-key"); - jail.set_env("DD_FLUSH_TIMEOUT", "10"); - let config = get_config(Path::new("")); - - assert_eq!(config.site, "us5.datadoghq.com"); - assert_eq!(config.api_key, "env-api-key"); - assert_eq!(config.log_level, LogLevel::Debug); - assert_eq!(config.flush_timeout, 10); - Ok(()) - }); - } - - #[test] - fn test_parse_duration_from_microseconds() { - #[derive(Deserialize, Debug, PartialEq, Eq)] - struct Value { - #[serde(default)] - #[serde(deserialize_with = "deserialize_optional_duration_from_microseconds")] - duration: Option, - } - - assert_eq!( - serde_json::from_str::("{}").expect("failed to parse JSON"), - Value { duration: None } - ); - serde_json::from_str::(r#"{"duration":-1}"#) - .expect_err("should have failed parsing"); - assert_eq!( - serde_json::from_str::(r#"{"duration":1000000}"#).expect("failed to parse JSON"), - Value { - duration: Some(Duration::from_secs(1)) - } - ); - serde_json::from_str::(r#"{"duration":-1.5}"#) - .expect_err("should have failed parsing"); - serde_json::from_str::(r#"{"duration":1.5}"#) - .expect_err("should have failed parsing"); - } - - #[test] - fn test_parse_duration_from_seconds() { - #[derive(Deserialize, Debug, PartialEq, Eq)] - struct Value { - #[serde(default)] - #[serde(deserialize_with = "deserialize_optional_duration_from_seconds")] - duration: Option, - } - - assert_eq!( - serde_json::from_str::("{}").expect("failed to parse JSON"), - Value { duration: None } - ); - assert_eq!( - serde_json::from_str::(r#"{"duration":-1}"#).expect("failed to parse JSON"), - Value { duration: None } - ); - assert_eq!( - serde_json::from_str::(r#"{"duration":1}"#).expect("failed to parse JSON"), - Value { - duration: Some(Duration::from_secs(1)) - } - ); - assert_eq!( - serde_json::from_str::(r#"{"duration":-1.5}"#).expect("failed to parse JSON"), - Value { duration: None } - ); - assert_eq!( - serde_json::from_str::(r#"{"duration":1.5}"#).expect("failed to parse JSON"), - Value { - duration: Some(Duration::from_millis(1500)) - } - ); - } - - #[test] - fn test_parse_duration_from_seconds_ignore_zero() { - #[derive(Deserialize, Debug, PartialEq, Eq)] - struct Value { - #[serde(default)] - #[serde(deserialize_with = "deserialize_optional_duration_from_seconds_ignore_zero")] - duration: Option, - } - - assert_eq!( - serde_json::from_str::(r#"{"duration":1}"#).expect("failed to parse JSON"), - Value { - duration: Some(Duration::from_secs(1)) - } - ); - - assert_eq!( - serde_json::from_str::(r#"{"duration":0}"#).expect("failed to parse JSON"), - Value { duration: None } - ); - } - - #[test] - fn test_deserialize_key_value_pairs_ignores_empty_keys() { - #[derive(Deserialize, Debug, PartialEq)] - struct TestStruct { - #[serde(deserialize_with = "deserialize_key_value_pairs")] - tags: HashMap, - } - - let result = serde_json::from_str::(r#"{"tags": ":value,valid:tag"}"#) - .expect("failed to parse JSON"); - let mut expected = HashMap::new(); - expected.insert("valid".to_string(), "tag".to_string()); - assert_eq!(result.tags, expected); - } - - #[test] - fn test_deserialize_key_value_pairs_ignores_empty_values() { - #[derive(Deserialize, Debug, PartialEq)] - struct TestStruct { - #[serde(deserialize_with = "deserialize_key_value_pairs")] - tags: HashMap, - } - - let result = serde_json::from_str::(r#"{"tags": "key:,valid:tag"}"#) - .expect("failed to parse JSON"); - let mut expected = HashMap::new(); - expected.insert("valid".to_string(), "tag".to_string()); - assert_eq!(result.tags, expected); - } - - #[test] - fn test_deserialize_key_value_pairs_with_url_values() { - #[derive(Deserialize, Debug, PartialEq)] - struct TestStruct { - #[serde(deserialize_with = "deserialize_key_value_pairs")] - tags: HashMap, - } - - let result = serde_json::from_str::( - r#"{"tags": "git.repository_url:https://gitlab.ddbuild.io/DataDog/serverless-e2e-tests.git,env:prod"}"# - ) - .expect("failed to parse JSON"); - let mut expected = HashMap::new(); - expected.insert( - "git.repository_url".to_string(), - "https://gitlab.ddbuild.io/DataDog/serverless-e2e-tests.git".to_string(), - ); - expected.insert("env".to_string(), "prod".to_string()); - assert_eq!(result.tags, expected); - } - - #[test] - fn test_deserialize_key_value_pair_array_with_urls() { - #[derive(Deserialize, Debug, PartialEq)] - struct TestStruct { - #[serde(deserialize_with = "deserialize_key_value_pair_array_to_hashmap")] - tags: HashMap, - } - - let result = serde_json::from_str::( - r#"{"tags": ["git.repository_url:https://gitlab.ddbuild.io/DataDog/serverless-e2e-tests.git", "env:prod", "version:1.2.3"]}"# - ) - .expect("failed to parse JSON"); - let mut expected = HashMap::new(); - expected.insert( - "git.repository_url".to_string(), - "https://gitlab.ddbuild.io/DataDog/serverless-e2e-tests.git".to_string(), - ); - expected.insert("env".to_string(), "prod".to_string()); - expected.insert("version".to_string(), "1.2.3".to_string()); - assert_eq!(result.tags, expected); - } +pub mod propagation_wrapper; + +// Re-export upstream config submodules so existing `crate::config::env::*`, +// `crate::config::flush_strategy::*`, etc. imports across bottlecap keep +// working without forcing every consumer to switch to the upstream path. +pub use datadog_agent_config::{ + TracePropagationStyle, additional_endpoints, apm_replace_rule, deserialize_apm_filter_tags, + deserialize_array_from_comma_separated_string, deserialize_key_value_pair_array_to_hashmap, + deserialize_key_value_pairs, deserialize_option_lossless, + 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, env, flush_strategy, get_config_with_extension, log_level, + logs_additional_endpoints, processing_rule, service_mapping, yaml, +}; - #[test] - fn test_deserialize_key_value_pair_array_ignores_invalid() { - #[derive(Deserialize, Debug, PartialEq)] - struct TestStruct { - #[serde(deserialize_with = "deserialize_key_value_pair_array_to_hashmap")] - tags: HashMap, - } +use std::path::Path; +use std::time::Duration; - let result = serde_json::from_str::( - r#"{"tags": ["valid:tag", "invalid_no_colon", "another:good:value:with:colons"]}"#, - ) - .expect("failed to parse JSON"); - let mut expected = HashMap::new(); - expected.insert("valid".to_string(), "tag".to_string()); - expected.insert("another".to_string(), "good:value:with:colons".to_string()); - assert_eq!(result.tags, expected); - } +use serde::Deserialize; - #[test] - fn test_deserialize_key_value_pair_array_empty() { - #[derive(Deserialize, Debug, PartialEq)] - struct TestStruct { - #[serde(deserialize_with = "deserialize_key_value_pair_array_to_hashmap")] - tags: HashMap, - } +/// Bottlecap's resolved configuration: the shared agent core plus a Lambda +/// extension under `.ext`. +pub type Config = datadog_agent_config::Config; - let result = - serde_json::from_str::(r#"{"tags": []}"#).expect("failed to parse JSON"); - assert_eq!(result.tags, HashMap::new()); - } +#[allow(clippy::module_name_repetitions)] +#[inline] +#[must_use] +pub fn get_config(config_directory: &Path) -> Config { + get_config_with_extension::(config_directory) } - // --------------------------------------------------------------------------- // LambdaConfig — bottlecap's `ConfigExtension` for the shared // `datadog-agent-config` crate. Lives alongside the core config under @@ -1725,6 +75,12 @@ pub struct LambdaConfig { pub api_security_enabled: bool, pub api_security_sample_delay: Duration, pub custom_metrics_exclude_tags: Vec, + + /// Maximum number of request IDs whose logs are held in `held_logs` waiting for durable + /// execution context. Set to 0 to disable log holding; logs will be flushed immediately + /// without durable execution context enrichment. Defaults to 0 until the tracer-side + /// durable execution support is released; set to 50 to re-enable enrichment. + pub lambda_durable_function_log_buffer_size: usize, } impl Default for LambdaConfig { @@ -1749,6 +105,7 @@ impl Default for LambdaConfig { api_security_enabled: true, api_security_sample_delay: Duration::from_secs(30), custom_metrics_exclude_tags: Vec::new(), + lambda_durable_function_log_buffer_size: 0, } } } @@ -1817,6 +174,12 @@ pub struct LambdaConfigSource { /// matches the env var; merges into `custom_metrics_exclude_tags`. #[serde(deserialize_with = "deser_csv")] pub lambda_customer_metrics_exclude_tags: Vec, + + /// `DD_LAMBDA_DURABLE_FUNCTION_LOG_BUFFER_SIZE` — max number of request IDs + /// whose logs are held waiting for durable execution context. Defaults to + /// 0 (hold mechanism disabled). + #[serde(deserialize_with = "deser_opt_lossless")] + pub lambda_durable_function_log_buffer_size: Option, } impl DatadogConfigExtension for LambdaConfig { @@ -1840,6 +203,7 @@ impl DatadogConfigExtension for LambdaConfig { appsec_waf_timeout, api_security_enabled, api_security_sample_delay, + lambda_durable_function_log_buffer_size, ], option: [span_dedup_timeout, api_key_secret_reload_interval, appsec_rules], ); diff --git a/bottlecap/src/config/processing_rule.rs b/bottlecap/src/config/processing_rule.rs deleted file mode 100644 index cae8a5ada..000000000 --- a/bottlecap/src/config/processing_rule.rs +++ /dev/null @@ -1,56 +0,0 @@ -use serde::{Deserialize, Deserializer}; -use serde_json::Value as JsonValue; - -#[derive(Clone, Copy, Debug, PartialEq, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Kind { - ExcludeAtMatch, - IncludeAtMatch, - MaskSequences, -} - -#[derive(Clone, Debug, PartialEq, Deserialize)] -pub struct ProcessingRule { - #[serde(rename = "type")] - pub kind: Kind, - pub name: String, - pub pattern: String, - pub replace_placeholder: Option, -} - -pub fn deserialize_processing_rules<'de, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - // Deserialize the JSON value using serde_json::Value - let value: JsonValue = Deserialize::deserialize(deserializer)?; - - match value { - JsonValue::String(s) => match serde_json::from_str(&s) { - Ok(values) => Ok(Some(values)), - Err(e) => { - tracing::error!("Failed to parse processing rules: {}, ignoring", e); - Ok(None) - } - }, - JsonValue::Array(a) => { - let mut values = Vec::new(); - for v in a { - match serde_json::from_value(v.clone()) { - Ok(rule) => values.push(rule), - Err(e) => { - tracing::error!("Failed to parse processing rule: {}, ignoring", e); - } - } - } - if values.is_empty() { - Ok(None) - } else { - Ok(Some(values)) - } - } - _ => Ok(None), - } -} diff --git a/bottlecap/src/config/propagation_wrapper.rs b/bottlecap/src/config/propagation_wrapper.rs new file mode 100644 index 000000000..164ecf5d8 --- /dev/null +++ b/bottlecap/src/config/propagation_wrapper.rs @@ -0,0 +1,52 @@ +use std::sync::Arc; + +use datadog_opentelemetry::propagation::PropagationConfig; + +use crate::config::{Config, TracePropagationStyle}; + +/// Newtype wrapper that lets us implement `PropagationConfig` for bottlecap's +/// `Config` without tripping Rust's orphan rule. Both the trait and the +/// underlying `datadog_agent_config::Config` are foreign, so the +/// wrapper is the local type the impl can attach to. Callers that need a +/// propagator hand it an `Arc` instead of an `Arc`. +#[derive(Debug, Clone)] +pub struct PropConfig(pub Arc); + +impl PropConfig { + #[must_use] + pub fn new(config: Arc) -> Self { + Self(config) + } +} + +impl PropagationConfig for PropConfig { + fn trace_propagation_style(&self) -> Option<&[TracePropagationStyle]> { + if self.0.trace_propagation_style.is_empty() { + None + } else { + Some(&self.0.trace_propagation_style) + } + } + + fn trace_propagation_style_extract(&self) -> Option<&[TracePropagationStyle]> { + if self.0.trace_propagation_style_extract.is_empty() { + None + } else { + Some(&self.0.trace_propagation_style_extract) + } + } + + fn trace_propagation_style_inject(&self) -> Option<&[TracePropagationStyle]> { + // Bottlecap does not configure injection styles separately. + None + } + + fn trace_propagation_extract_first(&self) -> bool { + self.0.trace_propagation_extract_first + } + + fn datadog_tags_max_length(&self) -> usize { + // Default max length matching dd-trace-rs + 512 + } +} diff --git a/bottlecap/src/config/service_mapping.rs b/bottlecap/src/config/service_mapping.rs deleted file mode 100644 index 5b1339895..000000000 --- a/bottlecap/src/config/service_mapping.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::collections::HashMap; - -use serde::{Deserialize, Deserializer}; - -#[allow(clippy::module_name_repetitions)] -pub fn deserialize_service_mapping<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let s: String = String::deserialize(deserializer)?; - - let map = s - .split(',') - .filter_map(|pair| { - let mut split = pair.split(':'); - - let service = split.next(); - let to_map = split.next(); - - if let (Some(service), Some(to_map)) = (service, to_map) { - Some((service.trim().to_string(), to_map.trim().to_string())) - } else { - tracing::error!("Failed to parse service mapping '{}', expected format 'service:mapped_service', ignoring", pair.trim()); - None - } - }) - .collect(); - - Ok(map) -} diff --git a/bottlecap/src/config/trace_propagation_style.rs b/bottlecap/src/config/trace_propagation_style.rs deleted file mode 100644 index 1e32e77f9..000000000 --- a/bottlecap/src/config/trace_propagation_style.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::str::FromStr; - -use datadog_opentelemetry::propagation::TracePropagationStyle; -use serde::{Deserialize, Deserializer}; -use tracing::error; - -#[allow(clippy::module_name_repetitions)] -pub fn deserialize_trace_propagation_style<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let s: String = String::deserialize(deserializer)?; - - Ok(s.split(',') - .filter_map( - |style| match TracePropagationStyle::from_str(style.trim()) { - Ok(parsed_style) => Some(parsed_style), - Err(e) => { - error!("Failed to parse trace propagation style: {}, ignoring", e); - None - } - }, - ) - .collect()) -} diff --git a/bottlecap/src/config/yaml.rs b/bottlecap/src/config/yaml.rs deleted file mode 100644 index e825e25b1..000000000 --- a/bottlecap/src/config/yaml.rs +++ /dev/null @@ -1,1098 +0,0 @@ -use std::time::Duration; -use std::{collections::HashMap, path::PathBuf}; - -use datadog_opentelemetry::propagation::TracePropagationStyle; - -use crate::{ - config::{ - Config, ConfigError, ConfigSource, ProcessingRule, - additional_endpoints::deserialize_additional_endpoints, deserialize_apm_replace_rules, - deserialize_key_value_pair_array_to_hashmap, deserialize_option_lossless, - 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_processing_rules, deserialize_string_or_int, flush_strategy::FlushStrategy, - log_level::LogLevel, logs_additional_endpoints::LogsAdditionalEndpoint, - service_mapping::deserialize_service_mapping, - trace_propagation_style::deserialize_trace_propagation_style, - }, - merge_hashmap, merge_option, merge_option_to_value, merge_string, merge_vec, -}; -use figment::{ - Figment, - providers::{Format, Yaml}, -}; -use libdd_trace_obfuscation::replacer::ReplaceRule; -use serde::Deserialize; - -/// `YamlConfig` is a struct that represents some of the fields in the `datadog.yaml` file. -/// -/// It is used to deserialize the `datadog.yaml` file into a struct that can be merged -/// with the `Config` struct. -#[derive(Debug, PartialEq, Deserialize, Clone, Default)] -#[serde(default)] -#[allow(clippy::module_name_repetitions)] -pub struct YamlConfig { - #[serde(deserialize_with = "deserialize_optional_string")] - pub site: Option, - #[serde(deserialize_with = "deserialize_optional_string")] - pub api_key: Option, - pub log_level: Option, - - #[serde(deserialize_with = "deserialize_option_lossless")] - pub flush_timeout: Option, - - #[serde(deserialize_with = "deserialize_option_lossless")] - pub compression_level: Option, - - // Proxy - pub proxy: ProxyConfig, - // nit: this should probably be in the endpoints section - #[serde(deserialize_with = "deserialize_optional_string")] - pub dd_url: Option, - #[serde(deserialize_with = "deserialize_optional_string")] - pub http_protocol: Option, - #[serde(deserialize_with = "deserialize_optional_string")] - pub tls_cert_file: Option, - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub skip_ssl_validation: Option, - - // Endpoints - #[serde(deserialize_with = "deserialize_additional_endpoints")] - /// Field used for Dual Shipping for Metrics - pub additional_endpoints: HashMap>, - - // Unified Service Tagging - #[serde(deserialize_with = "deserialize_string_or_int")] - pub env: Option, - #[serde(deserialize_with = "deserialize_string_or_int")] - pub service: Option, - #[serde(deserialize_with = "deserialize_string_or_int")] - pub version: Option, - #[serde(deserialize_with = "deserialize_key_value_pair_array_to_hashmap")] - pub tags: HashMap, - - // Logs - pub logs_config: LogsConfig, - - // APM - pub apm_config: ApmConfig, - #[serde(deserialize_with = "deserialize_service_mapping")] - pub service_mapping: HashMap, - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub trace_aws_service_representation_enabled: Option, - // Trace Propagation - #[serde(deserialize_with = "deserialize_trace_propagation_style")] - pub trace_propagation_style: Vec, - #[serde(deserialize_with = "deserialize_trace_propagation_style")] - pub trace_propagation_style_extract: Vec, - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub trace_propagation_extract_first: Option, - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub trace_propagation_http_baggage_enabled: Option, - - // Metrics - pub metrics_config: MetricsConfig, - - // DogStatsD - /// Size of the receive buffer for `DogStatsD` UDP packets, in bytes (`SO_RCVBUF`). - #[serde(deserialize_with = "deserialize_option_lossless")] - pub dogstatsd_so_rcvbuf: Option, - /// Maximum size of a single read from any transport (UDP or named pipe), in bytes. - #[serde(deserialize_with = "deserialize_option_lossless")] - pub dogstatsd_buffer_size: Option, - /// Internal queue capacity between the socket reader and metric processor. - #[serde(deserialize_with = "deserialize_option_lossless")] - pub dogstatsd_queue_size: Option, - - // OTLP - pub otlp_config: Option, - - // AWS Lambda - #[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_bool_from_anything")] - pub serverless_logs_enabled: Option, - #[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, - #[serde(deserialize_with = "deserialize_option_lossless")] - 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 api_key_secret_reload_interval: 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, -} - -/// Proxy Config -/// - -#[derive(Debug, PartialEq, Deserialize, Clone, Default)] -#[serde(default)] -#[allow(clippy::module_name_repetitions)] -pub struct ProxyConfig { - pub https: Option, - pub no_proxy: Option>, -} - -/// Logs Config -/// - -#[derive(Debug, PartialEq, Deserialize, Clone, Default)] -#[serde(default)] -#[allow(clippy::module_name_repetitions)] -pub struct LogsConfig { - pub logs_dd_url: Option, - #[serde(deserialize_with = "deserialize_processing_rules")] - pub processing_rules: Option>, - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub use_compression: Option, - #[serde(deserialize_with = "deserialize_option_lossless")] - pub compression_level: Option, - pub additional_endpoints: Vec, -} - -/// Metrics specific config -/// -#[derive(Debug, PartialEq, Deserialize, Clone, Copy, Default)] -#[serde(default)] -#[allow(clippy::module_name_repetitions)] -pub struct MetricsConfig { - #[serde(deserialize_with = "deserialize_option_lossless")] - pub compression_level: Option, -} - -/// APM Config -/// - -#[derive(Debug, PartialEq, Deserialize, Clone, Default)] -#[serde(default)] -#[allow(clippy::module_name_repetitions)] -pub struct ApmConfig { - pub apm_dd_url: Option, - #[serde(deserialize_with = "deserialize_apm_replace_rules")] - pub replace_tags: Option>, - pub obfuscation: Option, - #[serde(deserialize_with = "deserialize_option_lossless")] - pub compression_level: Option, - pub features: Vec, - #[serde(deserialize_with = "deserialize_additional_endpoints")] - pub additional_endpoints: HashMap>, -} - -impl ApmConfig { - #[must_use] - pub fn obfuscation_http_remove_query_string(&self) -> Option { - self.obfuscation - .as_ref() - .and_then(|obfuscation| obfuscation.http.remove_query_string) - } - - #[must_use] - pub fn obfuscation_http_remove_paths_with_digits(&self) -> Option { - self.obfuscation - .as_ref() - .and_then(|obfuscation| obfuscation.http.remove_paths_with_digits) - } -} - -#[derive(Debug, PartialEq, Deserialize, Clone, Copy, Default)] -#[serde(default)] -#[allow(clippy::module_name_repetitions)] -pub struct ApmObfuscation { - pub http: ApmHttpObfuscation, -} - -#[derive(Debug, PartialEq, Deserialize, Clone, Copy, Default)] -#[serde(default)] -#[allow(clippy::module_name_repetitions)] -pub struct ApmHttpObfuscation { - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub remove_query_string: Option, - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub remove_paths_with_digits: Option, -} - -/// OTLP Config -/// - -#[derive(Debug, PartialEq, Deserialize, Clone, Default)] -#[serde(default)] -#[allow(clippy::module_name_repetitions)] -pub struct OtlpConfig { - pub receiver: Option, - pub traces: Option, - - // NOT SUPPORTED - pub metrics: Option, - pub logs: Option, -} - -#[derive(Debug, PartialEq, Deserialize, Clone, Default)] -#[serde(default)] -#[allow(clippy::module_name_repetitions)] -pub struct OtlpReceiverConfig { - pub protocols: Option, -} - -#[derive(Debug, PartialEq, Deserialize, Clone, Default)] -#[serde(default)] -#[allow(clippy::module_name_repetitions)] -pub struct OtlpReceiverProtocolsConfig { - pub http: Option, - - // NOT SUPPORTED - pub grpc: Option, -} - -#[derive(Debug, PartialEq, Deserialize, Clone, Default)] -#[serde(default)] -#[allow(clippy::module_name_repetitions)] -pub struct OtlpReceiverHttpConfig { - pub endpoint: Option, -} - -#[derive(Debug, PartialEq, Deserialize, Clone, Default)] -#[serde(default)] -#[allow(clippy::module_name_repetitions)] -pub struct OtlpReceiverGrpcConfig { - pub endpoint: Option, - pub transport: Option, - #[serde(deserialize_with = "deserialize_option_lossless")] - pub max_recv_msg_size_mib: Option, -} - -#[derive(Debug, PartialEq, Deserialize, Clone, Default)] -#[serde(default)] -#[allow(clippy::module_name_repetitions)] -pub struct OtlpTracesConfig { - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub enabled: Option, - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub span_name_as_resource_name: Option, - pub span_name_remappings: HashMap, - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub ignore_missing_datadog_fields: Option, - - // NOT SUPORTED - pub probabilistic_sampler: Option, -} - -#[derive(Debug, PartialEq, Clone, Deserialize, Default, Copy)] -pub struct OtlpTracesProbabilisticSampler { - #[serde(deserialize_with = "deserialize_option_lossless")] - pub sampling_percentage: Option, -} - -#[derive(Debug, PartialEq, Deserialize, Clone, Default)] -#[serde(default)] -pub struct OtlpMetricsConfig { - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub enabled: Option, - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub resource_attributes_as_tags: Option, - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub instrumentation_scope_metadata_as_tags: Option, - pub tag_cardinality: Option, - #[serde(deserialize_with = "deserialize_option_lossless")] - pub delta_ttl: Option, - pub histograms: Option, - pub sums: Option, - pub summaries: Option, -} - -#[derive(Debug, PartialEq, Clone, Deserialize, Default)] -#[serde(default)] -pub struct OtlpMetricsHistograms { - pub mode: Option, - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub send_count_sum_metrics: Option, - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub send_aggregation_metrics: Option, -} - -#[derive(Debug, PartialEq, Clone, Deserialize, Default)] -#[serde(default)] -pub struct OtlpMetricsSums { - pub cumulative_monotonic_mode: Option, - pub initial_cumulative_monotonic_value: Option, -} - -#[derive(Debug, PartialEq, Clone, Deserialize, Default)] -#[serde(default)] -pub struct OtlpMetricsSummaries { - pub mode: Option, -} - -#[derive(Debug, PartialEq, Clone, Deserialize, Default, Copy)] -#[serde(default)] -pub struct OtlpLogsConfig { - #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] - pub enabled: Option, -} - -impl OtlpConfig { - #[must_use] - pub fn receiver_protocols_http_endpoint(&self) -> Option { - self.receiver.as_ref().and_then(|receiver| { - receiver.protocols.as_ref().and_then(|protocols| { - protocols - .http - .as_ref() - .and_then(|http| http.endpoint.clone()) - }) - }) - } - - #[must_use] - pub fn receiver_protocols_grpc(&self) -> Option<&OtlpReceiverGrpcConfig> { - self.receiver.as_ref().and_then(|receiver| { - receiver - .protocols - .as_ref() - .and_then(|protocols| protocols.grpc.as_ref()) - }) - } - - #[must_use] - pub fn traces_enabled(&self) -> Option { - self.traces.as_ref().and_then(|traces| traces.enabled) - } - - #[must_use] - pub fn traces_ignore_missing_datadog_fields(&self) -> Option { - self.traces - .as_ref() - .and_then(|traces| traces.ignore_missing_datadog_fields) - } - - #[must_use] - pub fn traces_span_name_as_resource_name(&self) -> Option { - self.traces - .as_ref() - .and_then(|traces| traces.span_name_as_resource_name) - } - - #[must_use] - pub fn traces_span_name_remappings(&self) -> HashMap { - self.traces - .as_ref() - .map(|traces| traces.span_name_remappings.clone()) - .unwrap_or_default() - } - - #[must_use] - pub fn traces_probabilistic_sampler(&self) -> Option<&OtlpTracesProbabilisticSampler> { - self.traces - .as_ref() - .and_then(|traces| traces.probabilistic_sampler.as_ref()) - } - - #[must_use] - pub fn logs(&self) -> Option<&OtlpLogsConfig> { - self.logs.as_ref() - } -} - -#[allow(clippy::too_many_lines)] -fn merge_config(config: &mut Config, yaml_config: &YamlConfig) { - // Basic fields - merge_string!(config, yaml_config, site); - merge_string!(config, yaml_config, api_key); - merge_option_to_value!(config, yaml_config, log_level); - merge_option_to_value!(config, yaml_config, flush_timeout); - - // Unified Service Tagging - merge_option!(config, yaml_config, env); - merge_option!(config, yaml_config, service); - merge_option!(config, yaml_config, version); - merge_hashmap!(config, yaml_config, tags); - - merge_option_to_value!(config, yaml_config, compression_level); - // Proxy - merge_option!(config, proxy_https, yaml_config.proxy, https); - merge_option_to_value!(config, proxy_no_proxy, yaml_config.proxy, no_proxy); - merge_option!(config, yaml_config, http_protocol); - merge_option!(config, yaml_config, tls_cert_file); - merge_option_to_value!(config, yaml_config, skip_ssl_validation); - - // Endpoints - merge_hashmap!(config, yaml_config, additional_endpoints); - merge_string!(config, yaml_config, dd_url); - - // Logs - merge_string!( - config, - logs_config_logs_dd_url, - yaml_config.logs_config, - logs_dd_url - ); - merge_option!( - config, - logs_config_processing_rules, - yaml_config.logs_config, - processing_rules - ); - merge_option_to_value!( - config, - logs_config_use_compression, - yaml_config.logs_config, - use_compression - ); - merge_option_to_value!( - config, - logs_config_compression_level, - yaml_config, - compression_level - ); - merge_option_to_value!( - config, - logs_config_compression_level, - yaml_config.logs_config, - compression_level - ); - merge_vec!( - config, - logs_config_additional_endpoints, - yaml_config.logs_config, - additional_endpoints - ); - - merge_option_to_value!( - config, - metrics_config_compression_level, - yaml_config, - compression_level - ); - - merge_option_to_value!( - config, - metrics_config_compression_level, - yaml_config.metrics_config, - compression_level - ); - - // DogStatsD - merge_option!(config, yaml_config, dogstatsd_so_rcvbuf); - merge_option!(config, yaml_config, dogstatsd_buffer_size); - merge_option!(config, yaml_config, dogstatsd_queue_size); - - // APM - merge_hashmap!(config, yaml_config, service_mapping); - merge_string!(config, apm_dd_url, yaml_config.apm_config, apm_dd_url); - merge_option!( - config, - apm_replace_tags, - yaml_config.apm_config, - replace_tags - ); - merge_option_to_value!( - config, - apm_config_compression_level, - yaml_config, - compression_level - ); - merge_option_to_value!( - config, - apm_config_compression_level, - yaml_config.apm_config, - compression_level - ); - merge_hashmap!( - config, - apm_additional_endpoints, - yaml_config.apm_config, - additional_endpoints - ); - - // Not using the macro here because we need to call a method on the struct - if let Some(remove_query_string) = yaml_config - .apm_config - .obfuscation_http_remove_query_string() - { - config - .apm_config_obfuscation_http_remove_query_string - .clone_from(&remove_query_string); - } - if let Some(remove_paths_with_digits) = yaml_config - .apm_config - .obfuscation_http_remove_paths_with_digits() - { - config - .apm_config_obfuscation_http_remove_paths_with_digits - .clone_from(&remove_paths_with_digits); - } - - merge_vec!(config, apm_features, yaml_config.apm_config, features); - - // Trace Propagation - merge_vec!(config, yaml_config, trace_propagation_style); - merge_vec!(config, yaml_config, trace_propagation_style_extract); - merge_option_to_value!(config, yaml_config, trace_propagation_extract_first); - merge_option_to_value!(config, yaml_config, trace_propagation_http_baggage_enabled); - merge_option_to_value!( - config, - yaml_config, - trace_aws_service_representation_enabled - ); - - // OTLP - if let Some(otlp_config) = &yaml_config.otlp_config { - // Traces - - // Not using macros in some cases because we need to call a method on the struct - if let Some(traces_enabled) = otlp_config.traces_enabled() { - config - .otlp_config_traces_enabled - .clone_from(&traces_enabled); - } - if let Some(traces_span_name_as_resource_name) = - otlp_config.traces_span_name_as_resource_name() - { - config - .otlp_config_traces_span_name_as_resource_name - .clone_from(&traces_span_name_as_resource_name); - } - - let traces_span_name_remappings = otlp_config.traces_span_name_remappings(); - if !traces_span_name_remappings.is_empty() { - config - .otlp_config_traces_span_name_remappings - .clone_from(&traces_span_name_remappings); - } - if let Some(traces_ignore_missing_datadog_fields) = - otlp_config.traces_ignore_missing_datadog_fields() - { - config - .otlp_config_ignore_missing_datadog_fields - .clone_from(&traces_ignore_missing_datadog_fields); - } - - if let Some(probabilistic_sampler) = otlp_config.traces_probabilistic_sampler() { - merge_option!( - config, - otlp_config_traces_probabilistic_sampler_sampling_percentage, - probabilistic_sampler, - sampling_percentage - ); - } - - // Receiver - let receiver_protocols_http_endpoint = otlp_config.receiver_protocols_http_endpoint(); - if receiver_protocols_http_endpoint.is_some() { - config - .otlp_config_receiver_protocols_http_endpoint - .clone_from(&receiver_protocols_http_endpoint); - } - - if let Some(receiver_protocols_grpc) = otlp_config.receiver_protocols_grpc() { - merge_option!( - config, - otlp_config_receiver_protocols_grpc_endpoint, - receiver_protocols_grpc, - endpoint - ); - merge_option!( - config, - otlp_config_receiver_protocols_grpc_transport, - receiver_protocols_grpc, - transport - ); - merge_option!( - config, - otlp_config_receiver_protocols_grpc_max_recv_msg_size_mib, - receiver_protocols_grpc, - max_recv_msg_size_mib - ); - } - - // Metrics - if let Some(metrics) = &otlp_config.metrics { - merge_option_to_value!(config, otlp_config_metrics_enabled, metrics, enabled); - merge_option_to_value!( - config, - otlp_config_metrics_resource_attributes_as_tags, - metrics, - resource_attributes_as_tags - ); - merge_option_to_value!( - config, - otlp_config_metrics_instrumentation_scope_metadata_as_tags, - metrics, - instrumentation_scope_metadata_as_tags - ); - merge_option!( - config, - otlp_config_metrics_tag_cardinality, - metrics, - tag_cardinality - ); - merge_option!(config, otlp_config_metrics_delta_ttl, metrics, delta_ttl); - if let Some(histograms) = &metrics.histograms { - merge_option_to_value!( - config, - otlp_config_metrics_histograms_send_count_sum_metrics, - histograms, - send_count_sum_metrics - ); - merge_option_to_value!( - config, - otlp_config_metrics_histograms_send_aggregation_metrics, - histograms, - send_aggregation_metrics - ); - merge_option!( - config, - otlp_config_metrics_histograms_mode, - histograms, - mode - ); - } - if let Some(sums) = &metrics.sums { - merge_option!( - config, - otlp_config_metrics_sums_cumulative_monotonic_mode, - sums, - cumulative_monotonic_mode - ); - merge_option!( - config, - otlp_config_metrics_sums_initial_cumulativ_monotonic_value, - sums, - initial_cumulative_monotonic_value - ); - } - if let Some(summaries) = &metrics.summaries { - merge_option!(config, otlp_config_metrics_summaries_mode, summaries, mode); - } - } - - // Logs - if let Some(logs) = &otlp_config.logs { - merge_option_to_value!(config, otlp_config_logs_enabled, logs, enabled); - } - } - - // AWS Lambda - merge_string!(config, yaml_config, api_key_secret_arn); - merge_string!(config, yaml_config, kms_api_key); - - // Handle serverless_logs_enabled with OR logic: if either logs_enabled or serverless_logs_enabled is true, enable logs - if yaml_config.serverless_logs_enabled.is_some() || yaml_config.logs_enabled.is_some() { - config.serverless_logs_enabled = yaml_config.serverless_logs_enabled.unwrap_or(false) - || yaml_config.logs_enabled.unwrap_or(false); - } - - merge_option_to_value!(config, yaml_config, serverless_flush_strategy); - merge_option_to_value!(config, yaml_config, enhanced_metrics); - merge_option_to_value!(config, yaml_config, lambda_proc_enhanced_metrics); - merge_option_to_value!(config, yaml_config, capture_lambda_payload); - merge_option_to_value!(config, yaml_config, capture_lambda_payload_max_depth); - merge_option_to_value!(config, yaml_config, compute_trace_stats_on_extension); - merge_option!(config, yaml_config, api_key_secret_reload_interval); - merge_option_to_value!(config, yaml_config, serverless_appsec_enabled); - merge_option!(config, yaml_config, appsec_rules); - merge_option_to_value!(config, yaml_config, appsec_waf_timeout); - merge_option_to_value!(config, yaml_config, api_security_enabled); - merge_option_to_value!(config, yaml_config, api_security_sample_delay); -} - -#[derive(Debug, PartialEq, Clone)] -#[allow(clippy::module_name_repetitions)] -pub struct YamlConfigSource { - pub path: PathBuf, -} - -impl ConfigSource for YamlConfigSource { - fn load(&self, config: &mut Config) -> Result<(), ConfigError> { - let figment = Figment::new().merge(Yaml::file(self.path.clone())); - - match figment.extract::() { - Ok(yaml_config) => merge_config(config, &yaml_config), - Err(e) => { - return Err(ConfigError::ParseError(format!( - "Failed to parse config from yaml file: {e}, using default config." - ))); - } - } - - Ok(()) - } -} - -#[cfg_attr(coverage_nightly, coverage(off))] // Test modules skew coverage metrics -#[cfg(test)] -mod tests { - use std::path::Path; - use std::time::Duration; - - use crate::config::{flush_strategy::PeriodicStrategy, processing_rule::Kind}; - - use super::*; - - #[test] - #[allow(clippy::too_many_lines)] - fn test_merge_config_overrides_with_yaml_file() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.create_file( - "datadog.yaml", - r#" -# Basic fields -site: "test-site" -api_key: "test-api-key" -log_level: "debug" -flush_timeout: 42 -compression_level: 4 -# Proxy -proxy: - https: "https://proxy.example.com" - no_proxy: ["localhost", "127.0.0.1"] -dd_url: "https://metrics.datadoghq.com" -http_protocol: "http1" -tls_cert_file: "/opt/ca-cert.pem" -skip_ssl_validation: true - -# Endpoints -additional_endpoints: - "https://app.datadoghq.com": - - apikey2 - - apikey3 - "https://app.datadoghq.eu": - - apikey4 - -# Unified Service Tagging -env: "test-env" -service: "test-service" -version: "1.0.0" -tags: - - "team:test-team" - - "project:test-project" - -# Logs -logs_config: - logs_dd_url: "https://logs.datadoghq.com" - processing_rules: - - name: "test-exclude" - type: "exclude_at_match" - pattern: "test-pattern" - use_compression: false - compression_level: 1 - additional_endpoints: - - api_key: "apikey2" - Host: "agent-http-intake.logs.datadoghq.com" - Port: 443 - is_reliable: true - -# APM -apm_config: - apm_dd_url: "https://apm.datadoghq.com" - replace_tags: [] - obfuscation: - http: - remove_query_string: true - remove_paths_with_digits: true - compression_level: 2 - features: - - "enable_otlp_compute_top_level_by_span_kind" - - "enable_stats_by_span_kind" - additional_endpoints: - "https://trace.agent.datadoghq.com": - - apikey2 - - apikey3 - "https://trace.agent.datadoghq.eu": - - apikey4 - -service_mapping: old-service:new-service - -# Trace Propagation -trace_propagation_style: "datadog" -trace_propagation_style_extract: "tracecontext" -trace_propagation_extract_first: true -trace_propagation_http_baggage_enabled: true -trace_aws_service_representation_enabled: true - -metrics_config: - compression_level: 3 - -dogstatsd_so_rcvbuf: 1048576 -dogstatsd_buffer_size: 65507 -dogstatsd_queue_size: 2048 - -# OTLP -otlp_config: - receiver: - protocols: - http: - endpoint: "http://localhost:4318" - grpc: - endpoint: "http://localhost:4317" - transport: "tcp" - max_recv_msg_size_mib: 4 - traces: - enabled: false - span_name_as_resource_name: true - span_name_remappings: - "old-span": "new-span" - ignore_missing_datadog_fields: true - probabilistic_sampler: - sampling_percentage: 50 - metrics: - enabled: true - resource_attributes_as_tags: true - instrumentation_scope_metadata_as_tags: true - tag_cardinality: "low" - delta_ttl: 3600 - histograms: - mode: "counters" - send_count_sum_metrics: true - send_aggregation_metrics: true - sums: - cumulative_monotonic_mode: "to_delta" - initial_cumulative_monotonic_value: "auto" - summaries: - mode: "quantiles" - logs: - enabled: true - -# AWS Lambda -api_key_secret_arn: "arn:aws:secretsmanager:region:account:secret:datadog-api-key" -kms_api_key: "test-kms-key" -serverless_logs_enabled: false -serverless_flush_strategy: "periodically,60000" -enhanced_metrics: false -lambda_proc_enhanced_metrics: false -capture_lambda_payload: true -capture_lambda_payload_max_depth: 5 -compute_trace_stats_on_extension: true -api_key_secret_reload_interval: 0 -serverless_appsec_enabled: true -appsec_rules: "/path/to/rules.json" -appsec_waf_timeout: 1000000 # Microseconds -api_security_enabled: false -api_security_sample_delay: 60 # Seconds -"#, - )?; - - let mut config = Config::default(); - let yaml_config_source = YamlConfigSource { - path: Path::new("datadog.yaml").to_path_buf(), - }; - yaml_config_source - .load(&mut config) - .expect("Failed to load config"); - - let expected_config = Config { - site: "test-site".to_string(), - api_key: "test-api-key".to_string(), - log_level: LogLevel::Debug, - flush_timeout: 42, - compression_level: 4, - proxy_https: Some("https://proxy.example.com".to_string()), - proxy_no_proxy: vec!["localhost".to_string(), "127.0.0.1".to_string()], - http_protocol: Some("http1".to_string()), - tls_cert_file: Some("/opt/ca-cert.pem".to_string()), - skip_ssl_validation: true, - dd_url: "https://metrics.datadoghq.com".to_string(), - url: String::new(), // doesnt exist in yaml - additional_endpoints: HashMap::from([ - ( - "https://app.datadoghq.com".to_string(), - vec!["apikey2".to_string(), "apikey3".to_string()], - ), - ( - "https://app.datadoghq.eu".to_string(), - vec!["apikey4".to_string()], - ), - ]), - env: Some("test-env".to_string()), - service: Some("test-service".to_string()), - version: Some("1.0.0".to_string()), - tags: HashMap::from([ - ("team".to_string(), "test-team".to_string()), - ("project".to_string(), "test-project".to_string()), - ]), - logs_config_logs_dd_url: "https://logs.datadoghq.com".to_string(), - logs_config_processing_rules: Some(vec![ProcessingRule { - name: "test-exclude".to_string(), - pattern: "test-pattern".to_string(), - kind: Kind::ExcludeAtMatch, - replace_placeholder: None, - }]), - logs_config_use_compression: false, - logs_config_compression_level: 1, - logs_config_additional_endpoints: vec![LogsAdditionalEndpoint { - api_key: "apikey2".to_string(), - host: "agent-http-intake.logs.datadoghq.com".to_string(), - port: 443, - is_reliable: true, - }], - observability_pipelines_worker_logs_enabled: false, - observability_pipelines_worker_logs_url: String::default(), - service_mapping: HashMap::from([( - "old-service".to_string(), - "new-service".to_string(), - )]), - apm_dd_url: "https://apm.datadoghq.com".to_string(), - apm_replace_tags: Some(vec![]), - apm_config_obfuscation_http_remove_query_string: true, - apm_config_obfuscation_http_remove_paths_with_digits: true, - apm_config_compression_level: 2, - apm_features: vec![ - "enable_otlp_compute_top_level_by_span_kind".to_string(), - "enable_stats_by_span_kind".to_string(), - ], - apm_additional_endpoints: HashMap::from([ - ( - "https://trace.agent.datadoghq.com".to_string(), - vec!["apikey2".to_string(), "apikey3".to_string()], - ), - ( - "https://trace.agent.datadoghq.eu".to_string(), - vec!["apikey4".to_string()], - ), - ]), - trace_propagation_style: vec![TracePropagationStyle::Datadog], - trace_propagation_style_extract: vec![TracePropagationStyle::TraceContext], - trace_propagation_extract_first: true, - trace_propagation_http_baggage_enabled: true, - trace_aws_service_representation_enabled: true, - metrics_config_compression_level: 3, - otlp_config_traces_enabled: false, - otlp_config_traces_span_name_as_resource_name: true, - otlp_config_traces_span_name_remappings: HashMap::from([( - "old-span".to_string(), - "new-span".to_string(), - )]), - otlp_config_ignore_missing_datadog_fields: true, - otlp_config_receiver_protocols_http_endpoint: Some( - "http://localhost:4318".to_string(), - ), - otlp_config_receiver_protocols_grpc_endpoint: Some( - "http://localhost:4317".to_string(), - ), - otlp_config_receiver_protocols_grpc_transport: Some("tcp".to_string()), - otlp_config_receiver_protocols_grpc_max_recv_msg_size_mib: Some(4), - otlp_config_metrics_enabled: true, - otlp_config_metrics_resource_attributes_as_tags: true, - otlp_config_metrics_instrumentation_scope_metadata_as_tags: true, - otlp_config_metrics_tag_cardinality: Some("low".to_string()), - otlp_config_metrics_delta_ttl: Some(3600), - otlp_config_metrics_histograms_mode: Some("counters".to_string()), - otlp_config_metrics_histograms_send_count_sum_metrics: true, - otlp_config_metrics_histograms_send_aggregation_metrics: true, - otlp_config_metrics_sums_cumulative_monotonic_mode: Some("to_delta".to_string()), - otlp_config_metrics_sums_initial_cumulativ_monotonic_value: Some( - "auto".to_string(), - ), - otlp_config_metrics_summaries_mode: Some("quantiles".to_string()), - otlp_config_traces_probabilistic_sampler_sampling_percentage: Some(50), - otlp_config_logs_enabled: true, - api_key_secret_arn: "arn:aws:secretsmanager:region:account:secret:datadog-api-key" - .to_string(), - kms_api_key: "test-kms-key".to_string(), - api_key_ssm_arn: String::default(), - serverless_logs_enabled: false, - serverless_flush_strategy: FlushStrategy::Periodically(PeriodicStrategy { - interval: 60000, - }), - enhanced_metrics: false, - lambda_proc_enhanced_metrics: false, - capture_lambda_payload: true, - capture_lambda_payload_max_depth: 5, - compute_trace_stats_on_extension: true, - span_dedup_timeout: None, - api_key_secret_reload_interval: None, - - serverless_appsec_enabled: true, - appsec_rules: Some("/path/to/rules.json".to_string()), - appsec_waf_timeout: Duration::from_secs(1), - api_security_enabled: false, - api_security_sample_delay: Duration::from_secs(60), - - apm_filter_tags_require: None, - apm_filter_tags_reject: None, - apm_filter_tags_regex_require: None, - apm_filter_tags_regex_reject: None, - statsd_metric_namespace: None, - custom_metrics_exclude_tags: vec![], - dogstatsd_so_rcvbuf: Some(1_048_576), - dogstatsd_buffer_size: Some(65507), - dogstatsd_queue_size: Some(2048), - - dd_org_uuid: String::default(), - lambda_durable_function_log_buffer_size: 0, - }; - - // Assert that - assert_eq!(config, expected_config); - - Ok(()) - }); - } - - #[test] - fn test_yaml_dogstatsd_config() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.create_file( - "datadog.yaml", - r" -dogstatsd_so_rcvbuf: 524288 -dogstatsd_buffer_size: 16384 -dogstatsd_queue_size: 512 -", - )?; - let mut config = Config::default(); - let yaml_config_source = YamlConfigSource { - path: Path::new("datadog.yaml").to_path_buf(), - }; - yaml_config_source - .load(&mut config) - .expect("Failed to load config"); - - assert_eq!(config.dogstatsd_so_rcvbuf, Some(524_288)); - assert_eq!(config.dogstatsd_buffer_size, Some(16384)); - assert_eq!(config.dogstatsd_queue_size, Some(512)); - Ok(()) - }); - } - - #[test] - fn test_yaml_dogstatsd_config_defaults_to_none() { - figment::Jail::expect_with(|jail| { - jail.clear_env(); - jail.create_file("datadog.yaml", "")?; - let mut config = Config::default(); - let yaml_config_source = YamlConfigSource { - path: Path::new("datadog.yaml").to_path_buf(), - }; - yaml_config_source - .load(&mut config) - .expect("Failed to load config"); - - assert_eq!(config.dogstatsd_so_rcvbuf, None); - assert_eq!(config.dogstatsd_buffer_size, None); - assert_eq!(config.dogstatsd_queue_size, None); - Ok(()) - }); - } -} diff --git a/bottlecap/src/lifecycle/invocation/processor.rs b/bottlecap/src/lifecycle/invocation/processor.rs index 1f42fa87d..f52bcc09a 100644 --- a/bottlecap/src/lifecycle/invocation/processor.rs +++ b/bottlecap/src/lifecycle/invocation/processor.rs @@ -195,7 +195,7 @@ impl Processor { .try_into() .unwrap_or_default(); - if self.config.lambda_proc_enhanced_metrics { + if self.config.ext.lambda_proc_enhanced_metrics { if self.aws_config.is_managed_instance_mode() { // In Managed Instance mode, track concurrent invocations self.active_invocations += 1; @@ -647,7 +647,7 @@ impl Processor { // span to the inferred trigger span. AppSec's process_span will set it again from the // security context when it runs, but this baseline guarantees the tag is always present // even when the context cannot be found at flush time. - if self.config.serverless_appsec_enabled { + if self.config.ext.serverless_appsec_enabled { context .invocation_span .metrics @@ -1075,12 +1075,12 @@ impl Processor { }; // Tag the invocation span with the request payload - if self.config.capture_lambda_payload { + if self.config.ext.capture_lambda_payload { let metadata = get_metadata_from_value( "function.request", &payload_value, 0, - self.config.capture_lambda_payload_max_depth, + self.config.ext.capture_lambda_payload_max_depth, ); context.invocation_span.meta.extend(metadata); } @@ -1293,12 +1293,12 @@ impl Processor { }; // Tag the invocation span with the request payload - if self.config.capture_lambda_payload { + if self.config.ext.capture_lambda_payload { let metadata = get_metadata_from_value( "function.response", &payload_value, 0, - self.config.capture_lambda_payload_max_depth, + self.config.ext.capture_lambda_payload_max_depth, ); context.invocation_span.meta.extend(metadata); } @@ -2735,7 +2735,10 @@ mod tests { }); let config = Arc::new(config::Config { service: Some("test-service".to_string()), - serverless_appsec_enabled: true, + ext: config::LambdaConfig { + serverless_appsec_enabled: true, + ..Default::default() + }, ..config::Config::default() }); let tags_provider = Arc::new(provider::Provider::new( @@ -3038,7 +3041,10 @@ mod tests { let config = Arc::new(config::Config { apm_dd_url: "https://trace.agent.datadoghq.com".to_string(), service: Some("test-service".to_string()), - compute_trace_stats_on_extension: compute_on_extension, + ext: config::LambdaConfig { + compute_trace_stats_on_extension: compute_on_extension, + ..Default::default() + }, ..config::Config::default() }); let mut processor = setup_with_config(Arc::clone(&config)); diff --git a/bottlecap/src/lifecycle/invocation/span_inferrer.rs b/bottlecap/src/lifecycle/invocation/span_inferrer.rs index 0332e3f80..50e80ba5a 100644 --- a/bottlecap/src/lifecycle/invocation/span_inferrer.rs +++ b/bottlecap/src/lifecycle/invocation/span_inferrer.rs @@ -313,7 +313,7 @@ impl SpanInferrer { invocation_span.service.clone(), ); s.meta.insert("span.kind".to_string(), "server".to_string()); - let appsec_enabled = self.config.serverless_appsec_enabled; + let appsec_enabled = self.config.ext.serverless_appsec_enabled; propagate_appsec(appsec_enabled, invocation_span, s); if let Some(ws) = &mut self.wrapped_inferred_span { @@ -716,7 +716,10 @@ mod tests { #[test] fn test_complete_inferred_spans_sets_appsec_when_enabled_in_config() { let config = Config { - serverless_appsec_enabled: true, + ext: crate::config::LambdaConfig { + serverless_appsec_enabled: true, + ..Default::default() + }, ..Config::default() }; let mut inferrer = SpanInferrer::new(Arc::new(config)); diff --git a/bottlecap/src/logs/lambda/processor.rs b/bottlecap/src/logs/lambda/processor.rs index 57ea64cf4..3f6714928 100644 --- a/bottlecap/src/logs/lambda/processor.rs +++ b/bottlecap/src/logs/lambda/processor.rs @@ -140,9 +140,9 @@ impl LambdaProcessor { let function_arn = tags_provider.get_canonical_id().unwrap_or_default(); let processing_rules = &datadog_config.logs_config_processing_rules; - let logs_enabled = datadog_config.serverless_logs_enabled; + let logs_enabled = datadog_config.ext.serverless_logs_enabled; let lambda_durable_function_log_buffer_size = - datadog_config.lambda_durable_function_log_buffer_size; + datadog_config.ext.lambda_durable_function_log_buffer_size; let rules = LambdaProcessor::compile_rules(processing_rules); LambdaProcessor { function_arn, @@ -1439,7 +1439,10 @@ mod tests { let config = Arc::new(config::Config { service: Some("test-service".to_string()), tags: HashMap::from([("test".to_string(), "tags".to_string())]), - serverless_logs_enabled: false, + ext: config::LambdaConfig { + serverless_logs_enabled: false, + ..Default::default() + }, ..config::Config::default() }); @@ -2531,7 +2534,10 @@ mod tests { let config = Arc::new(config::Config { service: Some("test-service".to_string()), tags: tags.clone(), - serverless_logs_enabled: true, + ext: config::LambdaConfig { + serverless_logs_enabled: true, + ..Default::default() + }, ..config::Config::default() }); let tags_provider = Arc::new(provider::Provider::new( diff --git a/bottlecap/src/metrics/enhanced/lambda.rs b/bottlecap/src/metrics/enhanced/lambda.rs index 9260064c0..b79f83cfc 100644 --- a/bottlecap/src/metrics/enhanced/lambda.rs +++ b/bottlecap/src/metrics/enhanced/lambda.rs @@ -107,7 +107,7 @@ impl Lambda { init_duration_ms: f64, timestamp: i64, ) { - if !self.config.enhanced_metrics { + if !self.config.ext.enhanced_metrics { return; } self.dynamic_value_tags @@ -129,7 +129,7 @@ impl Lambda { restore_duration_ms: f64, timestamp: i64, ) { - if !self.config.enhanced_metrics { + if !self.config.ext.enhanced_metrics { return; } let metric = Metric::new( @@ -149,7 +149,7 @@ impl Lambda { } fn increment_metric(&self, metric_name: &str, timestamp: i64) { - if !self.config.enhanced_metrics { + if !self.config.ext.enhanced_metrics { return; } let tags = self.get_dynamic_value_tags(); @@ -165,7 +165,7 @@ impl Lambda { } pub fn set_runtime_done_metrics(&self, metrics: &RuntimeDoneMetrics, timestamp: i64) { - if !self.config.enhanced_metrics { + if !self.config.ext.enhanced_metrics { return; } let metric = Metric::new( @@ -194,7 +194,7 @@ impl Lambda { } pub fn set_shutdown_metric(&self, timestamp: i64) { - if !self.config.enhanced_metrics { + if !self.config.ext.enhanced_metrics { return; } self.increment_metric(constants::SHUTDOWNS_METRIC, timestamp); @@ -207,7 +207,7 @@ impl Lambda { } pub fn set_post_runtime_duration_metric(&self, duration_ms: f64, timestamp: i64) { - if !self.config.enhanced_metrics { + if !self.config.ext.enhanced_metrics { return; } let metric = metric::Metric::new( @@ -270,7 +270,7 @@ impl Lambda { } pub fn set_network_enhanced_metrics(&self, network_offset: Option) { - if !self.config.enhanced_metrics { + if !self.config.ext.enhanced_metrics { return; } @@ -343,7 +343,7 @@ impl Lambda { } pub fn set_cpu_time_enhanced_metrics(&self, cpu_offset: Option) { - if !self.config.enhanced_metrics { + if !self.config.ext.enhanced_metrics { return; } @@ -473,7 +473,7 @@ impl Lambda { cpu_offset: Option, uptime_offset: Option, ) { - if !self.config.enhanced_metrics { + if !self.config.ext.enhanced_metrics { return; } @@ -515,7 +515,7 @@ impl Lambda { } pub fn set_report_log_metrics(&self, metrics: &ReportMetrics, timestamp: i64) { - if !self.config.enhanced_metrics { + if !self.config.ext.enhanced_metrics { return; } let metric = metric::Metric::new( @@ -585,7 +585,7 @@ impl Lambda { } pub fn start_usage_metrics_task(&self) { - if !self.config.enhanced_metrics { + if !self.config.ext.enhanced_metrics { return; } @@ -625,7 +625,7 @@ impl Lambda { // Reset metrics and resume monitoring for the next invocation pub fn restart_usage_metrics_monitoring(&self) { - if !self.config.enhanced_metrics { + if !self.config.ext.enhanced_metrics { return; } @@ -639,7 +639,7 @@ impl Lambda { /// Resume monitoring without resetting metrics. Used in managed instance mode to resume monitoring between invocations. pub fn resume_usage_metrics_monitoring(&self) { - if !self.config.enhanced_metrics { + if !self.config.ext.enhanced_metrics { return; } @@ -649,7 +649,7 @@ impl Lambda { /// Pause monitoring without emitting metrics. Used in managed instance mode to pause between invocations. pub fn pause_usage_metrics_monitoring(&self) { - if !self.config.enhanced_metrics { + if !self.config.ext.enhanced_metrics { return; } @@ -657,7 +657,7 @@ impl Lambda { } pub fn set_usage_enhanced_metrics(&self) { - if !self.config.enhanced_metrics { + if !self.config.ext.enhanced_metrics { return; } @@ -747,7 +747,7 @@ impl Lambda { } pub fn set_max_enhanced_metrics(&self) { - if !self.config.enhanced_metrics { + if !self.config.ext.enhanced_metrics { return; } @@ -919,7 +919,10 @@ mod tests { async fn test_disabled() { let (metrics_aggr, no_config) = setup(); let my_config = Arc::new(config::Config { - enhanced_metrics: false, + ext: config::LambdaConfig { + enhanced_metrics: false, + ..no_config.ext.clone() + }, ..no_config.as_ref().clone() }); let mut lambda = Lambda::new(metrics_aggr.clone(), my_config); @@ -1390,7 +1393,10 @@ mod tests { async fn test_snapstart_restore_duration_metric_disabled() { let (metrics_aggr, no_config) = setup(); let my_config = Arc::new(config::Config { - enhanced_metrics: false, + ext: config::LambdaConfig { + enhanced_metrics: false, + ..no_config.ext.clone() + }, ..no_config.as_ref().clone() }); let mut lambda = Lambda::new(metrics_aggr.clone(), my_config); diff --git a/bottlecap/src/otlp/agent.rs b/bottlecap/src/otlp/agent.rs index 977c5c7d4..27115f48c 100644 --- a/bottlecap/src/otlp/agent.rs +++ b/bottlecap/src/otlp/agent.rs @@ -58,7 +58,7 @@ impl TracePipeline { return Err("Not sending traces, processor returned empty data".to_string()); } - let compute_trace_stats_on_extension = self.config.compute_trace_stats_on_extension; + let compute_trace_stats_on_extension = self.config.ext.compute_trace_stats_on_extension; // Capture before `tracer_header_tags` is moved into process_traces below. let client_computed_stats = tracer_header_tags.client_computed_stats; let (send_data_builder, processed_traces) = self.trace_processor.process_traces( diff --git a/bottlecap/src/proxy/mod.rs b/bottlecap/src/proxy/mod.rs index 35c8139d0..6db2d818d 100644 --- a/bottlecap/src/proxy/mod.rs +++ b/bottlecap/src/proxy/mod.rs @@ -25,7 +25,8 @@ pub fn should_start_proxy(config: &Arc, aws_config: Arc) -> b env::var("DD_EXPERIMENTAL_ENABLE_PROXY").is_ok_and(|v| v.to_lowercase().eq("true")); lwa_proxy_set - || (datadog_wrapper_set && (config.serverless_appsec_enabled || experimental_proxy_enabled)) + || (datadog_wrapper_set + && (config.ext.serverless_appsec_enabled || experimental_proxy_enabled)) } #[cfg(test)] @@ -37,7 +38,10 @@ mod tests { fn test_should_start_proxy_everything_set() { let config = Arc::new(Config { // Appsec is enabled, so we should start the proxy - serverless_appsec_enabled: true, + ext: crate::config::LambdaConfig { + serverless_appsec_enabled: true, + ..Default::default() + }, ..Default::default() }); let aws_config = Arc::new(AwsConfig { @@ -71,7 +75,10 @@ mod tests { fn test_should_start_proxy_appsec_enabled_and_datadog_wrapper_set() { let config = Arc::new(Config { // Appsec is enabled, so we should start the proxy - serverless_appsec_enabled: true, + ext: crate::config::LambdaConfig { + serverless_appsec_enabled: true, + ..Default::default() + }, ..Default::default() }); let aws_config = Arc::new(AwsConfig { @@ -90,7 +97,10 @@ mod tests { fn test_should_start_proxy_appsec_disabled_and_datadog_wrapper_set() { let config = Arc::new(Config { // Appsec is disabled, so we should not start the proxy - serverless_appsec_enabled: false, + ext: crate::config::LambdaConfig { + serverless_appsec_enabled: false, + ..Default::default() + }, ..Default::default() }); let aws_config = Arc::new(AwsConfig { @@ -109,7 +119,10 @@ mod tests { fn test_should_start_proxy_appsec_enabled_datadog_wrapper_not_set() { let config = Arc::new(Config { // Appsec is enabled, so we should not start the proxy - serverless_appsec_enabled: true, + ext: crate::config::LambdaConfig { + serverless_appsec_enabled: true, + ..Default::default() + }, ..Default::default() }); let aws_config = Arc::new(AwsConfig { diff --git a/bottlecap/src/secrets/decrypt.rs b/bottlecap/src/secrets/decrypt.rs index b014c3b16..cde54a56a 100644 --- a/bottlecap/src/secrets/decrypt.rs +++ b/bottlecap/src/secrets/decrypt.rs @@ -23,10 +23,10 @@ pub async fn resolve_secrets( aws_config: Arc, shared_client: Client, ) -> Option { - let api_key_candidate = if !config.api_key_secret_arn.is_empty() - || !config.kms_api_key.is_empty() - || !config.api_key_ssm_arn.is_empty() - || !config.dd_org_uuid.is_empty() + let api_key_candidate = if !config.ext.api_key_secret_arn.is_empty() + || !config.ext.kms_api_key.is_empty() + || !config.ext.api_key_ssm_arn.is_empty() + || !config.ext.dd_org_uuid.is_empty() { let before_decrypt = Instant::now(); @@ -48,7 +48,7 @@ pub async fn resolve_secrets( let aws_credentials = get_aws_credentials(&client).await?; - let decrypted_key = if !config.dd_org_uuid.is_empty() { + let decrypted_key = if !config.ext.dd_org_uuid.is_empty() { delegated_auth::get_delegated_api_key( &config, &aws_config, @@ -56,18 +56,18 @@ pub async fn resolve_secrets( &aws_credentials, ) .await - } else if !config.kms_api_key.is_empty() { + } else if !config.ext.kms_api_key.is_empty() { decrypt_aws_kms( &client, - config.kms_api_key.clone(), + config.ext.kms_api_key.clone(), aws_config, &aws_credentials, ) .await - } else if !config.api_key_secret_arn.is_empty() { + } else if !config.ext.api_key_secret_arn.is_empty() { decrypt_aws_sm( &client, - config.api_key_secret_arn.clone(), + config.ext.api_key_secret_arn.clone(), aws_config, &aws_credentials, ) @@ -75,7 +75,7 @@ pub async fn resolve_secrets( } else { decrypt_aws_ssm( &client, - config.api_key_ssm_arn.clone(), + config.ext.api_key_ssm_arn.clone(), aws_config, &aws_credentials, ) diff --git a/bottlecap/src/secrets/delegated_auth/client.rs b/bottlecap/src/secrets/delegated_auth/client.rs index ae995d2e2..29ea70a54 100644 --- a/bottlecap/src/secrets/delegated_auth/client.rs +++ b/bottlecap/src/secrets/delegated_auth/client.rs @@ -35,7 +35,7 @@ pub async fn get_delegated_api_key( ) -> Result> { debug!("Attempting to get API key via delegated auth"); - let proof = generate_auth_proof(aws_credentials, &aws_config.region, &config.dd_org_uuid)?; + let proof = generate_auth_proof(aws_credentials, &aws_config.region, &config.ext.dd_org_uuid)?; let url = get_api_endpoint(&config.site); debug!("Requesting delegated API key from: {}", url); diff --git a/bottlecap/src/tags/lambda/tags.rs b/bottlecap/src/tags/lambda/tags.rs index cd6c4d69b..1ec215e62 100644 --- a/bottlecap/src/tags/lambda/tags.rs +++ b/bottlecap/src/tags/lambda/tags.rs @@ -119,7 +119,7 @@ fn tags_from_env( tags_map.insert(MEMORY_SIZE_KEY.to_string(), memory_size); } if let Ok(runtime) = std::env::var(RUNTIME_VAR) { - if config.serverless_appsec_enabled + if config.ext.serverless_appsec_enabled && let Some(runtime_family) = identify_runtime_family(&runtime) { tags_map.insert(RUNTIME_FAMILY_KEY.to_string(), runtime_family.to_string()); @@ -473,7 +473,10 @@ mod tests { ]), env: Some("test".to_string()), version: Some("1.0.0".to_string()), - serverless_appsec_enabled: true, + ext: crate::config::LambdaConfig { + serverless_appsec_enabled: true, + ..Default::default() + }, ..Config::default() }); let tags = Lambda::new_from_config(config, &metadata); diff --git a/bottlecap/src/traces/propagation/mod.rs b/bottlecap/src/traces/propagation/mod.rs index 95425ecfe..ebbd93671 100644 --- a/bottlecap/src/traces/propagation/mod.rs +++ b/bottlecap/src/traces/propagation/mod.rs @@ -29,14 +29,18 @@ pub fn extract_propagation_tags(tags_str: &str) -> HashMap { // Thin wrapper around dd-trace-rs's propagator to add `ot-baggage-*` header // extraction, which is not yet supported upstream in datadog-opentelemetry. pub struct DatadogCompositePropagator { - inner: dd_propagation::DatadogCompositePropagator, + inner: + dd_propagation::DatadogCompositePropagator, config: Arc, } impl DatadogCompositePropagator { #[must_use] pub fn new(config: Arc) -> Self { - let inner = dd_propagation::DatadogCompositePropagator::new(Arc::clone(&config)); + let prop_cfg = Arc::new(crate::config::propagation_wrapper::PropConfig::new( + Arc::clone(&config), + )); + let inner = dd_propagation::DatadogCompositePropagator::new(prop_cfg); Self { inner, config } } diff --git a/bottlecap/src/traces/trace_agent.rs b/bottlecap/src/traces/trace_agent.rs index ef85142b3..37b71d29f 100644 --- a/bottlecap/src/traces/trace_agent.rs +++ b/bottlecap/src/traces/trace_agent.rs @@ -547,7 +547,9 @@ impl TraceAgent { for mut span in original_chunk { // Check for duplicates let key = DedupKey::new(span.trace_id, span.span_id); - let should_keep = match deduper.check_and_add(key, config.span_dedup_timeout).await + let should_keep = match deduper + .check_and_add(key, config.ext.span_dedup_timeout) + .await { Ok(should_keep) => { if !should_keep { diff --git a/bottlecap/src/traces/trace_processor.rs b/bottlecap/src/traces/trace_processor.rs index da526b4e3..64a382126 100644 --- a/bottlecap/src/traces/trace_processor.rs +++ b/bottlecap/src/traces/trace_processor.rs @@ -90,7 +90,7 @@ impl TraceChunkProcessor for ChunkProcessor { // The stats-routing decision depends only on config and the per-request // client_computed_stats flag, so resolve it once instead of per span. let stamp_compute_stats = StatsComputedBy::resolve( - self.config.compute_trace_stats_on_extension, + self.config.ext.compute_trace_stats_on_extension, self.client_computed_stats, ) == StatsComputedBy::Backend; @@ -442,7 +442,7 @@ impl TraceProcessor for ServerlessTraceProcessor { // stats are still counted. SamplerPriority::None (-128) means no explicit priority // was set and the trace is kept; drop priorities are SamplerPriority::AutoDrop (0) // and UserDrop (-1, not represented in SamplerPriority). - if config.compute_trace_stats_on_extension + if config.ext.compute_trace_stats_on_extension && let TracerPayloadCollection::V07(ref mut tracer_payloads) = payload { for tp in tracer_payloads.iter_mut() { @@ -581,7 +581,7 @@ impl SendingTraceProcessor { // Skip extension-side stats generation when the tracer already computed stats // client-side (Datadog-Client-Computed-Stats), to avoid double-counting. if StatsComputedBy::resolve( - config.compute_trace_stats_on_extension, + config.ext.compute_trace_stats_on_extension, client_computed_stats, ) == StatsComputedBy::Extension && let Err(err) = self.stats_generator.send(&processed_traces) @@ -1182,7 +1182,10 @@ mod tests { let config = Arc::new(Config { apm_dd_url: "https://trace.agent.datadoghq.com".to_string(), - compute_trace_stats_on_extension: true, + ext: crate::config::LambdaConfig { + compute_trace_stats_on_extension: true, + ..Default::default() + }, ..Config::default() }); let tags_provider = Arc::new(Provider::new( @@ -1276,7 +1279,10 @@ mod tests { let config = Arc::new(Config { apm_dd_url: "https://trace.agent.datadoghq.com".to_string(), - compute_trace_stats_on_extension: true, + ext: crate::config::LambdaConfig { + compute_trace_stats_on_extension: true, + ..Default::default() + }, ..Config::default() }); let tags_provider = Arc::new(Provider::new( @@ -1354,7 +1360,10 @@ mod tests { let config = Arc::new(Config { apm_dd_url: "https://trace.agent.datadoghq.com".to_string(), - compute_trace_stats_on_extension: true, + ext: crate::config::LambdaConfig { + compute_trace_stats_on_extension: true, + ..Default::default() + }, ..Config::default() }); let tags_provider = Arc::new(Provider::new( @@ -1755,7 +1764,10 @@ mod tests { for (compute_on_extension, client_computed_stats, expected) in cases { let config = Arc::new(Config { - compute_trace_stats_on_extension: compute_on_extension, + ext: crate::config::LambdaConfig { + compute_trace_stats_on_extension: compute_on_extension, + ..Default::default() + }, ..Config::default() }); let mut processor = create_chunk_processor_with(config, client_computed_stats); @@ -1824,7 +1836,10 @@ mod tests { let config = Arc::new(Config { apm_dd_url: "https://trace.agent.datadoghq.com".to_string(), service: Some("test-service".to_string()), - compute_trace_stats_on_extension: compute_on_extension, + ext: crate::config::LambdaConfig { + compute_trace_stats_on_extension: compute_on_extension, + ..Default::default() + }, ..Config::default() }); diff --git a/bottlecap/tests/apm_integration_test.rs b/bottlecap/tests/apm_integration_test.rs index 232e95c6c..04254d431 100644 --- a/bottlecap/tests/apm_integration_test.rs +++ b/bottlecap/tests/apm_integration_test.rs @@ -314,7 +314,10 @@ async fn run_processor_pipeline_with_traces( // process_traces builds its trace endpoint directly from apm_dd_url. apm_dd_url: fake_intake.traces_url(), service: Some("fake-intake-trace-service".to_string()), - compute_trace_stats_on_extension: compute_on_extension, + ext: bottlecap::config::LambdaConfig { + compute_trace_stats_on_extension: compute_on_extension, + ..Default::default() + }, ..Config::default() }); diff --git a/bottlecap/tests/appsec_processor_test.rs b/bottlecap/tests/appsec_processor_test.rs index c7b0f7b56..2a18f8092 100644 --- a/bottlecap/tests/appsec_processor_test.rs +++ b/bottlecap/tests/appsec_processor_test.rs @@ -27,18 +27,21 @@ async fn test_processor() { } let cfg = Config { - serverless_appsec_enabled: true, - appsec_rules: Some( - PathBuf::from(file!()) - .parent() - .expect("failed to get parent directory of this file") - .join("appsec") - .join("test-ruleset.json") - .to_string_lossy() - .to_string(), - ), - appsec_waf_timeout: Duration::from_secs(60), // Ample so it does not time out on slow CI hosts - api_security_sample_delay: Duration::ZERO, // Sample all requests + ext: bottlecap::config::LambdaConfig { + serverless_appsec_enabled: true, + appsec_rules: Some( + PathBuf::from(file!()) + .parent() + .expect("failed to get parent directory of this file") + .join("appsec") + .join("test-ruleset.json") + .to_string_lossy() + .to_string(), + ), + appsec_waf_timeout: Duration::from_secs(60), // Ample so it does not time out on slow CI hosts + api_security_sample_delay: Duration::ZERO, // Sample all requests + ..Default::default() + }, ..Config::default() }; From 15150c4f9e28368894eb8b0cea5d6bde952190ad Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:27:28 -0400 Subject: [PATCH 4/6] refactor(propagation_wrapper): address PR #1251 review nits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three non-blocking nits from the independent review: 1. PropConfig's inner Arc is no longer pub — keeps the wrapper boundary tight; the new() constructor is sufficient and no callers outside the module reach for the inner field directly. 2. PropConfig::new returns Arc instead of Self. Drops the redundant `Arc::new(...)` wrap at the single call site in traces/propagation/mod.rs. 3. Documents the hard-coded 512 in datadog_tags_max_length — it matches dd-trace-rs's default; bottlecap intentionally doesn't expose DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH. Saves the next reader a round-trip through upstream to confirm parity. No behavior change. All 505 lib tests still pass. --- bottlecap/src/config/propagation_wrapper.rs | 9 +++++---- bottlecap/src/traces/propagation/mod.rs | 4 +--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/bottlecap/src/config/propagation_wrapper.rs b/bottlecap/src/config/propagation_wrapper.rs index 164ecf5d8..8ea536113 100644 --- a/bottlecap/src/config/propagation_wrapper.rs +++ b/bottlecap/src/config/propagation_wrapper.rs @@ -10,12 +10,12 @@ use crate::config::{Config, TracePropagationStyle}; /// wrapper is the local type the impl can attach to. Callers that need a /// propagator hand it an `Arc` instead of an `Arc`. #[derive(Debug, Clone)] -pub struct PropConfig(pub Arc); +pub struct PropConfig(Arc); impl PropConfig { #[must_use] - pub fn new(config: Arc) -> Self { - Self(config) + pub fn new(config: Arc) -> Arc { + Arc::new(Self(config)) } } @@ -46,7 +46,8 @@ impl PropagationConfig for PropConfig { } fn datadog_tags_max_length(&self) -> usize { - // Default max length matching dd-trace-rs + // Bottlecap does not expose DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH; 512 is + // upstream's default in dd-trace-rs. 512 } } diff --git a/bottlecap/src/traces/propagation/mod.rs b/bottlecap/src/traces/propagation/mod.rs index ebbd93671..aae59cdd5 100644 --- a/bottlecap/src/traces/propagation/mod.rs +++ b/bottlecap/src/traces/propagation/mod.rs @@ -37,9 +37,7 @@ pub struct DatadogCompositePropagator { impl DatadogCompositePropagator { #[must_use] pub fn new(config: Arc) -> Self { - let prop_cfg = Arc::new(crate::config::propagation_wrapper::PropConfig::new( - Arc::clone(&config), - )); + let prop_cfg = crate::config::propagation_wrapper::PropConfig::new(Arc::clone(&config)); let inner = dd_propagation::DatadogCompositePropagator::new(prop_cfg); Self { inner, config } } From 3872750d1eb546e712fc567d5bd70fef0a38834d Mon Sep 17 00:00:00 2001 From: Jordan Gonzalez <30836115+duncanista@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:42:18 -0400 Subject: [PATCH 5/6] docs(appsec): point doc comment to cfg.ext.appsec_rules after .ext refactor --- bottlecap/src/appsec/processor/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bottlecap/src/appsec/processor/mod.rs b/bottlecap/src/appsec/processor/mod.rs index d0acfee61..b8ca4431c 100644 --- a/bottlecap/src/appsec/processor/mod.rs +++ b/bottlecap/src/appsec/processor/mod.rs @@ -212,7 +212,7 @@ impl Processor { } /// Parses the App & API Protection ruleset from the provided [`Config`], or - /// the default built-in ruleset if the [`Config::appsec_rules`] field is + /// the default built-in ruleset if the `cfg.ext.appsec_rules` field is /// [`None`]. fn get_rules(cfg: &Config) -> Result { if let Some(ref rules) = cfg.ext.appsec_rules { From 00acd406d63c4c31607cb095367f2c0767cb2780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jordan=20gonz=C3=A1lez?= <30836115+duncanista@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:16:27 -0400 Subject: [PATCH 6/6] refactor(config): rename compute_trace_stats_on_extension -> lambda_extension_compute_stats (#1266) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Renames the public env var \`DD_COMPUTE_TRACE_STATS_ON_EXTENSION\` to \`DD_LAMBDA_EXTENSION_COMPUTE_STATS\`, plus the matching Rust field, so the name reflects the actual scope: this toggle only governs the Lambda extension's APM trace-stats pipeline and is not a generic Datadog Agent concept. ## Why Auditing the \`LambdaConfig\` extension fields surfaced this one as the one with an ambiguous env var name. \"Compute trace stats on extension\" reads as if there's a generic \"extension\" concept; in practice it's specifically the Lambda extension. Making that explicit avoids confusion if/when the same idea surfaces in other embedders (which would then need a differently-named knob). ## Changes - \`LambdaConfigSource\` field renamed: \`compute_trace_stats_on_extension\` → \`lambda_extension_compute_stats\`. Figment's \`DD_\` prefix maps to the new env var automatically. - \`LambdaConfig\` field renamed in lockstep. All call sites in \`trace_processor.rs\`, \`otlp/agent.rs\`, \`lifecycle/invocation/processor.rs\`, \`tags/lambda/tags.rs\`, and the integration test now read \`config.ext.lambda_extension_compute_stats\`. - \`merge_from\` keeps the field in the existing \`value:\` group of \`merge_fields!\` — no source-to-config rename needed since field names match. - **No back-compat alias** for the old env var. The \`.ext\` migration this depends on has not shipped, so the legacy name was never released externally. - Test renamed; default-false test added. ## Test plan - [x] \`cargo test\` — \`config::lambda_config_tests::lambda_extension_compute_stats_from_env\` and \`lambda_extension_compute_stats_defaults_false\` pass; 38/38 in the module. - [x] \`cargo clippy --workspace --all-targets --features default\` — clean. - [x] \`cargo fmt -- --check\` — clean. ## Notes for review Stacked on top of \`feat/migrate-bottlecap-to-upstream-config\` (the .ext migration PR). Merges into that branch. --- bottlecap/src/config/mod.rs | 22 ++++++++++----- .../src/lifecycle/invocation/processor.rs | 4 +-- bottlecap/src/otlp/agent.rs | 4 +-- bottlecap/src/tags/lambda/tags.rs | 2 +- bottlecap/src/traces/trace_processor.rs | 28 +++++++++---------- bottlecap/tests/apm_integration_test.rs | 4 +-- 6 files changed, 36 insertions(+), 28 deletions(-) diff --git a/bottlecap/src/config/mod.rs b/bottlecap/src/config/mod.rs index ce72fcf69..e9ce7f331 100644 --- a/bottlecap/src/config/mod.rs +++ b/bottlecap/src/config/mod.rs @@ -65,7 +65,7 @@ pub struct LambdaConfig { 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 lambda_extension_compute_stats: bool, pub span_dedup_timeout: Option, pub api_key_secret_reload_interval: Option, pub dd_org_uuid: String, @@ -95,7 +95,7 @@ impl Default for LambdaConfig { lambda_proc_enhanced_metrics: true, capture_lambda_payload: false, capture_lambda_payload_max_depth: 10, - compute_trace_stats_on_extension: false, + lambda_extension_compute_stats: false, span_dedup_timeout: None, api_key_secret_reload_interval: None, dd_org_uuid: String::new(), @@ -144,8 +144,10 @@ pub struct LambdaConfigSource { pub capture_lambda_payload: Option, #[serde(deserialize_with = "deser_opt_lossless")] pub capture_lambda_payload_max_depth: Option, + /// `DD_LAMBDA_EXTENSION_COMPUTE_STATS` — when true, the extension computes + /// APM trace stats locally instead of letting the backend do it. #[serde(deserialize_with = "deser_opt_bool")] - pub compute_trace_stats_on_extension: Option, + pub lambda_extension_compute_stats: Option, #[serde(deserialize_with = "deser_dur_secs_ignore_zero")] pub span_dedup_timeout: Option, @@ -198,7 +200,7 @@ impl DatadogConfigExtension for LambdaConfig { lambda_proc_enhanced_metrics, capture_lambda_payload, capture_lambda_payload_max_depth, - compute_trace_stats_on_extension, + lambda_extension_compute_stats, serverless_appsec_enabled, appsec_waf_timeout, api_security_enabled, @@ -487,12 +489,18 @@ mod lambda_config_tests { } #[test] - fn compute_trace_stats_on_extension_from_env() { + fn lambda_extension_compute_stats_from_env() { let config = load(|jail| { - jail.set_env("DD_COMPUTE_TRACE_STATS_ON_EXTENSION", "true"); + jail.set_env("DD_LAMBDA_EXTENSION_COMPUTE_STATS", "true"); Ok(()) }); - assert!(config.ext.compute_trace_stats_on_extension); + assert!(config.ext.lambda_extension_compute_stats); + } + + #[test] + fn lambda_extension_compute_stats_defaults_false() { + let config = load(|_| Ok(())); + assert!(!config.ext.lambda_extension_compute_stats); } // ---- Duration fields ---- diff --git a/bottlecap/src/lifecycle/invocation/processor.rs b/bottlecap/src/lifecycle/invocation/processor.rs index f52bcc09a..7d6943bdf 100644 --- a/bottlecap/src/lifecycle/invocation/processor.rs +++ b/bottlecap/src/lifecycle/invocation/processor.rs @@ -2958,7 +2958,7 @@ mod tests { } /// Build a [`Processor`] with a caller-supplied config (for toggling - /// `compute_trace_stats_on_extension`). + /// `lambda_extension_compute_stats`). fn setup_with_config(config: Arc) -> Processor { let aws_config = Arc::new(AwsConfig { region: "us-east-1".into(), @@ -3042,7 +3042,7 @@ mod tests { apm_dd_url: "https://trace.agent.datadoghq.com".to_string(), service: Some("test-service".to_string()), ext: config::LambdaConfig { - compute_trace_stats_on_extension: compute_on_extension, + lambda_extension_compute_stats: compute_on_extension, ..Default::default() }, ..config::Config::default() diff --git a/bottlecap/src/otlp/agent.rs b/bottlecap/src/otlp/agent.rs index 27115f48c..f302df7e4 100644 --- a/bottlecap/src/otlp/agent.rs +++ b/bottlecap/src/otlp/agent.rs @@ -58,7 +58,7 @@ impl TracePipeline { return Err("Not sending traces, processor returned empty data".to_string()); } - let compute_trace_stats_on_extension = self.config.ext.compute_trace_stats_on_extension; + let lambda_extension_compute_stats = self.config.ext.lambda_extension_compute_stats; // Capture before `tracer_header_tags` is moved into process_traces below. let client_computed_stats = tracer_header_tags.client_computed_stats; let (send_data_builder, processed_traces) = self.trace_processor.process_traces( @@ -83,7 +83,7 @@ impl TracePipeline { // performs obfuscation, and we need to compute stats on the obfuscated traces. // Skip extension-side stats generation when the tracer already computed stats // client-side (Datadog-Client-Computed-Stats), to avoid double-counting. - if StatsComputedBy::resolve(compute_trace_stats_on_extension, client_computed_stats) + if StatsComputedBy::resolve(lambda_extension_compute_stats, client_computed_stats) == StatsComputedBy::Extension && let Err(err) = self.stats_generator.send(&processed_traces) { diff --git a/bottlecap/src/tags/lambda/tags.rs b/bottlecap/src/tags/lambda/tags.rs index 1ec215e62..c0b428cf9 100644 --- a/bottlecap/src/tags/lambda/tags.rs +++ b/bottlecap/src/tags/lambda/tags.rs @@ -139,7 +139,7 @@ fn tags_from_env( // NOTE: `_dd.compute_stats` is intentionally NOT set here. It is a per-span backend // directive (whether the backend should compute trace stats) that depends on both - // `compute_trace_stats_on_extension` AND the tracer's `Datadog-Client-Computed-Stats` + // `lambda_extension_compute_stats` AND the tracer's `Datadog-Client-Computed-Stats` // header. The trace processor stamps it on each span's meta at processing time; baking // it into the function tags would leak it into `_dd.tags.function` and ignore the header. diff --git a/bottlecap/src/traces/trace_processor.rs b/bottlecap/src/traces/trace_processor.rs index 64a382126..256d8bf14 100644 --- a/bottlecap/src/traces/trace_processor.rs +++ b/bottlecap/src/traces/trace_processor.rs @@ -38,7 +38,7 @@ use crate::traces::trace_aggregator::{OwnedTracerHeaderTags, SendDataBuilderInfo use libdd_trace_normalization::normalizer::SamplerPriority; /// Which party is responsible for computing trace stats for a trace, derived from -/// `compute_trace_stats_on_extension` and the tracer's `Datadog-Client-Computed-Stats` +/// `lambda_extension_compute_stats` and the tracer's `Datadog-Client-Computed-Stats` /// signal. Exactly one party computes, so the per-span `_dd.compute_stats` stamp and the /// extension-side stats-generation guards cannot disagree. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -90,7 +90,7 @@ impl TraceChunkProcessor for ChunkProcessor { // The stats-routing decision depends only on config and the per-request // client_computed_stats flag, so resolve it once instead of per span. let stamp_compute_stats = StatsComputedBy::resolve( - self.config.ext.compute_trace_stats_on_extension, + self.config.ext.lambda_extension_compute_stats, self.client_computed_stats, ) == StatsComputedBy::Backend; @@ -128,7 +128,7 @@ impl TraceChunkProcessor for ChunkProcessor { }); // Stamp `_dd.compute_stats="1"` to tell the backend to compute trace stats ONLY - // when nobody else did: neither the extension (compute_trace_stats_on_extension) + // when nobody else did: neither the extension (lambda_extension_compute_stats) // nor the tracer (client_computed_stats). Otherwise leave the key absent, which the // backend treats as "do not compute" (matching the Go agent's // pkg/serverless/tags/tags.go semantics, which only ever set "1" and never "0"). @@ -442,7 +442,7 @@ impl TraceProcessor for ServerlessTraceProcessor { // stats are still counted. SamplerPriority::None (-128) means no explicit priority // was set and the trace is kept; drop priorities are SamplerPriority::AutoDrop (0) // and UserDrop (-1, not represented in SamplerPriority). - if config.ext.compute_trace_stats_on_extension + if config.ext.lambda_extension_compute_stats && let TracerPayloadCollection::V07(ref mut tracer_payloads) = payload { for tp in tracer_payloads.iter_mut() { @@ -581,7 +581,7 @@ impl SendingTraceProcessor { // Skip extension-side stats generation when the tracer already computed stats // client-side (Datadog-Client-Computed-Stats), to avoid double-counting. if StatsComputedBy::resolve( - config.ext.compute_trace_stats_on_extension, + config.ext.lambda_extension_compute_stats, client_computed_stats, ) == StatsComputedBy::Extension && let Err(err) = self.stats_generator.send(&processed_traces) @@ -1172,7 +1172,7 @@ mod tests { ); } - /// Verifies that when `compute_trace_stats_on_extension` is true, `process_traces` + /// Verifies that when `lambda_extension_compute_stats` is true, `process_traces` /// filters sampled-out chunks from the backend payload while preserving them in the /// stats collection. #[test] @@ -1183,7 +1183,7 @@ mod tests { let config = Arc::new(Config { apm_dd_url: "https://trace.agent.datadoghq.com".to_string(), ext: crate::config::LambdaConfig { - compute_trace_stats_on_extension: true, + lambda_extension_compute_stats: true, ..Default::default() }, ..Config::default() @@ -1272,7 +1272,7 @@ mod tests { } /// Verifies that `process_traces` returns `None` for the backend payload when all - /// traces are sampled out and `compute_trace_stats_on_extension` is true. + /// traces are sampled out and `lambda_extension_compute_stats` is true. #[test] fn test_process_traces_returns_none_when_all_sampled_out() { use libdd_trace_obfuscation::obfuscation_config::ObfuscationConfig; @@ -1280,7 +1280,7 @@ mod tests { let config = Arc::new(Config { apm_dd_url: "https://trace.agent.datadoghq.com".to_string(), ext: crate::config::LambdaConfig { - compute_trace_stats_on_extension: true, + lambda_extension_compute_stats: true, ..Default::default() }, ..Config::default() @@ -1361,7 +1361,7 @@ mod tests { let config = Arc::new(Config { apm_dd_url: "https://trace.agent.datadoghq.com".to_string(), ext: crate::config::LambdaConfig { - compute_trace_stats_on_extension: true, + lambda_extension_compute_stats: true, ..Default::default() }, ..Config::default() @@ -1754,7 +1754,7 @@ mod tests { /// is absent. Assert directly on `Span.meta` (NOT `TracerPayload.tags`) — guards #1118. #[test] fn test_compute_stats_truth_table() { - // (compute_trace_stats_on_extension, client_computed_stats) -> expected meta value + // (lambda_extension_compute_stats, client_computed_stats) -> expected meta value let cases = [ (false, false, Some("1")), // nobody computed -> tell backend to compute (false, true, None), // tracer computed -> absent @@ -1765,7 +1765,7 @@ mod tests { for (compute_on_extension, client_computed_stats, expected) in cases { let config = Arc::new(Config { ext: crate::config::LambdaConfig { - compute_trace_stats_on_extension: compute_on_extension, + lambda_extension_compute_stats: compute_on_extension, ..Default::default() }, ..Config::default() @@ -1811,7 +1811,7 @@ mod tests { } /// APMSVLS-487 Tier 1: `send_processed_traces` only generates extension-side stats when - /// `compute_trace_stats_on_extension == true AND client_computed_stats == false`. Drive the + /// `lambda_extension_compute_stats == true AND client_computed_stats == false`. Drive the /// real concentrator and assert a flushed payload is present/absent accordingly. #[tokio::test] #[allow(clippy::unwrap_used)] @@ -1837,7 +1837,7 @@ mod tests { apm_dd_url: "https://trace.agent.datadoghq.com".to_string(), service: Some("test-service".to_string()), ext: crate::config::LambdaConfig { - compute_trace_stats_on_extension: compute_on_extension, + lambda_extension_compute_stats: compute_on_extension, ..Default::default() }, ..Config::default() diff --git a/bottlecap/tests/apm_integration_test.rs b/bottlecap/tests/apm_integration_test.rs index 04254d431..9d3bab5f9 100644 --- a/bottlecap/tests/apm_integration_test.rs +++ b/bottlecap/tests/apm_integration_test.rs @@ -299,7 +299,7 @@ struct PipelineOutcome { } /// Drives `traces` through `SendingTraceProcessor::send_processed_traces` with the given -/// `compute_trace_stats_on_extension` / `client_computed_stats`, then flushes both the trace +/// `lambda_extension_compute_stats` / `client_computed_stats`, then flushes both the trace /// and stats pipelines into a fresh fake-intake and returns what it captured. async fn run_processor_pipeline_with_traces( compute_on_extension: bool, @@ -315,7 +315,7 @@ async fn run_processor_pipeline_with_traces( apm_dd_url: fake_intake.traces_url(), service: Some("fake-intake-trace-service".to_string()), ext: bottlecap::config::LambdaConfig { - compute_trace_stats_on_extension: compute_on_extension, + lambda_extension_compute_stats: compute_on_extension, ..Default::default() }, ..Config::default()