agent_data_plane_config/domains/dogstatsd.rs
1//! DogStatsD domain: source listeners, parsing, origin detection, aggregation, mapping, filters
2//! (some dynamic-capable), and debug logging.
3
4use std::collections::HashMap;
5use std::fmt;
6use std::num::{NonZeroU64, NonZeroUsize};
7use std::path::PathBuf;
8use std::str::FromStr;
9use std::time::Duration;
10
11use serde::{Deserialize, Serialize};
12
13use crate::defaults::{
14 DEFAULT_AGGREGATE_CONTEXT_LIMIT, DEFAULT_AGGREGATE_FLUSH_INTERVAL,
15 DEFAULT_AGGREGATE_PASSTHROUGH_IDLE_FLUSH_TIMEOUT, DEFAULT_AGGREGATE_WINDOW_DURATION_SECONDS,
16 DEFAULT_DOGSTATSD_ALLOW_CONTEXT_HEAP_ALLOCS, DEFAULT_DOGSTATSD_AUTOSCALE_UDP_LISTENERS,
17 DEFAULT_DOGSTATSD_BUFFER_COUNT, DEFAULT_DOGSTATSD_BUFFER_COUNT_MAX, DEFAULT_DOGSTATSD_CACHED_CONTEXTS_LIMIT,
18 DEFAULT_DOGSTATSD_CACHED_TAGSETS_LIMIT, DEFAULT_DOGSTATSD_MAPPER_STRING_INTERNER_SIZE_BYTES,
19 DEFAULT_DOGSTATSD_MINIMUM_SAMPLE_RATE, DEFAULT_DOGSTATSD_PERMISSIVE_DECODING, DEFAULT_DOGSTATSD_TCP_PORT,
20};
21use crate::Error;
22
23// TODO: better name than Domain? Pipeline? Topology? BlueprintConfig?
24/// Resolved DogStatsD configuration.
25#[derive(Clone, Debug, Default, PartialEq, Serialize)]
26pub struct Domain {
27 /// Source listeners and packet-decoding options.
28 pub listeners: Listeners,
29
30 /// Origin detection and tag cardinality.
31 pub origin: OriginDetection,
32
33 /// Context cache sizing and the sample-rate floor.
34 pub contexts: Contexts,
35
36 /// Metric aggregation window and flush behavior.
37 pub aggregation: Aggregation,
38
39 /// Metric-name mapper.
40 pub mapper: Mapper,
41
42 /// Which payload types are emitted.
43 pub enable_payloads: EnablePayloads,
44
45 /// Metric-name filtering.
46 pub metric_filter: MetricFilter,
47
48 /// Metric namespace prefixing.
49 pub prefix_filter: PrefixFilter,
50
51 /// Per-metric tag include/exclude rules.
52 pub tag_filterlist: Vec<MetricTagFilterEntry>,
53
54 /// Per-metric tag value allow-list rules.
55 pub tag_value_allowlist: Vec<MetricTagValueAllowlistEntry>,
56
57 /// Extra tags added to every metric.
58 pub tags: Vec<String>,
59
60 /// Telemetry emitted by the DogStatsD source.
61 pub telemetry: Telemetry,
62
63 /// Debug-log and verbose-log settings for the DogStatsD source.
64 pub debug_log: DebugLog,
65}
66
67/// Source listeners and packet-decoding options.
68#[derive(Clone, Debug, PartialEq, Serialize)]
69pub struct Listeners {
70 /// UDP port DogStatsD listens on.
71 pub port: u16,
72
73 /// TCP port DogStatsD listens on. (not in Datadog Agent config schema)
74 ///
75 /// Defaults to `0`, which disables TCP.
76 pub tcp_port: u16,
77
78 /// Path of the Unix datagram socket DogStatsD listens on.
79 pub socket: Option<String>,
80
81 /// Path of the Unix stream socket DogStatsD listens on.
82 pub stream_socket: Option<String>,
83
84 /// Windows named pipe name DogStatsD listens on. Unset when no named pipe is configured.
85 pub pipe_name: Option<String>,
86
87 /// SDDL security descriptor applied to the Windows named pipe listener.
88 pub windows_pipe_security_descriptor: String,
89
90 /// Whether the UDP listener accepts traffic from non-local addresses.
91 pub non_local_traffic: bool,
92
93 /// Host the UDP listener binds to.
94 pub bind_host: Option<String>,
95
96 /// Size, in bytes, requested for the socket receive buffer.
97 pub so_rcvbuf: usize,
98
99 /// Size, in bytes, of each packet receive buffer.
100 pub buffer_size: usize,
101
102 /// Number of receive buffers allocated. (not in Datadog Agent config schema)
103 pub buffer_count: usize,
104
105 /// Maximum number of receive buffers. (not in Datadog Agent config schema)
106 pub buffer_count_max: usize,
107
108 /// Number of connectionless packet decoder workers.
109 pub workers_count: usize,
110
111 /// Whether to bind multiple UDP sockets via `SO_REUSEPORT`. (not in Datadog Agent config
112 /// schema)
113 pub autoscale_udp_listeners: bool,
114
115 /// Path a traffic capture is written to or replayed from.
116 pub capture_path: PathBuf,
117
118 /// Maximum number of captured packets queued for persistence.
119 ///
120 /// The capture writer raises a depth below its own minimum, so the default, 0, selects that minimum.
121 pub capture_depth: usize,
122
123 /// End-of-line markers required to terminate a stream-socket message.
124 pub eol_required: Vec<String>,
125
126 /// Whether to log stream messages that exceed the buffer size.
127 pub stream_log_too_big: bool,
128
129 /// Whether to relax decoder strictness on malformed packets. (not in Datadog Agent config
130 /// schema)
131 pub permissive_decoding: bool,
132
133 /// Host that received metrics are additionally forwarded to.
134 pub forward_host: Option<String>,
135
136 /// Port that received metrics are additionally forwarded to.
137 pub forward_port: u16,
138}
139
140impl Default for Listeners {
141 fn default() -> Self {
142 Self {
143 // Saluki-only settings retain these defaults when unset.
144 buffer_count: DEFAULT_DOGSTATSD_BUFFER_COUNT,
145 buffer_count_max: DEFAULT_DOGSTATSD_BUFFER_COUNT_MAX,
146 permissive_decoding: DEFAULT_DOGSTATSD_PERMISSIVE_DECODING,
147 tcp_port: DEFAULT_DOGSTATSD_TCP_PORT,
148 autoscale_udp_listeners: DEFAULT_DOGSTATSD_AUTOSCALE_UDP_LISTENERS,
149 // Witnessed settings are overwritten during translation.
150 port: 0,
151 socket: None,
152 stream_socket: None,
153 pipe_name: None,
154 windows_pipe_security_descriptor: String::new(),
155 non_local_traffic: false,
156 bind_host: None,
157 so_rcvbuf: 0,
158 buffer_size: 0,
159 workers_count: 0,
160 capture_path: PathBuf::new(),
161 capture_depth: 0,
162 eol_required: Vec::new(),
163 stream_log_too_big: false,
164 forward_host: None,
165 forward_port: 0,
166 }
167 }
168}
169
170/// Origin detection and tag cardinality.
171#[derive(Clone, Debug, Default, PartialEq, Serialize)]
172pub struct OriginDetection {
173 /// Whether origin detection tags metrics with their source workload.
174 pub detection: bool,
175
176 /// Whether client-supplied origin information is honored.
177 pub detection_client: bool,
178
179 /// Whether the unified origin-detection scheme is used.
180 pub unified: bool,
181
182 /// Whether a client may opt out of origin detection per metric.
183 pub optout_enabled: bool,
184
185 /// Whether a client-supplied entity ID takes precedence over the detected origin.
186 pub entity_id_precedence: bool,
187
188 /// Tag cardinality applied to origin-detected tags, when a payload doesn't specify one itself.
189 pub tag_cardinality: OriginTagCardinality,
190}
191
192/// Tag cardinality applied during origin detection.
193#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
194pub enum OriginTagCardinality {
195 #[default]
196 Low,
197 Orchestrator,
198 High,
199 None,
200}
201
202impl FromStr for OriginTagCardinality {
203 type Err = Error;
204
205 fn from_str(value: &str) -> Result<Self, Self::Err> {
206 // The Datadog Agent lower-cases the value before matching, and accepts `orch` as a shorthand
207 // for `orchestrator`.
208 match value.to_ascii_lowercase().as_str() {
209 "low" => Ok(Self::Low),
210 "orch" | "orchestrator" => Ok(Self::Orchestrator),
211 "high" => Ok(Self::High),
212 "none" => Ok(Self::None),
213 other => Err(Error::new_without_source(format!(
214 "unknown tag cardinality `{other}`; expected low, orch, orchestrator, high, or none"
215 ))),
216 }
217 }
218}
219
220/// Telemetry emitted by the DogStatsD source.
221#[derive(Clone, Debug, Default, PartialEq, Serialize)]
222pub struct Telemetry {
223 /// Whether processed-metric telemetry is broken down by detected origin.
224 pub origin_breakdown: bool,
225}
226
227/// Context cache sizing and sample-rate floor.
228#[derive(Clone, Debug, PartialEq, Serialize)]
229pub struct Contexts {
230 /// Maximum number of metric contexts held in the cache. (not in Datadog Agent config schema)
231 pub cached_contexts_limit: usize,
232
233 /// Maximum number of tagsets held in the cache. (not in Datadog Agent config schema)
234 pub cached_tagsets_limit: usize,
235
236 /// Number of entries the context string interner holds.
237 pub string_interner_size: u64,
238
239 /// Byte budget for the context string interner, overriding the entry count when set. (not in
240 /// Datadog Agent config schema)
241 pub string_interner_size_bytes: Option<u64>,
242
243 /// Whether contexts may be heap-allocated when the interner is full. (not in Datadog Agent
244 /// config schema)
245 pub allow_context_heap_allocs: bool,
246
247 /// Lowest sample rate honored; the decoder warns and clamps anything below it. (not in Datadog
248 /// Agent config schema)
249 pub minimum_sample_rate: f64,
250}
251
252impl Default for Contexts {
253 fn default() -> Self {
254 Self {
255 // Saluki-only settings retain these defaults when unset.
256 cached_contexts_limit: DEFAULT_DOGSTATSD_CACHED_CONTEXTS_LIMIT,
257 cached_tagsets_limit: DEFAULT_DOGSTATSD_CACHED_TAGSETS_LIMIT,
258 string_interner_size_bytes: None,
259 allow_context_heap_allocs: DEFAULT_DOGSTATSD_ALLOW_CONTEXT_HEAP_ALLOCS,
260 minimum_sample_rate: DEFAULT_DOGSTATSD_MINIMUM_SAMPLE_RATE,
261 // Witnessed settings are overwritten during translation.
262 string_interner_size: 0,
263 }
264 }
265}
266
267/// Metric aggregation window and flush behavior.
268#[derive(Clone, Debug, PartialEq, Serialize)]
269pub struct Aggregation {
270 /// Length, in seconds, of each aggregation window. (not in Datadog Agent config schema)
271 pub window_duration_seconds: NonZeroU64,
272
273 /// Maximum number of contexts held per aggregation window. (not in Datadog Agent config schema)
274 pub context_limit: usize,
275
276 /// How often aggregated metrics are flushed. (not in Datadog Agent config schema)
277 pub flush_interval: Duration,
278
279 /// Whether windows that are still open are flushed on shutdown.
280 ///
281 /// Set by the Datadog `dogstatsd_flush_incomplete_buckets` key.
282 pub flush_open_windows: bool,
283
284 /// How long the no-aggregation passthrough waits before flushing while idle. (not in Datadog
285 /// Agent config schema)
286 pub passthrough_idle_flush_timeout: Duration,
287
288 /// How long, in seconds, a counter value is retained after its last update before expiring.
289 ///
290 /// Set by the Datadog `dogstatsd_expiry_seconds` key. A value of `0` disables zero-value counter
291 /// emission.
292 pub counter_expiry_seconds: Option<u64>,
293
294 /// How long, in seconds, a context is retained after its last update before expiring.
295 pub context_expiry_seconds: u64,
296
297 /// Whether metrics bypass aggregation and are forwarded directly.
298 pub no_aggregation_pipeline: bool,
299
300 /// Capacity of the aggregator's tag-filter result cache.
301 pub aggregator_tag_filter_cache_capacity: usize,
302}
303
304impl Default for Aggregation {
305 fn default() -> Self {
306 Self {
307 // Saluki-schema-only knobs: the Datadog Agent schema does not publish these, so they are
308 // seeded only when set; absent that, these defaults stand.
309 window_duration_seconds: DEFAULT_AGGREGATE_WINDOW_DURATION_SECONDS,
310 context_limit: DEFAULT_AGGREGATE_CONTEXT_LIMIT,
311 flush_interval: DEFAULT_AGGREGATE_FLUSH_INTERVAL,
312 passthrough_idle_flush_timeout: DEFAULT_AGGREGATE_PASSTHROUGH_IDLE_FLUSH_TIMEOUT,
313 // Datadog-schema knobs: always written by the witness driver, so these values are
314 // placeholders that never survive translation.
315 flush_open_windows: false,
316 counter_expiry_seconds: None,
317 context_expiry_seconds: 0,
318 no_aggregation_pipeline: false,
319 aggregator_tag_filter_cache_capacity: 0,
320 }
321 }
322}
323
324/// DogStatsD metric mapper.
325#[derive(Clone, Debug, PartialEq, Serialize)]
326pub struct Mapper {
327 /// Mapper profiles that rewrite matching metric names and tags.
328 pub profiles: Vec<MapperProfile>,
329
330 /// Number of mapper match results cached.
331 pub cache_size: usize,
332
333 /// Byte capacity of the mapper's string interner. (not in Datadog Agent config schema)
334 pub string_interner_size_bytes: NonZeroUsize,
335}
336
337impl Default for Mapper {
338 fn default() -> Self {
339 Self {
340 profiles: Vec::new(),
341 // Written by the Datadog witness driver.
342 cache_size: 0,
343 string_interner_size_bytes: DEFAULT_DOGSTATSD_MAPPER_STRING_INTERNER_SIZE_BYTES,
344 }
345 }
346}
347
348/// One mapper profile: a name, a metric prefix, and the mappings under it.
349#[derive(Clone, Debug, Default, PartialEq, Serialize)]
350pub struct MapperProfile {
351 /// Profile name, for diagnostics.
352 pub name: String,
353
354 /// Metric-name prefix the profile's mappings apply to.
355 pub prefix: String,
356
357 /// The name/tag mappings under this profile.
358 pub mappings: Vec<MetricMapping>,
359}
360
361/// A single metric-name mapping within a [`MapperProfile`].
362#[derive(Clone, Debug, Default, PartialEq, Serialize)]
363pub struct MetricMapping {
364 /// Pattern a metric name must match.
365 pub metric_match: String,
366
367 /// How `metric_match` is interpreted (for example, `wildcard` or `regex`).
368 pub match_type: String,
369
370 /// Replacement name emitted for a matching metric.
371 pub name: String,
372
373 /// Tags added to a matching metric, with values captured from the match.
374 pub tags: HashMap<String, String>,
375}
376
377/// Which payload types are emitted.
378#[derive(Clone, Debug, Default, PartialEq, Serialize)]
379pub struct EnablePayloads {
380 /// Whether event payloads are emitted.
381 pub events: bool,
382
383 /// Whether series (metric) payloads are emitted.
384 pub series: bool,
385
386 /// Whether service-check payloads are emitted.
387 pub service_checks: bool,
388
389 /// Whether sketch (distribution) payloads are emitted.
390 pub sketches: bool,
391}
392
393/// Metric-name filtering (dynamic-capable).
394#[derive(Clone, Debug, Default, PartialEq, Serialize)]
395pub struct MetricFilter {
396 /// Metric names or prefixes that are dropped.
397 ///
398 /// Defaults to an empty list, which disables metric-name filtering.
399 pub values: Vec<String>,
400
401 /// Whether entries match by prefix rather than exact name.
402 ///
403 /// Defaults to `false`, which requires exact matches.
404 pub match_prefix: bool,
405}
406
407/// Metric namespace prefixing.
408#[derive(Clone, Debug, Default, PartialEq, Serialize)]
409pub struct PrefixFilter {
410 /// Namespace prepended to every metric name.
411 pub metric_namespace: String,
412
413 /// Namespaces excluded from the metric-namespace prefixing.
414 pub metric_namespace_blocklist: Vec<String>,
415}
416
417/// One tag-filterlist entry (dynamic-capable).
418#[derive(Clone, Debug, Default, PartialEq, Serialize)]
419pub struct MetricTagFilterEntry {
420 /// Metric name the entry applies to.
421 pub metric_name: String,
422
423 /// Whether the listed tags are included or excluded.
424 pub action: FilterAction,
425
426 /// Tags the action applies to.
427 pub tags: Vec<String>,
428}
429
430/// One tag value allow-list entry.
431///
432/// Rules apply to counters and sketch-backed metrics after mapper rewrites and metric namespace prefixing. Distinct
433/// prefixes must not overlap. Multiple rules may use the same prefix when they target different tags.
434#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
435pub struct MetricTagValueAllowlistEntry {
436 /// Non-empty metric-name prefix the entry applies to.
437 ///
438 /// Matching is exact and case-sensitive, including any whitespace. Empty prefixes and overlapping distinct
439 /// prefixes are invalid. Multiple rules may use the same prefix when they target different tags.
440 pub metric_prefix: String,
441
442 /// Non-empty tag key whose values are constrained.
443 ///
444 /// Bare tags have no value and are not changed. Key/value tags with an empty value are processed normally. Empty
445 /// names and names containing `:` are invalid. Matching is exact and preserves whitespace.
446 pub tag_name: String,
447
448 /// Tag values retained unchanged.
449 ///
450 /// The default is an empty list, which treats every key/value tag as a mismatch. The empty string is a valid list
451 /// member and retains tags with an empty value. Matching is exact and preserves whitespace.
452 #[serde(default)]
453 pub values: Vec<String>,
454
455 /// Action applied when a tag value is absent from [`values`][Self::values].
456 ///
457 /// The default is [`Remove`][TagValueMismatchAction::Remove].
458 #[serde(default)]
459 pub on_miss: TagValueMismatchAction,
460
461 /// Replacement value used when `on_miss` is [`Replace`][TagValueMismatchAction::Replace].
462 ///
463 /// The default is `other`. This field has no effect when `on_miss` is
464 /// [`Remove`][TagValueMismatchAction::Remove]. The replacement is emitted exactly as configured, including
465 /// whitespace.
466 #[serde(default = "default_tag_value_replacement")]
467 pub replacement: String,
468}
469
470fn default_tag_value_replacement() -> String {
471 "other".to_string()
472}
473
474impl Default for MetricTagValueAllowlistEntry {
475 fn default() -> Self {
476 Self {
477 metric_prefix: String::new(),
478 tag_name: String::new(),
479 values: Vec::new(),
480 on_miss: TagValueMismatchAction::Remove,
481 replacement: default_tag_value_replacement(),
482 }
483 }
484}
485
486/// Reports why a metric tag value allow-list cannot be represented.
487#[derive(Clone, Debug, PartialEq, Eq)]
488pub struct InvalidMetricTagValueAllowlist(String);
489
490impl fmt::Display for InvalidMetricTagValueAllowlist {
491 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
492 f.write_str(&self.0)
493 }
494}
495
496impl std::error::Error for InvalidMetricTagValueAllowlist {}
497
498/// Validates metric tag value allow-list entries.
499///
500/// # Errors
501///
502/// Returns an error for empty prefixes or tag names, tag names containing `:`, overlapping distinct prefixes, or a
503/// duplicate prefix and tag pair.
504pub fn validate_metric_tag_value_allowlists(
505 entries: &[MetricTagValueAllowlistEntry],
506) -> Result<(), InvalidMetricTagValueAllowlist> {
507 for (index, entry) in entries.iter().enumerate() {
508 let rule = index + 1;
509 if entry.metric_prefix.is_empty() {
510 return Err(InvalidMetricTagValueAllowlist(format!(
511 "metric tag value allow-list rule {rule} has an empty `metric_prefix`; configure a non-empty metric-name prefix"
512 )));
513 }
514 if entry.tag_name.is_empty() {
515 return Err(InvalidMetricTagValueAllowlist(format!(
516 "metric tag value allow-list rule {rule} for prefix '{}' has an empty `tag_name`; configure a non-empty tag name",
517 entry.metric_prefix
518 )));
519 }
520 if entry.tag_name.contains(':') {
521 return Err(InvalidMetricTagValueAllowlist(format!(
522 "metric tag value allow-list tag name '{}' contains ':'; configure only the tag key, without a colon or value",
523 entry.tag_name
524 )));
525 }
526 }
527
528 let mut sorted_entries = entries.iter().collect::<Vec<_>>();
529 sorted_entries.sort_unstable_by(|left, right| {
530 left.metric_prefix
531 .cmp(&right.metric_prefix)
532 .then_with(|| left.tag_name.cmp(&right.tag_name))
533 });
534
535 // After sorting by prefix and then tag, duplicate prefix/tag pairs are adjacent. Any distinct prefix that extends
536 // another prefix follows the complete group for the shorter prefix, so one adjacent pair also exposes that overlap.
537 for pair in sorted_entries.windows(2) {
538 let [left, right] = pair else {
539 unreachable!("a two-entry window must contain two entries");
540 };
541 if left.metric_prefix == right.metric_prefix {
542 if left.tag_name == right.tag_name {
543 return Err(InvalidMetricTagValueAllowlist(format!(
544 "metric prefix '{}' is configured more than once for tag '{}'; configure each prefix and tag pair only once",
545 left.metric_prefix, left.tag_name
546 )));
547 }
548 } else if right.metric_prefix.starts_with(&left.metric_prefix) {
549 return Err(InvalidMetricTagValueAllowlist(format!(
550 "overlapping metric prefixes '{}' and '{}' are configured; configure distinct prefixes that do not overlap",
551 left.metric_prefix, right.metric_prefix
552 )));
553 }
554 }
555
556 Ok(())
557}
558
559/// Action applied when a tag value is absent from its allow-list.
560#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
561#[serde(rename_all = "snake_case")]
562pub enum TagValueMismatchAction {
563 /// Removes the tag.
564 #[default]
565 Remove,
566 /// Replaces the tag value with the configured sentinel.
567 Replace,
568}
569
570/// Whether a tag-filterlist entry includes or excludes the listed tags.
571#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
572pub enum FilterAction {
573 Include,
574 #[default]
575 Exclude,
576}
577
578/// DogStatsD debug-log and verbose-log settings.
579#[derive(Clone, Debug, Default, PartialEq, Serialize)]
580pub struct DebugLog {
581 /// Whether the metric debug-log destination is added at startup.
582 ///
583 /// Defaults to `true`. Set this to `false` to remove the destination from the topology.
584 pub logging_enabled: bool,
585
586 /// Path of the DogStatsD metric debug log.
587 ///
588 /// Defaults to `None`, which selects the platform-specific Agent log path at startup.
589 /// Set a path to write the diagnostic log elsewhere.
590 pub log_file: Option<PathBuf>,
591
592 /// Number of rotated debug log files retained.
593 ///
594 /// Defaults to `3`. The file writer retains one rotated file when this is `0`.
595 pub log_file_max_rolls: usize,
596
597 /// Maximum size, in bytes, of the active debug log file before rotation.
598 ///
599 /// Defaults to `10,000,000` bytes. A value of `0` is accepted as the rotation threshold.
600 /// Set this based on the diagnostic data and disk space to retain.
601 pub log_file_max_size: u64,
602
603 /// Whether per-metric processing statistics are written to the debug log.
604 ///
605 /// Defaults to `false` and can change at runtime. Enable this when collecting metric-level diagnostics.
606 pub metrics_stats_enable: bool,
607
608 /// Whether per-packet parse errors are logged at debug rather than error level.
609 ///
610 /// Defaults to `false`. Enable this to reduce error-level log volume from malformed packets.
611 pub disable_verbose_logs: bool,
612}