Skip to main content

saluki_components/sources/dogstatsd/
mod.rs

1//! DogStatsD source.
2//!
3//! # Missing
4//!
5//! - Create a health handle for each listener.
6//! - Handle UDS stream framing without treating EOF the same way as UDP and UDS datagram framing.
7//! - Track dispatch failures without depending on whether all events were already iterated.
8
9use std::{
10    collections::VecDeque,
11    future::Future,
12    num::NonZeroUsize,
13    path::PathBuf,
14    pin::Pin,
15    sync::{Arc, LazyLock},
16    time::{Duration, SystemTime, UNIX_EPOCH},
17};
18
19use async_trait::async_trait;
20use bytes::{Buf, BufMut};
21use bytesize::ByteSize;
22use resource_accounting::{MemoryBounds, MemoryBoundsBuilder, UsageExpr};
23use saluki_common::{
24    sync::shutdown::{ShutdownCoordinator, ShutdownHandle},
25    task::spawn_traced_named,
26};
27use saluki_config::{deserialize_space_separated_or_seq, GenericConfiguration};
28use saluki_context::{
29    origin::RawOrigin,
30    tags::{RawTags, RawTagsFilter},
31    TagsResolver,
32};
33use saluki_core::data_model::event::{
34    eventd::EventD,
35    metric::{Metric, MetricMetadata, MetricOrigin},
36    service_check::ServiceCheck,
37    Event, EventType,
38};
39use saluki_core::{
40    components::{sources::*, ComponentContext},
41    pooling::ElasticObjectPool,
42    topology::{interconnect::EventBufferManager, EventsBuffer, OutputDefinition},
43};
44use saluki_env::{workload::CaptureEntityResolver, WorkloadProvider};
45use saluki_error::{generic_error, ErrorContext as _, GenericError};
46use saluki_io::{
47    buf::{BytesBuffer, ClearableIoBuffer as _, FixedSizeVec},
48    deser::{
49        codec::dogstatsd::*,
50        framing::{Framer as _, FramingError, LengthDelimitedFramer},
51    },
52    net::{
53        listener::{Listener, ListenerError},
54        ConnectionAddress, ListenAddress, ProcessCredentials, ProcessIdentity, Stream,
55    },
56};
57use serde::{Deserialize, Deserializer};
58use serde_with::{serde_as, NoneAsEmptyString};
59use snafu::{ResultExt as _, Snafu};
60use stringtheory::MetaString;
61use tokio::{
62    pin, select,
63    time::{interval, MissedTickBehavior},
64};
65use tracing::{debug, error, info, trace, warn};
66
67mod forwarder;
68use self::forwarder::{PacketForwarder, PacketForwarderTarget};
69
70mod framer;
71use self::framer::{get_framer, DsdFramer};
72use crate::sources::dogstatsd::tags::{WellKnownTags, WellKnownTagsFilterPredicate};
73
74mod filters;
75use self::filters::EnablePayloadsFilter;
76
77mod io_buffer;
78use self::io_buffer::IoBufferManager;
79
80mod metrics;
81use self::metrics::{build_metrics, Metrics};
82
83mod replay;
84use self::replay::{CaptureRecord, CapturedTaggerHandle, TrafficCapture};
85pub use self::replay::{
86    DogStatsDCaptureAPIHandler, DogStatsDCaptureControl, DogStatsDReplayAPIHandler, DogStatsDReplayControl,
87    ReplaySession, TimestampResolution, TrafficCaptureReader, DEFAULT_REPLAY_LOOPS, REPLAY_CREDENTIALS_GID,
88};
89
90mod origin;
91use self::origin::{
92    mark_replay_process_id, origin_from_event_packet, origin_from_metric_packet, origin_from_service_check_packet,
93    DogStatsDOriginTagResolver, OriginEnrichmentConfiguration,
94};
95
96mod resolver;
97use self::resolver::ContextResolvers;
98
99mod tags;
100
101#[derive(Debug, Snafu)]
102#[snafu(context(suffix(false)))]
103enum Error {
104    #[snafu(display("Failed to create {} listener: {}", listener_type, source))]
105    FailedToCreateListener {
106        listener_type: &'static str,
107        source: ListenerError,
108    },
109
110    #[snafu(display("No listeners configured. Please specify a port (`dogstatsd_port`) or a socket path (`dogstatsd_socket` or `dogstatsd_stream_socket`) to enable a listener."))]
111    NoListenersConfigured,
112
113    #[snafu(display("Could not resolve bind_host '{}': {}", host, source))]
114    UnresolvableBindHost { host: String, source: std::io::Error },
115
116    #[snafu(display("bind_host '{}' resolved to zero IP addresses.", host))]
117    BindHostHasNoAddresses { host: String },
118}
119
120/// Baseline byte cost per interner entry, used to convert the Core Agent's entry-count-based
121/// `dogstatsd_string_interner_size` to a byte size.
122///
123/// 4096 entries × 512 bytes = 2 MiB, matching ADP's previous default.
124const INTERNER_BASELINE_BYTES_PER_ENTRY: u64 = 512;
125
126const fn default_buffer_size() -> usize {
127    8192
128}
129
130const fn default_buffer_count() -> usize {
131    128
132}
133
134const fn default_buffer_count_max() -> usize {
135    256
136}
137
138const fn default_port() -> u16 {
139    8125
140}
141
142const fn default_tcp_port() -> u16 {
143    0
144}
145
146const fn default_statsd_forward_port() -> u16 {
147    0
148}
149
150const fn default_socket_receive_buffer_size() -> usize {
151    0
152}
153
154const fn default_allow_context_heap_allocations() -> bool {
155    true
156}
157
158const fn default_no_aggregation_pipeline_support() -> bool {
159    true
160}
161
162const fn default_context_string_interner_entry_count() -> u64 {
163    4096
164}
165
166const fn default_cached_contexts_limit() -> usize {
167    500_000
168}
169
170const fn default_cached_tagsets_limit() -> usize {
171    500_000
172}
173
174const fn default_context_expiry_seconds() -> u64 {
175    20
176}
177
178const fn default_dogstatsd_permissive_decoding() -> bool {
179    true
180}
181
182const fn default_dogstatsd_minimum_sample_rate() -> f64 {
183    0.000000003845
184}
185
186const fn default_true() -> bool {
187    true
188}
189
190/// Returns the core Agent default SDDL applied to DogStatsD Windows named pipes.
191const fn default_windows_pipe_security_descriptor() -> &'static str {
192    "D:AI(A;;GA;;;WD)"
193}
194
195fn default_windows_pipe_security_descriptor_string() -> String {
196    default_windows_pipe_security_descriptor().to_string()
197}
198
199/// Controls which payload types are forwarded to the backend.
200#[derive(Deserialize)]
201#[cfg_attr(test, derive(PartialEq, serde::Serialize))]
202pub struct EnablePayloadsConfiguration {
203    /// Whether or not to enable sending series (counter/gauge/rate) payloads.
204    ///
205    /// Defaults to `true`.
206    #[serde(default = "default_true")]
207    pub series: bool,
208
209    /// Whether or not to enable sending sketch (distribution) payloads.
210    ///
211    /// Defaults to `true`.
212    #[serde(default = "default_true")]
213    pub sketches: bool,
214
215    /// Whether or not to enable sending event payloads.
216    ///
217    /// Defaults to `true`.
218    #[serde(default = "default_true")]
219    pub events: bool,
220
221    /// Whether or not to enable sending service check payloads.
222    ///
223    /// Defaults to `true`.
224    #[serde(default = "default_true")]
225    pub service_checks: bool,
226}
227
228impl Default for EnablePayloadsConfiguration {
229    fn default() -> Self {
230        Self {
231            series: true,
232            sketches: true,
233            events: true,
234            service_checks: true,
235        }
236    }
237}
238
239const MIN_CAPTURE_DEPTH: usize = 1024;
240
241const fn default_capture_depth() -> usize {
242    MIN_CAPTURE_DEPTH
243}
244
245const DOGSTATSD_CAPTURE_DIR: &str = "dsd_capture";
246
247fn deserialize_empty_metastring_as_none<'de, D>(deserializer: D) -> Result<Option<MetaString>, D::Error>
248where
249    D: Deserializer<'de>,
250{
251    let value = Option::<MetaString>::deserialize(deserializer)?;
252    Ok(value.filter(|host| !host.is_empty()))
253}
254
255#[derive(Deserialize, Default)]
256#[cfg_attr(test, derive(PartialEq, serde::Serialize))]
257struct DogStatsDTelemetryConfiguration {
258    /// Whether to break down DogStatsD processed-metric telemetry by UDS origin.
259    ///
260    /// When enabled, metric-message `dogstatsd.processed` telemetry includes an `origin` label derived from the
261    /// sender's UDS origin. This can add one telemetry series per origin and should primarily be used for diagnostics.
262    ///
263    /// Defaults to `false`.
264    #[serde(default)]
265    dogstatsd_origin: bool,
266}
267
268/// DogStatsD source.
269///
270/// Accepts metrics over TCP, UDP, or Unix Domain Sockets in the StatsD/DogStatsD format.
271#[serde_as]
272#[derive(Deserialize, Default)]
273#[cfg_attr(test, derive(derive_where::DeriveWhere, serde::Serialize))]
274#[cfg_attr(test, derive_where(PartialEq))]
275pub struct DogStatsDConfiguration {
276    /// The size of the buffer used to receive messages into, in bytes.
277    ///
278    /// Payloads can't exceed this size, or they will be truncated, leading to discarded messages.
279    ///
280    /// Defaults to 8192 bytes.
281    #[serde(rename = "dogstatsd_buffer_size", default = "default_buffer_size")]
282    buffer_size: usize,
283
284    /// The number of message buffers to allocate up front.
285    ///
286    /// This is the baseline pool size allocated at startup. The pool then grows on demand up to
287    /// `dogstatsd_buffer_count_max` as active stream connections need additional buffers. The default value should be
288    /// suitable for the majority of workloads.
289    ///
290    /// Defaults to 128.
291    #[serde(rename = "dogstatsd_buffer_count", default = "default_buffer_count")]
292    buffer_count: usize,
293
294    /// The maximum number of message buffers to allocate overall.
295    ///
296    /// The pool starts at `dogstatsd_buffer_count` buffers and grows on demand up to this limit as active stream
297    /// connections need them, which loosely correlates with how many messages can be received per second. This caps
298    /// memory growth. The default value should be suitable for the majority of workloads, but high-throughput or
299    /// high-fan-in workloads may consider increasing this value.
300    ///
301    /// The pool never holds fewer buffers than `dogstatsd_buffer_count`, so a value below the baseline is treated as
302    /// equal to it.
303    ///
304    /// Defaults to 256, or `dogstatsd_buffer_count` if that is larger.
305    #[serde(rename = "dogstatsd_buffer_count_max", default = "default_buffer_count_max")]
306    buffer_count_max: usize,
307
308    /// The port to listen on in UDP mode.
309    ///
310    /// If set to `0`, UDP isn't used.
311    ///
312    /// Defaults to 8125.
313    #[serde(rename = "dogstatsd_port", default = "default_port")]
314    port: u16,
315
316    /// The size of the DogStatsD UDP/UDS socket receive buffer, in bytes.
317    ///
318    /// If set to `0`, the OS default is used.
319    ///
320    /// Defaults to 0.
321    #[serde(rename = "dogstatsd_so_rcvbuf", default = "default_socket_receive_buffer_size")]
322    socket_receive_buffer_size: usize,
323
324    /// The port to listen on in TCP mode.
325    ///
326    /// If set to `0`, TCP isn't used.
327    ///
328    /// Defaults to 0.
329    #[serde(rename = "dogstatsd_tcp_port", default = "default_tcp_port")]
330    tcp_port: u16,
331
332    /// The host to forward framed DogStatsD messages to over UDP.
333    ///
334    /// Forwarding is enabled only when this value is non-empty and `statsd_forward_port` is non-zero. Setup failures
335    /// are logged, and send failures are tracked through telemetry.
336    ///
337    /// Defaults to unset.
338    #[serde(
339        rename = "statsd_forward_host",
340        default,
341        deserialize_with = "deserialize_empty_metastring_as_none"
342    )]
343    statsd_forward_host: Option<MetaString>,
344
345    /// The port to forward framed DogStatsD messages to over UDP.
346    ///
347    /// Forwarding is enabled only when this value is non-zero and `statsd_forward_host` is non-empty.
348    ///
349    /// Defaults to 0.
350    #[serde(rename = "statsd_forward_port", default = "default_statsd_forward_port")]
351    statsd_forward_port: u16,
352
353    /// The Unix domain socket path to listen on, in datagram mode.
354    ///
355    /// If not set, UDS (in datagram mode) isn't used.
356    ///
357    /// Defaults to unset.
358    #[serde(rename = "dogstatsd_socket", default)]
359    #[serde_as(as = "NoneAsEmptyString")]
360    socket_path: Option<String>,
361
362    /// The Unix domain socket path to listen on, in stream mode.
363    ///
364    /// If not set, UDS (in stream mode) isn't used.
365    ///
366    /// Defaults to unset.
367    #[serde(rename = "dogstatsd_stream_socket", default)]
368    #[serde_as(as = "NoneAsEmptyString")]
369    socket_stream_path: Option<String>,
370
371    /// Controls whether ADP logs oversized DogStatsD stream frames.
372    ///
373    /// When set to `true`, ADP emits a warning when a UDS stream frame exceeds the
374    /// configured DogStatsD buffer size. The frame is still rejected either way.
375    ///
376    /// Enable this when diagnosing clients that send oversized UDS stream frames.
377    ///
378    /// Defaults to `false`.
379    #[serde(rename = "dogstatsd_stream_log_too_big", default)]
380    stream_log_too_big: bool,
381
382    /// The Windows named pipe name to listen on.
383    ///
384    /// If set, ADP listens for DogStatsD stream traffic on `\\.\pipe\<name>` on Windows.
385    /// The listener is unsupported on non-Windows platforms.
386    ///
387    /// Defaults to unset.
388    #[serde(rename = "dogstatsd_pipe_name", default)]
389    #[serde_as(as = "NoneAsEmptyString")]
390    pipe_name: Option<String>,
391
392    /// Windows named pipe security descriptor.
393    ///
394    /// This SDDL descriptor is applied when creating the named pipe listener.
395    ///
396    /// Defaults to `D:AI(A;;GA;;;WD)`.
397    #[serde(
398        rename = "dogstatsd_windows_pipe_security_descriptor",
399        default = "default_windows_pipe_security_descriptor_string"
400    )]
401    windows_pipe_security_descriptor: String,
402
403    /// Whether ADP lowers DogStatsD parse-failure logs to debug level.
404    ///
405    /// When set to `true`, invalid metrics, events, and service checks still increment decode-failure telemetry, but
406    /// their parse-failure logs are emitted at debug level instead of warning level. Enable this to suppress noisy
407    /// parse-error logs from misbehaving clients.
408    ///
409    /// Defaults to `false`.
410    #[serde(rename = "dogstatsd_disable_verbose_logs", default)]
411    disable_verbose_logs: bool,
412
413    /// Listener types that require DogStatsD messages to be newline-terminated.
414    ///
415    /// Valid values are `udp`, `uds`, and `named_pipe`. Invalid values are ignored.
416    ///
417    /// Enable this when DogStatsD clients must reject packets or stream frames that don't end with a newline.
418    ///
419    /// Defaults to unset, which accepts the final message without a newline.
420    #[serde(
421        rename = "dogstatsd_eol_required",
422        default,
423        deserialize_with = "deserialize_space_separated_or_seq"
424    )]
425    eol_required: Vec<String>,
426
427    /// The host address to bind DogStatsD UDP and TCP listeners to.
428    ///
429    /// When set, UDP and TCP listeners bind to this address. Accepts either an IP literal (for example,
430    /// `192.168.1.50`, `::1`) or a hostname that resolves via DNS (for example, `agent.internal`).
431    /// Ignored when `dogstatsd_non_local_traffic` is `true`.
432    ///
433    /// Defaults to unset, which binds to `127.0.0.1`.
434    #[serde(rename = "bind_host", default)]
435    #[serde_as(as = "NoneAsEmptyString")]
436    bind_host: Option<String>,
437
438    /// Whether or not to listen for non-local traffic in UDP mode.
439    ///
440    /// If set to `true`, the listener will accept packets from any interface/address. Otherwise, the source will only
441    /// listen on the address specified by `bind_host`, or `127.0.0.1` if `bind_host` isn't set.
442    ///
443    /// Defaults to `false`.
444    #[serde(rename = "dogstatsd_non_local_traffic", default)]
445    non_local_traffic: bool,
446
447    /// Whether to autoscale UDP stream handlers using `SO_REUSEPORT`.
448    ///
449    /// When enabled on Linux, the DogStatsD source binds multiple UDP sockets to the configured port with
450    /// `SO_REUSEPORT`, allowing the kernel to load-balance incoming datagrams across independent stream handler
451    /// tasks. The number of sockets scales with available vCPUs: one stream handler base, plus one additional
452    /// per 8 vCPUs, capped at 4 total.
453    ///
454    /// Has no effect on non-Linux platforms because `SO_REUSEPORT` doesn't provide kernel-level load balancing
455    /// there; a warning is logged at startup if enabled outside of Linux.
456    ///
457    /// Enable this on multi-vCPU Linux deployments where UDP DogStatsD throughput is bottlenecked on a single
458    /// receive task.
459    ///
460    /// Defaults to `false`.
461    #[serde(rename = "dogstatsd_autoscale_udp_listeners", default)]
462    autoscale_udp_listeners: bool,
463
464    /// Whether or not to allow heap allocations when resolving contexts.
465    ///
466    /// When resolving contexts during parsing, the metric name and tags are interned to reduce memory usage. The
467    /// interner has a fixed size, however, which means some strings can fail to be interned if the interner is full.
468    /// When set to `true`, we allow these strings to be allocated on the heap like normal, but this can lead to
469    /// increased (unbounded) memory usage. When set to `false`, if the metric name and all of its tags can't be
470    /// interned, the metric is skipped.
471    ///
472    /// Defaults to `true`.
473    #[serde(
474        rename = "dogstatsd_allow_context_heap_allocs",
475        default = "default_allow_context_heap_allocations"
476    )]
477    allow_context_heap_allocations: bool,
478
479    /// Whether or not to enable support for no-aggregation pipelines.
480    ///
481    /// When enabled, this influences how metrics are parsed, specifically around user-provided metric timestamps. When
482    /// metric timestamps are present, it's used as a signal to any aggregation transforms that the metric shouldn't
483    /// be aggregated.
484    ///
485    /// Defaults to `true`.
486    #[serde(
487        rename = "dogstatsd_no_aggregation_pipeline",
488        default = "default_no_aggregation_pipeline_support"
489    )]
490    no_aggregation_pipeline_support: bool,
491
492    /// Number of entries for the string interner, as interpreted by the Core Datadog Agent.
493    ///
494    /// When `dogstatsd_string_interner_size_bytes` isn't set, this value is multiplied by 512 bytes per entry to
495    /// derive the interner byte size. This provides backwards compatibility for customers migrating configurations
496    /// from the Core Agent, where this setting represents an entry count rather than a byte size.
497    ///
498    /// Defaults to 4096 entries, which yields 2 MiB when converted.
499    #[serde(
500        rename = "dogstatsd_string_interner_size",
501        default = "default_context_string_interner_entry_count"
502    )]
503    context_string_interner_entry_count: u64,
504
505    /// Total size of the string interner used for contexts, in bytes.
506    ///
507    /// When set, this takes priority over `dogstatsd_string_interner_size`. This controls the amount of memory that
508    /// can be used to intern metric names and tags. If the interner is full, metrics with contexts that haven't
509    /// already been resolved may or may not be dropped, depending on the value of `allow_context_heap_allocations`.
510    #[serde(rename = "dogstatsd_string_interner_size_bytes", default)]
511    context_string_interner_size_bytes: Option<ByteSize>,
512
513    /// The maximum number of cached contexts to allow.
514    ///
515    /// This is the maximum number of resolved contexts that can be cached at any given time. This limit doesn't affect
516    /// the total number of contexts that can be _alive_ at any given time, which is dependent on the interner capacity
517    /// and whether or not heap allocations are allowed.
518    ///
519    /// Defaults to 500,000.
520    #[serde(
521        rename = "dogstatsd_cached_contexts_limit",
522        default = "default_cached_contexts_limit"
523    )]
524    cached_contexts_limit: usize,
525
526    /// The maximum number of cached tagsets to allow.
527    ///
528    /// This is the maximum number of resolved tagsets that can be cached at any given time. This limit doesn't affect
529    /// the total number of tagsets that can be _alive_ at any given time, which is dependent on the interner capacity
530    /// and whether or not heap allocations are allowed.
531    ///
532    /// Defaults to 500,000.
533    #[serde(rename = "dogstatsd_cached_tagsets_limit", default = "default_cached_tagsets_limit")]
534    cached_tagsets_limit: usize,
535
536    /// The number of seconds after which cached contexts will expire.
537    ///
538    /// Higher values allow for more effective caching for sparse metrics at the cost of increased memory usage.
539    ///
540    /// Defaults to 20 seconds.
541    #[serde(
542        rename = "dogstatsd_context_expiry_seconds",
543        default = "default_context_expiry_seconds"
544    )]
545    context_expiry_seconds: u64,
546
547    /// Whether or not to enable permissive mode in the decoder.
548    ///
549    /// Permissive mode allows the decoder to relax its strictness around the allowed payloads, which lets it match the
550    /// decoding behavior of the Datadog Agent.
551    ///
552    /// Defaults to `true`.
553    #[serde(
554        rename = "dogstatsd_permissive_decoding",
555        default = "default_dogstatsd_permissive_decoding"
556    )]
557    permissive_decoding: bool,
558
559    /// The minimum sample rate allowed for metrics.
560    ///
561    /// When metrics are sent with a sample rate _lower_ than this value then it will be clamped to this value. This is
562    /// done in order to ensure an upper bound on how many equivalent samples are tracked for the metric, as high sample
563    /// rates (very small numbers, such as `0.00000001`) can lead to large memory growth.
564    ///
565    /// A warning log will be emitted when clamping occurs, as this represents an effective loss of metric samples.
566    ///
567    /// Defaults to `0.000000003845`. (~260M samples)
568    #[serde(
569        rename = "dogstatsd_minimum_sample_rate",
570        default = "default_dogstatsd_minimum_sample_rate"
571    )]
572    minimum_sample_rate: f64,
573
574    /// Which payload types to forward to the backend.
575    #[serde(rename = "enable_payloads", default)]
576    enable_payloads: EnablePayloadsConfiguration,
577
578    /// Configuration related to origin detection and enrichment.
579    #[serde(flatten, default)]
580    origin_enrichment: OriginEnrichmentConfiguration,
581
582    /// Configuration related to DogStatsD telemetry.
583    #[serde(default)]
584    telemetry: DogStatsDTelemetryConfiguration,
585
586    /// Workload provider to utilize for origin detection/enrichment.
587    #[serde(skip)]
588    #[cfg_attr(test, derive_where(skip))]
589    workload_provider: Option<Arc<dyn WorkloadProvider + Send + Sync>>,
590
591    /// Resolver to use for mapping live sender PIDs to container entities during traffic capture.
592    #[serde(skip, default)]
593    #[cfg_attr(test, derive_where(skip))]
594    capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
595
596    /// Additional tags to add to all metrics.
597    #[serde(rename = "dogstatsd_tags", default)]
598    additional_tags: Vec<String>,
599
600    /// The directory where DogStatsD capture files are written by default.
601    ///
602    /// When set to an empty path, the source attempts to derive the directory from `run_path` by appending
603    /// `dsd_capture`. If neither value is available, callers must provide an explicit capture path when starting a
604    /// capture session.
605    ///
606    /// Defaults to empty.
607    #[serde(rename = "dogstatsd_capture_path", default)]
608    capture_path: PathBuf,
609
610    /// The maximum number of captured packets that can be queued for persistence.
611    ///
612    /// This controls the depth of the in-process capture queue. Values below `1024` are raised to `1024` before the
613    /// capture writer starts, preventing a zero-depth rendezvous channel from serializing DogStatsD stream handlers
614    /// behind capture persistence.
615    ///
616    /// Defaults to `1024`.
617    #[serde(rename = "dogstatsd_capture_depth", default = "default_capture_depth")]
618    capture_depth: usize,
619
620    #[serde(skip, default)]
621    #[cfg_attr(test, derive_where(skip))]
622    capture_control: DogStatsDCaptureControl,
623
624    #[serde(skip, default)]
625    #[cfg_attr(test, derive_where(skip))]
626    replay_control: DogStatsDReplayControl,
627
628    /// Provider kind tag appended to all metrics as `provider_kind:<value>`.
629    ///
630    /// Set via `DD_PROVIDER_KIND` by the Helm chart on GKE Autopilot (`gke-autopilot`) and GKE on
631    /// Google Distributed Cloud (`gke-gdc`). When empty or absent, no tag is added.
632    ///
633    /// Defaults to `""` (disabled).
634    #[serde(default)]
635    provider_kind: String,
636}
637
638#[derive(Clone, Copy, Default)]
639struct EolRequired {
640    udp: bool,
641    uds: bool,
642    named_pipe: bool,
643}
644
645impl EolRequired {
646    fn from_config_values(values: &[String]) -> Self {
647        let mut eol_required = Self::default();
648
649        for value in values {
650            match value.as_str() {
651                "udp" => eol_required.udp = true,
652                "uds" => eol_required.uds = true,
653                "named_pipe" => eol_required.named_pipe = true,
654                _ => warn!(
655                    value,
656                    "Invalid dogstatsd_eol_required value. Expected 'udp', 'uds', or 'named_pipe'."
657                ),
658            }
659        }
660
661        eol_required
662    }
663
664    fn for_listener(&self, listen_addr: &ListenAddress) -> bool {
665        match listen_addr {
666            ListenAddress::Udp(_) => self.udp,
667            ListenAddress::Tcp(_) => false,
668            ListenAddress::Unixgram(_) | ListenAddress::Unix(_) => self.uds,
669            ListenAddress::NamedPipe { .. } => self.named_pipe,
670        }
671    }
672}
673
674/// Resolves a `bind_host` string to an `IpAddr`.
675///
676/// Accepts either an IP literal (no DNS required) or a hostname (resolved via async DNS). Returns
677/// `UnresolvableBindHost` if the lookup fails, or `BindHostHasNoAddresses` if it succeeds but
678/// returns no addresses.
679async fn resolve_bind_host(host: &str) -> Result<std::net::IpAddr, Error> {
680    let mut addrs = tokio::net::lookup_host((host, 0u16))
681        .await
682        .context(UnresolvableBindHost { host: host.to_string() })?;
683    addrs
684        .next()
685        .map(|sa| sa.ip())
686        .ok_or_else(|| Error::BindHostHasNoAddresses { host: host.to_string() })
687}
688
689impl DogStatsDConfiguration {
690    /// Creates a new `DogStatsDConfiguration` from the given configuration.
691    pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
692        let mut dogstatsd_config: Self = config.as_typed()?;
693        dogstatsd_config.fix_empty_capture_path(config);
694        dogstatsd_config.fix_capture_depth();
695        Ok(dogstatsd_config)
696    }
697
698    /// Gets both the `additional_tags` and any others specified by other configuration fields, such as `provider_kind`.
699    fn additional_tags(&self) -> Vec<String> {
700        if self.provider_kind.is_empty() {
701            return self.additional_tags.clone();
702        }
703
704        let mut tags = self.additional_tags.clone();
705        tags.push(format!("provider_kind:{}", self.provider_kind.clone()));
706        tags
707    }
708
709    fn fix_capture_depth(&mut self) {
710        self.capture_depth = self.capture_depth.max(MIN_CAPTURE_DEPTH);
711    }
712
713    /// Returns the effective string interner size in bytes.
714    ///
715    /// If `dogstatsd_string_interner_size_bytes` is set, it's used directly. Otherwise,
716    /// `dogstatsd_string_interner_size` (an entry count) is multiplied by 512 bytes per entry to derive the byte
717    /// size.
718    fn effective_context_string_interner_bytes(&self) -> ByteSize {
719        match self.context_string_interner_size_bytes {
720            Some(explicit_bytes) => explicit_bytes,
721            None => {
722                saluki_antithesis::always_le!(
723                    self.context_string_interner_entry_count,
724                    u64::MAX / INTERNER_BASELINE_BYTES_PER_ENTRY,
725                    "dogstatsd interner byte-size multiply does not overflow",
726                    { "entry_count": self.context_string_interner_entry_count }
727                );
728                ByteSize::b(
729                    self.context_string_interner_entry_count
730                        .saturating_mul(INTERNER_BASELINE_BYTES_PER_ENTRY),
731                )
732            }
733        }
734    }
735
736    fn eol_required(&self) -> EolRequired {
737        EolRequired::from_config_values(&self.eol_required)
738    }
739
740    fn statsd_forward_target(&self) -> Option<(&MetaString, u16)> {
741        let host = self.statsd_forward_host.as_ref()?;
742        if self.statsd_forward_port == 0 {
743            return None;
744        }
745
746        Some((host, self.statsd_forward_port))
747    }
748
749    fn packet_forwarder_target(&self) -> Option<PacketForwarderTarget> {
750        let (host, port) = self.statsd_forward_target()?;
751        Some(PacketForwarderTarget::new(host.clone(), port))
752    }
753
754    /// Returns the number of UDP stream handlers to spawn, derived from `dogstatsd_autoscale_udp_listeners` and
755    /// the number of available vCPUs.
756    ///
757    /// Returns `None` when autoscaling is disabled, which keeps the legacy single-socket behavior. The platform
758    /// gate for `SO_REUSEPORT` lives inside the listener—this method intentionally stays platform-agnostic.
759    fn udp_streams_to_yield(&self) -> Option<NonZeroUsize> {
760        if !self.autoscale_udp_listeners {
761            return None;
762        }
763
764        #[cfg(not(target_os = "linux"))]
765        if self.autoscale_udp_listeners {
766            warn!("UDP stream handler autoscaling not supported on non-Linux platforms. Default to single stream handler.");
767            return None;
768        }
769
770        let vcpus = std::thread::available_parallelism().map(NonZeroUsize::get).unwrap_or(1);
771        let streams = (1 + vcpus / 8).min(4);
772        NonZeroUsize::new(streams)
773    }
774
775    /// Returns the effective maximum size of the I/O buffer pool.
776    ///
777    /// The pool can never hold fewer buffers than the configured baseline, so a `dogstatsd_buffer_count_max` below
778    /// `dogstatsd_buffer_count` (including a legacy config that only raised `dogstatsd_buffer_count`) is treated as
779    /// equal to the baseline rather than reducing capacity.
780    fn effective_max_buffer_count(&self) -> usize {
781        self.buffer_count_max.max(self.buffer_count)
782    }
783
784    /// Sets the workload provider to use for configuring origin detection/enrichment.
785    ///
786    /// A workload provider must be set otherwise origin detection/enrichment won't be enabled.
787    ///
788    /// Defaults to unset.
789    pub fn with_workload_provider<W>(mut self, workload_provider: W) -> Self
790    where
791        W: WorkloadProvider + Send + Sync + 'static,
792    {
793        self.workload_provider = Some(Arc::new(workload_provider));
794        self
795    }
796
797    /// Sets the resolver to use for mapping live sender PIDs while capturing DogStatsD traffic.
798    ///
799    /// This resolver is intentionally configured separately from the workload provider because capture only needs a
800    /// narrow live-PID lookup, while normal origin enrichment uses the broader workload provider contract.
801    ///
802    /// Defaults to unset.
803    pub fn with_capture_entity_resolver<R>(mut self, capture_entity_resolver: R) -> Self
804    where
805        R: CaptureEntityResolver + Send + Sync + 'static,
806    {
807        self.capture_entity_resolver = Some(Arc::new(capture_entity_resolver));
808        self
809    }
810
811    /// Returns the shared control handle for DogStatsD traffic capture.
812    pub fn capture_control(&self) -> DogStatsDCaptureControl {
813        self.capture_control.clone()
814    }
815
816    /// Returns an HTTP API handler exposing the DogStatsD capture control surface.
817    pub fn capture_api_handler(&self) -> DogStatsDCaptureAPIHandler {
818        DogStatsDCaptureAPIHandler::new(self.capture_control.clone())
819    }
820
821    /// Returns the shared control handle for DogStatsD traffic replay.
822    pub fn replay_control(&self) -> DogStatsDReplayControl {
823        self.replay_control.clone()
824    }
825
826    /// Returns an HTTP API handler exposing the DogStatsD replay control surface.
827    pub fn replay_api_handler(&self) -> DogStatsDReplayAPIHandler {
828        DogStatsDReplayAPIHandler::new(self.replay_control.clone())
829    }
830
831    fn fix_empty_capture_path(&mut self, config: &GenericConfiguration) {
832        if self.capture_path.parent().is_some() {
833            return;
834        }
835
836        let capture_path = match config.try_get_typed::<PathBuf>("run_path") {
837            Ok(Some(mut run_path)) => {
838                run_path.push(DOGSTATSD_CAPTURE_DIR);
839                run_path
840            }
841            Ok(None) => {
842                debug!(
843                    "`dogstatsd_capture_path` and `run_path` were empty. Default DogStatsD capture path is unavailable."
844                );
845                return;
846            }
847            Err(e) => {
848                debug!(
849                    error = %e,
850                    "Failed to read `run_path` from configuration. Default DogStatsD capture path is unavailable."
851                );
852                return;
853            }
854        };
855
856        self.capture_path = capture_path;
857    }
858
859    /// Using the current configuration, determines which listeners should be created and adds an address for each into
860    /// a `Vec<ListenAddress>`. This function has no side effects so that it can be unit tested whereas build_listeners`
861    /// actually binds the listeners on the system.
862    ///
863    /// `bind_host` is the pre-resolved IP that UDP and TCP listeners should bind to (provided by
864    /// `resolve_bind_host`). Precedence matches the Agent:
865    ///   - `non_local_traffic=true` → `0.0.0.0` (`bind_host` ignored)
866    ///   - `bind_host=Some(ip)`     → `ip`
867    ///   - `bind_host=None`         → `127.0.0.1`
868    fn build_addresses(&self, bind_host: Option<std::net::IpAddr>) -> Vec<ListenAddress> {
869        let bind_ip: std::net::IpAddr = if self.non_local_traffic {
870            [0, 0, 0, 0].into()
871        } else {
872            bind_host.unwrap_or_else(|| [127, 0, 0, 1].into())
873        };
874
875        let mut addresses: Vec<ListenAddress> = Vec::new();
876
877        if self.port != 0 {
878            addresses.push(ListenAddress::Udp(std::net::SocketAddr::new(bind_ip, self.port)));
879        }
880
881        if self.tcp_port != 0 {
882            addresses.push(ListenAddress::Tcp(std::net::SocketAddr::new(bind_ip, self.tcp_port)));
883        }
884
885        if let Some(socket_path) = &self.socket_path {
886            addresses.push(ListenAddress::Unixgram(socket_path.into()));
887        }
888
889        if let Some(socket_stream_path) = &self.socket_stream_path {
890            addresses.push(ListenAddress::Unix(socket_stream_path.into()));
891        }
892
893        if let Some(pipe_name) = &self.pipe_name {
894            addresses.push(ListenAddress::named_pipe_with_input_buffer_size(
895                pipe_name,
896                &self.windows_pipe_security_descriptor,
897                self.buffer_size as u32,
898            ));
899        }
900
901        addresses
902    }
903
904    fn uds_origin_detection_unsupported_on_platform(&self, addresses: &[ListenAddress]) -> bool {
905        self.origin_enrichment.enabled()
906            && cfg!(not(target_os = "linux"))
907            && addresses
908                .iter()
909                .any(|address| matches!(address, ListenAddress::Unixgram(_) | ListenAddress::Unix(_)))
910    }
911
912    fn warn_if_uds_origin_detection_unsupported(&self, addresses: &[ListenAddress]) {
913        if self.uds_origin_detection_unsupported_on_platform(addresses) {
914            warn!(
915                "DogStatsD UDS origin detection is enabled, but PID-based Unix socket credentials are unsupported on \
916                 this platform. Metrics are accepted without PID-based origin enrichment."
917            );
918        }
919    }
920
921    /// Builds the appropriate `Listener` objects.
922    async fn build_listeners(&self) -> Result<Vec<Listener>, Error> {
923        // Resolve `bind_host` to an IP (via DNS if needed). Skip the lookup when
924        // `non_local_traffic=true` since `bind_host` is ignored in that branch—matches Go's
925        // laziness and avoids failing startup on an unresolvable hostname that wouldn't be used.
926        let bind_host: Option<std::net::IpAddr> = if self.non_local_traffic {
927            None
928        } else {
929            match &self.bind_host {
930                Some(host) => Some(resolve_bind_host(host).await?),
931                None => None,
932            }
933        };
934
935        let addresses = self.build_addresses(bind_host);
936        self.warn_if_uds_origin_detection_unsupported(&addresses);
937        let mut listeners = Vec::new();
938        let socket_receive_buffer_size =
939            (self.socket_receive_buffer_size != 0).then_some(self.socket_receive_buffer_size);
940        let udp_streams_to_yield = self.udp_streams_to_yield();
941        for address in addresses {
942            let listener_type = address.listener_type();
943            let listener_streams = matches!(address, ListenAddress::Udp(_))
944                .then_some(udp_streams_to_yield)
945                .flatten();
946            let listener = Listener::from_listen_address(address, listener_streams)
947                .await
948                .context(FailedToCreateListener { listener_type })?
949                .with_receive_buffer_size(socket_receive_buffer_size);
950
951            listeners.push(listener);
952        }
953        Ok(listeners)
954    }
955}
956
957#[async_trait]
958impl SourceBuilder for DogStatsDConfiguration {
959    async fn build(&self, context: ComponentContext) -> Result<Box<dyn Source + Send>, GenericError> {
960        let listeners = self.build_listeners().await?;
961        if listeners.is_empty() {
962            return Err(Error::NoListenersConfigured.into());
963        }
964
965        // Every listener requires at least one I/O buffer to ensure that all listeners can be serviced without
966        // deadlocking any of the others. Connectionless listeners retain their buffer for the lifetime of the stream,
967        // so multi-socket UDP listeners require one buffer per yielded socket.
968        let min_buffers: usize = listeners.iter().map(Listener::min_buffer_reservation).sum();
969        let max_buffers = self.effective_max_buffer_count();
970        if max_buffers < min_buffers {
971            return Err(generic_error!(
972                "The maximum I/O buffer count ({}) must be at least {} to service all configured listeners.",
973                max_buffers,
974                min_buffers,
975            ));
976        }
977
978        let origin_detection_enabled = self.origin_enrichment.enabled();
979        // Single CapturedTaggerHandle is cloned to both the resolver (reader of the captured store) and the replay
980        // control surface (writer). Both sides reference the same atomic slot.
981        let captured_tagger = CapturedTaggerHandle::new();
982
983        let maybe_origin_tags_resolver = self.workload_provider.clone().map(|provider| {
984            DogStatsDOriginTagResolver::new(self.origin_enrichment.clone(), provider, captured_tagger.clone())
985        });
986        let context_resolvers = ContextResolvers::new(self, &context, maybe_origin_tags_resolver)
987            .error_context("Failed to create context resolvers.")?;
988
989        let codec_config = DogStatsDCodecConfiguration::default()
990            .with_timestamps(self.no_aggregation_pipeline_support)
991            .with_permissive_mode(self.permissive_decoding)
992            .with_minimum_sample_rate(self.minimum_sample_rate)
993            .with_client_origin_detection(self.origin_enrichment.origin_detection_client);
994
995        let codec = DogStatsDCodec::from_configuration(codec_config);
996        let eol_required = self.eol_required();
997
998        let enable_payloads_filter = EnablePayloadsFilter::default()
999            .with_allow_series(self.enable_payloads.series)
1000            .with_allow_sketches(self.enable_payloads.sketches)
1001            .with_allow_events(self.enable_payloads.events)
1002            .with_allow_service_checks(self.enable_payloads.service_checks);
1003        let traffic_capture = TrafficCapture::with_workload_provider(
1004            self.capture_path.clone(),
1005            self.capture_depth.max(MIN_CAPTURE_DEPTH),
1006            self.workload_provider.clone(),
1007        );
1008        self.capture_control.bind(traffic_capture.clone());
1009        let packet_forwarder_target = self.packet_forwarder_target();
1010
1011        self.replay_control.bind(captured_tagger);
1012
1013        // The pool allocates `buffer_count` buffers up front and may grow on demand up to `max_buffers`. The effective
1014        // maximum is never below the baseline, so configs that only raise `dogstatsd_buffer_count` keep their full
1015        // capacity instead of being silently reduced to the `dogstatsd_buffer_count_max` default.
1016        let (io_buffer_pool, io_buffer_pool_shrinker) =
1017            build_io_buffer_pool(self.buffer_count, max_buffers, self.buffer_size);
1018
1019        Ok(Box::new(DogStatsD {
1020            listeners,
1021            io_buffer_pool,
1022            io_buffer_pool_shrinker: Box::pin(io_buffer_pool_shrinker),
1023            codec,
1024            context_resolvers,
1025            enabled_filter: enable_payloads_filter,
1026            origin_detection_enabled,
1027            origin_telemetry_enabled: self.telemetry.dogstatsd_origin,
1028            stream_log_too_big: self.stream_log_too_big,
1029            disable_verbose_logs: self.disable_verbose_logs,
1030            eol_required,
1031            additional_tags: self.additional_tags().into(),
1032            capture_entity_resolver: self.capture_entity_resolver.clone(),
1033            traffic_capture,
1034            packet_forwarder_target,
1035        }))
1036    }
1037
1038    fn outputs(&self) -> &[OutputDefinition<EventType>] {
1039        static OUTPUTS: LazyLock<Vec<OutputDefinition<EventType>>> = LazyLock::new(|| {
1040            vec![
1041                OutputDefinition::named_output("metrics", EventType::Metric),
1042                OutputDefinition::named_output("events", EventType::EventD),
1043                OutputDefinition::named_output("service_checks", EventType::ServiceCheck),
1044            ]
1045        });
1046        &OUTPUTS
1047    }
1048}
1049
1050impl MemoryBounds for DogStatsDConfiguration {
1051    fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
1052        let additional_buffers = self.effective_max_buffer_count().saturating_sub(self.buffer_count);
1053        let adjusted_buffer_size = get_adjusted_buffer_size(self.buffer_size);
1054
1055        builder
1056            .minimum()
1057            // Capture the size of the heap allocation when the component is built.
1058            .with_single_value::<DogStatsD>("source struct")
1059            // We allocate the baseline buffer pool up front.
1060            .with_expr(UsageExpr::product(
1061                "buffers",
1062                UsageExpr::config("dogstatsd_buffer_count", self.buffer_count),
1063                UsageExpr::config("dogstatsd_buffer_size", adjusted_buffer_size),
1064            ))
1065            // We also allocate the backing storage for the string interner up front, which is used by our context
1066            // resolver.
1067            .with_expr(UsageExpr::config(
1068                "dogstatsd_string_interner_size_bytes",
1069                self.effective_context_string_interner_bytes().as_u64() as usize,
1070            ));
1071
1072        // The pool can grow on demand up to its maximum, so account for the additional headroom as firm usage.
1073        builder.firm().with_expr(UsageExpr::product(
1074            "elastic buffers",
1075            UsageExpr::constant("dogstatsd_buffer_count_max_extra", additional_buffers),
1076            UsageExpr::config("dogstatsd_buffer_size", adjusted_buffer_size),
1077        ));
1078    }
1079}
1080
1081/// DogStatsD source.
1082pub struct DogStatsD {
1083    listeners: Vec<Listener>,
1084    io_buffer_pool: ElasticObjectPool<BytesBuffer>,
1085    io_buffer_pool_shrinker: Pin<Box<dyn Future<Output = ()> + Send>>,
1086    codec: DogStatsDCodec,
1087    context_resolvers: ContextResolvers,
1088    enabled_filter: EnablePayloadsFilter,
1089    origin_detection_enabled: bool,
1090    origin_telemetry_enabled: bool,
1091    stream_log_too_big: bool,
1092    disable_verbose_logs: bool,
1093    eol_required: EolRequired,
1094    additional_tags: Arc<[String]>,
1095    capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
1096    traffic_capture: TrafficCapture,
1097    packet_forwarder_target: Option<PacketForwarderTarget>,
1098}
1099
1100struct ListenerContext {
1101    shutdown_handle: ShutdownHandle,
1102    listener: Listener,
1103    io_buffer_pool: ElasticObjectPool<BytesBuffer>,
1104    codec: DogStatsDCodec,
1105    context_resolvers: ContextResolvers,
1106    origin_detection_enabled: bool,
1107    origin_telemetry_enabled: bool,
1108    stream_log_too_big: bool,
1109    disable_verbose_logs: bool,
1110    eol_required: EolRequired,
1111    additional_tags: Arc<[String]>,
1112    capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
1113    traffic_capture: TrafficCapture,
1114    packet_forwarder_target: Option<PacketForwarderTarget>,
1115}
1116
1117struct HandlerContext {
1118    listen_addr: ListenAddress,
1119    framer: DsdFramer,
1120    codec: DogStatsDCodec,
1121    io_buffer_pool: ElasticObjectPool<BytesBuffer>,
1122    metrics: Metrics,
1123    context_resolvers: ContextResolvers,
1124    origin_detection_enabled: bool,
1125    stream_log_too_big: bool,
1126    disable_verbose_logs: bool,
1127    additional_tags: Arc<[String]>,
1128    capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
1129    traffic_capture: TrafficCapture,
1130    packet_forwarder: Option<PacketForwarder>,
1131}
1132
1133#[async_trait]
1134impl Source for DogStatsD {
1135    async fn run(mut self: Box<Self>, mut context: SourceContext) -> Result<(), GenericError> {
1136        let global_shutdown = context.take_shutdown_handle();
1137        pin!(global_shutdown);
1138
1139        let mut health = context.take_health_handle();
1140
1141        let mut listener_shutdown_coordinator = ShutdownCoordinator::default();
1142        spawn_traced_named(
1143            "dogstatsd-io-buffer-pool-shrinker",
1144            process_io_buffer_pool_shrinker(self.io_buffer_pool_shrinker, listener_shutdown_coordinator.register()),
1145        );
1146
1147        // For each listener, spawn a dedicated task to run it.
1148        for listener in self.listeners {
1149            let task_name = format!("dogstatsd-listener-{}", listener.listen_address().listener_type());
1150
1151            // TODO: Create a health handle for each listener.
1152            //
1153            // We need to rework `HealthRegistry` to look a little more like `ComponentRegistry` so that we can have it
1154            // already be scoped properly, otherwise all we can do here at present is either have a relative name, like
1155            // `uds-stream`, or try and hardcode the full component name, which we will inevitably forget to update if
1156            // we tweak the topology configuration, etc.
1157            let listener_context = ListenerContext {
1158                shutdown_handle: listener_shutdown_coordinator.register(),
1159                listener,
1160                io_buffer_pool: self.io_buffer_pool.clone(),
1161                codec: self.codec.clone(),
1162                context_resolvers: self.context_resolvers.clone(),
1163                origin_detection_enabled: self.origin_detection_enabled,
1164                origin_telemetry_enabled: self.origin_telemetry_enabled,
1165                stream_log_too_big: self.stream_log_too_big,
1166                disable_verbose_logs: self.disable_verbose_logs,
1167                eol_required: self.eol_required,
1168                additional_tags: self.additional_tags.clone(),
1169                capture_entity_resolver: self.capture_entity_resolver.clone(),
1170                traffic_capture: self.traffic_capture.clone(),
1171                packet_forwarder_target: self.packet_forwarder_target.clone(),
1172            };
1173
1174            spawn_traced_named(
1175                task_name,
1176                process_listener(context.clone(), listener_context, self.enabled_filter),
1177            );
1178        }
1179
1180        health.mark_ready();
1181        debug!("DogStatsD source started.");
1182
1183        // Wait for the global shutdown signal, then notify listeners to shutdown.
1184        //
1185        // We also handle liveness here, which doesn't really matter for _this_ task, since the real work is happening
1186        // in the listeners, but we need to satisfy the health checker.
1187        loop {
1188            select! {
1189                _ = &mut global_shutdown => {
1190                    debug!("Received shutdown signal.");
1191                    break
1192                },
1193                _ = health.live() => continue,
1194            }
1195        }
1196
1197        debug!("Stopping DogStatsD source...");
1198
1199        listener_shutdown_coordinator.shutdown_and_wait().await;
1200
1201        debug!("DogStatsD source stopped.");
1202
1203        Ok(())
1204    }
1205}
1206
1207async fn process_io_buffer_pool_shrinker(
1208    io_buffer_pool_shrinker: Pin<Box<dyn Future<Output = ()> + Send>>, shutdown_handle: ShutdownHandle,
1209) {
1210    pin!(shutdown_handle);
1211
1212    select! {
1213        _ = &mut shutdown_handle => {
1214            debug!("I/O buffer pool shrinker received shutdown signal.");
1215        },
1216        _ = io_buffer_pool_shrinker => {
1217            debug!("I/O buffer pool shrinker stopped.");
1218        },
1219    }
1220}
1221
1222fn build_io_buffer_pool(
1223    min_buffers: usize, max_buffers: usize, buffer_size: usize,
1224) -> (ElasticObjectPool<BytesBuffer>, impl Future<Output = ()> + Send) {
1225    saluki_antithesis::always_le!(
1226        buffer_size,
1227        usize::MAX - 4,
1228        "dogstatsd buffer size add does not overflow",
1229        { "buffer_size": buffer_size }
1230    );
1231    let adjusted_buffer_size = get_adjusted_buffer_size(buffer_size);
1232    ElasticObjectPool::with_builder("dsd_packet_bufs", min_buffers, max_buffers, move || {
1233        FixedSizeVec::with_capacity(adjusted_buffer_size)
1234    })
1235}
1236
1237async fn process_listener(
1238    source_context: SourceContext, listener_context: ListenerContext, enabled_filter: EnablePayloadsFilter,
1239) {
1240    let ListenerContext {
1241        shutdown_handle,
1242        mut listener,
1243        io_buffer_pool,
1244        codec,
1245        context_resolvers,
1246        origin_detection_enabled,
1247        origin_telemetry_enabled,
1248        stream_log_too_big,
1249        disable_verbose_logs,
1250        eol_required,
1251        additional_tags,
1252        capture_entity_resolver,
1253        traffic_capture,
1254        packet_forwarder_target,
1255    } = listener_context;
1256
1257    pin!(shutdown_handle);
1258
1259    let listen_addr = listener.listen_address().clone();
1260    let metrics = build_metrics(
1261        &listen_addr,
1262        source_context.component_context(),
1263        origin_telemetry_enabled,
1264    );
1265    let packet_forwarder = packet_forwarder_target
1266        .as_ref()
1267        .map(|target| target.to_forwarder(metrics.clone()));
1268    if let Some(packet_forwarder) = &packet_forwarder {
1269        packet_forwarder.spawn_connect();
1270    }
1271
1272    let mut stream_shutdown_coordinator = ShutdownCoordinator::default();
1273
1274    info!(%listen_addr, "DogStatsD listener started.");
1275
1276    loop {
1277        select! {
1278            _ = &mut shutdown_handle => {
1279                debug!(%listen_addr, "Received shutdown signal. Waiting for existing stream handlers to finish...");
1280                break;
1281            }
1282            result = listener.accept() => match result {
1283                Ok(stream) => {
1284                    debug!(%listen_addr, "Spawning new stream handler.");
1285
1286                    let handler_context = HandlerContext {
1287                        listen_addr: listen_addr.clone(),
1288                        framer: get_framer(&listen_addr, eol_required.for_listener(&listen_addr)),
1289                        codec: codec.clone(),
1290                        io_buffer_pool: io_buffer_pool.clone(),
1291                        metrics: metrics.clone(),
1292                        context_resolvers: context_resolvers.clone(),
1293                        origin_detection_enabled,
1294                        stream_log_too_big,
1295                        disable_verbose_logs,
1296                        additional_tags: additional_tags.clone(),
1297                        capture_entity_resolver: capture_entity_resolver.clone(),
1298                        traffic_capture: traffic_capture.clone(),
1299                        packet_forwarder: packet_forwarder.clone(),
1300                    };
1301
1302                    let task_name = format!(
1303                        "dogstatsd-stream-handler-{}",
1304                        listen_addr.listener_type(),
1305                    );
1306                    spawn_traced_named(task_name, process_stream(stream, source_context.clone(), handler_context, stream_shutdown_coordinator.register(), enabled_filter));
1307                }
1308                Err(e) => {
1309                    error!(%listen_addr, error = %e, "Failed to accept connection. Stopping listener.");
1310                    break
1311                }
1312            }
1313        }
1314    }
1315
1316    stream_shutdown_coordinator.shutdown_and_wait().await;
1317
1318    info!(%listen_addr, "DogStatsD listener stopped.");
1319}
1320
1321async fn process_stream(
1322    stream: Stream, source_context: SourceContext, handler_context: HandlerContext, shutdown_handle: ShutdownHandle,
1323    enabled_filter: EnablePayloadsFilter,
1324) {
1325    select! {
1326        _ = shutdown_handle => {
1327            debug!("Stream handler received shutdown signal.");
1328        },
1329        _ = drive_stream(stream, source_context, handler_context, enabled_filter) => {},
1330    }
1331}
1332
1333fn origin_detection_failed_for_telemetry(
1334    origin_detection_enabled: bool, bytes_read: usize, peer_addr: &ConnectionAddress,
1335) -> bool {
1336    origin_detection_enabled && bytes_read > 0 && peer_addr.has_process_credential_telemetry_error()
1337}
1338
1339async fn drive_stream(
1340    mut stream: Stream, source_context: SourceContext, handler_context: HandlerContext,
1341    enabled_filter: EnablePayloadsFilter,
1342) {
1343    let HandlerContext {
1344        listen_addr,
1345        mut framer,
1346        codec,
1347        io_buffer_pool,
1348        metrics,
1349        mut context_resolvers,
1350        origin_detection_enabled,
1351        stream_log_too_big,
1352        disable_verbose_logs,
1353        additional_tags,
1354        capture_entity_resolver,
1355        traffic_capture,
1356        packet_forwarder,
1357    } = handler_context;
1358
1359    debug!(%listen_addr, "Stream handler started.");
1360
1361    if !stream.is_connectionless() {
1362        metrics.connections_active().increment(1);
1363    }
1364
1365    let mut stream_capture = StreamCaptureState::new();
1366    // Set a buffer flush interval of 100ms, which will ensure we always flush buffered events at least every 100ms if
1367    // we're otherwise idle and not receiving packets from the client.
1368    let mut buffer_flush = interval(Duration::from_millis(100));
1369    buffer_flush.set_missed_tick_behavior(MissedTickBehavior::Delay);
1370
1371    let mut event_buffer_manager = EventBufferManager::default();
1372    let mut io_buffer_manager = IoBufferManager::new(&io_buffer_pool, &stream);
1373    let memory_limiter = source_context.topology_context().memory_limiter();
1374
1375    'read: loop {
1376        let mut eof = false;
1377
1378        let mut io_buffer = io_buffer_manager.get_buffer_mut().await;
1379
1380        memory_limiter.wait_for_capacity().await;
1381
1382        select! {
1383            // We read from the stream.
1384            read_result = stream.receive(&mut io_buffer) => match read_result {
1385                Ok((bytes_read, peer_addr)) => {
1386                    if bytes_read == 0 {
1387                        eof = true;
1388                    }
1389
1390                    let is_connectionless = stream.is_connectionless();
1391                    let payload = received_payload(io_buffer, bytes_read);
1392
1393                    capture_uds_traffic(
1394                        &listen_addr,
1395                        &traffic_capture,
1396                        capture_entity_resolver.as_deref(),
1397                        &peer_addr,
1398                        payload,
1399                        &mut stream_capture,
1400                    );
1401
1402                    if is_connectionless {
1403                        metrics.packet_receive_success().increment(1);
1404                    }
1405                    metrics.bytes_received().increment(bytes_read as u64);
1406                    metrics.bytes_received_size().record(bytes_read as f64);
1407                    let origin_detection_failed =
1408                        origin_detection_failed_for_telemetry(origin_detection_enabled, bytes_read, &peer_addr);
1409                    if origin_detection_failed && is_connectionless {
1410                        metrics.origin_detection_errors().increment(1);
1411                    }
1412
1413                    // When we're actually at EOF, or we're dealing with a connectionless stream, we try to decode in EOF mode.
1414                    //
1415                    // For connectionless streams, we always try to decode the buffer as if it's EOF, since it effectively _is_
1416                    // always the end of file after a receive. For connection-oriented streams, we only want to do this once we've
1417                    // actually hit true EOF.
1418                    let reached_eof = eof || is_connectionless;
1419
1420                    trace!(
1421                        buffer_len = io_buffer.remaining(),
1422                        buffer_cap = io_buffer.remaining_mut(),
1423                        eof = reached_eof,
1424                        %listen_addr,
1425                        %peer_addr,
1426                        "Received {} bytes from stream.",
1427                        bytes_read
1428                    );
1429
1430                    if should_drop_oversized_named_pipe_frame(&listen_addr, io_buffer) {
1431                        metrics.framing_errors().increment(1);
1432                        debug!(%listen_addr, %peer_addr, "DogStatsD named pipe frame exceeded the configured buffer size. Dropping frame.");
1433                        io_buffer.clear();
1434                        continue 'read;
1435                    }
1436
1437                    'frame: loop {
1438                        let frame_result = framer.next_frame(io_buffer, reached_eof);
1439                        let completed_outer_frames = framer.take_completed_outer_frames();
1440                        if !is_connectionless && completed_outer_frames > 0 {
1441                            metrics.packet_receive_success().increment(completed_outer_frames as u64);
1442                        }
1443                        if origin_detection_failed && completed_outer_frames > 0 {
1444                            metrics.origin_detection_errors().increment(completed_outer_frames as u64);
1445                        }
1446
1447                        match frame_result {
1448                            Ok(Some(frame)) => {
1449                                if matches!(listen_addr, ListenAddress::NamedPipe { .. }) {
1450                                    metrics.packet_receive_success().increment(1);
1451                                }
1452                                trace!(%listen_addr, %peer_addr, ?frame, "Decoded frame.");
1453                                if let Some(forwarder) = &packet_forwarder {
1454                                    forwarder.forward(frame.clone()).await;
1455                                }
1456                                match handle_frame(
1457                                    &frame[..],
1458                                    &codec,
1459                                    &mut context_resolvers,
1460                                    &metrics,
1461                                    capture_entity_resolver.as_deref(),
1462                                    origin_detection_enabled,
1463                                    &peer_addr,
1464                                    enabled_filter,
1465                                    &additional_tags,
1466                                ) {
1467                                    Ok(Some(event)) => {
1468                                        if let Some(event_buffer) = event_buffer_manager.try_push(event) {
1469                                            debug!(%listen_addr, %peer_addr, "Event buffer is full. Forwarding events.");
1470                                            dispatch_events(event_buffer, &source_context, &listen_addr).await;
1471                                        }
1472                                    },
1473                                    Ok(None) => {
1474                                        // We didn't decode an event, but there was no inherent error. This is likely
1475                                        // due to hitting resource limits, etc.
1476                                        //
1477                                        // Simply continue on.
1478                                        continue
1479                                    },
1480                                    Err(e) => {
1481                                        log_parse_failure(disable_verbose_logs, &listen_addr, &peer_addr, &frame, &e);
1482                                    },
1483                                }
1484                            }
1485                            Err(e) => {
1486                                metrics.framing_errors().increment(1);
1487                                if should_warn_stream_log_too_big(&listen_addr, &e, stream_log_too_big) {
1488                                    warn!(
1489                                        %listen_addr,
1490                                        %peer_addr,
1491                                        error = %e,
1492                                        "DogStatsD stream frame exceeded the configured buffer size."
1493                                    );
1494                                }
1495
1496                                if stream.is_connectionless() {
1497                                    io_buffer.clear();
1498                                    // For connectionless streams, we don't want to shutdown the stream since we can just keep
1499                                    // reading more packets.
1500                                    debug!(%listen_addr, %peer_addr, error = %e, "Error decoding frame. Continuing stream.");
1501                                    continue 'read;
1502                                } else {
1503                                    debug!(%listen_addr, %peer_addr, error = %e, "Error decoding frame. Stopping stream.");
1504                                    break 'read;
1505                                }
1506                            }
1507                            Ok(None) => {
1508                                trace!(%listen_addr, %peer_addr, "Not enough data to decode another frame.");
1509                                if eof && !stream.is_connectionless() {
1510                                    debug!(%listen_addr, %peer_addr, "Stream received EOF. Shutting down handler.");
1511                                    break 'read;
1512                                } else {
1513                                    break 'frame;
1514                                }
1515                            }
1516                        }
1517                    }
1518                },
1519                Err(e) => {
1520                    metrics.packet_receive_failure().increment(1);
1521
1522                    if stream.is_connectionless() {
1523                        // For connectionless streams, we don't want to shutdown the stream since we can just keep
1524                        // reading more packets.
1525                        warn!(%listen_addr, error = %e, "I/O error while decoding. Continuing stream.");
1526                        continue 'read;
1527                    } else {
1528                        warn!(%listen_addr, error = %e, "I/O error while decoding. Stopping stream.");
1529                        break 'read;
1530                    }
1531                }
1532            },
1533
1534            _ = buffer_flush.tick() => {
1535                if let Some(event_buffer) = event_buffer_manager.consume() {
1536                    dispatch_events(event_buffer, &source_context, &listen_addr).await;
1537                }
1538            },
1539
1540        }
1541    }
1542
1543    if let Some(event_buffer) = event_buffer_manager.consume() {
1544        dispatch_events(event_buffer, &source_context, &listen_addr).await;
1545    }
1546
1547    metrics.connections_active().decrement(1);
1548
1549    debug!(%listen_addr, "Stream handler stopped.");
1550}
1551
1552fn should_drop_oversized_named_pipe_frame(listen_addr: &ListenAddress, buffer: &BytesBuffer) -> bool {
1553    matches!(listen_addr, ListenAddress::NamedPipe { .. })
1554        && buffer.remaining_mut() == 0
1555        && memchr::memchr(b'\n', buffer.chunk()).is_none()
1556}
1557
1558fn should_warn_stream_log_too_big(listen_addr: &ListenAddress, error: &FramingError, stream_log_too_big: bool) -> bool {
1559    stream_log_too_big
1560        && matches!(listen_addr, ListenAddress::Unix(_))
1561        && matches!(error, FramingError::InvalidFrame { .. })
1562}
1563
1564fn log_parse_failure(
1565    disable_verbose_logs: bool, listen_addr: &ListenAddress, peer_addr: &ConnectionAddress, frame: &[u8],
1566    error: &ParseError,
1567) {
1568    let frame = String::from_utf8_lossy(frame);
1569    if disable_verbose_logs {
1570        debug!(%listen_addr, %peer_addr, %frame, %error, "Failed to parse frame.");
1571    } else {
1572        warn!(%listen_addr, %peer_addr, %frame, %error, "Failed to parse frame.");
1573    }
1574}
1575
1576fn capture_uds_traffic(
1577    listen_addr: &ListenAddress, traffic_capture: &TrafficCapture,
1578    capture_entity_resolver: Option<&(dyn CaptureEntityResolver + Send + Sync)>, peer_addr: &ConnectionAddress,
1579    payload: &[u8], stream_capture: &mut StreamCaptureState,
1580) {
1581    if payload.is_empty() || !traffic_capture.is_ongoing() {
1582        return;
1583    }
1584
1585    match listen_addr {
1586        ListenAddress::Unixgram(_) => {
1587            let _ = traffic_capture.enqueue(build_capture_record(
1588                capture_entity_resolver,
1589                process_id_from_peer_addr(peer_addr),
1590                payload,
1591            ));
1592        }
1593        ListenAddress::Unix(_) => {
1594            stream_capture.update_peer_metadata(peer_addr);
1595            stream_capture.pending.extend(payload);
1596
1597            while let Ok(Some(outer_payload)) = stream_capture
1598                .outer_framer
1599                .next_frame(&mut stream_capture.pending, false)
1600            {
1601                let _ = traffic_capture.enqueue(build_capture_record(
1602                    capture_entity_resolver,
1603                    stream_capture.last_pid,
1604                    &outer_payload,
1605                ));
1606            }
1607        }
1608        _ => {}
1609    }
1610}
1611
1612struct StreamCaptureState {
1613    outer_framer: LengthDelimitedFramer,
1614    pending: VecDeque<u8>,
1615    last_pid: Option<i32>,
1616}
1617
1618impl StreamCaptureState {
1619    fn new() -> Self {
1620        Self {
1621            outer_framer: LengthDelimitedFramer,
1622            pending: VecDeque::new(),
1623            last_pid: None,
1624        }
1625    }
1626
1627    fn update_peer_metadata(&mut self, peer_addr: &ConnectionAddress) {
1628        if let Some(process_id) = process_id_from_peer_addr(peer_addr) {
1629            self.last_pid = Some(process_id);
1630        }
1631    }
1632}
1633
1634fn build_capture_record(
1635    capture_entity_resolver: Option<&(dyn CaptureEntityResolver + Send + Sync)>, process_id: Option<i32>,
1636    payload: &[u8],
1637) -> CaptureRecord {
1638    CaptureRecord {
1639        timestamp_ns: capture_timestamp_ns(),
1640        payload: payload.to_vec(),
1641        pid: process_id,
1642        ancillary: Vec::new(),
1643        container_id: resolve_capture_container_id(capture_entity_resolver, process_id),
1644    }
1645}
1646
1647fn resolve_capture_container_id(
1648    capture_entity_resolver: Option<&(dyn CaptureEntityResolver + Send + Sync)>, process_id: Option<i32>,
1649) -> Option<String> {
1650    let process_id = u32::try_from(process_id?).ok()?;
1651    capture_entity_resolver
1652        .and_then(|resolver| resolver.resolve_container_entity_for_live_pid(process_id))
1653        .map(|entity_id| entity_id.to_string())
1654}
1655
1656fn process_id_from_peer_addr(peer_addr: &ConnectionAddress) -> Option<i32> {
1657    match peer_addr {
1658        ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(creds)) => Some(creds.pid),
1659        _ => None,
1660    }
1661}
1662
1663/// Applies SCM_CREDENTIALS to the origin, dispatching on the replay marker GID.
1664///
1665/// Live packets carry the sender process's real PID/UID/GID; we use `creds.pid` as the origin's process ID. Replay
1666/// packets carry `gid == REPLAY_CREDENTIALS_GID` and pack the captured (original) PID into `creds.uid`; we recover
1667/// that PID with an internal marker so downstream tag resolution consults the captured tagger store.
1668fn apply_credentials_to_origin(origin: &mut RawOrigin<'_>, creds: &ProcessCredentials) {
1669    if creds.gid == REPLAY_CREDENTIALS_GID {
1670        origin.set_process_id(mark_replay_process_id(creds.uid));
1671    } else {
1672        origin.set_process_id(creds.pid as u32);
1673    }
1674}
1675
1676fn received_payload(buffer: &BytesBuffer, bytes_read: usize) -> &[u8] {
1677    let chunk = buffer.chunk();
1678    let start = chunk.len().saturating_sub(bytes_read);
1679    &chunk[start..]
1680}
1681
1682fn capture_timestamp_ns() -> i64 {
1683    SystemTime::now()
1684        .duration_since(UNIX_EPOCH)
1685        .map(|duration| duration.as_nanos().min(i64::MAX as u128) as i64)
1686        .unwrap_or_default()
1687}
1688
1689#[allow(clippy::too_many_arguments)]
1690fn handle_frame(
1691    frame: &[u8], codec: &DogStatsDCodec, context_resolvers: &mut ContextResolvers, source_metrics: &Metrics,
1692    capture_entity_resolver: Option<&(dyn CaptureEntityResolver + Send + Sync)>, origin_detection_enabled: bool,
1693    peer_addr: &ConnectionAddress, enabled_filter: EnablePayloadsFilter, additional_tags: &[String],
1694) -> Result<Option<Event>, ParseError> {
1695    // Resolving the origin requires a (potentially uncached) PID-to-container lookup, so only do it for metric frames,
1696    // which are the only frames that record per-origin telemetry.
1697    let resolve_telemetry_origin = || {
1698        (source_metrics.origin_telemetry_enabled() && origin_detection_enabled)
1699            .then(|| resolve_capture_container_id(capture_entity_resolver, process_id_from_peer_addr(peer_addr)))
1700            .flatten()
1701    };
1702
1703    let parsed = match codec.decode_packet(frame) {
1704        Ok(parsed) => parsed,
1705        Err(e) => {
1706            // Try and determine what the message type was, if possible, to increment the correct error counter.
1707            match parse_message_type(frame) {
1708                MessageType::MetricSample => {
1709                    source_metrics.record_metric_parse_failed(resolve_telemetry_origin().as_deref())
1710                }
1711                MessageType::Event => source_metrics.event_decode_failed().increment(1),
1712                MessageType::ServiceCheck => source_metrics.service_check_decode_failed().increment(1),
1713            }
1714
1715            return Err(e);
1716        }
1717    };
1718
1719    let event = match parsed {
1720        ParsedPacket::Metric(metric_packet) => {
1721            if metric_packet.num_points == 0 {
1722                return Ok(None);
1723            }
1724            let events_len = metric_packet.num_points;
1725            if !enabled_filter.allow_metric(&metric_packet) {
1726                trace!(
1727                    metric.name = metric_packet.metric_name,
1728                    "Skipping metric due to filter configuration."
1729                );
1730                return Ok(None);
1731            }
1732
1733            match handle_metric_packet(metric_packet, context_resolvers, peer_addr, additional_tags) {
1734                Some(metric) => {
1735                    source_metrics.record_metrics_received(events_len, resolve_telemetry_origin().as_deref());
1736                    Event::Metric(metric)
1737                }
1738                None => {
1739                    // We can only fail to get a metric back if we failed to resolve the context.
1740                    source_metrics.failed_context_resolve_total().increment(1);
1741                    return Ok(None);
1742                }
1743            }
1744        }
1745        ParsedPacket::Event(event) => {
1746            if !enabled_filter.allow_event(&event) {
1747                trace!("Skipping event {} due to filter configuration.", event.title);
1748                return Ok(None);
1749            }
1750            let tags_resolver = context_resolvers.tags();
1751            match handle_event_packet(event, tags_resolver, peer_addr, additional_tags) {
1752                Some(event) => {
1753                    source_metrics.events_received().increment(1);
1754                    Event::EventD(event)
1755                }
1756                None => {
1757                    source_metrics.failed_context_resolve_total().increment(1);
1758                    return Ok(None);
1759                }
1760            }
1761        }
1762        ParsedPacket::ServiceCheck(service_check) => {
1763            if !enabled_filter.allow_service_check(&service_check) {
1764                trace!(
1765                    "Skipping service check {} due to filter configuration.",
1766                    service_check.name
1767                );
1768                return Ok(None);
1769            }
1770            let tags_resolver = context_resolvers.tags();
1771            match handle_service_check_packet(service_check, tags_resolver, peer_addr, additional_tags) {
1772                Some(service_check) => {
1773                    source_metrics.service_checks_received().increment(1);
1774                    Event::ServiceCheck(service_check)
1775                }
1776                None => {
1777                    source_metrics.failed_context_resolve_total().increment(1);
1778                    return Ok(None);
1779                }
1780            }
1781        }
1782    };
1783
1784    Ok(Some(event))
1785}
1786
1787fn handle_metric_packet(
1788    packet: MetricPacket, context_resolvers: &mut ContextResolvers, peer_addr: &ConnectionAddress,
1789    additional_tags: &[String],
1790) -> Option<Metric> {
1791    let well_known_tags = WellKnownTags::from_raw_tags(packet.tags.clone());
1792
1793    let mut origin = origin_from_metric_packet(&packet, &well_known_tags);
1794    if let Some(creds) = peer_addr.process_credentials() {
1795        apply_credentials_to_origin(&mut origin, creds);
1796    }
1797
1798    // Choose the right context resolver based on whether or not this metric is pre-aggregated.
1799    let context_resolver = if packet.timestamp.is_some() {
1800        context_resolvers.no_agg()
1801    } else {
1802        context_resolvers.primary()
1803    };
1804
1805    let tags = get_filtered_tags_iterator(packet.tags, additional_tags);
1806
1807    // Try to resolve the context for this metric.
1808    match context_resolver.resolve(packet.metric_name, tags, Some(origin)) {
1809        Some(context) => {
1810            let metric_origin = well_known_tags
1811                .jmx_check_name
1812                .map(MetricOrigin::jmx_check)
1813                .unwrap_or_else(MetricOrigin::dogstatsd);
1814            let metadata = MetricMetadata::default()
1815                .with_origin(metric_origin)
1816                .with_hostname(well_known_tags.hostname.map(Arc::from))
1817                .with_unit(packet.unit.map_or_else(MetaString::empty, MetaString::from_static));
1818
1819            Some(Metric::from_parts(context, packet.values, metadata))
1820        }
1821        // We failed to resolve the context, likely due to not having enough interner capacity.
1822        None => None,
1823    }
1824}
1825
1826fn handle_event_packet(
1827    packet: EventPacket, tags_resolver: &mut TagsResolver, peer_addr: &ConnectionAddress, additional_tags: &[String],
1828) -> Option<EventD> {
1829    let well_known_tags = WellKnownTags::from_raw_tags(packet.tags.clone());
1830
1831    let mut origin = origin_from_event_packet(&packet, &well_known_tags);
1832    if let Some(creds) = peer_addr.process_credentials() {
1833        apply_credentials_to_origin(&mut origin, creds);
1834    }
1835    let origin_tags = tags_resolver.resolve_origin_tags(Some(origin));
1836
1837    let tags = get_filtered_tags_iterator(packet.tags, additional_tags);
1838    let tags = tags_resolver.create_tag_set(tags)?;
1839
1840    // When no d: field is present, backfill the current time—matching the stock Datadog Agent's
1841    // behavior in pkg/aggregator/aggregator.go (addEvent), which sets e.Ts = time.Now().Unix()
1842    // for any event with Ts == 0.
1843    let timestamp = packet
1844        .timestamp
1845        .or_else(|| SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()));
1846
1847    let eventd = EventD::new(packet.title, packet.text)
1848        .with_timestamp(timestamp)
1849        .with_hostname(packet.hostname.map(|s| s.into()))
1850        .with_aggregation_key(packet.aggregation_key.map(|s| s.into()))
1851        .with_alert_type(packet.alert_type)
1852        .with_priority(packet.priority)
1853        // When no source type is provided, default to "api"—the same default the stock Datadog
1854        // Agent applies when serializing DogStatsD events to the intake JSON format. The agent
1855        // groups events by source type name and uses "api" as the key for events without an
1856        // explicit `s:` field. See: pkg/serializer/internal/metrics/events.go (writeItem).
1857        .with_source_type_name(Some(
1858            packet
1859                .source_type_name
1860                .map(|s| s.into())
1861                .unwrap_or_else(|| "api".into()),
1862        ))
1863        .with_alert_type(packet.alert_type)
1864        .with_tags(tags)
1865        .with_origin_tags(origin_tags);
1866
1867    Some(eventd)
1868}
1869
1870fn handle_service_check_packet(
1871    packet: ServiceCheckPacket, tags_resolver: &mut TagsResolver, peer_addr: &ConnectionAddress,
1872    additional_tags: &[String],
1873) -> Option<ServiceCheck> {
1874    let well_known_tags = WellKnownTags::from_raw_tags(packet.tags.clone());
1875
1876    let mut origin = origin_from_service_check_packet(&packet, &well_known_tags);
1877    if let Some(creds) = peer_addr.process_credentials() {
1878        apply_credentials_to_origin(&mut origin, creds);
1879    }
1880    let origin_tags = tags_resolver.resolve_origin_tags(Some(origin));
1881
1882    let tags = get_filtered_tags_iterator(packet.tags, additional_tags);
1883    let tags = tags_resolver.create_tag_set(tags)?;
1884
1885    // When no d: field is present, backfill the current time—matching the stock Datadog Agent's
1886    // behavior, which sets the timestamp to time.Now().Unix() for any service check with a zero
1887    // timestamp.
1888    let timestamp = packet
1889        .timestamp
1890        .or_else(|| SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()));
1891
1892    let service_check = ServiceCheck::new(packet.name, packet.status)
1893        .with_timestamp(timestamp)
1894        .with_hostname(packet.hostname.map(|s| s.into()))
1895        .with_tags(tags)
1896        .with_origin_tags(origin_tags)
1897        .with_message(packet.message.map(|s| s.into()));
1898
1899    Some(service_check)
1900}
1901
1902fn get_filtered_tags_iterator<'a>(
1903    raw_tags: RawTags<'a>, additional_tags: &'a [String],
1904) -> impl Iterator<Item = &'a str> + Clone {
1905    // This filters out "well-known" tags from the raw tags in the DogStatsD packet, and then chains on any additional tags
1906    // that were configured on the source.
1907    RawTagsFilter::exclude(raw_tags, WellKnownTagsFilterPredicate).chain(additional_tags.iter().map(|s| s.as_str()))
1908}
1909
1910async fn dispatch_events(mut event_buffer: EventsBuffer, source_context: &SourceContext, listen_addr: &ListenAddress) {
1911    debug!(%listen_addr, events_len = event_buffer.len(), "Forwarding events.");
1912
1913    // TODO: This is maybe a little dicey because if we fail to dispatch the events, we may not have iterated over all of
1914    // them, so there might still be eventd events when get to the service checks point, and eventd events and/or service
1915    // check events when we get to the metrics point, and so on.
1916    //
1917    // There's probably something to be said for erroring out fully if this happens, since we should only fail to
1918    // dispatch if the downstream component fails entirely... and unless we have a way to restart the component, then
1919    // we're going to continue to fail to dispatch any more events until the process is restarted anyways.
1920
1921    // Dispatch any eventd events, if present.
1922    if event_buffer.has_event_type(EventType::EventD) {
1923        let eventd_events = event_buffer.extract(Event::is_eventd);
1924        let events_output = source_context.dispatcher().buffered_named("events");
1925
1926        // The `events` output is always wired in the DSD topology, so a missing output is an invariant violation that
1927        // crashes this component.
1928        if events_output.is_err() {
1929            saluki_antithesis::unreachable!("dsd 'events' output missing at dispatch");
1930        }
1931
1932        if let Err(e) = events_output
1933            .expect("events output should always exist")
1934            .send_all(eventd_events)
1935            .await
1936        {
1937            error!(%listen_addr, error = %e, "Failed to dispatch eventd events.");
1938
1939            // Dispatch failure increments no counter, so this assertion is the only in-SUT signal that the failure
1940            // path ran.
1941            saluki_antithesis::sometimes!(
1942                true,
1943                "dsd dispatch failed mid-buffer",
1944                { "stream": "events" }
1945            );
1946        }
1947    }
1948
1949    // Dispatch any service check events, if present.
1950    if event_buffer.has_event_type(EventType::ServiceCheck) {
1951        let service_check_events = event_buffer.extract(Event::is_service_check);
1952        let service_checks_output = source_context.dispatcher().buffered_named("service_checks");
1953
1954        if service_checks_output.is_err() {
1955            saluki_antithesis::unreachable!("dsd 'service_checks' output missing at dispatch");
1956        }
1957
1958        if let Err(e) = service_checks_output
1959            .expect("service checks output should always exist")
1960            .send_all(service_check_events)
1961            .await
1962        {
1963            error!(%listen_addr, error = %e, "Failed to dispatch service check events.");
1964
1965            saluki_antithesis::sometimes!(
1966                true,
1967                "dsd dispatch failed mid-buffer",
1968                { "stream": "service_checks" }
1969            );
1970        }
1971    }
1972
1973    // Finally, if there are events left, they'll be metrics, so dispatch them.
1974    if !event_buffer.is_empty() {
1975        if let Err(e) = source_context
1976            .dispatcher()
1977            .dispatch_named("metrics", event_buffer)
1978            .await
1979        {
1980            error!(%listen_addr, error = %e, "Failed to dispatch metric events.");
1981
1982            saluki_antithesis::sometimes!(
1983                true,
1984                "dsd dispatch failed mid-buffer",
1985                { "stream": "metrics" }
1986            );
1987        }
1988    }
1989}
1990
1991const fn get_adjusted_buffer_size(buffer_size: usize) -> usize {
1992    // This is a little goofy, but hear me out:
1993    //
1994    // In the Datadog Agent, the way the UDS listener works is that if it's in stream mode, it will do a standalone
1995    // socket read to get _just_ the length delimiter, which is 4 bytes. After that, it will do a read to get the packet
1996    // data itself, up to the limit of `dogstatsd_buffer_size`. This means that a _full_ UDS stream packet can be up to
1997    // `dogstatsd_buffer_size + 4` bytes.
1998    //
1999    // This isn't a problem in the Agent due to how it does the reads, but it's a problem for us because we want to be
2000    // able to get an entire frame in a single buffer for the purpose of decoding the frame. Rather than rewriting our
2001    // read loop such that we have to change the logic depending on UDP/UDS datagram vs UDS stream, we simply increase
2002    // the buffer size by 4 bytes to account for the length delimiter.
2003    //
2004    // We do it this way so that we don't have to change the buffer size in the configuration, since if you just ported
2005    // over a Datadog Agent configuration, the value would be too small, and vise versa.
2006    buffer_size + 4
2007}
2008
2009#[cfg(test)]
2010mod tests {
2011    use std::{
2012        collections::HashMap,
2013        io::ErrorKind,
2014        net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
2015        path::PathBuf,
2016        sync::{Arc, OnceLock},
2017        time::Duration,
2018    };
2019
2020    use bytes::{BufMut as _, Bytes};
2021    use bytesize::ByteSize;
2022    use metrics::{Key, Label};
2023    use saluki_config::ConfigurationLoader;
2024    use saluki_context::{origin::RawOrigin, ContextResolverBuilder, TagsResolverBuilder};
2025    use saluki_core::{
2026        components::ComponentContext,
2027        pooling::{helpers::get_pooled_object_via_builder, ObjectPool as _},
2028        topology::ComponentId,
2029    };
2030    use saluki_env::workload::{CaptureEntityResolver, EntityId};
2031    use saluki_io::{
2032        buf::{BytesBuffer, FixedSizeVec},
2033        deser::codec::dogstatsd::{DogStatsDCodec, DogStatsDCodecConfiguration, ParsedPacket},
2034        net::{ConnectionAddress, ListenAddress, ProcessCredentials, ProcessIdentity},
2035    };
2036    use saluki_metrics::test::TestRecorder;
2037    use serde_json::json;
2038    use stringtheory::MetaString;
2039    use tokio::{net::UdpSocket, sync::mpsc, time::timeout};
2040
2041    use super::{
2042        build_io_buffer_pool, default_buffer_size, default_windows_pipe_security_descriptor,
2043        filters::EnablePayloadsFilter,
2044        forwarder::{
2045            ConnectedPacketForwarder, ForwardPacket, PacketForwarder, PacketForwarderTarget, FORWARDER_QUEUE_CAPACITY,
2046        },
2047        handle_frame, handle_metric_packet,
2048        metrics::build_metrics,
2049        origin_detection_failed_for_telemetry, resolve_capture_container_id, ContextResolvers, DogStatsDConfiguration,
2050        DOGSTATSD_CAPTURE_DIR, MIN_CAPTURE_DEPTH,
2051    };
2052
2053    const LINUX_EAFNOSUPPORT: i32 = 97;
2054    const MACOS_EAFNOSUPPORT: i32 = 47;
2055
2056    fn is_ipv6_unavailable_error(error: &std::io::Error) -> bool {
2057        matches!(error.kind(), ErrorKind::AddrNotAvailable | ErrorKind::Unsupported)
2058            || matches!(error.raw_os_error(), Some(LINUX_EAFNOSUPPORT | MACOS_EAFNOSUPPORT))
2059    }
2060
2061    #[derive(Default)]
2062    struct CaptureTestEntityResolver {
2063        pid_map: HashMap<u32, EntityId>,
2064    }
2065
2066    impl CaptureTestEntityResolver {
2067        fn with_pid_mapping(process_id: u32, entity_id: EntityId) -> Self {
2068            let mut pid_map = HashMap::new();
2069            pid_map.insert(process_id, entity_id);
2070            Self { pid_map }
2071        }
2072    }
2073
2074    impl CaptureEntityResolver for CaptureTestEntityResolver {
2075        fn resolve_container_entity_for_live_pid(&self, process_id: u32) -> Option<EntityId> {
2076            self.pid_map.get(&process_id).cloned()
2077        }
2078    }
2079
2080    fn packet_forwarder_from_sender(
2081        target_port: u16, packets_tx: mpsc::Sender<ForwardPacket>, metrics: super::metrics::Metrics,
2082    ) -> PacketForwarder {
2083        let mut forwarder =
2084            PacketForwarderTarget::new(MetaString::from_static("127.0.0.1"), target_port).to_forwarder(metrics);
2085        forwarder.connected = Arc::new(OnceLock::from(packets_tx));
2086        forwarder
2087    }
2088
2089    fn processed_metric_key(listener_type: &'static str, origin: Option<&str>) -> Key {
2090        let mut labels = vec![
2091            Label::from_static_parts("component_id", "dogstatsd_test"),
2092            Label::from_static_parts("component_type", "source"),
2093            Label::from_static_parts("listener_type", listener_type),
2094            Label::from_static_parts("message_type", "metrics"),
2095        ];
2096        if let Some(origin) = origin {
2097            labels.push(Label::new("origin", origin.to_string()));
2098        }
2099
2100        Key::from_parts("component_events_received_total", labels)
2101    }
2102
2103    fn test_context_resolvers() -> ContextResolvers {
2104        let tags_resolver = TagsResolverBuilder::for_tests().build();
2105        let context_resolver = ContextResolverBuilder::for_tests()
2106            .with_tags_resolver(Some(tags_resolver.clone()))
2107            .build();
2108        ContextResolvers::manual(context_resolver.clone(), context_resolver, tags_resolver)
2109    }
2110
2111    #[test]
2112    fn origin_telemetry_does_not_resolve_origin_when_origin_detection_is_disabled() {
2113        let recorder = TestRecorder::default();
2114        let _recorder_guard = metrics::set_default_local_recorder(&recorder);
2115        let listen_addr = ListenAddress::Unixgram("/tmp/dsd.sock".into());
2116        let context = ComponentContext::source(ComponentId::try_from("dogstatsd_test").expect("valid component ID"));
2117        let metrics = build_metrics(&listen_addr, &context, true);
2118        let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
2119        let mut context_resolvers = test_context_resolvers();
2120        let capture_entity_resolver = CaptureTestEntityResolver::with_pid_mapping(
2121            42,
2122            EntityId::from_local_data("ci-pid-container").expect("container entity"),
2123        );
2124        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(ProcessCredentials {
2125            pid: 42,
2126            uid: 0,
2127            gid: 0,
2128        }));
2129
2130        let event = handle_frame(
2131            b"test_metric:1|c",
2132            &codec,
2133            &mut context_resolvers,
2134            &metrics,
2135            Some(&capture_entity_resolver),
2136            false,
2137            &peer_addr,
2138            EnablePayloadsFilter::default(),
2139            &[],
2140        )
2141        .expect("frame should parse");
2142
2143        assert!(event.is_some());
2144        assert_eq!(
2145            recorder.counter(processed_metric_key("unixgram", Some("container_id://pid-container"))),
2146            None
2147        );
2148        assert_eq!(recorder.counter(processed_metric_key("unixgram", Some(""))), Some(1));
2149    }
2150
2151    #[test]
2152    fn origin_telemetry_records_resolved_origin_when_origin_detection_is_enabled() {
2153        let recorder = TestRecorder::default();
2154        let _recorder_guard = metrics::set_default_local_recorder(&recorder);
2155        let listen_addr = ListenAddress::Unixgram("/tmp/dsd.sock".into());
2156        let context = ComponentContext::source(ComponentId::try_from("dogstatsd_test").expect("valid component ID"));
2157        let metrics = build_metrics(&listen_addr, &context, true);
2158        let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
2159        let mut context_resolvers = test_context_resolvers();
2160        let capture_entity_resolver = CaptureTestEntityResolver::with_pid_mapping(
2161            42,
2162            EntityId::from_local_data("ci-pid-container").expect("container entity"),
2163        );
2164        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(ProcessCredentials {
2165            pid: 42,
2166            uid: 0,
2167            gid: 0,
2168        }));
2169
2170        let event = handle_frame(
2171            b"test_metric:1|c",
2172            &codec,
2173            &mut context_resolvers,
2174            &metrics,
2175            Some(&capture_entity_resolver),
2176            true,
2177            &peer_addr,
2178            EnablePayloadsFilter::default(),
2179            &[],
2180        )
2181        .expect("frame should parse");
2182
2183        assert!(event.is_some());
2184        assert_eq!(
2185            recorder.counter(processed_metric_key("unixgram", Some("container_id://pid-container"))),
2186            Some(1)
2187        );
2188        assert_eq!(recorder.counter(processed_metric_key("unixgram", Some(""))), Some(0));
2189    }
2190
2191    #[test]
2192    fn no_metrics_when_interner_full_allocations_disallowed() {
2193        // We're specifically testing here that when we don't allow outside allocations, we should not be able to
2194        // resolve a context if the interner is full. A no-op interner has the smallest possible size, so that's going
2195        // to assure we can't intern anything... but we also need a string (name or one of the tags) that can't be
2196        // _inlined_ either, since that will get around the interner being full.
2197        //
2198        // We set our metric name to be longer than 31 bytes (the inlining limit) to ensure this.
2199
2200        let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
2201        let tags_resolver = TagsResolverBuilder::for_tests().build();
2202        let context_resolver = ContextResolverBuilder::for_tests()
2203            .with_heap_allocations(false)
2204            .with_tags_resolver(Some(tags_resolver.clone()))
2205            .build();
2206        let mut context_resolvers = ContextResolvers::manual(context_resolver.clone(), context_resolver, tags_resolver);
2207        let peer_addr = ConnectionAddress::from("1.1.1.1:1234".parse::<SocketAddr>().unwrap());
2208
2209        let input = "big_metric_name_that_cant_possibly_be_inlined:1|c|#tag1:value1,tag2:value2,tag3:value3";
2210
2211        let Ok(ParsedPacket::Metric(packet)) = codec.decode_packet(input.as_bytes()) else {
2212            panic!("Failed to parse packet.");
2213        };
2214
2215        let maybe_metric = handle_metric_packet(packet, &mut context_resolvers, &peer_addr, &[]);
2216        assert!(maybe_metric.is_none());
2217    }
2218
2219    #[test]
2220    fn metric_with_additional_tags() {
2221        let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
2222        let tags_resolver = TagsResolverBuilder::for_tests().build();
2223        let context_resolver = ContextResolverBuilder::for_tests()
2224            .with_heap_allocations(false)
2225            .with_tags_resolver(Some(tags_resolver.clone()))
2226            .build();
2227        let mut context_resolvers = ContextResolvers::manual(context_resolver.clone(), context_resolver, tags_resolver);
2228        let peer_addr = ConnectionAddress::from("1.1.1.1:1234".parse::<SocketAddr>().unwrap());
2229
2230        let existing_tags = ["tag1:value1", "tag2:value2", "tag3:value3"];
2231        let existing_tags_str = existing_tags.join(",");
2232
2233        let input = format!("test_metric_name:1|c|#{}", existing_tags_str);
2234        let additional_tags = [
2235            "tag4:value4".to_string(),
2236            "tag5:value5".to_string(),
2237            "tag6:value6".to_string(),
2238        ];
2239
2240        let Ok(ParsedPacket::Metric(packet)) = codec.decode_packet(input.as_bytes()) else {
2241            panic!("Failed to parse packet.");
2242        };
2243        let maybe_metric = handle_metric_packet(packet, &mut context_resolvers, &peer_addr, &additional_tags);
2244        assert!(maybe_metric.is_some());
2245
2246        let metric = maybe_metric.unwrap();
2247        let context = metric.context();
2248
2249        for tag in existing_tags {
2250            assert!(context.tags().has_tag(tag));
2251        }
2252
2253        for tag in additional_tags {
2254            assert!(context.tags().has_tag(tag));
2255        }
2256    }
2257
2258    fn deser_config(json: &str) -> DogStatsDConfiguration {
2259        serde_json::from_str(json).expect("failed to deserialize config")
2260    }
2261
2262    fn udp_listen_address() -> ListenAddress {
2263        ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8125)))
2264    }
2265
2266    fn tcp_listen_address() -> ListenAddress {
2267        ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8125)))
2268    }
2269
2270    fn named_pipe_listen_address() -> ListenAddress {
2271        ListenAddress::named_pipe_with_input_buffer_size(
2272            "datadog-dogstatsd",
2273            default_windows_pipe_security_descriptor(),
2274            default_buffer_size() as u32,
2275        )
2276    }
2277
2278    #[test]
2279    fn build_addresses_includes_named_pipe_when_configured() {
2280        let config = deser_config(
2281            r#"{
2282                "dogstatsd_port": 0,
2283                "dogstatsd_pipe_name": "datadog-dogstatsd"
2284            }"#,
2285        );
2286
2287        let addresses = config.build_addresses(None);
2288
2289        assert_eq!(addresses, vec![named_pipe_listen_address()]);
2290    }
2291
2292    #[test]
2293    fn build_addresses_uses_dogstatsd_buffer_size_for_named_pipe_input_buffer() {
2294        let config = deser_config(
2295            r#"{
2296                "dogstatsd_port": 0,
2297                "dogstatsd_pipe_name": "datadog-dogstatsd",
2298                "dogstatsd_buffer_size": 16384
2299            }"#,
2300        );
2301
2302        let addresses = config.build_addresses(None);
2303
2304        let [ListenAddress::NamedPipe { input_buffer_size, .. }] = addresses.as_slice() else {
2305            panic!("expected only a named pipe listen address, got {addresses:?}");
2306        };
2307        assert_eq!(*input_buffer_size, Some(16_384));
2308    }
2309
2310    #[test]
2311    fn eol_required_matches_named_pipe_listener_type() {
2312        let config = deser_config(r#"{"dogstatsd_eol_required": ["named_pipe"]}"#);
2313        let eol_required = config.eol_required();
2314
2315        assert!(eol_required.for_listener(&named_pipe_listen_address()));
2316        assert!(!eol_required.for_listener(&udp_listen_address()));
2317        assert!(!eol_required.for_listener(&tcp_listen_address()));
2318    }
2319
2320    #[test]
2321    fn interner_size_defaults_to_2mib() {
2322        let config = deser_config("{}");
2323        assert_eq!(config.effective_context_string_interner_bytes(), ByteSize::mib(2));
2324    }
2325
2326    #[test]
2327    fn socket_receive_buffer_size_defaults_to_zero() {
2328        let config = deser_config("{}");
2329        assert_eq!(config.socket_receive_buffer_size, 0);
2330    }
2331
2332    #[test]
2333    fn socket_receive_buffer_size_from_config() {
2334        let config = deser_config(r#"{"dogstatsd_so_rcvbuf": 131072}"#);
2335        assert_eq!(config.socket_receive_buffer_size, 131_072);
2336    }
2337
2338    #[test]
2339    fn stream_log_too_big_defaults_to_false() {
2340        let config = deser_config("{}");
2341        assert!(!config.stream_log_too_big);
2342    }
2343
2344    #[test]
2345    fn stream_log_too_big_from_config() {
2346        let config = deser_config(r#"{"dogstatsd_stream_log_too_big": true}"#);
2347        assert!(config.stream_log_too_big);
2348    }
2349
2350    #[test]
2351    fn disable_verbose_logs_defaults_to_false() {
2352        let config = deser_config("{}");
2353        assert!(!config.disable_verbose_logs);
2354    }
2355
2356    #[test]
2357    fn disable_verbose_logs_from_config() {
2358        let config = deser_config(r#"{"dogstatsd_disable_verbose_logs": true}"#);
2359        assert!(config.disable_verbose_logs);
2360    }
2361
2362    #[test]
2363    fn statsd_forward_defaults_disabled() {
2364        let config = deser_config("{}");
2365        assert!(config.statsd_forward_host.is_none());
2366        assert_eq!(config.statsd_forward_port, 0);
2367        assert!(config.statsd_forward_target().is_none());
2368    }
2369
2370    #[test]
2371    fn statsd_forward_empty_host_disabled() {
2372        let config = deser_config(r#"{"statsd_forward_host": "", "statsd_forward_port": 9125}"#);
2373        assert!(config.statsd_forward_host.is_none());
2374        assert!(config.statsd_forward_target().is_none());
2375    }
2376
2377    #[test]
2378    fn statsd_forward_zero_port_disabled() {
2379        let config = deser_config(r#"{"statsd_forward_host": "127.0.0.1", "statsd_forward_port": 0}"#);
2380        assert_eq!(config.statsd_forward_host.as_deref(), Some("127.0.0.1"));
2381        assert!(config.statsd_forward_target().is_none());
2382    }
2383
2384    #[test]
2385    fn statsd_forward_host_and_port_enabled() {
2386        let config = deser_config(r#"{"statsd_forward_host": "127.0.0.1", "statsd_forward_port": 9125}"#);
2387        let (host, port) = config.statsd_forward_target().expect("forwarding should be enabled");
2388        assert_eq!(host.as_ref(), "127.0.0.1");
2389        assert_eq!(port, 9125);
2390    }
2391
2392    #[test]
2393    fn statsd_forward_invalid_target_still_builds_forwarder_handle() {
2394        let config = deser_config(r#"{"statsd_forward_host": "not a valid host", "statsd_forward_port": 9125}"#);
2395        assert!(config.packet_forwarder_target().is_some());
2396    }
2397
2398    #[tokio::test]
2399    async fn packet_forwarder_sends_payload_bytes() {
2400        let receiver = UdpSocket::bind("127.0.0.1:0").await.expect("receiver should bind");
2401        let receiver_addr = receiver.local_addr().expect("receiver should have an address");
2402        let forwarder = ConnectedPacketForwarder::connect("127.0.0.1", receiver_addr.port())
2403            .await
2404            .expect("forwarder should connect");
2405        let payload = b"daemon:666|g|#sometag1:somevalue1,sometag2:somevalue2";
2406
2407        let recorder = TestRecorder::default();
2408        let _recorder_guard = metrics::set_default_local_recorder(&recorder);
2409        let listen_addr = ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8125)));
2410        let context = ComponentContext::source(ComponentId::try_from("dogstatsd_test").expect("valid component ID"));
2411        let metrics = build_metrics(&listen_addr, &context, false);
2412        let (packets_tx, packets_rx) = mpsc::channel(1);
2413        let worker = tokio::spawn(forwarder.run(packets_rx, metrics.clone()));
2414        let packet_forwarder = packet_forwarder_from_sender(receiver_addr.port(), packets_tx, metrics);
2415
2416        packet_forwarder.forward(Bytes::copy_from_slice(payload)).await;
2417
2418        let mut actual = [0u8; 128];
2419        let (received_len, _) = timeout(Duration::from_secs(1), receiver.recv_from(&mut actual))
2420            .await
2421            .expect("receive should not time out")
2422            .expect("receiver should receive payload");
2423
2424        assert_eq!(&actual[..received_len], payload);
2425        assert_eq!(
2426            recorder.counter((
2427                "component_packets_forwarded_total",
2428                &[
2429                    ("component_id", "dogstatsd_test"),
2430                    ("component_type", "source"),
2431                    ("listener_type", "udp"),
2432                    ("state", "ok"),
2433                ]
2434            )),
2435            Some(1)
2436        );
2437        assert_eq!(
2438            recorder.counter((
2439                "component_bytes_forwarded_total",
2440                &[
2441                    ("component_id", "dogstatsd_test"),
2442                    ("component_type", "source"),
2443                    ("listener_type", "udp"),
2444                ]
2445            )),
2446            Some(payload.len() as u64)
2447        );
2448        worker.abort();
2449    }
2450
2451    #[tokio::test]
2452    async fn packet_forwarder_sends_payload_bytes_to_ipv6_target() {
2453        let receiver = match UdpSocket::bind("[::1]:0").await {
2454            Ok(receiver) => receiver,
2455            Err(e) if is_ipv6_unavailable_error(&e) => return,
2456            Err(e) => panic!("receiver should bind: {e}"),
2457        };
2458        let receiver_addr = receiver.local_addr().expect("receiver should have an address");
2459        let forwarder = ConnectedPacketForwarder::connect("::1", receiver_addr.port())
2460            .await
2461            .expect("forwarder should connect");
2462        let payload = b"daemon:666|g|#ip:6";
2463
2464        let recorder = TestRecorder::default();
2465        let _recorder_guard = metrics::set_default_local_recorder(&recorder);
2466        let listen_addr = ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8125)));
2467        let context = ComponentContext::source(ComponentId::try_from("dogstatsd_test").expect("valid component ID"));
2468        let metrics = build_metrics(&listen_addr, &context, false);
2469        let (packets_tx, packets_rx) = mpsc::channel(1);
2470        let worker = tokio::spawn(forwarder.run(packets_rx, metrics.clone()));
2471        let packet_forwarder = packet_forwarder_from_sender(receiver_addr.port(), packets_tx, metrics);
2472
2473        packet_forwarder.forward(Bytes::copy_from_slice(payload)).await;
2474
2475        let mut actual = [0u8; 128];
2476        let (received_len, _) = timeout(Duration::from_secs(1), receiver.recv_from(&mut actual))
2477            .await
2478            .expect("receive should not time out")
2479            .expect("receiver should receive payload");
2480
2481        assert_eq!(&actual[..received_len], payload);
2482        worker.abort();
2483    }
2484
2485    #[tokio::test]
2486    async fn packet_forwarder_waits_when_queue_is_full() {
2487        let recorder = TestRecorder::default();
2488        let _recorder_guard = metrics::set_default_local_recorder(&recorder);
2489        let listen_addr = ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8125)));
2490        let context = ComponentContext::source(ComponentId::try_from("dogstatsd_test").expect("valid component ID"));
2491        let metrics = build_metrics(&listen_addr, &context, false);
2492        let (packets_tx, _packets_rx) = mpsc::channel(FORWARDER_QUEUE_CAPACITY);
2493        let packet_forwarder = packet_forwarder_from_sender(9125, packets_tx, metrics);
2494
2495        for _ in 0..FORWARDER_QUEUE_CAPACITY {
2496            packet_forwarder.forward(Bytes::from_static(b"queued:1|c")).await;
2497        }
2498
2499        assert!(
2500            timeout(
2501                Duration::from_millis(100),
2502                packet_forwarder.forward(Bytes::from_static(b"blocked:1|c")),
2503            )
2504            .await
2505            .is_err(),
2506            "forwarding should wait for queue capacity instead of dropping"
2507        );
2508    }
2509
2510    #[tokio::test]
2511    async fn packet_forwarder_send_error_increments_error_telemetry() {
2512        let recorder = TestRecorder::default();
2513        let _recorder_guard = metrics::set_default_local_recorder(&recorder);
2514        let listen_addr = ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8125)));
2515        let context = ComponentContext::source(ComponentId::try_from("dogstatsd_test").expect("valid component ID"));
2516        let metrics = build_metrics(&listen_addr, &context, false);
2517        let socket = UdpSocket::bind("127.0.0.1:0").await.expect("socket should bind");
2518        let forwarder = ConnectedPacketForwarder {
2519            socket,
2520            target: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9125)),
2521        };
2522        let (packets_tx, packets_rx) = mpsc::channel(1);
2523        let worker = tokio::spawn(forwarder.run(packets_rx, metrics.clone()));
2524        let packet_forwarder = packet_forwarder_from_sender(9125, packets_tx, metrics);
2525
2526        packet_forwarder.forward(Bytes::from_static(b"daemon:666|g")).await;
2527
2528        let deadline = tokio::time::Instant::now() + Duration::from_secs(1);
2529        loop {
2530            if recorder.counter((
2531                "component_packets_forwarded_total",
2532                &[
2533                    ("component_id", "dogstatsd_test"),
2534                    ("component_type", "source"),
2535                    ("listener_type", "udp"),
2536                    ("state", "error"),
2537                ],
2538            )) == Some(1)
2539            {
2540                break;
2541            }
2542
2543            assert!(
2544                tokio::time::Instant::now() < deadline,
2545                "forwarding error telemetry should be recorded"
2546            );
2547            tokio::time::sleep(Duration::from_millis(10)).await;
2548        }
2549        worker.abort();
2550    }
2551
2552    #[test]
2553    fn unsupported_platform_process_credentials_do_not_count_as_origin_detection_telemetry_errors() {
2554        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Error(
2555            saluki_io::net::ProcessCredentialsError::UnsupportedPlatform,
2556        ));
2557
2558        assert!(!origin_detection_failed_for_telemetry(true, 1, &peer_addr));
2559    }
2560
2561    #[test]
2562    fn invalid_process_credentials_count_as_origin_detection_telemetry_errors() {
2563        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Error(
2564            saluki_io::net::ProcessCredentialsError::InvalidCredentials,
2565        ));
2566
2567        assert!(origin_detection_failed_for_telemetry(true, 1, &peer_addr));
2568    }
2569
2570    #[test]
2571    fn autoscale_udp_listeners_defaults_to_false() {
2572        let config = deser_config("{}");
2573        assert!(!config.autoscale_udp_listeners);
2574        assert!(config.udp_streams_to_yield().is_none());
2575    }
2576
2577    #[test]
2578    fn effective_max_buffer_count_never_below_baseline() {
2579        // A legacy config that only raised `dogstatsd_buffer_count` keeps its full capacity rather than being capped
2580        // to the `dogstatsd_buffer_count_max` default.
2581        let legacy = deser_config(r#"{"dogstatsd_buffer_count": 1024}"#);
2582        assert_eq!(legacy.effective_max_buffer_count(), 1024);
2583
2584        // An explicit maximum above the baseline is honored as-is.
2585        let explicit = deser_config(r#"{"dogstatsd_buffer_count": 128, "dogstatsd_buffer_count_max": 512}"#);
2586        assert_eq!(explicit.effective_max_buffer_count(), 512);
2587
2588        // A maximum below the baseline is treated as equal to the baseline.
2589        let below = deser_config(r#"{"dogstatsd_buffer_count": 200, "dogstatsd_buffer_count_max": 64}"#);
2590        assert_eq!(below.effective_max_buffer_count(), 200);
2591    }
2592
2593    #[tokio::test]
2594    async fn dogstatsd_io_buffer_pool_grows_on_demand_until_limit() {
2595        let min_buffers = 2;
2596        let max_buffers = 3;
2597        let (pool, shrinker) = build_io_buffer_pool(min_buffers, max_buffers, default_buffer_size());
2598
2599        let mut initial_buffers = Vec::with_capacity(min_buffers);
2600        for _ in 0..min_buffers {
2601            initial_buffers.push(
2602                timeout(Duration::from_secs(1), pool.acquire())
2603                    .await
2604                    .expect("initial buffer should be available"),
2605            );
2606        }
2607        let on_demand_buffer = timeout(Duration::from_secs(1), pool.acquire())
2608            .await
2609            .expect("pool should grow on demand before hitting the limit");
2610
2611        let capped_acquire = timeout(Duration::from_millis(25), pool.acquire()).await;
2612        assert!(capped_acquire.is_err(), "pool should wait once it reaches the limit");
2613
2614        drop(initial_buffers.pop().expect("initial buffer should still be held"));
2615        timeout(Duration::from_secs(1), pool.acquire())
2616            .await
2617            .expect("returned buffer should unblock acquisition");
2618
2619        drop(on_demand_buffer);
2620        drop(shrinker);
2621    }
2622
2623    #[test]
2624    #[cfg(target_os = "linux")]
2625    fn autoscale_udp_listeners_from_config_linux() {
2626        let config = deser_config(r#"{"dogstatsd_autoscale_udp_listeners": true}"#);
2627        assert!(config.autoscale_udp_listeners);
2628
2629        let streams = config
2630            .udp_streams_to_yield()
2631            .expect("autoscale yields at least 1 stream");
2632        let n = streams.get();
2633        assert!(
2634            (1..=4).contains(&n),
2635            "expected 1..=4 streams from vCPU formula, got {n}"
2636        );
2637    }
2638
2639    #[test]
2640    #[cfg(not(target_os = "linux"))]
2641    fn warns_for_uds_origin_detection_on_non_linux() {
2642        let config = deser_config(
2643            r#"{
2644                "dogstatsd_origin_detection": true,
2645                "dogstatsd_port": 0,
2646                "dogstatsd_socket": "/tmp/dsd.sock"
2647            }"#,
2648        );
2649        let addresses = config.build_addresses(None);
2650
2651        assert!(config.uds_origin_detection_unsupported_on_platform(&addresses));
2652    }
2653
2654    #[test]
2655    #[cfg(not(target_os = "linux"))]
2656    fn does_not_warn_for_udp_origin_detection_on_non_linux() {
2657        let config = deser_config(r#"{"dogstatsd_origin_detection": true}"#);
2658        let addresses = config.build_addresses(None);
2659
2660        assert!(!config.uds_origin_detection_unsupported_on_platform(&addresses));
2661    }
2662
2663    #[test]
2664    #[cfg(not(target_os = "linux"))]
2665    fn autoscale_udp_listeners_from_config_non_linux() {
2666        let config = deser_config(r#"{"dogstatsd_autoscale_udp_listeners": true}"#);
2667        assert!(config.autoscale_udp_listeners);
2668
2669        assert_eq!(None, config.udp_streams_to_yield());
2670    }
2671
2672    #[test]
2673    fn eol_required_defaults_to_no_listeners() {
2674        let config = deser_config("{}");
2675        let eol_required = config.eol_required();
2676
2677        assert!(!eol_required.for_listener(&udp_listen_address()));
2678        assert!(!eol_required.for_listener(&tcp_listen_address()));
2679    }
2680
2681    #[test]
2682    fn eol_required_matches_configured_listener_types() {
2683        let config = deser_config(r#"{"dogstatsd_eol_required": ["udp", "uds"]}"#);
2684        let eol_required = config.eol_required();
2685
2686        assert!(eol_required.for_listener(&udp_listen_address()));
2687        assert!(!eol_required.for_listener(&tcp_listen_address()));
2688
2689        #[cfg(unix)]
2690        {
2691            assert!(eol_required.for_listener(&ListenAddress::Unixgram("/tmp/dsd.sock".into())));
2692            assert!(eol_required.for_listener(&ListenAddress::Unix("/tmp/dsd-stream.sock".into())));
2693        }
2694    }
2695
2696    #[test]
2697    fn eol_required_accepts_space_separated_string() {
2698        let config = deser_config(r#"{"dogstatsd_eol_required": "udp uds"}"#);
2699        let eol_required = config.eol_required();
2700
2701        assert!(eol_required.for_listener(&udp_listen_address()));
2702    }
2703
2704    #[test]
2705    fn drops_full_named_pipe_buffer_without_newline() {
2706        let named_pipe_stream = named_pipe_listen_address();
2707        let mut buffer = get_pooled_object_via_builder::<_, BytesBuffer>(|| FixedSizeVec::with_capacity(8));
2708        buffer.put_slice(b"12345678");
2709
2710        assert!(super::should_drop_oversized_named_pipe_frame(
2711            &named_pipe_stream,
2712            &buffer
2713        ));
2714    }
2715
2716    #[test]
2717    fn keeps_named_pipe_partial_frame_when_buffer_has_capacity() {
2718        let named_pipe_stream = named_pipe_listen_address();
2719        let mut buffer = get_pooled_object_via_builder::<_, BytesBuffer>(|| FixedSizeVec::with_capacity(9));
2720        buffer.put_slice(b"12345678");
2721
2722        assert!(!super::should_drop_oversized_named_pipe_frame(
2723            &named_pipe_stream,
2724            &buffer
2725        ));
2726    }
2727
2728    #[test]
2729    fn keeps_full_named_pipe_buffer_with_newline() {
2730        let named_pipe_stream = named_pipe_listen_address();
2731        let mut buffer = get_pooled_object_via_builder::<_, BytesBuffer>(|| FixedSizeVec::with_capacity(8));
2732        buffer.put_slice(b"1234567\n");
2733
2734        assert!(!super::should_drop_oversized_named_pipe_frame(
2735            &named_pipe_stream,
2736            &buffer
2737        ));
2738    }
2739
2740    #[test]
2741    fn stream_log_too_big_warns_for_enabled_length_delimited_stream_invalid_frames() {
2742        let uds_stream = ListenAddress::Unix("/tmp/dsd-stream.sock".into());
2743        let named_pipe_stream = named_pipe_listen_address();
2744        let tcp_stream = ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8125)));
2745        let error = saluki_io::deser::framing::FramingError::InvalidFrame {
2746            frame_len: 8193,
2747            reason: "frame length exceeds buffer capacity",
2748        };
2749
2750        assert!(super::should_warn_stream_log_too_big(&uds_stream, &error, true));
2751        assert!(!super::should_warn_stream_log_too_big(&uds_stream, &error, false));
2752        assert!(!super::should_warn_stream_log_too_big(&named_pipe_stream, &error, true));
2753        assert!(!super::should_warn_stream_log_too_big(&tcp_stream, &error, true));
2754    }
2755
2756    #[test]
2757    fn interner_size_from_entry_count() {
2758        // A Core Agent migration config with entry count 4096 should yield 2 MiB, not 4096 bytes.
2759        let config = deser_config(r#"{"dogstatsd_string_interner_size": 4096}"#);
2760        assert_eq!(config.effective_context_string_interner_bytes(), ByteSize::mib(2));
2761    }
2762
2763    #[test]
2764    fn interner_size_from_explicit_bytes() {
2765        let config = deser_config(r#"{"dogstatsd_string_interner_size_bytes": 4194304}"#);
2766        assert_eq!(config.effective_context_string_interner_bytes(), ByteSize::b(4194304));
2767    }
2768
2769    #[test]
2770    fn interner_size_explicit_bytes_takes_priority() {
2771        let config = deser_config(
2772            r#"{"dogstatsd_string_interner_size": 4096, "dogstatsd_string_interner_size_bytes": 8388608}"#,
2773        );
2774        // The _bytes key (8 MiB) takes priority over the entry count.
2775        assert_eq!(config.effective_context_string_interner_bytes(), ByteSize::b(8388608));
2776    }
2777
2778    #[test]
2779    fn interner_size_custom_entry_count() {
2780        let config = deser_config(r#"{"dogstatsd_string_interner_size": 8192}"#);
2781        // 8192 entries * 512 bytes = 4 MiB
2782        assert_eq!(config.effective_context_string_interner_bytes(), ByteSize::mib(4));
2783    }
2784
2785    /// Asserts that two lists of ListenAddress are equivalent.
2786    fn address_list_eq(expected: &mut [ListenAddress], actual: &mut [ListenAddress]) -> Result<(), String> {
2787        if expected.len() != actual.len() {
2788            return Err(format!(
2789                "length mismatch: expected {} addresses, got {}",
2790                expected.len(),
2791                actual.len()
2792            ));
2793        }
2794
2795        expected.sort_by_key(|a| a.to_string());
2796        actual.sort_by_key(|a| a.to_string());
2797
2798        for (e, a) in expected.iter().zip(actual.iter()) {
2799            let (es, as_) = (e.to_string(), a.to_string());
2800            if es != as_ {
2801                return Err(format!("address mismatch: expected {}, got {}", es, as_));
2802            }
2803        }
2804
2805        Ok(())
2806    }
2807
2808    /// This test verifies that we didn't accidentally break the `build_addresses_no_listeners` helper function which
2809    /// would render all further tests useless.
2810    #[test]
2811    fn build_addresses_assertion_function_works() {
2812        let config = DogStatsDConfiguration {
2813            port: 0,
2814            tcp_port: 123,
2815            socket_path: None,
2816            socket_stream_path: None,
2817            non_local_traffic: false,
2818            ..Default::default()
2819        };
2820        let mut expected = vec![ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(
2821            // Close, but not quite! This is intentionally *not* 127.0.0.1 to test that the assertion will fail
2822            Ipv4Addr::new(127, 0, 0, 2),
2823            123,
2824        )))];
2825        let mut actual = config.build_addresses(None);
2826        assert!(address_list_eq(&mut expected, &mut actual).is_err())
2827    }
2828
2829    /// With all four listener gates off, `build_addresses` returns an empty Vec.
2830    #[test]
2831    fn build_addresses_no_listeners() {
2832        let config = DogStatsDConfiguration {
2833            port: 0,
2834            tcp_port: 0,
2835            socket_path: None,
2836            socket_stream_path: None,
2837            non_local_traffic: false,
2838            ..Default::default()
2839        };
2840        let mut expected = vec![];
2841        let mut actual = config.build_addresses(None);
2842        address_list_eq(&mut expected, &mut actual).unwrap();
2843    }
2844
2845    /// UDP port set, `non_local_traffic=false` -> UDP listener bound to `127.0.0.1`.
2846    #[test]
2847    fn build_addresses_udp_local_only() {
2848        let config = DogStatsDConfiguration {
2849            port: 8125,
2850            tcp_port: 0,
2851            socket_path: None,
2852            socket_stream_path: None,
2853            non_local_traffic: false,
2854            ..Default::default()
2855        };
2856        let mut expected = vec![ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(
2857            Ipv4Addr::new(127, 0, 0, 1),
2858            8125,
2859        )))];
2860        let mut actual = config.build_addresses(None);
2861        address_list_eq(&mut expected, &mut actual).unwrap();
2862    }
2863
2864    /// UDP port set, `non_local_traffic=true` -> UDP listener bound to `0.0.0.0`.
2865    #[test]
2866    fn build_addresses_udp_non_local_only() {
2867        let config = DogStatsDConfiguration {
2868            port: 8125,
2869            tcp_port: 0,
2870            socket_path: None,
2871            socket_stream_path: None,
2872            non_local_traffic: true,
2873            ..Default::default()
2874        };
2875        let mut expected = vec![ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(
2876            Ipv4Addr::new(0, 0, 0, 0),
2877            8125,
2878        )))];
2879        let mut actual = config.build_addresses(None);
2880        address_list_eq(&mut expected, &mut actual).unwrap();
2881    }
2882
2883    /// TCP port set, `non_local_traffic=false` -> TCP listener bound to `127.0.0.1`.
2884    #[test]
2885    fn build_addresses_tcp_local_only() {
2886        let config = DogStatsDConfiguration {
2887            port: 0,
2888            tcp_port: 9000,
2889            socket_path: None,
2890            socket_stream_path: None,
2891            non_local_traffic: false,
2892            ..Default::default()
2893        };
2894        let mut expected = vec![ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(
2895            Ipv4Addr::new(127, 0, 0, 1),
2896            9000,
2897        )))];
2898        let mut actual = config.build_addresses(None);
2899        address_list_eq(&mut expected, &mut actual).unwrap();
2900    }
2901
2902    /// TCP port set, `non_local_traffic=true` -> TCP listener bound to `0.0.0.0`.
2903    #[test]
2904    fn build_addresses_tcp_non_local_only() {
2905        let config = DogStatsDConfiguration {
2906            port: 0,
2907            tcp_port: 9000,
2908            socket_path: None,
2909            socket_stream_path: None,
2910            non_local_traffic: true,
2911            ..Default::default()
2912        };
2913        let mut expected = vec![ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(
2914            Ipv4Addr::new(0, 0, 0, 0),
2915            9000,
2916        )))];
2917        let mut actual = config.build_addresses(None);
2918        address_list_eq(&mut expected, &mut actual).unwrap();
2919    }
2920
2921    /// `socket_path` set -> a `Unixgram` address is produced with that path.
2922    #[test]
2923    fn build_addresses_unixgram_only() {
2924        let config = DogStatsDConfiguration {
2925            port: 0,
2926            tcp_port: 0,
2927            socket_path: Some("/tmp/dsd.sock".to_string()),
2928            socket_stream_path: None,
2929            non_local_traffic: false,
2930            ..Default::default()
2931        };
2932        let mut expected = vec![ListenAddress::Unixgram("/tmp/dsd.sock".into())];
2933        let mut actual = config.build_addresses(None);
2934        address_list_eq(&mut expected, &mut actual).unwrap();
2935    }
2936
2937    /// `socket_stream_path` set -> a `Unix` (stream) address is produced with that path.
2938    #[test]
2939    fn build_addresses_unix_stream_only() {
2940        let config = DogStatsDConfiguration {
2941            port: 0,
2942            tcp_port: 0,
2943            socket_path: None,
2944            socket_stream_path: Some("/tmp/dsd-stream.sock".to_string()),
2945            non_local_traffic: false,
2946            ..Default::default()
2947        };
2948        let mut expected = vec![ListenAddress::Unix("/tmp/dsd-stream.sock".into())];
2949        let mut actual = config.build_addresses(None);
2950        address_list_eq(&mut expected, &mut actual).unwrap();
2951    }
2952
2953    /// All four listener types enabled at once, with `non_local_traffic=true`.
2954    #[test]
2955    fn build_addresses_all_four_non_local() {
2956        let config = DogStatsDConfiguration {
2957            port: 8125,
2958            tcp_port: 9000,
2959            socket_path: Some("/tmp/dsd.sock".to_string()),
2960            socket_stream_path: Some("/tmp/dsd-stream.sock".to_string()),
2961            non_local_traffic: true,
2962            ..Default::default()
2963        };
2964        let mut expected = vec![
2965            ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 8125))),
2966            ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 9000))),
2967            ListenAddress::Unixgram("/tmp/dsd.sock".into()),
2968            ListenAddress::Unix("/tmp/dsd-stream.sock".into()),
2969        ];
2970        let mut actual = config.build_addresses(None);
2971        address_list_eq(&mut expected, &mut actual).unwrap();
2972    }
2973
2974    /// All four listener types enabled at once, with `non_local_traffic=false`.
2975    #[test]
2976    fn build_addresses_all_four_local() {
2977        let config = DogStatsDConfiguration {
2978            port: 8125,
2979            tcp_port: 9000,
2980            socket_path: Some("/tmp/dsd.sock".to_string()),
2981            socket_stream_path: Some("/tmp/dsd-stream.sock".to_string()),
2982            non_local_traffic: false,
2983            ..Default::default()
2984        };
2985        let mut expected = vec![
2986            ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 8125))),
2987            ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 9000))),
2988            ListenAddress::Unixgram("/tmp/dsd.sock".into()),
2989            ListenAddress::Unix("/tmp/dsd-stream.sock".into()),
2990        ];
2991        let mut actual = config.build_addresses(None);
2992        address_list_eq(&mut expected, &mut actual).unwrap();
2993    }
2994
2995    /// Passing `Some(ip)` to `build_addresses` with `non_local_traffic=false` -> both UDP and TCP
2996    /// bind to that IP. Includes a UDS datagram socket to confirm `bind_host` doesn't affect it.
2997    #[test]
2998    fn build_addresses_bind_host_applies_to_udp_and_tcp() {
2999        let config = DogStatsDConfiguration {
3000            port: 8125,
3001            tcp_port: 9000,
3002            socket_path: Some("/tmp/dsd.sock".to_string()),
3003            socket_stream_path: None,
3004            non_local_traffic: false,
3005            ..Default::default()
3006        };
3007        let bind_host = Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50)));
3008        let mut expected = vec![
3009            ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 50), 8125))),
3010            ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 50), 9000))),
3011            ListenAddress::Unixgram("/tmp/dsd.sock".into()),
3012        ];
3013        let mut actual = config.build_addresses(bind_host);
3014        address_list_eq(&mut expected, &mut actual).unwrap();
3015    }
3016
3017    /// Passing `Some(ip)` to `build_addresses` with `non_local_traffic=true` -> both UDP and TCP
3018    /// bind to `0.0.0.0`; the `bind_host` parameter is ignored (precedence matches the Agent).
3019    /// Includes a UDS stream socket to confirm `bind_host` doesn't affect it.
3020    #[test]
3021    fn build_addresses_non_local_clobbers_bind_host() {
3022        let config = DogStatsDConfiguration {
3023            port: 8125,
3024            tcp_port: 9000,
3025            socket_path: None,
3026            socket_stream_path: Some("/tmp/dsd-stream.sock".to_string()),
3027            non_local_traffic: true,
3028            ..Default::default()
3029        };
3030        let bind_host = Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50)));
3031        let mut expected = vec![
3032            ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 8125))),
3033            ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 9000))),
3034            ListenAddress::Unix("/tmp/dsd-stream.sock".into()),
3035        ];
3036        let mut actual = config.build_addresses(bind_host);
3037        address_list_eq(&mut expected, &mut actual).unwrap();
3038    }
3039
3040    #[test]
3041    fn non_finite_metric_values_are_silently_dropped() {
3042        // The Datadog Agent sends NaN gauges (for example, encode_ms.avg computed as 0.0/0.0 in Go).
3043        // FloatIter skips non-finite values with a debug log, so decode_packet returns Ok with
3044        // num_points == 0. handle_frame then returns Ok(None) for zero-point packets, which is
3045        // the existing silent-drop path (no warning emitted).
3046        let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
3047        for input in &[b"my.gauge:NaN|g" as &[u8], b"my.gauge:inf|g", b"my.gauge:-inf|g"] {
3048            match codec.decode_packet(input).expect("should decode without error") {
3049                ParsedPacket::Metric(packet) => assert_eq!(
3050                    packet.num_points, 0,
3051                    "non-finite value should be dropped, leaving 0 valid points"
3052                ),
3053                _ => panic!("expected Metric packet"),
3054            }
3055        }
3056    }
3057
3058    #[tokio::test]
3059    async fn fix_empty_capture_path_sets_path_from_run_path() {
3060        const RUN_PATH: &str = "/my/little/run_path";
3061
3062        let base_config_values = json!({ "run_path": RUN_PATH });
3063        let (config, _) = ConfigurationLoader::for_tests(Some(base_config_values), None, false).await;
3064
3065        let dogstatsd_config = DogStatsDConfiguration::from_configuration(&config).expect("should deserialize");
3066
3067        let expected = PathBuf::from(RUN_PATH).join(DOGSTATSD_CAPTURE_DIR);
3068        assert_eq!(expected, dogstatsd_config.capture_path);
3069    }
3070
3071    #[tokio::test]
3072    async fn fix_empty_capture_path_keeps_explicit_path() {
3073        const RUN_PATH: &str = "/my/little/run_path";
3074        const CAPTURE_PATH: &str = "/custom/path/to/capture";
3075
3076        let base_config_values = json!({ "run_path": RUN_PATH, "dogstatsd_capture_path": CAPTURE_PATH });
3077        let (config, _) = ConfigurationLoader::for_tests(Some(base_config_values), None, false).await;
3078
3079        let dogstatsd_config = DogStatsDConfiguration::from_configuration(&config).expect("should deserialize");
3080
3081        assert_eq!(PathBuf::from(CAPTURE_PATH), dogstatsd_config.capture_path);
3082    }
3083
3084    #[tokio::test]
3085    async fn from_configuration_normalizes_capture_depth() {
3086        let cases = [
3087            (json!({}), MIN_CAPTURE_DEPTH),
3088            (json!({ "dogstatsd_capture_depth": 0 }), MIN_CAPTURE_DEPTH),
3089            (json!({ "dogstatsd_capture_depth": 2048 }), 2048),
3090        ];
3091
3092        for (base_config_values, expected_depth) in cases {
3093            let (config, _) = ConfigurationLoader::for_tests(Some(base_config_values), None, false).await;
3094            let dogstatsd_config = DogStatsDConfiguration::from_configuration(&config).expect("should deserialize");
3095
3096            assert_eq!(expected_depth, dogstatsd_config.capture_depth);
3097        }
3098    }
3099
3100    #[test]
3101    fn capture_entity_resolver_is_configured_separately_from_workload_provider() {
3102        let config =
3103            DogStatsDConfiguration::default().with_capture_entity_resolver(CaptureTestEntityResolver::default());
3104
3105        assert!(config.capture_entity_resolver.is_some());
3106        assert!(config.workload_provider.is_none());
3107    }
3108
3109    #[test]
3110    fn resolve_capture_container_id_uses_live_pid_mapping() {
3111        let capture_entity_resolver = CaptureTestEntityResolver::with_pid_mapping(
3112            42,
3113            EntityId::from_local_data("ci-pid-container").expect("container entity"),
3114        );
3115
3116        assert_eq!(
3117            resolve_capture_container_id(Some(&capture_entity_resolver), Some(42)),
3118            Some("container_id://pid-container".to_string())
3119        );
3120    }
3121
3122    #[test]
3123    fn build_capture_record_ignores_payload_local_data() {
3124        let record = super::build_capture_record(None, None, b"test.metric:1|c|c:ci-local-container\n");
3125
3126        assert_eq!(record.container_id, None);
3127        assert!(record.ancillary.is_empty());
3128    }
3129
3130    #[test]
3131    fn stream_capture_state_preserves_last_pid_without_new_creds() {
3132        let mut stream_capture = super::StreamCaptureState::new();
3133
3134        stream_capture.update_peer_metadata(&ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(
3135            ProcessCredentials {
3136                pid: 42,
3137                uid: 0,
3138                gid: 0,
3139            },
3140        )));
3141        stream_capture.update_peer_metadata(&ConnectionAddress::ProcessLike(ProcessIdentity::Unavailable));
3142
3143        assert_eq!(stream_capture.last_pid, Some(42));
3144    }
3145
3146    #[test]
3147    fn apply_credentials_uses_live_pid_for_normal_packet() {
3148        let mut origin = RawOrigin::default();
3149        let creds = ProcessCredentials {
3150            pid: 12345,
3151            uid: 1000,
3152            gid: 1000,
3153        };
3154        super::apply_credentials_to_origin(&mut origin, &creds);
3155
3156        assert_eq!(origin.process_id(), Some(12345));
3157    }
3158
3159    #[test]
3160    fn apply_credentials_unpacks_captured_pid_when_replay_gid_present() {
3161        let mut origin = RawOrigin::default();
3162        let captured_pid: u32 = 99887766;
3163        let creds = ProcessCredentials {
3164            pid: 12345,        // our PID (irrelevant for replay)
3165            uid: captured_pid, // captured PID packed by the sender
3166            gid: super::REPLAY_CREDENTIALS_GID,
3167        };
3168        super::apply_credentials_to_origin(&mut origin, &creds);
3169
3170        assert_eq!(
3171            origin.process_id(),
3172            Some(super::origin::mark_replay_process_id(captured_pid))
3173        );
3174    }
3175}
3176
3177#[cfg(test)]
3178mod config_smoke {
3179    use datadog_agent_config_testing::config_registry::structs;
3180    use datadog_agent_config_testing::run_config_smoke_tests;
3181    use serde_json::json;
3182
3183    use super::DogStatsDConfiguration;
3184    use crate::config::{DatadogRemapper, KEY_ALIASES};
3185
3186    #[tokio::test]
3187    async fn smoke_test() {
3188        run_config_smoke_tests(
3189            structs::DOGSTATSD_CONFIGURATION,
3190            &[],
3191            json!({}),
3192            |cfg| {
3193                cfg.as_typed::<DogStatsDConfiguration>()
3194                    .expect("DogStatsDConfiguration should deserialize")
3195            },
3196            KEY_ALIASES,
3197            DatadogRemapper::new,
3198        )
3199        .await
3200    }
3201}