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 ef1ec263e..000000000 --- a/bottlecap/src/config/env.rs +++ /dev/null @@ -1,1326 +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, -} - -#[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); -} - -#[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(), - }; - - 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 c05551a4b..ab3aafcb0 100644 --- a/bottlecap/src/config/mod.rs +++ b/bottlecap/src/config/mod.rs @@ -1,1676 +1,35 @@ -pub mod additional_endpoints; -pub mod apm_replace_rule; pub mod aws; -pub mod env; -pub mod flush_strategy; -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; +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, +}; 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, -} - -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), - } - } -} - -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 - } +use serde::Deserialize; - fn datadog_tags_max_length(&self) -> usize { - // Default max length matching dd-trace-rs - 512 - } -} +/// Bottlecap's resolved configuration: the shared agent core plus a Lambda +/// extension under `.ext`. +pub type Config = datadog_agent_config::Config; #[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) + get_config_with_extension::(config_directory) } - -#[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); - } - - #[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, - } - - 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); - } - - #[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, - } - - let result = - serde_json::from_str::(r#"{"tags": []}"#).expect("failed to parse JSON"); - assert_eq!(result.tags, HashMap::new()); - } -} - // --------------------------------------------------------------------------- // LambdaConfig — bottlecap's `ConfigExtension` for the shared // `datadog-agent-config` crate. Lives alongside the core config under 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..8ea536113 --- /dev/null +++ b/bottlecap/src/config/propagation_wrapper.rs @@ -0,0 +1,53 @@ +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(Arc); + +impl PropConfig { + #[must_use] + pub fn new(config: Arc) -> Arc { + Arc::new(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 { + // 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/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 8323a1690..000000000 --- a/bottlecap/src/config/yaml.rs +++ /dev/null @@ -1,1097 +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(), - }; - - // 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 7f7336bb8..0888031f5 100644 --- a/bottlecap/src/lifecycle/invocation/processor.rs +++ b/bottlecap/src/lifecycle/invocation/processor.rs @@ -186,7 +186,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; @@ -630,7 +630,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 @@ -1024,12 +1024,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); } @@ -1240,12 +1240,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); } @@ -2404,7 +2404,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( 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 88397b280..f07604219 100644 --- a/bottlecap/src/logs/lambda/processor.rs +++ b/bottlecap/src/logs/lambda/processor.rs @@ -142,7 +142,7 @@ 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 rules = LambdaProcessor::compile_rules(processing_rules); LambdaProcessor { function_arn, @@ -1422,7 +1422,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() }); @@ -2514,7 +2517,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 ee648c7f2..06d8cab89 100644 --- a/bottlecap/src/otlp/agent.rs +++ b/bottlecap/src/otlp/agent.rs @@ -57,7 +57,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; let (send_data_builder, processed_traces) = self.trace_processor.process_traces( self.config.clone(), self.tags_provider.clone(), 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 db053c44b..77517b737 100644 --- a/bottlecap/src/tags/lambda/tags.rs +++ b/bottlecap/src/tags/lambda/tags.rs @@ -117,7 +117,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()); @@ -138,7 +138,7 @@ fn tags_from_env( // The value of _dd.compute_stats is the opposite of config.compute_trace_stats_on_extension. // "config.compute_trace_stats_on_extension == true" means computing stats on the extension side, // so we set _dd.compute_stats to 0 so stats won't be computed on the backend side. - let compute_stats = i32::from(!config.compute_trace_stats_on_extension); + let compute_stats = i32::from(!config.ext.compute_trace_stats_on_extension); tags_map.insert(COMPUTE_STATS_KEY.to_string(), compute_stats.to_string()); tags_map @@ -463,7 +463,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..aae59cdd5 100644 --- a/bottlecap/src/traces/propagation/mod.rs +++ b/bottlecap/src/traces/propagation/mod.rs @@ -29,14 +29,16 @@ 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 = 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 0a6e2279c..4a0a1e41e 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 e2694a13a..0a8b944d3 100644 --- a/bottlecap/src/traces/trace_processor.rs +++ b/bottlecap/src/traces/trace_processor.rs @@ -397,7 +397,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). - let body_size = if config.compute_trace_stats_on_extension + let body_size = if config.ext.compute_trace_stats_on_extension && let TracerPayloadCollection::V07(ref mut tracer_payloads) = payload { for tp in tracer_payloads.iter_mut() { @@ -531,7 +531,7 @@ impl SendingTraceProcessor { // This needs to be after process_traces() because process_traces() // performs obfuscation, and we need to compute stats on the obfuscated traces. - if config.compute_trace_stats_on_extension + if config.ext.compute_trace_stats_on_extension && let Err(err) = self.stats_generator.send(&processed_traces) { // Just log the error. We don't think trace stats are critical, so we don't want to @@ -1121,7 +1121,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( @@ -1215,7 +1218,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( @@ -1293,7 +1299,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( 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() };