Skip to content
174 changes: 169 additions & 5 deletions bottlecap/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ pub type Config = datadog_agent_config::Config<LambdaConfig>;
#[inline]
#[must_use]
pub fn get_config(config_directory: &Path) -> Config {
get_config_with_extension::<LambdaConfig>(config_directory)
let mut config = get_config_with_extension::<LambdaConfig>(config_directory);
config.ext.apply_experimental_features_gate();
config
}
// ---------------------------------------------------------------------------
// LambdaConfig β€” bottlecap's `ConfigExtension` for the shared
Expand Down Expand Up @@ -80,6 +82,19 @@ pub struct LambdaConfig {
/// 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,

/// `DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED` β€” gates `additional_metric_tags` and
/// `additional_metric_tags_cardinality_limit` below, matching the Serverless
/// Compatibility Layer (`datadog-trace-agent`).
pub trace_experimental_features_enabled: bool,
/// `DD_TRACE_STATS_ADDITIONAL_TAGS` β€” comma-separated span `meta` keys included as
/// additional dimensions on trace stats aggregation (`ClientGroupedStats.additional_metric_tags`).
/// Only honored when `trace_experimental_features_enabled` is true.
pub additional_metric_tags: Vec<String>,
/// `DD_TRACE_STATS_ADDITIONAL_TAGS_CARDINALITY_LIMIT` β€” per-bucket cap on distinct
/// `additional_metric_tags` value combinations; `None` uses libdatadog's default (100).
/// Only honored when `trace_experimental_features_enabled` is true.
pub additional_metric_tags_cardinality_limit: Option<usize>,
}

impl Default for LambdaConfig {
Expand All @@ -104,6 +119,9 @@ impl Default for LambdaConfig {
api_security_sample_delay: Duration::from_secs(30),
custom_metrics_exclude_tags: Vec::new(),
lambda_durable_function_log_buffer_size: 0,
trace_experimental_features_enabled: false,
additional_metric_tags: Vec::new(),
additional_metric_tags_cardinality_limit: None,
}
}
}
Expand Down Expand Up @@ -180,6 +198,23 @@ pub struct LambdaConfigSource {
/// 0 (hold mechanism disabled).
#[serde(deserialize_with = "deser_opt_lossless")]
pub lambda_durable_function_log_buffer_size: Option<usize>,

/// `DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED` β€” see `LambdaConfig::trace_experimental_features_enabled`.
#[serde(deserialize_with = "deser_opt_bool")]
pub trace_experimental_features_enabled: Option<bool>,
/// `DD_TRACE_STATS_ADDITIONAL_TAGS` β€” see `LambdaConfig::additional_metric_tags`.
/// Gated on `trace_experimental_features_enabled` in `merge_from`, not here. Field is
/// named `trace_stats_additional_tags` (rather than `additional_metric_tags`) so it maps
/// to the `DD_TRACE_STATS_ADDITIONAL_TAGS` env var via the field-name-to-env-var convention.
#[serde(deserialize_with = "deser_csv")]
pub trace_stats_additional_tags: Vec<String>,
/// `DD_TRACE_STATS_ADDITIONAL_TAGS_CARDINALITY_LIMIT` β€” see
/// `LambdaConfig::additional_metric_tags_cardinality_limit`. Gated on
/// `trace_experimental_features_enabled` in `merge_from`, not here. See
/// `trace_stats_additional_tags` above for why the field name differs from the
/// `LambdaConfig` field it merges into.
#[serde(deserialize_with = "deser_opt_lossless")]
pub trace_stats_additional_tags_cardinality_limit: Option<usize>,
}

impl DatadogConfigExtension for LambdaConfig {
Expand All @@ -204,6 +239,7 @@ impl DatadogConfigExtension for LambdaConfig {
api_security_enabled,
api_security_sample_delay,
lambda_durable_function_log_buffer_size,
trace_experimental_features_enabled,
],
option: [span_dedup_timeout, api_key_secret_reload_interval, appsec_rules],
);
Expand All @@ -227,16 +263,44 @@ impl DatadogConfigExtension for LambdaConfig {
self.custom_metrics_exclude_tags
.clone_from(&source.lambda_customer_metrics_exclude_tags);
}

// trace_stats_additional_tags (source) β†’ additional_metric_tags (config), and likewise
// for the cardinality limit. Merged unconditionally here: `merge_from` runs once per
// config source (datadog.yaml, then env vars), so gating on
// `trace_experimental_features_enabled` at this point would discard a value read from
// datadog.yaml whenever the gate itself only arrives with the later env-var pass.
// `apply_experimental_features_gate` applies the gate once, after every source has
// merged.
if !source.trace_stats_additional_tags.is_empty() {
Comment thread
lucaspimentel marked this conversation as resolved.
self.additional_metric_tags
.clone_from(&source.trace_stats_additional_tags);
}
if let Some(limit) = source.trace_stats_additional_tags_cardinality_limit {
self.additional_metric_tags_cardinality_limit = Some(limit);
}
}
}

impl LambdaConfig {
/// Drop `additional_metric_tags` / `additional_metric_tags_cardinality_limit` unless
/// `trace_experimental_features_enabled` is set, matching the Serverless Compatibility
/// Layer (`datadog-trace-agent`).
///
/// Applied after all config sources have merged, not inside `merge_from`, so that the gate
/// and the values it gates can come from different sources in either order.
fn apply_experimental_features_gate(&mut self) {
if !self.trace_experimental_features_enabled {
self.additional_metric_tags.clear();
self.additional_metric_tags_cardinality_limit = None;
}
}
}

#[cfg_attr(coverage_nightly, coverage(off))] // Test modules skew coverage metrics
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod lambda_config_tests {
use datadog_agent_config::{
Config as UpstreamConfig, flush_strategy::PeriodicStrategy, get_config_with_extension,
};
use datadog_agent_config::{Config as UpstreamConfig, flush_strategy::PeriodicStrategy};
use figment::Jail;

use super::*;
Expand All @@ -248,7 +312,9 @@ mod lambda_config_tests {
Jail::expect_with(|jail| {
jail.clear_env();
jail_setup(jail)?;
result = Some(get_config_with_extension::<LambdaConfig>(Path::new("")));
// `get_config`, not `get_config_with_extension`, so the post-merge
// `apply_experimental_features_gate` step is covered too.
result = Some(get_config(Path::new("")));
Ok(())
});
result.unwrap()
Expand Down Expand Up @@ -709,4 +775,102 @@ mod lambda_config_tests {
// Default is true.
assert!(config.ext.enhanced_metrics);
}

// ---- additional_metric_tags (span-derived primary tags), gated on
// trace_experimental_features_enabled, matching the Serverless Compatibility Layer
// (datadog-trace-agent) ----

#[test]
fn additional_metric_tags_ignored_when_experimental_features_disabled() {
let config = load(|jail| {
jail.set_env("DD_TRACE_STATS_ADDITIONAL_TAGS", "region,tenant_id");
Ok(())
});
assert!(!config.ext.trace_experimental_features_enabled);
assert!(config.ext.additional_metric_tags.is_empty());
}

#[test]
fn additional_metric_tags_from_env_when_trace_experimental_features_enabled() {
let config = load(|jail| {
jail.set_env("DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED", "true");
jail.set_env("DD_TRACE_STATS_ADDITIONAL_TAGS", "region, tenant_id");
Ok(())
});
assert!(config.ext.trace_experimental_features_enabled);
assert_eq!(
config.ext.additional_metric_tags,
vec!["region".to_string(), "tenant_id".to_string()]
);
}

#[test]
fn additional_metric_tags_cardinality_limit_ignored_when_experimental_features_disabled() {
let config = load(|jail| {
jail.set_env("DD_TRACE_STATS_ADDITIONAL_TAGS_CARDINALITY_LIMIT", "5");
Ok(())
});
assert_eq!(config.ext.additional_metric_tags_cardinality_limit, None);
}

#[test]
fn additional_metric_tags_cardinality_limit_from_env_when_experimental_gate_enabled() {
let config = load(|jail| {
jail.set_env("DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED", "true");
jail.set_env("DD_TRACE_STATS_ADDITIONAL_TAGS_CARDINALITY_LIMIT", "5");
Ok(())
});
assert_eq!(config.ext.additional_metric_tags_cardinality_limit, Some(5));
}

#[test]
fn additional_metric_tags_cardinality_limit_invalid_value_falls_back_to_none() {
let config = load(|jail| {
jail.set_env("DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED", "true");
jail.set_env(
"DD_TRACE_STATS_ADDITIONAL_TAGS_CARDINALITY_LIMIT",
"not-a-number",
);
Ok(())
});
assert_eq!(config.ext.additional_metric_tags_cardinality_limit, None);
}

/// The gate and the values it gates may come from different config sources. Sources merge
/// one at a time (datadog.yaml first, then env vars), so gating during the merge would
/// drop the yaml values before the env-var pass ever enables the gate.
#[test]
fn additional_metric_tags_from_yaml_survive_an_env_only_experimental_gate() {
let config = load(|jail| {
jail.create_file(
"datadog.yaml",
"trace_stats_additional_tags: \"region,zone\"\n\
trace_stats_additional_tags_cardinality_limit: 7\n",
)?;
jail.set_env("DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED", "true");
Ok(())
});
assert_eq!(
config.ext.additional_metric_tags,
vec!["region".to_string(), "zone".to_string()]
);
assert_eq!(config.ext.additional_metric_tags_cardinality_limit, Some(7));
}

/// The mirror of the above: an env-var gate of `false` must still win over yaml values.
#[test]
fn additional_metric_tags_from_yaml_dropped_when_env_disables_the_gate() {
let config = load(|jail| {
jail.create_file(
"datadog.yaml",
"trace_experimental_features_enabled: true\n\
trace_stats_additional_tags: \"region,zone\"\n\
trace_stats_additional_tags_cardinality_limit: 7\n",
)?;
jail.set_env("DD_TRACE_EXPERIMENTAL_FEATURES_ENABLED", "false");
Ok(())
});
assert!(config.ext.additional_metric_tags.is_empty());
assert_eq!(config.ext.additional_metric_tags_cardinality_limit, None);
}
}
Loading
Loading