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