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}