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/// Assumed maximum size, in bytes, of a single payload in the retry queue.
14///
15/// The deprecated `forwarder_retry_queue_max_size` setting bounds the retry queue by payload count,
16/// while the queue itself is bounded by bytes. Converting between the two requires assuming a
17/// per-payload size, and this value matches the Agent's own assumption so that the same
18/// configuration yields the same byte budget in ADP and the Agent.
19pub const MAX_PAYLOAD_SIZE_BYTES: u64 = 2 * 1024 * 1024;
20
21/// Cross-cutting configuration shared across domains.
22#[derive(Clone, Debug, Default, PartialEq, Serialize)]
23pub struct SharedConfiguration {
24    /// Primary forwarder endpoints and transport.
25    pub endpoints: Endpoints,
26
27    /// Global and host-level tagging.
28    pub tags: GlobalTags,
29
30    /// Inputs used to derive deployment-wide static tags.
31    pub static_tags: StaticTagSettings,
32
33    /// Tags attached to basic liveness telemetry.
34    pub basic_telemetry: BasicTelemetry,
35
36    /// Metrics-encoder settings reused across the metrics-emitting pipelines.
37    pub metrics_encoding: MetricsEncoding,
38
39    /// Cluster Agent connection, shared by checks, DogStatsD, and OTLP.
40    pub cluster_agent: ClusterAgent,
41
42    /// Autoscaling failover, shared by checks, DogStatsD, and OTLP.
43    pub autoscaling_failover: AutoscalingFailover,
44
45    /// Secrets management, read by the Datadog intake forwarders.
46    pub secrets: Secrets,
47
48    /// Host and container runtime discovery, read by the environment providers.
49    pub environment: Environment,
50
51    /// Verbosity of the internal telemetry emitted about the runtime itself. (not in Datadog Agent
52    /// config schema)
53    pub metrics_level: String,
54
55    /// Base directory for runtime-state files.
56    ///
57    /// ADP uses this to derive default locations for forwarder retry data, DogStatsD captures, and
58    /// DogStatsD context dumps. Defaults to unset when configuration does not provide a concrete
59    /// `run_path`.
60    pub run_path: Option<PathBuf>,
61}
62
63/// Host identity and container runtime discovery inputs.
64#[derive(Clone, Debug, Default, PartialEq, Serialize)]
65pub struct Environment {
66    /// Hostname reported for all emitted data.
67    ///
68    /// Only read in standalone mode, where it is reported verbatim. In connected mode the hostname comes from the
69    /// Datadog Agent and this value is ignored.
70    ///
71    /// Defaults to empty, and a defaulted or empty value is treated as absent. Operators running standalone must set
72    /// it explicitly; startup fails otherwise.
73    pub hostname: ConfigValue<String>,
74
75    /// containerd runtime discovery and client timeouts.
76    pub containerd: Containerd,
77
78    /// Filesystem roots describing container workloads.
79    pub container_roots: ContainerRoots,
80}
81
82/// containerd runtime discovery and client timeouts.
83#[derive(Clone, Debug, Default, PartialEq, Serialize)]
84pub struct Containerd {
85    /// containerd gRPC socket. A defaulted empty value enables path probing.
86    pub socket_path: ConfigValue<PathBuf>,
87
88    /// Timeout for establishing a containerd gRPC connection. Defaults to 1 second; `0` never connects.
89    pub connection_timeout: Duration,
90
91    /// Per-RPC timeout for containerd API calls. Defaults to 5 seconds; `0` fails every call.
92    pub query_timeout: Duration,
93}
94
95/// Filesystem roots describing container workloads.
96#[derive(Clone, Debug, Default, PartialEq, Serialize)]
97pub struct ContainerRoots {
98    /// procfs root. Defaults to `/host/proc`, which is only used when set explicitly.
99    pub proc_root: ConfigValue<PathBuf>,
100
101    /// cgroupfs root. Defaults to `/host/sys/fs/cgroup/`, which is only used when set explicitly.
102    pub cgroup_root: ConfigValue<PathBuf>,
103}
104
105/// Inputs used to derive deployment-wide static tags.
106#[derive(Clone, Debug, Default, PartialEq, Serialize)]
107pub struct StaticTagSettings {
108    /// Deployment-provider classification added as `provider_kind:<value>` when non-empty.
109    ///
110    /// Defaults to empty, which adds no provider-kind tag.
111    pub provider_kind: String,
112
113    /// Whether the deployment uses EKS Fargate.
114    ///
115    /// Defaults to `false`. When enabled, the static-tag resolver adds EKS-specific tags in addition to global tags.
116    pub eks_fargate: bool,
117
118    /// Kubernetes node name used for the EKS Fargate node tag.
119    ///
120    /// Defaults to empty, which omits `eks_fargate_node` and emits a warning when EKS Fargate is enabled.
121    pub kubernetes_kubelet_nodename: String,
122
123    /// Kubernetes cluster name used for the EKS Fargate cluster tag.
124    ///
125    /// Defaults to empty, which omits `kube_cluster_name` unless the configured global tags already provide one.
126    pub cluster_name: String,
127}
128
129/// Primary outbound endpoints plus the forwarder, proxy, TLS, and compression settings that apply
130/// to every pipeline emitting to the intake.
131#[derive(Clone, Debug, Default, PartialEq, Serialize)]
132pub struct Endpoints {
133    /// API key for the primary intake.
134    pub api_key: String,
135
136    /// Base site domain for the primary intake (for example, `datadoghq.com`).
137    ///
138    /// The Datadog schema supplies a default value when nothing sets this key. Provenance is
139    /// `Explicit` only when the value was explicitly configured.
140    pub site: ConfigValue<String>,
141
142    /// Full primary intake URL, which overrides [`site`](Self::site) when set explicitly.
143    ///
144    /// The Core Agent supplies this key at its schema default even when not set by the user or
145    /// operator. Provenance is preserved so that we know when this was explicitly set and should
146    /// override `site`.
147    pub dd_url: ConfigValue<String>,
148
149    /// Additional dual-shipping endpoints, keyed by intake URL with their API keys.
150    pub additional_endpoints: HashMap<String, Vec<String>>,
151
152    /// Whether metrics may carry arbitrary tags.
153    pub allow_arbitrary_tags: bool,
154
155    /// Outbound HTTP proxy settings.
156    pub proxy: Proxy,
157
158    /// Outbound TLS client settings.
159    pub tls: Tls,
160
161    /// Payload compression settings.
162    pub compression: Compression,
163
164    /// Forwarder retry, backoff, worker, and disk-storage settings.
165    pub forwarder: Forwarder,
166
167    /// Alternate metrics intake for the Observability Pipelines Worker, used in place of the
168    /// default intake when enabled.
169    pub opw_intake: AltMetricsIntake,
170
171    /// Alternate metrics intake for Vector, used in place of the default intake when enabled.
172    pub vector_intake: AltMetricsIntake,
173}
174
175impl Endpoints {
176    /// Returns the primary intake endpoint, as configured and without normalization.
177    ///
178    /// An explicitly configured [`dd_url`](Self::dd_url) overrides [`site`](Self::site), even when
179    /// its value equals the schema default: the operator asked for that URL. Otherwise the endpoint
180    /// is derived from `site`. An empty `site` cannot produce an endpoint, so the effective `dd_url`
181    /// value is used instead; it already carries the source schema's default URL.
182    pub fn primary_endpoint(&self) -> String {
183        if self.dd_url.is_explicit() || self.site.value.is_empty() {
184            self.dd_url.value.clone()
185        } else {
186            format!("https://app.{}", self.site.value)
187        }
188    }
189}
190
191/// An alternate metrics intake (Observability Pipelines Worker or Vector) that replaces the Datadog
192/// intake when enabled.
193#[derive(Clone, Debug, Default, PartialEq, Serialize)]
194pub struct AltMetricsIntake {
195    /// Whether this alternate intake replaces the default one.
196    pub enabled: bool,
197
198    /// URL of the alternate metrics intake.
199    pub url: String,
200
201    /// Whether metrics ship to this intake over the V3 series protocol
202    /// (`observability_pipelines_worker.metrics.use_v3_api.series` / `vector.metrics.use_v3_api.series`).
203    pub use_v3_series: bool,
204}
205
206/// Outbound HTTP proxy settings.
207#[derive(Clone, Debug, Default, PartialEq, Serialize)]
208pub struct Proxy {
209    /// Proxy URL for plain HTTP requests.
210    pub http: String,
211
212    /// Proxy URL for HTTPS requests.
213    pub https: String,
214
215    /// Hosts that bypass the proxy.
216    pub no_proxy: Vec<String>,
217
218    /// Whether no-proxy entries match by suffix rather than exact host.
219    pub no_proxy_nonexact_match: bool,
220
221    /// Whether cloud-metadata requests also go through the proxy.
222    pub use_proxy_for_cloud_metadata: bool,
223}
224
225/// Outbound TLS client settings.
226#[derive(Clone, Debug, Default, PartialEq, Serialize)]
227pub struct Tls {
228    /// Whether server certificate validation is skipped.
229    pub skip_ssl_validation: bool,
230
231    /// Minimum TLS version enforced on outbound connections.
232    pub min_tls_version: String,
233
234    /// Path to which TLS session keys are logged, for debugging.
235    pub sslkeylogfile: String,
236
237    /// Timeout for completing the TLS handshake after a connection is established.
238    ///
239    /// Defaults to 10 seconds. Bounds only the handshake step, distinct from the overall request timeout. A value
240    /// of zero disables the handshake-specific deadline, leaving the overall request timeout as the only bound.
241    pub handshake_timeout: Duration,
242}
243
244/// Payload compression settings applied before transmission.
245#[derive(Clone, Debug, PartialEq, Serialize)]
246pub struct Compression {
247    /// Which compression algorithm the encoder uses.
248    // TODO: enum?
249    pub compressor_kind: String,
250
251    /// ADP's own zstd compression level (`data_plane.serializer_zstd_compressor_level`).
252    ///
253    /// Defaults to `3`, which is higher than the Agent's default of `1` because ADP compresses more
254    /// cheaply than the Agent. Its provenance says whether an operator asked for the level, which
255    /// separates a configured `3` from the default `3`. Read [`effective_zstd_level`] rather than this
256    /// field.
257    ///
258    /// [`effective_zstd_level`]: Compression::effective_zstd_level
259    pub adp_zstd_level: ConfigValue<i32>,
260
261    /// The Core Agent's zstd compression level (`serializer_zstd_compressor_level`).
262    ///
263    /// The Agent supplies this key at its own default of `1` even when nothing sets it, so only its
264    /// provenance says whether an operator asked for the level. Read [`effective_zstd_level`] rather
265    /// than this field.
266    ///
267    /// [`effective_zstd_level`]: Compression::effective_zstd_level
268    pub agent_zstd_level: ConfigValue<i32>,
269}
270
271impl Compression {
272    /// Returns the effective compression level used when the algorithm is zstd.
273    ///
274    /// Defaults to `3`, which is higher than the Agent's default of `1` because ADP compresses more
275    /// cheaply than the Agent. The setting is determined as followed:
276    /// - If an operator explicitly sets `data_plane.serializer_zstd_compressor_level`, it wins.
277    /// - If an operator explicitly sets `serializer_zstd_compressor_level`, ADP uses it.
278    /// - If neither is set, the ADP default of `3` is used.
279    pub fn effective_zstd_level(&self) -> i32 {
280        // Resolution options that depend on the order in which values are applied to a single field
281        // are more brittle and harder to understand. Keeping these fields separate and applying
282        // precedence with this helper function makes the override logic easier to understand.
283
284        // If the operator explicitly set ADP zstd level, use it.
285        if self.adp_zstd_level.is_explicit() {
286            return self.adp_zstd_level.value;
287        }
288
289        // If the operator explicitly set Agent zstd level, use it (even if it happens to equal the
290        // default).
291        if self.agent_zstd_level.is_explicit() {
292            return self.agent_zstd_level.value;
293        }
294
295        // If nothing was explicitly set, use ADP's default.
296        self.adp_zstd_level.value
297    }
298}
299
300impl Default for Compression {
301    fn default() -> Self {
302        Self {
303            compressor_kind: String::new(),
304            adp_zstd_level: ConfigValue::defaulted(DEFAULT_ZSTD_COMPRESSOR_LEVEL),
305            agent_zstd_level: ConfigValue::default(),
306        }
307    }
308}
309
310/// HTTP protocol the forwarder negotiates with the intake.
311#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
312pub enum ForwarderHttpProtocol {
313    #[default]
314    Auto,
315    Http1,
316}
317
318/// Forwarder retry, backoff, worker, and disk-storage settings.
319#[derive(Clone, Debug, Default, PartialEq, Serialize)]
320pub struct Forwarder {
321    /// How often, in seconds, API keys are checked for validity against the intake.
322    pub apikey_validation_interval: i64,
323
324    /// Base delay, in seconds, for retry backoff.
325    pub backoff_base: f64,
326
327    /// Multiplier applied to the backoff delay after each failed attempt.
328    pub backoff_factor: f64,
329
330    /// Maximum retry backoff delay, in seconds.
331    pub backoff_max: f64,
332
333    /// How often, in seconds, idle connections are reset.
334    pub connection_reset_interval: u64,
335
336    /// Fraction of the in-memory retry queue at which payloads spill to disk.
337    pub flush_to_disk_mem_ratio: f64,
338
339    /// Capacity of the high-priority send buffer.
340    pub high_prio_buffer_size: usize,
341
342    /// HTTP protocol the forwarder negotiates with the intake.
343    pub http_protocol: ForwarderHttpProtocol,
344
345    /// Maximum number of in-flight requests to the intake.
346    pub max_concurrent_requests: usize,
347
348    /// Number of forwarder worker tasks.
349    pub num_workers: usize,
350
351    /// Age, in days, after which payloads queued on disk are discarded.
352    pub outdated_file_in_days: u32,
353
354    /// Number of retry cycles between attempts to recover a failed endpoint.
355    pub recovery_interval: u32,
356
357    /// Whether the recovery interval resets after a successful send.
358    pub recovery_reset: bool,
359
360    /// Retry-queue capacity expressed as seconds of buffered payloads.
361    pub retry_queue_capacity_time_interval_sec: u64,
362
363    /// Maximum number of payloads held in the in-memory retry queue.
364    ///
365    /// Deprecated in favor of [`retry_queue_payloads_max_size`](Self::retry_queue_payloads_max_size).
366    /// This setting counts payloads, not bytes;
367    /// [`effective_retry_queue_max_size_bytes`](Self::effective_retry_queue_max_size_bytes) converts
368    /// it using [`MAX_PAYLOAD_SIZE_BYTES`]. The Datadog schema supplies `0` when nothing sets this
369    /// key. Because `0` is also a value an operator can set, honor this setting only when it is
370    /// explicit.
371    pub retry_queue_max_size: ConfigValue<u64>,
372
373    /// Maximum total size, in bytes, of payloads held in the retry queue.
374    ///
375    /// The Datadog schema supplies 15 MiB when nothing sets this key. Takes precedence over
376    /// [`retry_queue_max_size`](Self::retry_queue_max_size) when set explicitly.
377    pub retry_queue_payloads_max_size: ConfigValue<u64>,
378
379    /// Grace period the forwarder is given to drain before shutdown.
380    pub stop_timeout: Duration,
381
382    /// Fraction of available disk the on-disk retry store may use.
383    pub storage_max_disk_ratio: f64,
384
385    /// Maximum size, in bytes, of the on-disk retry store.
386    pub storage_max_size_in_bytes: u64,
387
388    /// Directory where retry payloads are persisted to disk.
389    pub storage_path: PathBuf,
390
391    /// Per-request timeout, in seconds, for calls to the intake.
392    pub timeout: u64,
393}
394
395impl Forwarder {
396    /// Returns the effective maximum size, in bytes, of the in-memory retry queue.
397    ///
398    /// An explicit [`retry_queue_payloads_max_size`](Self::retry_queue_payloads_max_size) wins, then
399    /// an explicit [`retry_queue_max_size`](Self::retry_queue_max_size), and otherwise the effective
400    /// payload-size value, which carries the source schema's default. Selection cannot look at the
401    /// values themselves: `0` is both the deprecated setting's schema default and a value an
402    /// operator can mean.
403    ///
404    /// The deprecated setting counts payloads rather than bytes, so it is scaled by
405    /// [`MAX_PAYLOAD_SIZE_BYTES`] to reach a byte budget. A count large enough to overflow saturates
406    /// at [`u64::MAX`], which the retry queue treats as effectively unbounded.
407    pub fn effective_retry_queue_max_size_bytes(&self) -> u64 {
408        if !self.retry_queue_payloads_max_size.is_explicit() && self.retry_queue_max_size.is_explicit() {
409            self.retry_queue_max_size.value.saturating_mul(MAX_PAYLOAD_SIZE_BYTES)
410        } else {
411            self.retry_queue_payloads_max_size.value
412        }
413    }
414}
415
416/// Global / host tagging.
417#[derive(Clone, Debug, Default, PartialEq, Serialize)]
418pub struct GlobalTags {
419    /// Tags configured through `tags` / `DD_TAGS`.
420    pub tags: Vec<String>,
421
422    /// Tags configured through `extra_tags` / `DD_EXTRA_TAGS`.
423    pub extra_tags: Vec<String>,
424
425    /// How long, after startup, host tags remain attached to emitted data.
426    pub expected_tags_duration: Duration,
427}
428
429/// Tagging options for basic liveness telemetry.
430#[derive(Clone, Debug, Default, PartialEq, Serialize)]
431pub struct BasicTelemetry {
432    /// Whether liveness signals include the process container's low-cardinality tags.
433    ///
434    /// Defaults to `false`. Enable this for containerized deployments that need to associate basic
435    /// telemetry with the running container. If the container cannot be resolved, liveness signals
436    /// are emitted without these tags.
437    pub add_container_tags: bool,
438}
439
440/// Metrics-encoder settings reused across the metrics-emitting pipelines (DogStatsD, checks, and
441/// OTLP): histogram settings, payload limits, and the encoder flush timeout.
442#[derive(Clone, Debug, PartialEq, Serialize)]
443pub struct MetricsEncoding {
444    /// How long the encoder waits before flushing a partially filled payload. (not in Datadog Agent
445    /// config schema)
446    ///
447    /// Shared by the metrics-emitting pipelines and the traces encoder, all of which read the
448    /// `flush_timeout_secs` key. Defaults to 2 seconds.
449    pub flush_timeout: Duration,
450
451    /// Maximum number of metrics packed into a single payload. (not in Datadog Agent config schema)
452    ///
453    /// Defaults to [`DEFAULT_MAX_METRICS_PER_PAYLOAD`].
454    pub max_metrics_per_payload: usize,
455
456    /// Maximum compressed payload size, in bytes.
457    pub max_payload_size: usize,
458
459    /// Maximum compressed size, in bytes, of a series payload.
460    pub max_series_payload_size: usize,
461
462    /// Maximum number of series data points per payload.
463    pub max_series_points_per_payload: usize,
464
465    /// Maximum uncompressed size, in bytes, of a series payload.
466    pub max_series_uncompressed_payload_size: usize,
467
468    /// Maximum uncompressed payload size, in bytes.
469    pub max_uncompressed_payload_size: usize,
470
471    /// Whether series are submitted via the v2 intake API.
472    pub use_v2_series_api: bool,
473
474    /// Whether outgoing payloads are logged for debugging.
475    pub log_payloads: bool,
476
477    /// Histogram aggregation and encoding settings.
478    pub histogram: HistogramEncoding,
479
480    /// Experimental V3 sketches settings (`serializer_experimental_use_v3_api.*`).
481    pub v3_api: V3ApiEncoding,
482
483    /// Global V3 series routing mode (`use_v3_api.series.enabled`).
484    pub v3_series_mode: V3SeriesMode,
485
486    /// Per-endpoint V3 series routing overrides, keyed by endpoint URL
487    /// (`use_v3_api.series.endpoints`).
488    pub v3_series_endpoint_modes: HashMap<String, V3SeriesMode>,
489}
490
491impl Default for MetricsEncoding {
492    fn default() -> Self {
493        Self {
494            // The `flush_timeout_secs` key is Saluki-only, so its default belongs to the ADP config
495            // crate rather than a source schema.
496            flush_timeout: DEFAULT_ENCODER_FLUSH_TIMEOUT,
497            max_metrics_per_payload: DEFAULT_MAX_METRICS_PER_PAYLOAD,
498            max_payload_size: 0,
499            max_series_payload_size: 0,
500            max_series_points_per_payload: 0,
501            max_series_uncompressed_payload_size: 0,
502            max_uncompressed_payload_size: 0,
503            use_v2_series_api: false,
504            log_payloads: false,
505            histogram: HistogramEncoding::default(),
506            v3_api: V3ApiEncoding::default(),
507            v3_series_mode: V3SeriesMode::default(),
508            v3_series_endpoint_modes: HashMap::new(),
509        }
510    }
511}
512
513/// Experimental V3 sketches settings (`serializer_experimental_use_v3_api.*`).
514#[derive(Clone, Debug, Default, PartialEq, Serialize)]
515pub struct V3ApiEncoding {
516    /// Endpoints using the V3 sketches intake.
517    pub sketches: V3ApiSettings,
518
519    /// zstd compression level for V3 payloads.
520    pub compression_level: i32,
521}
522
523/// V3 sketches intake settings.
524#[derive(Clone, Debug, Default, PartialEq, Serialize)]
525pub struct V3ApiSettings {
526    /// Endpoints enabled for the V3 intake.
527    pub endpoints: Vec<String>,
528}
529
530/// Whether series are routed to the V3 metrics intake (`use_v3_api.series.*`).
531///
532/// Each variant serializes to the spelling [`FromStr`] reads, so a serialized mode round-trips.
533#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
534pub enum V3SeriesMode {
535    /// Route series to the V3 intake.
536    #[serde(rename = "true")]
537    Enabled,
538
539    /// Route series to the older intake.
540    #[serde(rename = "false")]
541    Disabled,
542
543    /// Route series to the V3 intake only for endpoints that are Datadog intake URLs.
544    #[default]
545    #[serde(rename = "datadog_only")]
546    DatadogOnly,
547}
548
549impl FromStr for V3SeriesMode {
550    type Err = Error;
551
552    // The Agent reads this setting as a string and then interprets it, accepting more spellings than
553    // `strconv.ParseBool` does, so the accepted set is wider than that of a `boolean` leaf.
554    fn from_str(value: &str) -> Result<Self, Self::Err> {
555        match value.trim().to_ascii_lowercase().as_str() {
556            "true" | "1" | "t" | "yes" | "on" => Ok(Self::Enabled),
557            "false" | "0" | "f" | "no" | "off" | "" => Ok(Self::Disabled),
558            "datadog_only" => Ok(Self::DatadogOnly),
559            other => Err(Error::new_without_source(format!(
560                "unknown V3 series mode `{other}`; expected a boolean or `datadog_only`"
561            ))),
562        }
563    }
564}
565
566/// Histogram aggregation/encoding settings, shared by the DogStatsD and checks metrics pipelines.
567#[derive(Clone, Debug, Default, PartialEq, Serialize)]
568pub struct HistogramEncoding {
569    /// Which histogram aggregations (for example, `max` or `median`) are computed.
570    pub aggregates: Vec<String>,
571
572    /// Whether histograms are also emitted as distributions.
573    pub copy_to_distribution: bool,
574
575    /// Metric-name prefix applied to the distribution copies.
576    pub copy_to_distribution_prefix: String,
577
578    /// Which percentile aggregations are computed for histograms.
579    pub percentiles: Vec<String>,
580}
581
582/// Cluster Agent connection, shared by checks, DogStatsD, and OTLP.
583///
584/// The defaults named on each field are the Datadog schema defaults, which translation writes whenever the key is
585/// absent. They are not the values `Default` produces: that is the zero value of each field, which for
586/// `kubernetes_service_name` is the empty string and therefore not the schema default.
587#[derive(Clone, Debug, Default, PartialEq, Serialize)]
588pub struct ClusterAgent {
589    /// Whether the Cluster Agent connection is used.
590    ///
591    /// Defaults to `false`. Turn it on in a deployment that runs a Cluster Agent; while it is off, nothing talks to it.
592    pub enabled: bool,
593
594    /// URL of the Cluster Agent.
595    ///
596    /// Defaults to unset, which leaves the endpoint to Kubernetes service discovery through
597    /// `kubernetes_service_name`. A blank value is normalized to unset. Set this in a deployment where the Cluster
598    /// Agent is not reachable through an injected Kubernetes service, and give an `https` endpoint: consumers use only
599    /// `https`.
600    pub url: Option<String>,
601
602    /// Token used to authenticate to the Cluster Agent.
603    ///
604    /// Defaults to unset, which leaves the Cluster Agent unreachable: there is no anonymous access. Set it wherever the
605    /// Cluster Agent is enabled, to that Agent's own token; a blank value is normalized to unset.
606    pub auth_token: Option<String>,
607
608    /// Kubernetes service name used to discover the Cluster Agent.
609    ///
610    /// Defaults to `datadog-cluster-agent`. The name is turned into the `<NAME>_SERVICE_HOST` and
611    /// `<NAME>_SERVICE_PORT` environment variables that Kubernetes injects into the pod. Set this when the Cluster
612    /// Agent runs under a different service name, or set it to the empty string to turn the lookup off, which leaves
613    /// `url` as the only way to reach the Cluster Agent.
614    pub kubernetes_service_name: String,
615}
616
617/// Secrets management, as configured for the Core Agent.
618///
619/// ADP resolves no secrets itself; the Core Agent does. These settings mirror the Agent's own configuration, and ADP
620/// reads them for one purpose: to decide whether a rejected API key might be replaced. See
621/// [`in_use`](Self::in_use).
622#[derive(Clone, Debug, Default, PartialEq, Serialize)]
623pub struct Secrets {
624    /// Path to the executable the Core Agent runs to fetch secrets.
625    ///
626    /// Defaults to unset, and a blank value is normalized to unset. ADP does not run it. A configured command makes
627    /// [`in_use`](Self::in_use) true, which makes an intake's `403 Forbidden` response retriable. Set this only to match
628    /// the Core Agent's own configuration.
629    pub backend_command: Option<String>,
630
631    /// Minutes between the secret refreshes the Core Agent triggers after an API key is rejected.
632    ///
633    /// Defaults to `0`, which turns those refreshes off. A negative value from the source means the same thing and is
634    /// clamped to `0`. A positive value makes [`in_use`](Self::in_use) true on its own, and `0` does not make it false
635    /// when [`backend_command`](Self::backend_command) is set. Set this only to match the Core Agent's own
636    /// configuration.
637    pub refresh_on_api_key_failure_interval: u64,
638}
639
640impl Secrets {
641    /// Returns whether secret resolution might replace a rejected API key.
642    ///
643    /// This is true when a [`backend_command`](Self::backend_command) is configured or
644    /// [`refresh_on_api_key_failure_interval`](Self::refresh_on_api_key_failure_interval) is positive. Either says the
645    /// key an intake just rejected may be a secret that gets re-resolved, so the same request is worth retrying. When
646    /// neither is configured, nothing is going to replace the key, and retrying the request only wastes it.
647    pub const fn in_use(&self) -> bool {
648        self.refresh_on_api_key_failure_interval > 0 || self.backend_command.is_some()
649    }
650}
651
652/// Autoscaling failover, shared by checks, DogStatsD, and OTLP.
653#[derive(Clone, Debug, Default, PartialEq, Serialize)]
654pub struct AutoscalingFailover {
655    /// Whether metrics designated for autoscaling failover are forwarded to the Cluster Agent.
656    ///
657    /// Defaults to `false`. Also needs `cluster_agent.enabled`, `cluster_agent.auth_token`, a resolvable Cluster Agent
658    /// endpoint, and a non-empty `metrics`; otherwise the branch is not built and primary forwarding continues.
659    pub enabled: bool,
660
661    /// Names of the metrics designated for autoscaling failover.
662    ///
663    /// Defaults to `container.memory.usage` and `container.cpu.usage`. An empty list turns the failover branch off even
664    /// when `enabled` is set, because there is nothing left to forward. Set this when autoscaling reads metrics other
665    /// than the two defaults.
666    pub metrics: Vec<String>,
667}
668
669#[cfg(test)]
670mod tests {
671    use super::{Compression, Endpoints, Forwarder, V3SeriesMode};
672    use crate::defaults::DEFAULT_ZSTD_COMPRESSOR_LEVEL;
673    use crate::ConfigValue;
674
675    #[test]
676    fn v3_series_mode_parses_every_form_the_agent_interprets() {
677        for (value, expected) in [
678            ("true", V3SeriesMode::Enabled),
679            ("TRUE", V3SeriesMode::Enabled),
680            ("1", V3SeriesMode::Enabled),
681            ("t", V3SeriesMode::Enabled),
682            ("yes", V3SeriesMode::Enabled),
683            ("on", V3SeriesMode::Enabled),
684            ("false", V3SeriesMode::Disabled),
685            ("0", V3SeriesMode::Disabled),
686            ("f", V3SeriesMode::Disabled),
687            ("no", V3SeriesMode::Disabled),
688            ("off", V3SeriesMode::Disabled),
689            ("", V3SeriesMode::Disabled),
690            (" datadog_only ", V3SeriesMode::DatadogOnly),
691        ] {
692            assert_eq!(
693                value.parse::<V3SeriesMode>().expect("mode should parse"),
694                expected,
695                "{value}"
696            );
697        }
698    }
699
700    #[test]
701    fn v3_series_mode_rejects_an_uninterpretable_value() {
702        let error = "sometimes"
703            .parse::<V3SeriesMode>()
704            .expect_err("an uninterpretable mode should be rejected");
705
706        assert_eq!(
707            error.to_string(),
708            "unknown V3 series mode `sometimes`; expected a boolean or `datadog_only`"
709        );
710    }
711
712    #[test]
713    fn v3_series_mode_defaults_to_datadog_only() {
714        assert_eq!(V3SeriesMode::default(), V3SeriesMode::DatadogOnly);
715    }
716
717    #[test]
718    fn explicit_dd_url_overrides_site_even_at_the_schema_default() {
719        // The source supplies `dd_url` at its schema default even when nothing set it, so an
720        // explicit URL equal to that default still expresses an override.
721        let endpoints = Endpoints {
722            site: ConfigValue::explicit("datadoghq.eu".to_string()),
723            dd_url: ConfigValue::explicit("https://app.datadoghq.com".to_string()),
724            ..Default::default()
725        };
726
727        assert_eq!("https://app.datadoghq.com", endpoints.primary_endpoint());
728    }
729
730    #[test]
731    fn defaulted_dd_url_leaves_the_endpoint_to_site() {
732        let endpoints = Endpoints {
733            site: ConfigValue::explicit("datadoghq.eu".to_string()),
734            dd_url: ConfigValue::defaulted("https://app.datadoghq.com".to_string()),
735            ..Default::default()
736        };
737
738        assert_eq!("https://app.datadoghq.eu", endpoints.primary_endpoint());
739    }
740
741    #[test]
742    fn an_override_url_is_used_verbatim() {
743        let endpoints = Endpoints {
744            site: ConfigValue::defaulted("datadoghq.com".to_string()),
745            dd_url: ConfigValue::explicit("https://proxy.internal.example.com:3128".to_string()),
746            ..Default::default()
747        };
748
749        assert_eq!("https://proxy.internal.example.com:3128", endpoints.primary_endpoint());
750    }
751
752    #[test]
753    fn an_empty_site_falls_back_to_the_effective_dd_url() {
754        // `https://app.` is not an endpoint, and the model does not restate the source schema's
755        // default site. The effective `dd_url` already carries the schema default URL.
756        let endpoints = Endpoints {
757            site: ConfigValue::explicit(String::new()),
758            dd_url: ConfigValue::defaulted("https://app.datadoghq.com".to_string()),
759            ..Default::default()
760        };
761
762        assert_eq!("https://app.datadoghq.com", endpoints.primary_endpoint());
763    }
764
765    #[test]
766    fn retry_queue_size_prefers_the_explicit_payload_size() {
767        let forwarder = Forwarder {
768            retry_queue_payloads_max_size: ConfigValue::explicit(2048),
769            retry_queue_max_size: ConfigValue::explicit(1024),
770            ..Default::default()
771        };
772
773        assert_eq!(2048, forwarder.effective_retry_queue_max_size_bytes());
774    }
775
776    #[test]
777    fn retry_queue_size_falls_back_to_the_explicit_deprecated_size() {
778        // The deprecated setting is a payload count, so 1024 payloads is a 2 GiB byte budget. Using
779        // the count as a byte budget would leave a queue too small to hold a single payload.
780        let forwarder = Forwarder {
781            retry_queue_payloads_max_size: ConfigValue::defaulted(15 * 1024 * 1024),
782            retry_queue_max_size: ConfigValue::explicit(1024),
783            ..Default::default()
784        };
785
786        assert_eq!(1024 * 2 * 1024 * 1024, forwarder.effective_retry_queue_max_size_bytes());
787    }
788
789    #[test]
790    fn a_deprecated_retry_queue_size_that_would_overflow_saturates() {
791        // A count this large cannot be scaled into a u64. Wrapping would turn an enormous queue into
792        // a tiny one, so the conversion saturates into an effectively unbounded queue instead.
793        let forwarder = Forwarder {
794            retry_queue_payloads_max_size: ConfigValue::defaulted(15 * 1024 * 1024),
795            retry_queue_max_size: ConfigValue::explicit(u64::MAX),
796            ..Default::default()
797        };
798
799        assert_eq!(u64::MAX, forwarder.effective_retry_queue_max_size_bytes());
800    }
801
802    #[test]
803    fn the_effective_zstd_level_prefers_adp_then_an_explicit_agent_level() {
804        let defaulted = Compression::default();
805        assert_eq!(DEFAULT_ZSTD_COMPRESSOR_LEVEL, defaulted.effective_zstd_level());
806
807        // The Agent supplies its own default of 1 on every load, so only an explicit value counts.
808        let agent_defaulted = Compression {
809            agent_zstd_level: ConfigValue::defaulted(1),
810            ..Default::default()
811        };
812        assert_eq!(DEFAULT_ZSTD_COMPRESSOR_LEVEL, agent_defaulted.effective_zstd_level());
813
814        let agent_explicit = Compression {
815            agent_zstd_level: ConfigValue::explicit(1),
816            ..Default::default()
817        };
818        assert_eq!(1, agent_explicit.effective_zstd_level());
819
820        // ADP's own key wins over an explicit Agent level, including at ADP's default value.
821        let both_explicit = Compression {
822            adp_zstd_level: ConfigValue::explicit(DEFAULT_ZSTD_COMPRESSOR_LEVEL),
823            agent_zstd_level: ConfigValue::explicit(5),
824            ..Default::default()
825        };
826        assert_eq!(DEFAULT_ZSTD_COMPRESSOR_LEVEL, both_explicit.effective_zstd_level());
827    }
828
829    #[test]
830    fn an_explicit_zero_retry_queue_size_is_honored() {
831        // Zero is the deprecated setting's schema default and also a value an operator can mean, so
832        // only provenance can tell the two apart.
833        let explicitly_zero = Forwarder {
834            retry_queue_payloads_max_size: ConfigValue::defaulted(15 * 1024 * 1024),
835            retry_queue_max_size: ConfigValue::explicit(0),
836            ..Default::default()
837        };
838        // Zero payloads scales to zero bytes either way.
839        assert_eq!(0, explicitly_zero.effective_retry_queue_max_size_bytes());
840
841        let defaulted_zero = Forwarder {
842            retry_queue_payloads_max_size: ConfigValue::defaulted(15 * 1024 * 1024),
843            retry_queue_max_size: ConfigValue::defaulted(0),
844            ..Default::default()
845        };
846        assert_eq!(15 * 1024 * 1024, defaulted_zero.effective_retry_queue_max_size_bytes());
847    }
848}