From 827c8489098135b83e3bd7d6b91d9fece4e65d3d Mon Sep 17 00:00:00 2001 From: Zarir Hamza Date: Thu, 2 Jul 2026 17:35:32 -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. Implemented on the datadog-agent-config Config model: - LambdaConfig / LambdaConfigSource gain a serverless_apm_only field, merged from both env (DD_SERVERLESS_APM_ONLY) and YAML. Defaults to false. - get_config() applies a post-merge override that forces serverless_logs_enabled, enhanced_metrics, lambda_proc_enhanced_metrics and the core OTLP metrics/logs toggles off, overriding any individually set values so the guarantee holds regardless of other configuration. Enforced defensively at the egress points as well: - 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. Adds unit tests covering the default-off case and the forced-override case. --- bottlecap/src/bin/bottlecap/main.rs | 11 ++++ bottlecap/src/config/mod.rs | 88 ++++++++++++++++++++++++++++- bottlecap/src/logs/flusher.rs | 8 +++ 3 files changed, 105 insertions(+), 2 deletions(-) diff --git a/bottlecap/src/bin/bottlecap/main.rs b/bottlecap/src/bin/bottlecap/main.rs index 39f352149..049e3a00e 100644 --- a/bottlecap/src/bin/bottlecap/main.rs +++ b/bottlecap/src/bin/bottlecap/main.rs @@ -1300,6 +1300,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.ext.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/mod.rs b/bottlecap/src/config/mod.rs index db0a1f192..d13954454 100644 --- a/bottlecap/src/config/mod.rs +++ b/bottlecap/src/config/mod.rs @@ -25,10 +25,42 @@ use serde::Deserialize; pub type Config = datadog_agent_config::Config; #[allow(clippy::module_name_repetitions)] -#[inline] #[must_use] pub fn get_config(config_directory: &Path) -> Config { - get_config_with_extension::(config_directory) + let mut config = get_config_with_extension::(config_directory); + apply_serverless_apm_only(&mut config); + config +} + +/// 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`), and +/// log egress is guarded in the logs flusher. +fn apply_serverless_apm_only(config: &mut Config) { + if !config.ext.serverless_apm_only { + return; + } + + if config.ext.serverless_logs_enabled + || config.ext.enhanced_metrics + || config.ext.lambda_proc_enhanced_metrics + || config.otlp_config_metrics_enabled + || config.otlp_config_logs_enabled + { + tracing::debug!( + "DD_SERVERLESS_APM_ONLY is enabled: forcing logs and all metrics off (traces-only mode)" + ); + } + + config.ext.serverless_logs_enabled = false; + config.ext.enhanced_metrics = false; + config.ext.lambda_proc_enhanced_metrics = false; + config.otlp_config_metrics_enabled = false; + config.otlp_config_logs_enabled = false; } // --------------------------------------------------------------------------- // LambdaConfig — bottlecap's `ConfigExtension` for the shared @@ -60,6 +92,12 @@ pub struct LambdaConfig { 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. Defaults to `false`. + pub serverless_apm_only: bool, pub serverless_flush_strategy: UpstreamFlushStrategy, pub enhanced_metrics: bool, pub lambda_proc_enhanced_metrics: bool, @@ -89,6 +127,7 @@ impl Default for LambdaConfig { kms_api_key: String::new(), api_key_ssm_arn: String::new(), serverless_logs_enabled: true, + serverless_apm_only: false, serverless_flush_strategy: UpstreamFlushStrategy::Default, enhanced_metrics: true, lambda_proc_enhanced_metrics: true, @@ -138,6 +177,13 @@ pub struct LambdaConfigSource { #[serde(deserialize_with = "deser_opt_bool")] pub logs_enabled: Option, + /// `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. Traces and APM trace + /// stats are unaffected. Defaults to `false`. + #[serde(deserialize_with = "deser_opt_bool")] + pub serverless_apm_only: Option, + pub serverless_flush_strategy: Option, #[serde(deserialize_with = "deser_opt_bool")] @@ -193,6 +239,7 @@ impl DatadogConfigExtension for LambdaConfig { datadog_agent_config::merge_fields!(self, source, string: [api_key_secret_arn, kms_api_key, api_key_ssm_arn], value: [ + serverless_apm_only, serverless_flush_strategy, enhanced_metrics, lambda_proc_enhanced_metrics, @@ -260,6 +307,43 @@ mod lambda_config_tests { assert_eq!(config.ext, LambdaConfig::default()); } + #[test] + fn serverless_apm_only_defaults_off() { + let config = load(|_| Ok(())); + assert!(!config.ext.serverless_apm_only); + // Defaults remain unchanged when APM-only is not set. + assert!(config.ext.serverless_logs_enabled); + assert!(config.ext.enhanced_metrics); + assert!(config.ext.lambda_proc_enhanced_metrics); + } + + #[test] + fn serverless_apm_only_forces_metrics_and_logs_off() { + // Exercised through `get_config` (not `load`) because the override is + // applied there, after the shared env/yaml merge. + 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.ext.serverless_apm_only); + assert!(!config.ext.serverless_logs_enabled); + assert!(!config.ext.enhanced_metrics); + assert!(!config.ext.lambda_proc_enhanced_metrics); + assert!(!config.otlp_config_metrics_enabled); + assert!(!config.otlp_config_logs_enabled); + Ok(()) + }); + } + // ---- string fields from env / yaml ---- #[test] diff --git a/bottlecap/src/logs/flusher.rs b/bottlecap/src/logs/flusher.rs index 5f68a09c7..16e8ee761 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.ext.serverless_apm_only { + return Vec::new(); + } + let mut failed_requests = Vec::new(); // If retry_request is provided, only process that request