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}