From a35de23616dce769b41d21c80e4058da71e86d21 Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Thu, 2 Jul 2026 17:08:06 -0400 Subject: [PATCH] feat(config): add DD_SERVERLESS_APM_ONLY traces-only mode Introduce a single master switch, DD_SERVERLESS_APM_ONLY, that puts the extension in APM-only ("traces only") mode. When enabled, every billable metrics and logs egress path is suppressed so the customer incurs no infrastructure-monitoring or log-ingestion charges, while traces and APM trace stats are unaffected. The flag is enforced defensively at multiple layers so the guarantee holds regardless of other configuration: - config: forces serverless_logs_enabled, enhanced_metrics, lambda_proc_enhanced_metrics, and OTLP metrics/logs off, overriding any individually set toggles. - main: start_metrics_flushers returns no flushers, so custom DogStatsD / enhanced / process metrics drained from the aggregator are discarded. - logs flusher: flush() short-circuits to guarantee no log egress even if something is queued. Readable from both env (DD_SERVERLESS_APM_ONLY) and YAML. Defaults to false, preserving existing behavior. Adds unit tests covering the default-off case and the forced-override case. --- bottlecap/src/bin/bottlecap/main.rs | 11 +++++ bottlecap/src/config/env.rs | 10 +++++ bottlecap/src/config/mod.rs | 70 +++++++++++++++++++++++++++++ bottlecap/src/config/yaml.rs | 4 ++ bottlecap/src/logs/flusher.rs | 8 ++++ 5 files changed, 103 insertions(+) diff --git a/bottlecap/src/bin/bottlecap/main.rs b/bottlecap/src/bin/bottlecap/main.rs index 3dcc13bb2..fe6837963 100644 --- a/bottlecap/src/bin/bottlecap/main.rs +++ b/bottlecap/src/bin/bottlecap/main.rs @@ -1275,6 +1275,17 @@ fn start_metrics_flushers( ) -> Vec { let mut flushers = Vec::new(); + // APM-only ("traces only") mode: do not create any metrics flushers, so that + // no metrics (custom DogStatsD, enhanced, or process) ever reach intake. The + // DogStatsD server still runs and the aggregator is still drained on flush, but + // the drained data is discarded because there is no flusher to send it. This is + // the authoritative guarantee that no infrastructure-monitoring charges are + // incurred (see DD_SERVERLESS_APM_ONLY). + if config.serverless_apm_only { + debug!("DD_SERVERLESS_APM_ONLY is enabled: not starting any metrics flushers"); + return flushers; + } + let metrics_intake_url = if !config.dd_url.is_empty() { let dd_dd_url = DdDdUrl::new(config.dd_url.clone()).expect("can't parse DD_DD_URL"); diff --git a/bottlecap/src/config/env.rs b/bottlecap/src/config/env.rs index 84f9a820c..0333dcc73 100644 --- a/bottlecap/src/config/env.rs +++ b/bottlecap/src/config/env.rs @@ -401,6 +401,14 @@ pub struct EnvConfig { /// 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_APM_ONLY` + /// + /// Run the extension in APM-only ("traces only") mode. When `true`, logs and + /// all metrics (enhanced, process, custom `DogStatsD`, and OTLP) are suppressed + /// at intake so no infrastructure-monitoring or log-ingestion charges are + /// incurred. Traces and APM trace stats are unaffected. Default is `false`. + #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] + pub serverless_apm_only: Option, /// @env `DD_SERVERLESS_FLUSH_STRATEGY` /// /// The flush strategy to use for AWS Lambda. @@ -677,6 +685,7 @@ fn merge_config(config: &mut Config, env_config: &EnvConfig) { || env_config.logs_enabled.unwrap_or(false); } + merge_option_to_value!(config, env_config, serverless_apm_only); 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); @@ -1037,6 +1046,7 @@ mod tests { kms_api_key: "test-kms-key".to_string(), api_key_ssm_arn: String::default(), serverless_logs_enabled: false, + serverless_apm_only: false, serverless_flush_strategy: FlushStrategy::Periodically(PeriodicStrategy { interval: 60000, }), diff --git a/bottlecap/src/config/mod.rs b/bottlecap/src/config/mod.rs index 87903961c..1cfd9b597 100644 --- a/bottlecap/src/config/mod.rs +++ b/bottlecap/src/config/mod.rs @@ -186,6 +186,31 @@ impl ConfigBuilder { self.config.site = "datadoghq.com".to_string(); } + // APM-only ("traces only") mode: suppress every billable metrics and logs + // egress path so the customer incurs no infrastructure-monitoring or + // log-ingestion charges. This intentionally overrides any individually + // configured metrics/logs toggles, since the guarantee must hold even if a + // user also set, e.g., DD_ENHANCED_METRICS=true. Traces and APM trace stats + // are unaffected. The custom DogStatsD egress is additionally disabled in + // the metrics flusher wiring (see start_metrics_flushers in main.rs). + if self.config.serverless_apm_only { + if self.config.serverless_logs_enabled + || self.config.enhanced_metrics + || self.config.lambda_proc_enhanced_metrics + || self.config.otlp_config_metrics_enabled + || self.config.otlp_config_logs_enabled + { + debug!( + "DD_SERVERLESS_APM_ONLY is enabled: forcing logs and all metrics off (traces-only mode)" + ); + } + self.config.serverless_logs_enabled = false; + self.config.enhanced_metrics = false; + self.config.lambda_proc_enhanced_metrics = false; + self.config.otlp_config_metrics_enabled = false; + self.config.otlp_config_logs_enabled = false; + } + // 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") @@ -355,6 +380,11 @@ pub struct Config { pub kms_api_key: String, pub api_key_ssm_arn: String, pub serverless_logs_enabled: bool, + /// When true, the extension operates in APM-only ("traces only") mode: + /// logs and all metrics (enhanced, process, custom `DogStatsD`, and OTLP) are + /// suppressed at intake so that no infrastructure-monitoring or log-ingestion + /// charges are incurred. Traces and APM trace stats are unaffected. + pub serverless_apm_only: bool, pub serverless_flush_strategy: FlushStrategy, pub enhanced_metrics: bool, pub lambda_proc_enhanced_metrics: bool, @@ -472,6 +502,7 @@ impl Default for Config { kms_api_key: String::default(), api_key_ssm_arn: String::default(), serverless_logs_enabled: true, + serverless_apm_only: false, serverless_flush_strategy: FlushStrategy::Default, enhanced_metrics: true, lambda_proc_enhanced_metrics: true, @@ -1454,6 +1485,45 @@ pub mod tests { }); } + #[test] + fn test_serverless_apm_only_defaults_off() { + figment::Jail::expect_with(|jail| { + jail.clear_env(); + let config = get_config(Path::new("")); + assert!(!config.serverless_apm_only); + // Defaults remain unchanged when APM-only is not set. + assert!(config.serverless_logs_enabled); + assert!(config.enhanced_metrics); + assert!(config.lambda_proc_enhanced_metrics); + Ok(()) + }); + } + + #[test] + fn test_serverless_apm_only_forces_metrics_and_logs_off() { + figment::Jail::expect_with(|jail| { + jail.clear_env(); + jail.set_env("DD_SERVERLESS_APM_ONLY", "true"); + // Even when a user explicitly enables these, APM-only must override them + // so that no metrics or logs reach intake (billing guarantee). + jail.set_env("DD_SERVERLESS_LOGS_ENABLED", "true"); + jail.set_env("DD_ENHANCED_METRICS", "true"); + jail.set_env("DD_LAMBDA_PROC_ENHANCED_METRICS", "true"); + jail.set_env("DD_OTLP_CONFIG_METRICS_ENABLED", "true"); + jail.set_env("DD_OTLP_CONFIG_LOGS_ENABLED", "true"); + + let config = get_config(Path::new("")); + + assert!(config.serverless_apm_only); + assert!(!config.serverless_logs_enabled); + assert!(!config.enhanced_metrics); + assert!(!config.lambda_proc_enhanced_metrics); + assert!(!config.otlp_config_metrics_enabled); + assert!(!config.otlp_config_logs_enabled); + Ok(()) + }); + } + #[test] fn test_overrides_config_based_on_priority() { figment::Jail::expect_with(|jail| { diff --git a/bottlecap/src/config/yaml.rs b/bottlecap/src/config/yaml.rs index d1300f67e..894dbc526 100644 --- a/bottlecap/src/config/yaml.rs +++ b/bottlecap/src/config/yaml.rs @@ -117,6 +117,8 @@ pub struct YamlConfig { pub serverless_logs_enabled: Option, #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] pub logs_enabled: Option, + #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] + pub serverless_apm_only: Option, pub serverless_flush_strategy: Option, #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] pub enhanced_metrics: Option, @@ -701,6 +703,7 @@ fn merge_config(config: &mut Config, yaml_config: &YamlConfig) { || yaml_config.logs_enabled.unwrap_or(false); } + merge_option_to_value!(config, yaml_config, serverless_apm_only); 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); @@ -1011,6 +1014,7 @@ api_security_sample_delay: 60 # Seconds kms_api_key: "test-kms-key".to_string(), api_key_ssm_arn: String::default(), serverless_logs_enabled: false, + serverless_apm_only: false, serverless_flush_strategy: FlushStrategy::Periodically(PeriodicStrategy { interval: 60000, }), diff --git a/bottlecap/src/logs/flusher.rs b/bottlecap/src/logs/flusher.rs index 5f68a09c7..a35360575 100644 --- a/bottlecap/src/logs/flusher.rs +++ b/bottlecap/src/logs/flusher.rs @@ -259,6 +259,14 @@ impl LogsFlusher { &self, retry_request: Option, ) -> Vec { + // APM-only ("traces only") mode: never send logs to intake. Logs are also + // dropped upstream (serverless_logs_enabled is forced off, so the processor + // never queues them), but this guard guarantees no log egress regardless of + // aggregator or redrive state. See DD_SERVERLESS_APM_ONLY. + if self.config.serverless_apm_only { + return Vec::new(); + } + let mut failed_requests = Vec::new(); // If retry_request is provided, only process that request