agent_data_plane_config/
shared.rs

1//! Cross-cutting values consumed by more than one domain.
2
3use std::collections::HashMap;
4use std::path::PathBuf;
5use std::str::FromStr;
6use std::time::Duration;
7
8use serde::Serialize;
9
10use crate::defaults::{DEFAULT_ENCODER_FLUSH_TIMEOUT, DEFAULT_MAX_METRICS_PER_PAYLOAD, DEFAULT_ZSTD_COMPRESSOR_LEVEL};
11use crate::{ConfigValue, Error};
12
13/// Cross-cutting configuration shared across domains.
14#[derive(Clone, Debug, Default, PartialEq, Serialize)]
15pub struct SharedConfiguration {
16    /// Primary forwarder endpoints and transport.
17    pub endpoints: Endpoints,
18
19    /// Global and host-level tagging.
20    pub tags: GlobalTags,
21
22    /// Inputs used to derive deployment-wide static tags.
23    pub static_tags: StaticTagSettings,
24
25    /// Tags attached to basic liveness telemetry.
26    pub basic_telemetry: BasicTelemetry,
27
28    /// Metrics-encoder settings reused across the metrics-emitting pipelines.
29    pub metrics_encoding: MetricsEncoding,
30
31    /// Cluster Agent connection, shared by checks, DogStatsD, and OTLP.
32    pub cluster_agent: ClusterAgent,
33
34    /// Autoscaling failover, shared by checks, DogStatsD, and OTLP.
35    pub autoscaling_failover: AutoscalingFailover,
36
37    /// Verbosity of the internal telemetry emitted about the runtime itself. (not in Datadog Agent
38    /// config schema)
39    pub metrics_level: String,
40}
41
42/// Inputs used to derive deployment-wide static tags.
43#[derive(Clone, Debug, Default, PartialEq, Serialize)]
44pub struct StaticTagSettings {
45    /// Deployment-provider classification added as `provider_kind:<value>` when non-empty.
46    ///
47    /// Defaults to empty, which adds no provider-kind tag.
48    pub provider_kind: String,
49
50    /// Whether the deployment uses EKS Fargate.
51    ///
52    /// Defaults to `false`. When enabled, the static-tag resolver adds EKS-specific tags in addition to global tags.
53    pub eks_fargate: bool,
54
55    /// Kubernetes node name used for the EKS Fargate node tag.
56    ///
57    /// Defaults to empty, which omits `eks_fargate_node` and emits a warning when EKS Fargate is enabled.
58    pub kubernetes_kubelet_nodename: String,
59
60    /// Kubernetes cluster name used for the EKS Fargate cluster tag.
61    ///
62    /// Defaults to empty, which omits `kube_cluster_name` unless the configured global tags already provide one.
63    pub cluster_name: String,
64}
65
66/// Primary outbound endpoints plus the forwarder, proxy, TLS, and compression settings that apply
67/// to every pipeline emitting to the intake.
68#[derive(Clone, Debug, Default, PartialEq, Serialize)]
69pub struct Endpoints {
70    /// API key for the primary intake.
71    pub api_key: String,
72
73    /// Base site domain for the primary intake (for example, `datadoghq.com`).
74    ///
75    /// The Datadog schema supplies a default value when nothing sets this key. Provenance is
76    /// `Explicit` only when the value was explicitly configured.
77    pub site: ConfigValue<String>,
78
79    /// Full primary intake URL, which overrides [`site`](Self::site) when set explicitly.
80    ///
81    /// The Core Agent supplies this key at its schema default even when not set by the user or
82    /// operator. Provenance is preserved so that we know when this was explicitly set and should
83    /// override `site`.
84    pub dd_url: ConfigValue<String>,
85
86    /// Additional dual-shipping endpoints, keyed by intake URL with their API keys.
87    pub additional_endpoints: HashMap<String, Vec<String>>,
88
89    /// Whether metrics may carry arbitrary tags.
90    pub allow_arbitrary_tags: bool,
91
92    /// Outbound HTTP proxy settings.
93    pub proxy: Proxy,
94
95    /// Outbound TLS client settings.
96    pub tls: Tls,
97
98    /// Payload compression settings.
99    pub compression: Compression,
100
101    /// Forwarder retry, backoff, worker, and disk-storage settings.
102    pub forwarder: Forwarder,
103
104    /// Alternate metrics intake for the Observability Pipelines Worker, used in place of the
105    /// default intake when enabled.
106    pub opw_intake: AltMetricsIntake,
107
108    /// Alternate metrics intake for Vector, used in place of the default intake when enabled.
109    pub vector_intake: AltMetricsIntake,
110}
111
112impl Endpoints {
113    /// Returns the primary intake endpoint, as configured and without normalization.
114    ///
115    /// An explicitly configured [`dd_url`](Self::dd_url) overrides [`site`](Self::site), even when
116    /// its value equals the schema default: the operator asked for that URL. Otherwise the endpoint
117    /// is derived from `site`. An empty `site` cannot produce an endpoint, so the effective `dd_url`
118    /// value is used instead; it already carries the source schema's default URL.
119    pub fn primary_endpoint(&self) -> String {
120        if self.dd_url.is_explicit() || self.site.value.is_empty() {
121            self.dd_url.value.clone()
122        } else {
123            format!("https://app.{}", self.site.value)
124        }
125    }
126}
127
128/// An alternate metrics intake (Observability Pipelines Worker or Vector) that replaces the Datadog
129/// intake when enabled.
130#[derive(Clone, Debug, Default, PartialEq, Serialize)]
131pub struct AltMetricsIntake {
132    /// Whether this alternate intake replaces the default one.
133    pub enabled: bool,
134
135    /// URL of the alternate metrics intake.
136    pub url: String,
137
138    /// Whether metrics ship to this intake over the V3 series protocol
139    /// (`observability_pipelines_worker.metrics.use_v3_api.series` / `vector.metrics.use_v3_api.series`).
140    pub use_v3_series: bool,
141}
142
143/// Outbound HTTP proxy settings.
144#[derive(Clone, Debug, Default, PartialEq, Serialize)]
145pub struct Proxy {
146    /// Proxy URL for plain HTTP requests.
147    pub http: String,
148
149    /// Proxy URL for HTTPS requests.
150    pub https: String,
151
152    /// Hosts that bypass the proxy.
153    pub no_proxy: Vec<String>,
154
155    /// Whether no-proxy entries match by suffix rather than exact host.
156    pub no_proxy_nonexact_match: bool,
157
158    /// Whether cloud-metadata requests also go through the proxy.
159    pub use_proxy_for_cloud_metadata: bool,
160}
161
162/// Outbound TLS client settings.
163#[derive(Clone, Debug, Default, PartialEq, Serialize)]
164pub struct Tls {
165    /// Whether server certificate validation is skipped.
166    pub skip_ssl_validation: bool,
167
168    /// Minimum TLS version enforced on outbound connections.
169    pub min_tls_version: String,
170
171    /// Path to which TLS session keys are logged, for debugging.
172    pub sslkeylogfile: String,
173
174    /// Timeout for completing the TLS handshake after a connection is established.
175    ///
176    /// Defaults to 10 seconds. Bounds only the handshake step, distinct from the overall request timeout. A value
177    /// of zero disables the handshake-specific deadline, leaving the overall request timeout as the only bound.
178    pub handshake_timeout: Duration,
179}
180
181/// Payload compression settings applied before transmission.
182#[derive(Clone, Debug, PartialEq, Serialize)]
183pub struct Compression {
184    /// Which compression algorithm the encoder uses.
185    // TODO: enum?
186    pub compressor_kind: String,
187
188    /// ADP's own zstd compression level (`data_plane.serializer_zstd_compressor_level`).
189    ///
190    /// Defaults to `3`, which is higher than the Agent's default of `1` because ADP compresses more
191    /// cheaply than the Agent. Its provenance says whether an operator asked for the level, which
192    /// separates a configured `3` from the default `3`. Read [`effective_zstd_level`] rather than this
193    /// field.
194    ///
195    /// [`effective_zstd_level`]: Compression::effective_zstd_level
196    pub adp_zstd_level: ConfigValue<i32>,
197
198    /// The Core Agent's zstd compression level (`serializer_zstd_compressor_level`).
199    ///
200    /// The Agent supplies this key at its own default of `1` even when nothing sets it, so only its
201    /// provenance says whether an operator asked for the level. Read [`effective_zstd_level`] rather
202    /// than this field.
203    ///
204    /// [`effective_zstd_level`]: Compression::effective_zstd_level
205    pub agent_zstd_level: ConfigValue<i32>,
206}
207
208impl Compression {
209    /// Returns the effective compression level used when the algorithm is zstd.
210    ///
211    /// Defaults to `3`, which is higher than the Agent's default of `1` because ADP compresses more
212    /// cheaply than the Agent. The setting is determined as followed:
213    /// - If an operator explicitly sets `data_plane.serializer_zstd_compressor_level`, it wins.
214    /// - If an operator explicitly sets `serializer_zstd_compressor_level`, ADP uses it.
215    /// - If neither is set, the ADP default of `3` is used.
216    pub fn effective_zstd_level(&self) -> i32 {
217        // Resolution options that depend on the order in which values are applied to a single field
218        // are more brittle and harder to understand. Keeping these fields separate and applying
219        // precedence with this helper function makes the override logic easier to understand.
220
221        // If the operator explicitly set ADP zstd level, use it.
222        if self.adp_zstd_level.is_explicit() {
223            return self.adp_zstd_level.value;
224        }
225
226        // If the operator explicitly set Agent zstd level, use it (even if it happens to equal the
227        // default).
228        if self.agent_zstd_level.is_explicit() {
229            return self.agent_zstd_level.value;
230        }
231
232        // If nothing was explicitly set, use ADP's default.
233        self.adp_zstd_level.value
234    }
235}
236
237impl Default for Compression {
238    fn default() -> Self {
239        Self {
240            compressor_kind: String::new(),
241            adp_zstd_level: ConfigValue::defaulted(DEFAULT_ZSTD_COMPRESSOR_LEVEL),
242            agent_zstd_level: ConfigValue::default(),
243        }
244    }
245}
246
247/// HTTP protocol the forwarder negotiates with the intake.
248#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
249pub enum ForwarderHttpProtocol {
250    #[default]
251    Auto,
252    Http1,
253}
254
255/// Forwarder retry, backoff, worker, and disk-storage settings.
256#[derive(Clone, Debug, Default, PartialEq, Serialize)]
257pub struct Forwarder {
258    /// How often, in seconds, API keys are checked for validity against the intake.
259    pub apikey_validation_interval: i64,
260
261    /// Base delay, in seconds, for retry backoff.
262    pub backoff_base: f64,
263
264    /// Multiplier applied to the backoff delay after each failed attempt.
265    pub backoff_factor: f64,
266
267    /// Maximum retry backoff delay, in seconds.
268    pub backoff_max: f64,
269
270    /// How often, in seconds, idle connections are reset.
271    pub connection_reset_interval: u64,
272
273    /// Fraction of the in-memory retry queue at which payloads spill to disk.
274    pub flush_to_disk_mem_ratio: f64,
275
276    /// Capacity of the high-priority send buffer.
277    pub high_prio_buffer_size: usize,
278
279    /// HTTP protocol the forwarder negotiates with the intake.
280    pub http_protocol: ForwarderHttpProtocol,
281
282    /// Maximum number of in-flight requests to the intake.
283    pub max_concurrent_requests: usize,
284
285    /// Number of forwarder worker tasks.
286    pub num_workers: usize,
287
288    /// Age, in days, after which payloads queued on disk are discarded.
289    pub outdated_file_in_days: u32,
290
291    /// Number of retry cycles between attempts to recover a failed endpoint.
292    pub recovery_interval: u32,
293
294    /// Whether the recovery interval resets after a successful send.
295    pub recovery_reset: bool,
296
297    /// Retry-queue capacity expressed as seconds of buffered payloads.
298    pub retry_queue_capacity_time_interval_sec: u64,
299
300    /// Maximum number of payloads held in the in-memory retry queue.
301    ///
302    /// Deprecated in favor of [`retry_queue_payloads_max_size`](Self::retry_queue_payloads_max_size).
303    /// The Datadog schema supplies `0` when nothing sets this key. Because `0` is also a value an
304    /// operator can set, honor this setting only when it is explicit.
305    pub retry_queue_max_size: ConfigValue<u64>,
306
307    /// Maximum total size, in bytes, of payloads held in the retry queue.
308    ///
309    /// The Datadog schema supplies 15 MiB when nothing sets this key. Takes precedence over
310    /// [`retry_queue_max_size`](Self::retry_queue_max_size) when set explicitly.
311    pub retry_queue_payloads_max_size: ConfigValue<u64>,
312
313    /// Grace period the forwarder is given to drain before shutdown.
314    pub stop_timeout: Duration,
315
316    /// Fraction of available disk the on-disk retry store may use.
317    pub storage_max_disk_ratio: f64,
318
319    /// Maximum size, in bytes, of the on-disk retry store.
320    pub storage_max_size_in_bytes: u64,
321
322    /// Directory where retry payloads are persisted to disk.
323    pub storage_path: PathBuf,
324
325    /// Per-request timeout, in seconds, for calls to the intake.
326    pub timeout: u64,
327}
328
329impl Forwarder {
330    /// Returns the effective maximum size, in bytes, of the in-memory retry queue.
331    ///
332    /// An explicit [`retry_queue_payloads_max_size`](Self::retry_queue_payloads_max_size) wins, then
333    /// an explicit [`retry_queue_max_size`](Self::retry_queue_max_size), and otherwise the effective
334    /// payload-size value, which carries the source schema's default. Selection cannot look at the
335    /// values themselves: `0` is both the deprecated setting's schema default and a value an
336    /// operator can mean.
337    pub fn effective_retry_queue_max_size_bytes(&self) -> u64 {
338        if !self.retry_queue_payloads_max_size.is_explicit() && self.retry_queue_max_size.is_explicit() {
339            self.retry_queue_max_size.value
340        } else {
341            self.retry_queue_payloads_max_size.value
342        }
343    }
344}
345
346/// Global / host tagging.
347#[derive(Clone, Debug, Default, PartialEq, Serialize)]
348pub struct GlobalTags {
349    /// Tags configured through `tags` / `DD_TAGS`.
350    pub tags: Vec<String>,
351
352    /// Tags configured through `extra_tags` / `DD_EXTRA_TAGS`.
353    pub extra_tags: Vec<String>,
354
355    /// How long, after startup, host tags remain attached to emitted data.
356    pub expected_tags_duration: Duration,
357}
358
359/// Tagging options for basic liveness telemetry.
360#[derive(Clone, Debug, Default, PartialEq, Serialize)]
361pub struct BasicTelemetry {
362    /// Whether liveness signals include the process container's low-cardinality tags.
363    ///
364    /// Defaults to `false`. Enable this for containerized deployments that need to associate basic
365    /// telemetry with the running container. If the container cannot be resolved, liveness signals
366    /// are emitted without these tags.
367    pub add_container_tags: bool,
368}
369
370/// Metrics-encoder settings reused across the metrics-emitting pipelines (DogStatsD, checks, and
371/// OTLP): histogram settings, payload limits, and the encoder flush timeout.
372#[derive(Clone, Debug, PartialEq, Serialize)]
373pub struct MetricsEncoding {
374    /// How long the encoder waits before flushing a partially filled payload. (not in Datadog Agent
375    /// config schema)
376    ///
377    /// Shared by the metrics-emitting pipelines and the traces encoder, all of which read the
378    /// `flush_timeout_secs` key. Defaults to 2 seconds.
379    pub flush_timeout: Duration,
380
381    /// Maximum number of metrics packed into a single payload. (not in Datadog Agent config schema)
382    ///
383    /// Defaults to [`DEFAULT_MAX_METRICS_PER_PAYLOAD`].
384    pub max_metrics_per_payload: usize,
385
386    /// Maximum compressed payload size, in bytes.
387    pub max_payload_size: usize,
388
389    /// Maximum compressed size, in bytes, of a series payload.
390    pub max_series_payload_size: usize,
391
392    /// Maximum number of series data points per payload.
393    pub max_series_points_per_payload: usize,
394
395    /// Maximum uncompressed size, in bytes, of a series payload.
396    pub max_series_uncompressed_payload_size: usize,
397
398    /// Maximum uncompressed payload size, in bytes.
399    pub max_uncompressed_payload_size: usize,
400
401    /// Whether series are submitted via the v2 intake API.
402    pub use_v2_series_api: bool,
403
404    /// Whether outgoing payloads are logged for debugging.
405    pub log_payloads: bool,
406
407    /// Histogram aggregation and encoding settings.
408    pub histogram: HistogramEncoding,
409
410    /// V3 metrics-intake protocol settings (`serializer_experimental_use_v3_api.*`).
411    pub v3_api: V3ApiEncoding,
412
413    /// Global V3 series routing mode (`use_v3_api.series.enabled`).
414    pub v3_series_mode: V3SeriesMode,
415
416    /// Per-endpoint V3 series routing overrides, keyed by endpoint URL
417    /// (`use_v3_api.series.endpoints`).
418    pub v3_series_endpoint_modes: HashMap<String, V3SeriesMode>,
419}
420
421impl Default for MetricsEncoding {
422    fn default() -> Self {
423        Self {
424            // The `flush_timeout_secs` key is Saluki-only, so its default belongs to the ADP config
425            // crate rather than a source schema.
426            flush_timeout: DEFAULT_ENCODER_FLUSH_TIMEOUT,
427            max_metrics_per_payload: DEFAULT_MAX_METRICS_PER_PAYLOAD,
428            max_payload_size: 0,
429            max_series_payload_size: 0,
430            max_series_points_per_payload: 0,
431            max_series_uncompressed_payload_size: 0,
432            max_uncompressed_payload_size: 0,
433            use_v2_series_api: false,
434            log_payloads: false,
435            histogram: HistogramEncoding::default(),
436            v3_api: V3ApiEncoding::default(),
437            v3_series_mode: V3SeriesMode::default(),
438            v3_series_endpoint_modes: HashMap::new(),
439        }
440    }
441}
442
443/// V3 metrics-intake protocol settings for the series and sketches payloads
444/// (`serializer_experimental_use_v3_api.*`).
445#[derive(Clone, Debug, Default, PartialEq, Serialize)]
446pub struct V3ApiEncoding {
447    /// V3 series intake settings.
448    pub series: V3ApiSettings,
449
450    /// V3 sketches intake settings (the series-only fields stay at their defaults).
451    pub sketches: V3ApiSettings,
452
453    /// zstd compression level for V3 payloads.
454    pub compression_level: i32,
455}
456
457/// Per-payload V3 intake settings, reused for both series and sketches.
458#[derive(Clone, Debug, Default, PartialEq, Serialize)]
459pub struct V3ApiSettings {
460    /// Endpoints enabled for the V3 intake.
461    pub endpoints: Vec<String>,
462}
463
464/// Whether series are routed to the V3 metrics intake (`use_v3_api.series.*`).
465///
466/// Each variant serializes to the spelling [`FromStr`] reads, so a serialized mode round-trips.
467#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
468pub enum V3SeriesMode {
469    /// Route series to the V3 intake.
470    #[serde(rename = "true")]
471    Enabled,
472
473    /// Route series to the older intake.
474    #[serde(rename = "false")]
475    Disabled,
476
477    /// Route series to the V3 intake only for endpoints that are Datadog intake URLs.
478    #[default]
479    #[serde(rename = "datadog_only")]
480    DatadogOnly,
481}
482
483impl FromStr for V3SeriesMode {
484    type Err = Error;
485
486    // The Agent reads this setting as a string and then interprets it, accepting more spellings than
487    // `strconv.ParseBool` does, so the accepted set is wider than that of a `boolean` leaf.
488    fn from_str(value: &str) -> Result<Self, Self::Err> {
489        match value.trim().to_ascii_lowercase().as_str() {
490            "true" | "1" | "t" | "yes" | "on" => Ok(Self::Enabled),
491            "false" | "0" | "f" | "no" | "off" | "" => Ok(Self::Disabled),
492            "datadog_only" => Ok(Self::DatadogOnly),
493            other => Err(Error::new_without_source(format!(
494                "unknown V3 series mode `{other}`; expected a boolean or `datadog_only`"
495            ))),
496        }
497    }
498}
499
500/// Histogram aggregation/encoding settings, shared by the DogStatsD and checks metrics pipelines.
501#[derive(Clone, Debug, Default, PartialEq, Serialize)]
502pub struct HistogramEncoding {
503    /// Which histogram aggregations (for example, `max` or `median`) are computed.
504    pub aggregates: Vec<String>,
505
506    /// Whether histograms are also emitted as distributions.
507    pub copy_to_distribution: bool,
508
509    /// Metric-name prefix applied to the distribution copies.
510    pub copy_to_distribution_prefix: String,
511
512    /// Which percentile aggregations are computed for histograms.
513    pub percentiles: Vec<String>,
514}
515
516/// Cluster Agent connection, shared by checks, DogStatsD, and OTLP.
517#[derive(Clone, Debug, Default, PartialEq, Serialize)]
518pub struct ClusterAgent {
519    /// Whether the Cluster Agent connection is used.
520    pub enabled: bool,
521
522    /// URL of the Cluster Agent.
523    pub url: Option<String>,
524
525    /// Token used to authenticate to the Cluster Agent.
526    pub auth_token: Option<String>,
527
528    /// Kubernetes service name used to discover the Cluster Agent.
529    pub kubernetes_service_name: Option<String>,
530}
531
532/// Autoscaling failover, shared by checks, DogStatsD, and OTLP.
533#[derive(Clone, Debug, Default, PartialEq, Serialize)]
534pub struct AutoscalingFailover {
535    /// Whether autoscaling metrics failover is active.
536    pub enabled: bool,
537
538    /// Metrics designated for failover.
539    pub metrics: Vec<String>,
540}
541
542#[cfg(test)]
543mod tests {
544    use super::{Compression, Endpoints, Forwarder, V3SeriesMode};
545    use crate::defaults::DEFAULT_ZSTD_COMPRESSOR_LEVEL;
546    use crate::ConfigValue;
547
548    #[test]
549    fn v3_series_mode_parses_every_form_the_agent_interprets() {
550        for (value, expected) in [
551            ("true", V3SeriesMode::Enabled),
552            ("TRUE", V3SeriesMode::Enabled),
553            ("1", V3SeriesMode::Enabled),
554            ("t", V3SeriesMode::Enabled),
555            ("yes", V3SeriesMode::Enabled),
556            ("on", V3SeriesMode::Enabled),
557            ("false", V3SeriesMode::Disabled),
558            ("0", V3SeriesMode::Disabled),
559            ("f", V3SeriesMode::Disabled),
560            ("no", V3SeriesMode::Disabled),
561            ("off", V3SeriesMode::Disabled),
562            ("", V3SeriesMode::Disabled),
563            (" datadog_only ", V3SeriesMode::DatadogOnly),
564        ] {
565            assert_eq!(
566                value.parse::<V3SeriesMode>().expect("mode should parse"),
567                expected,
568                "{value}"
569            );
570        }
571    }
572
573    #[test]
574    fn v3_series_mode_rejects_an_uninterpretable_value() {
575        let error = "sometimes"
576            .parse::<V3SeriesMode>()
577            .expect_err("an uninterpretable mode should be rejected");
578
579        assert_eq!(
580            error.to_string(),
581            "unknown V3 series mode `sometimes`; expected a boolean or `datadog_only`"
582        );
583    }
584
585    #[test]
586    fn v3_series_mode_defaults_to_datadog_only() {
587        assert_eq!(V3SeriesMode::default(), V3SeriesMode::DatadogOnly);
588    }
589
590    #[test]
591    fn explicit_dd_url_overrides_site_even_at_the_schema_default() {
592        // The source supplies `dd_url` at its schema default even when nothing set it, so an
593        // explicit URL equal to that default still expresses an override.
594        let endpoints = Endpoints {
595            site: ConfigValue::explicit("datadoghq.eu".to_string()),
596            dd_url: ConfigValue::explicit("https://app.datadoghq.com".to_string()),
597            ..Default::default()
598        };
599
600        assert_eq!("https://app.datadoghq.com", endpoints.primary_endpoint());
601    }
602
603    #[test]
604    fn defaulted_dd_url_leaves_the_endpoint_to_site() {
605        let endpoints = Endpoints {
606            site: ConfigValue::explicit("datadoghq.eu".to_string()),
607            dd_url: ConfigValue::defaulted("https://app.datadoghq.com".to_string()),
608            ..Default::default()
609        };
610
611        assert_eq!("https://app.datadoghq.eu", endpoints.primary_endpoint());
612    }
613
614    #[test]
615    fn an_override_url_is_used_verbatim() {
616        let endpoints = Endpoints {
617            site: ConfigValue::defaulted("datadoghq.com".to_string()),
618            dd_url: ConfigValue::explicit("https://proxy.internal.example.com:3128".to_string()),
619            ..Default::default()
620        };
621
622        assert_eq!("https://proxy.internal.example.com:3128", endpoints.primary_endpoint());
623    }
624
625    #[test]
626    fn an_empty_site_falls_back_to_the_effective_dd_url() {
627        // `https://app.` is not an endpoint, and the model does not restate the source schema's
628        // default site. The effective `dd_url` already carries the schema default URL.
629        let endpoints = Endpoints {
630            site: ConfigValue::explicit(String::new()),
631            dd_url: ConfigValue::defaulted("https://app.datadoghq.com".to_string()),
632            ..Default::default()
633        };
634
635        assert_eq!("https://app.datadoghq.com", endpoints.primary_endpoint());
636    }
637
638    #[test]
639    fn retry_queue_size_prefers_the_explicit_payload_size() {
640        let forwarder = Forwarder {
641            retry_queue_payloads_max_size: ConfigValue::explicit(2048),
642            retry_queue_max_size: ConfigValue::explicit(1024),
643            ..Default::default()
644        };
645
646        assert_eq!(2048, forwarder.effective_retry_queue_max_size_bytes());
647    }
648
649    #[test]
650    fn retry_queue_size_falls_back_to_the_explicit_deprecated_size() {
651        let forwarder = Forwarder {
652            retry_queue_payloads_max_size: ConfigValue::defaulted(15 * 1024 * 1024),
653            retry_queue_max_size: ConfigValue::explicit(1024),
654            ..Default::default()
655        };
656
657        assert_eq!(1024, forwarder.effective_retry_queue_max_size_bytes());
658    }
659
660    #[test]
661    fn the_effective_zstd_level_prefers_adp_then_an_explicit_agent_level() {
662        let defaulted = Compression::default();
663        assert_eq!(DEFAULT_ZSTD_COMPRESSOR_LEVEL, defaulted.effective_zstd_level());
664
665        // The Agent supplies its own default of 1 on every load, so only an explicit value counts.
666        let agent_defaulted = Compression {
667            agent_zstd_level: ConfigValue::defaulted(1),
668            ..Default::default()
669        };
670        assert_eq!(DEFAULT_ZSTD_COMPRESSOR_LEVEL, agent_defaulted.effective_zstd_level());
671
672        let agent_explicit = Compression {
673            agent_zstd_level: ConfigValue::explicit(1),
674            ..Default::default()
675        };
676        assert_eq!(1, agent_explicit.effective_zstd_level());
677
678        // ADP's own key wins over an explicit Agent level, including at ADP's default value.
679        let both_explicit = Compression {
680            adp_zstd_level: ConfigValue::explicit(DEFAULT_ZSTD_COMPRESSOR_LEVEL),
681            agent_zstd_level: ConfigValue::explicit(5),
682            ..Default::default()
683        };
684        assert_eq!(DEFAULT_ZSTD_COMPRESSOR_LEVEL, both_explicit.effective_zstd_level());
685    }
686
687    #[test]
688    fn an_explicit_zero_retry_queue_size_is_honored() {
689        // Zero is the deprecated setting's schema default and also a value an operator can mean, so
690        // only provenance can tell the two apart.
691        let explicitly_zero = Forwarder {
692            retry_queue_payloads_max_size: ConfigValue::defaulted(15 * 1024 * 1024),
693            retry_queue_max_size: ConfigValue::explicit(0),
694            ..Default::default()
695        };
696        assert_eq!(0, explicitly_zero.effective_retry_queue_max_size_bytes());
697
698        let defaulted_zero = Forwarder {
699            retry_queue_payloads_max_size: ConfigValue::defaulted(15 * 1024 * 1024),
700            retry_queue_max_size: ConfigValue::defaulted(0),
701            ..Default::default()
702        };
703        assert_eq!(15 * 1024 * 1024, defaulted_zero.effective_retry_queue_max_size_bytes());
704    }
705}