Skip to main content

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