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