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    future::Future,
11    io, mem,
12    num::NonZeroUsize,
13    path::PathBuf,
14    pin::Pin,
15    sync::{Arc, LazyLock, Mutex as StdMutex},
16    time::{Duration, SystemTime, UNIX_EPOCH},
17};
18
19use async_trait::async_trait;
20use bytes::{Buf, BufMut, Bytes};
21use bytesize::ByteSize;
22use saluki_common::sync::shutdown::{ShutdownCoordinator, ShutdownHandle};
23use saluki_core::{
24    accounting::{MemoryBounds, MemoryBoundsBuilder, MemoryLimiter, UsageExpr},
25    components::{sources::*, BuildContext},
26    data_model::{
27        event::{
28            eventd::EventD,
29            metric::{Metric, MetricMetadata, MetricOrigin},
30            service_check::ServiceCheck,
31            Event, EventType,
32        },
33        tags::{RawTags, RawTagsFilter},
34    },
35    pooling::{ElasticObjectPool, ObjectPool as _},
36    runtime::{
37        self,
38        state::{AcquireError, ResourceLease},
39        InitializationError, ShutdownStrategy, Supervisable, SupervisorFuture,
40    },
41    topology::{EventsBuffer, OutputDefinition},
42};
43use saluki_env::{workload::CaptureEntityResolver, WorkloadProvider};
44use saluki_error::{generic_error, ErrorContext as _, GenericError};
45use saluki_io::{
46    buf::{BytesBuffer, ClearableIoBuffer as _, CollapsibleReadWriteIoBuffer as _, FixedSizeVec, ReadIoBuffer as _},
47    deser::{
48        codec::dogstatsd::*,
49        framing::{Framer as _, FramingError},
50    },
51    net::{
52        dns::{resolve_host, ResolveHostError},
53        listener::Listener,
54        ConnectionAddress, ListenAddress, ProcessCredentialsError, ProcessIdentity, SocketSpecification, Stream,
55    },
56};
57use snafu::Snafu;
58use stringtheory::MetaString;
59use tokio::{
60    pin, select,
61    sync::{mpsc, oneshot, Mutex},
62    time::{interval, MissedTickBehavior},
63};
64use tracing::{debug, error, info, trace, warn};
65
66mod forwarder;
67use self::forwarder::{PacketForwarder, PacketForwarderTarget};
68
69mod framer;
70use self::framer::{get_framer, DsdFramer};
71use crate::sources::dogstatsd::tags::{WellKnownTags, WellKnownTagsFilterPredicate};
72
73mod filters;
74use self::filters::EnablePayloadsFilter;
75
76mod metrics;
77use self::metrics::{build_metrics, Metrics};
78
79mod replay;
80use self::replay::{CaptureRecord, CapturedTaggerHandle, TrafficCapture};
81pub use self::replay::{
82    DogStatsDCaptureAPIHandler, DogStatsDCaptureControl, DogStatsDReplayAPIHandler, DogStatsDReplayControl,
83    ReplaySession, TimestampResolution, TrafficCaptureReader, DEFAULT_REPLAY_LOOPS, REPLAY_CREDENTIALS_GID,
84};
85
86mod origin;
87pub use self::origin::OriginEnrichmentConfiguration;
88use self::origin::{
89    origin_from_event_packet, origin_from_metric_packet, origin_from_service_check_packet, DogStatsDOriginTagResolver,
90    ProcessOrigin,
91};
92
93mod resolver;
94use self::resolver::ContextResolvers;
95
96mod tags;
97
98#[derive(Debug, Snafu)]
99#[snafu(context(suffix(false)))]
100enum Error {
101    #[snafu(display("Failed to acquire {} listener: {}", listener_type, source))]
102    FailedToAcquireListener {
103        listener_type: &'static str,
104        source: AcquireError,
105    },
106
107    #[snafu(display("No listeners could be created. Please specify a port (`dogstatsd_port`) or a socket path (`dogstatsd_socket` or `dogstatsd_stream_socket`) that can be listened on."))]
108    NoListeners,
109
110    #[snafu(context(false), display("Could not resolve bind_host: {}", source))]
111    UnresolvableBindHost { source: ResolveHostError },
112}
113
114/// Baseline byte cost per interner entry, used to convert the Core Agent's entry-count-based
115/// `dogstatsd_string_interner_size` to a byte size.
116///
117/// 4096 entries × 512 bytes = 2 MiB, matching ADP's previous default.
118const INTERNER_BASELINE_BYTES_PER_ENTRY: u64 = 512;
119const DOGSTATSD_LISTENER_WORKER_COUNT: usize = 1;
120const DOGSTATSD_PIPELINE_COUNT: usize = 1;
121const MIN_DOGSTATSD_WORKER_COUNT: usize = 2;
122
123fn default_decoder_worker_count(vcpus: usize) -> usize {
124    vcpus
125        .saturating_sub(DOGSTATSD_LISTENER_WORKER_COUNT + DOGSTATSD_PIPELINE_COUNT)
126        .max(MIN_DOGSTATSD_WORKER_COUNT)
127}
128
129/// Controls which payload types are forwarded to the backend.
130pub struct EnablePayloadsConfiguration {
131    /// Whether or not to enable sending series (counter/gauge/rate) payloads.
132    pub series: bool,
133
134    /// Whether or not to enable sending sketch (distribution) payloads.
135    pub sketches: bool,
136
137    /// Whether or not to enable sending event payloads.
138    pub events: bool,
139
140    /// Whether or not to enable sending service check payloads.
141    pub service_checks: bool,
142}
143
144const MIN_CAPTURE_DEPTH: usize = 1024;
145
146/// Which of the configured listeners the source requires.
147///
148/// This decides what happens when a listener can't be created, for example because its address is already in use.
149#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
150pub enum ListenerRequirement {
151    /// Every configured listener must be created, and at least one must be configured.
152    ///
153    /// A listener that can't be created stops the source from starting.
154    #[default]
155    All,
156
157    /// At least one listener must be created.
158    ///
159    /// A listener that can't be created is logged and skipped. The source fails to start if
160    /// no listener remains.
161    AtLeastOne,
162
163    /// No listener is required.
164    ///
165    /// A listener that can't be created is logged and skipped. If no listener remains, the source runs without
166    /// listeners until shutdown, and the rest of the server process keeps running.
167    Optional,
168}
169
170/// DogStatsD source.
171///
172/// Accepts metrics over TCP, UDP, or Unix Domain Sockets in the StatsD/DogStatsD format.
173pub struct DogStatsDConfiguration {
174    /// Hostname used when DogStatsD metrics do not carry an explicit `host:` tag.
175    pub default_hostname: MetaString,
176
177    /// The size of the buffer used to receive messages into, in bytes.
178    ///
179    /// Payloads can't exceed this size, or they will be truncated, leading to discarded messages.
180    pub buffer_size: usize,
181
182    /// The number of message buffers to allocate up front.
183    ///
184    /// This is the baseline pool size allocated at startup. The pool then grows on demand up to
185    /// `buffer_count_max` as active stream connections and datagram queues need additional buffers.
186    /// Higher values allocate more memory at startup but reduce on-demand allocations during bursts.
187    pub buffer_count: usize,
188
189    /// The maximum number of message buffers to allocate overall.
190    ///
191    /// The global pool starts at `buffer_count` buffers and grows on demand up to this limit. Active stream
192    /// connections use these buffers for reads, while connectionless listeners use them to queue received packets for
193    /// decoding. Increasing this value lets datagram listeners absorb larger bursts at the cost of up to one additional
194    /// `buffer_size` allocation per buffer. This limit bounds payload buffers, but not per-connection task and channel
195    /// bookkeeping. After a short grace period without pool growth, ADP releases idle buffers until the pool returns to
196    /// `buffer_count`.
197    ///
198    /// The pool never holds fewer buffers than `buffer_count`, so a value below the baseline is treated as equal to it.
199    pub buffer_count_max: usize,
200
201    /// The number of workers in the global pool that decodes connectionless DogStatsD packets.
202    ///
203    /// If set to `0`, the worker count is derived from the available vCPUs using the Core Agent's default formula.
204    /// Positive values force an exact worker count. Higher values can improve throughput when decoding is CPU-bound,
205    /// but also increase task scheduling and per-worker event buffering overhead.
206    pub workers_count: usize,
207
208    /// The port to listen on in UDP mode.
209    ///
210    /// If set to `0`, UDP isn't used.
211    pub port: u16,
212
213    /// The size of the receive buffer requested for each DogStatsD socket, in bytes.
214    ///
215    /// Applies to the UDP, TCP, and UDS sockets alike. If set to `0`, the OS default is used.
216    pub socket_receive_buffer_size: usize,
217
218    /// The port to listen on in TCP mode.
219    ///
220    /// If set to `0`, TCP isn't used.
221    pub tcp_port: u16,
222
223    /// The host to forward framed DogStatsD messages to over UDP.
224    ///
225    /// Forwarding is enabled only when this value is set and `statsd_forward_port` is non-zero. Setup failures
226    /// are logged, and send failures are tracked through telemetry.
227    pub statsd_forward_host: Option<MetaString>,
228
229    /// The port to forward framed DogStatsD messages to over UDP.
230    ///
231    /// Forwarding is enabled only when this value is non-zero and `statsd_forward_host` is set.
232    pub statsd_forward_port: u16,
233
234    /// The Unix domain socket path to listen on, in datagram mode.
235    ///
236    /// If not set, UDS (in datagram mode) isn't used.
237    pub socket_path: Option<String>,
238
239    /// The Unix domain socket path to listen on, in stream mode.
240    ///
241    /// If not set, UDS (in stream mode) isn't used.
242    pub socket_stream_path: Option<String>,
243
244    /// Controls whether ADP logs oversized DogStatsD stream frames.
245    ///
246    /// When set to `true`, ADP emits a warning when a UDS stream frame exceeds the
247    /// configured DogStatsD buffer size. The frame is still rejected either way.
248    ///
249    /// Enable this when diagnosing clients that send oversized UDS stream frames.
250    pub stream_log_too_big: bool,
251
252    /// Which of the configured listeners the source requires.
253    ///
254    /// Defaults to [`ListenerRequirement::All`], where a listener that can't be created prevents the source from
255    /// starting. With [`ListenerRequirement::AtLeastOne`] or [`ListenerRequirement::Optional`], such a listener
256    /// is logged and skipped instead. If no listener remains, the source only fails to start under `AtLeastOne`;
257    /// under `Optional` the source runs without listeners until shutdown.
258    ///
259    /// Keep the default when every configured address is needed. Relax it when the server process should keep
260    /// running while some addresses are unavailable, and use `AtLeastOne` when a process supervisor should still
261    /// detect and restart a process that can receive no traffic.
262    pub listener_requirement: ListenerRequirement,
263
264    /// The Windows named pipe name to listen on.
265    ///
266    /// If set, ADP listens for DogStatsD stream traffic on `\\.\pipe\<name>` on Windows.
267    /// The listener is unsupported on non-Windows platforms.
268    pub pipe_name: Option<String>,
269
270    /// Windows named pipe security descriptor.
271    ///
272    /// This SDDL descriptor is applied when creating the named pipe listener.
273    pub windows_pipe_security_descriptor: String,
274
275    /// Whether ADP lowers DogStatsD parse-failure logs to debug level.
276    ///
277    /// When set to `true`, invalid metrics, events, and service checks still increment decode-failure telemetry, but
278    /// their parse-failure logs are emitted at debug level instead of warning level. Enable this to suppress noisy
279    /// parse-error logs from misbehaving clients.
280    pub disable_verbose_logs: bool,
281
282    /// Listener types that require DogStatsD messages to be newline-terminated.
283    ///
284    /// Valid values are `udp`, `uds`, and `named_pipe`. Invalid values are ignored.
285    ///
286    /// An empty list accepts the final message without a newline.
287    pub eol_required: Vec<String>,
288
289    /// The host address to bind DogStatsD UDP and TCP listeners to.
290    ///
291    /// When set, UDP and TCP listeners bind to this address. Accepts either an IP literal (for example,
292    /// `192.168.1.50`, `::1`) or a hostname that resolves via DNS (for example, `agent.internal`).
293    /// Ignored when `non_local_traffic` is `true`, and binds to `127.0.0.1` when unset.
294    pub bind_host: Option<String>,
295
296    /// Whether or not to listen for non-local traffic.
297    ///
298    /// If set to `true`, the UDP and TCP listeners bind to `0.0.0.0` and accept traffic from any interface. Otherwise,
299    /// they bind to `bind_host`, or `127.0.0.1` if `bind_host` isn't set.
300    pub non_local_traffic: bool,
301
302    /// Whether to autoscale UDP stream handlers using `SO_REUSEPORT`.
303    ///
304    /// When enabled on Linux, the DogStatsD source binds multiple UDP sockets to the configured port with
305    /// `SO_REUSEPORT`, allowing the kernel to load-balance incoming datagrams across independent stream handler
306    /// tasks. The number of sockets scales with available vCPUs: one stream handler base, plus one additional
307    /// per 8 vCPUs, capped at 4 total.
308    ///
309    /// Has no effect on non-Linux platforms because `SO_REUSEPORT` doesn't provide kernel-level load balancing
310    /// there; a warning is logged at startup if enabled outside of Linux.
311    ///
312    /// Enable this on multi-vCPU Linux deployments where UDP DogStatsD throughput is bottlenecked on a single
313    /// receive task.
314    pub autoscale_udp_listeners: bool,
315
316    /// Whether or not to allow heap allocations when resolving contexts.
317    ///
318    /// When resolving contexts during parsing, the metric name and tags are interned to reduce memory usage. The
319    /// interner has a fixed size, however, which means some strings can fail to be interned if the interner is full.
320    /// When set to `true`, we allow these strings to be allocated on the heap like normal, but this can lead to
321    /// increased (unbounded) memory usage. When set to `false`, if the metric name and all of its tags can't be
322    /// interned, the metric is skipped.
323    pub allow_context_heap_allocations: bool,
324
325    /// Whether or not to enable support for no-aggregation pipelines.
326    ///
327    /// When enabled, this influences how metrics are parsed, specifically around user-provided metric timestamps. When
328    /// metric timestamps are present, it's used as a signal to any aggregation transforms that the metric shouldn't
329    /// be aggregated.
330    pub no_aggregation_pipeline_support: bool,
331
332    /// Number of entries for the string interner, as interpreted by the Core Datadog Agent.
333    ///
334    /// When `context_string_interner_size_bytes` isn't set, this value is multiplied by 512 bytes per entry to
335    /// derive the interner byte size. This provides backwards compatibility for customers migrating configurations
336    /// from the Core Agent, where this setting represents an entry count rather than a byte size.
337    pub context_string_interner_entry_count: u64,
338
339    /// Total size of the string interner used for contexts, in bytes.
340    ///
341    /// When set, this takes priority over `context_string_interner_entry_count`. This controls the amount of memory
342    /// that can be used to intern metric names and tags. If the interner is full, metrics with contexts that haven't
343    /// already been resolved may or may not be dropped, depending on the value of `allow_context_heap_allocations`.
344    pub context_string_interner_size_bytes: Option<ByteSize>,
345
346    /// The maximum number of cached contexts to allow.
347    ///
348    /// This is the maximum number of resolved contexts that can be cached at any given time. This limit doesn't affect
349    /// the total number of contexts that can be _alive_ at any given time, which is dependent on the interner capacity
350    /// and whether or not heap allocations are allowed.
351    pub cached_contexts_limit: usize,
352
353    /// The maximum number of cached tagsets to allow.
354    ///
355    /// This is the maximum number of resolved tagsets that can be cached at any given time. This limit doesn't affect
356    /// the total number of tagsets that can be _alive_ at any given time, which is dependent on the interner capacity
357    /// and whether or not heap allocations are allowed.
358    pub cached_tagsets_limit: usize,
359
360    /// The number of seconds after which cached contexts will expire.
361    ///
362    /// Higher values allow for more effective caching for sparse metrics at the cost of increased memory usage.
363    pub context_expiry_seconds: u64,
364
365    /// Whether or not to enable permissive mode in the decoder.
366    ///
367    /// Permissive mode allows the decoder to relax its strictness around the allowed payloads, which lets it match the
368    /// decoding behavior of the Datadog Agent.
369    pub permissive_decoding: bool,
370
371    /// The minimum sample rate allowed for metrics.
372    ///
373    /// When metrics are sent with a sample rate _lower_ than this value then it will be clamped to this value. This is
374    /// done in order to ensure an upper bound on how many equivalent samples are tracked for the metric, as high sample
375    /// rates (very small numbers, such as `0.00000001`) can lead to large memory growth.
376    ///
377    /// A warning log will be emitted when clamping occurs, as this represents an effective loss of metric samples.
378    pub minimum_sample_rate: f64,
379
380    /// Which payload types to forward to the backend.
381    pub enable_payloads: EnablePayloadsConfiguration,
382
383    /// Configuration related to origin detection and enrichment.
384    pub origin_enrichment: OriginEnrichmentConfiguration,
385
386    /// Whether to break down DogStatsD processed-metric telemetry by UDS origin.
387    ///
388    /// When enabled, metric-message `dogstatsd.processed` telemetry includes an `origin` label derived from the
389    /// sender's UDS origin. This can add one telemetry series per origin and should primarily be used for diagnostics.
390    pub origin_telemetry_enabled: bool,
391
392    /// Workload provider to utilize for origin enrichment.
393    ///
394    /// Detected origins are only resolved to workload tags when a provider is set. Origin detection itself is
395    /// controlled by `origin_enrichment`.
396    pub workload_provider: Option<Arc<dyn WorkloadProvider + Send + Sync>>,
397
398    /// Resolver to use for mapping live sender PIDs to container entities before deferred processing.
399    ///
400    /// This resolver pins the sender entity while socket credentials are current so origin enrichment and traffic
401    /// capture do not resolve a stale or reused PID later. It is set separately from the workload provider because it
402    /// only needs a narrow live-PID lookup.
403    pub capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
404
405    /// Additional tags to add to all metrics.
406    ///
407    /// These are sorted and deduplicated before use, so callers may append tags required by the running environment.
408    pub additional_tags: Vec<String>,
409
410    /// The directory where DogStatsD capture files are written by default.
411    ///
412    /// When set to an empty path, callers must provide an explicit capture path when starting a capture session.
413    pub capture_path: PathBuf,
414
415    /// The maximum number of captured packets that can be queued for persistence.
416    ///
417    /// This controls the depth of the in-process capture queue. Values below `1024` are raised to `1024` before the
418    /// capture writer starts, preventing a zero-depth rendezvous channel from serializing DogStatsD stream handlers
419    /// behind capture persistence.
420    pub capture_depth: usize,
421
422    /// Control surface that starts and stops DogStatsD traffic capture.
423    ///
424    /// This is runtime wiring rather than configuration: the source binds the capture it creates to this handle, so
425    /// the caller creates the handle and keeps a clone to serve the capture API.
426    pub capture_control: DogStatsDCaptureControl,
427
428    /// Control surface that starts and stops DogStatsD traffic replay.
429    ///
430    /// This is runtime wiring rather than configuration: the source binds the captured tagger to this handle, so the
431    /// caller creates the handle and keeps a clone to serve the replay API.
432    pub replay_control: DogStatsDReplayControl,
433}
434
435#[derive(Clone, Copy, Default)]
436struct EolRequired {
437    udp: bool,
438    uds: bool,
439    named_pipe: bool,
440}
441
442impl EolRequired {
443    fn from_config_values(values: &[String]) -> Self {
444        let mut eol_required = Self::default();
445
446        for value in values {
447            match value.as_str() {
448                "udp" => eol_required.udp = true,
449                "uds" => eol_required.uds = true,
450                "named_pipe" => eol_required.named_pipe = true,
451                _ => warn!(
452                    value,
453                    "Invalid dogstatsd_eol_required value. Expected 'udp', 'uds', or 'named_pipe'."
454                ),
455            }
456        }
457
458        eol_required
459    }
460
461    fn for_listener(&self, listen_addr: &ListenAddress) -> bool {
462        match listen_addr {
463            ListenAddress::Udp(_) => self.udp,
464            ListenAddress::Tcp(_) => false,
465            ListenAddress::Unixgram(_) | ListenAddress::Unix(_) => self.uds,
466            ListenAddress::NamedPipe { .. } => self.named_pipe,
467        }
468    }
469}
470
471#[cfg(test)]
472impl DogStatsDConfiguration {
473    /// Creates a fixture configuration, for tests that exercise source behavior rather than configuration.
474    ///
475    /// Values match the effective defaults the configuration system resolves, so a test only states the settings it
476    /// cares about. `capture_depth` is the exception: a default configuration leaves it at `0`, which the capture
477    /// writer raises to its minimum, so the fixture states that minimum directly.
478    fn for_test() -> Self {
479        Self {
480            default_hostname: MetaString::default(),
481            buffer_size: 8192,
482            buffer_count: 128,
483            buffer_count_max: 32_768,
484            workers_count: 0,
485            port: 8125,
486            socket_receive_buffer_size: 0,
487            tcp_port: 0,
488            statsd_forward_host: None,
489            statsd_forward_port: 0,
490            socket_path: None,
491            socket_stream_path: None,
492            stream_log_too_big: false,
493            listener_requirement: ListenerRequirement::default(),
494            pipe_name: None,
495            windows_pipe_security_descriptor: "D:AI(A;;GA;;;WD)".to_string(),
496            disable_verbose_logs: false,
497            eol_required: Vec::new(),
498            bind_host: None,
499            non_local_traffic: false,
500            autoscale_udp_listeners: false,
501            allow_context_heap_allocations: true,
502            no_aggregation_pipeline_support: true,
503            context_string_interner_entry_count: 4096,
504            context_string_interner_size_bytes: None,
505            cached_contexts_limit: 500_000,
506            cached_tagsets_limit: 500_000,
507            context_expiry_seconds: 20,
508            permissive_decoding: true,
509            minimum_sample_rate: 0.000000003845,
510            enable_payloads: EnablePayloadsConfiguration {
511                series: true,
512                sketches: true,
513                events: true,
514                service_checks: true,
515            },
516            origin_enrichment: OriginEnrichmentConfiguration::for_test(),
517            origin_telemetry_enabled: false,
518            workload_provider: None,
519            capture_entity_resolver: None,
520            additional_tags: Vec::new(),
521            capture_path: PathBuf::new(),
522            capture_depth: MIN_CAPTURE_DEPTH,
523            capture_control: DogStatsDCaptureControl::default(),
524            replay_control: DogStatsDReplayControl::default(),
525        }
526    }
527}
528
529impl DogStatsDConfiguration {
530    /// Gets the effective source-wide DogStatsD tags.
531    fn additional_tags(&self) -> Vec<String> {
532        let mut tags = self.additional_tags.clone();
533        tags.sort_unstable();
534        tags.dedup();
535        tags
536    }
537
538    /// Returns the effective string interner size in bytes.
539    ///
540    /// If `context_string_interner_size_bytes` is set, it's used directly. Otherwise,
541    /// `context_string_interner_entry_count` is multiplied by 512 bytes per entry to derive the byte size.
542    fn effective_context_string_interner_bytes(&self) -> ByteSize {
543        match self.context_string_interner_size_bytes {
544            Some(explicit_bytes) => explicit_bytes,
545            None => {
546                saluki_antithesis::always_le!(
547                    self.context_string_interner_entry_count,
548                    u64::MAX / INTERNER_BASELINE_BYTES_PER_ENTRY,
549                    "dogstatsd interner byte-size multiply does not overflow",
550                    { "entry_count": self.context_string_interner_entry_count }
551                );
552                ByteSize::b(
553                    self.context_string_interner_entry_count
554                        .saturating_mul(INTERNER_BASELINE_BYTES_PER_ENTRY),
555                )
556            }
557        }
558    }
559
560    fn eol_required(&self) -> EolRequired {
561        EolRequired::from_config_values(&self.eol_required)
562    }
563
564    fn statsd_forward_target(&self) -> Option<(&MetaString, u16)> {
565        let host = self.statsd_forward_host.as_ref()?;
566        if self.statsd_forward_port == 0 {
567            return None;
568        }
569
570        Some((host, self.statsd_forward_port))
571    }
572
573    fn packet_forwarder_target(&self) -> Option<PacketForwarderTarget> {
574        let (host, port) = self.statsd_forward_target()?;
575        Some(PacketForwarderTarget::new(host.clone(), port))
576    }
577
578    /// Returns the number of UDP stream handlers to spawn, derived from `dogstatsd_autoscale_udp_listeners` and
579    /// the number of available vCPUs.
580    ///
581    /// Returns `None` when autoscaling is disabled, which keeps the legacy single-socket behavior. The platform
582    /// gate for `SO_REUSEPORT` lives inside the listener—this method intentionally stays platform-agnostic.
583    fn udp_streams_to_yield(&self) -> Option<NonZeroUsize> {
584        if !self.autoscale_udp_listeners {
585            return None;
586        }
587
588        #[cfg(not(target_os = "linux"))]
589        if self.autoscale_udp_listeners {
590            warn!("UDP stream handler autoscaling not supported on non-Linux platforms. Default to single stream handler.");
591            return None;
592        }
593
594        let vcpus = std::thread::available_parallelism().map(NonZeroUsize::get).unwrap_or(1);
595        let streams = (1 + vcpus / 8).min(4);
596        NonZeroUsize::new(streams)
597    }
598
599    /// Returns the effective maximum size of the I/O buffer pool.
600    ///
601    /// The pool can never hold fewer buffers than the configured baseline, so a `dogstatsd_buffer_count_max` below
602    /// `dogstatsd_buffer_count` (including a legacy config that only raised `dogstatsd_buffer_count`) is treated as
603    /// equal to the baseline rather than reducing capacity.
604    fn effective_max_buffer_count(&self) -> usize {
605        self.buffer_count_max.max(self.buffer_count)
606    }
607
608    fn decoder_worker_count(&self) -> NonZeroUsize {
609        let worker_count = if self.workers_count == 0 {
610            let vcpus = std::thread::available_parallelism().map(NonZeroUsize::get).unwrap_or(1);
611            default_decoder_worker_count(vcpus)
612        } else {
613            self.workers_count
614        };
615
616        NonZeroUsize::new(worker_count).expect("DogStatsD decoder worker count must be non-zero")
617    }
618
619    /// Using the current configuration, determines which listeners should be created and adds an address for each into
620    /// a `Vec<ListenAddress>`. This function has no side effects so that it can be unit tested whereas build_listeners`
621    /// actually binds the listeners on the system.
622    ///
623    /// `bind_host` is the pre-resolved IP that UDP and TCP listeners should bind to (provided by
624    /// `resolve_bind_ip`). Precedence matches the Agent:
625    ///   - `non_local_traffic=true` → `0.0.0.0` (`bind_host` ignored)
626    ///   - `bind_host=Some(ip)`     → `ip`
627    ///   - `bind_host=None`         → `127.0.0.1`
628    fn build_addresses(&self, bind_host: Option<std::net::IpAddr>) -> Vec<ListenAddress> {
629        let bind_ip: std::net::IpAddr = if self.non_local_traffic {
630            [0, 0, 0, 0].into()
631        } else {
632            bind_host.unwrap_or_else(|| [127, 0, 0, 1].into())
633        };
634
635        let mut addresses: Vec<ListenAddress> = Vec::new();
636
637        if self.port != 0 {
638            addresses.push(ListenAddress::Udp(std::net::SocketAddr::new(bind_ip, self.port)));
639        }
640
641        if self.tcp_port != 0 {
642            addresses.push(ListenAddress::Tcp(std::net::SocketAddr::new(bind_ip, self.tcp_port)));
643        }
644
645        if let Some(socket_path) = &self.socket_path {
646            addresses.push(ListenAddress::Unixgram(socket_path.into()));
647        }
648
649        if let Some(socket_stream_path) = &self.socket_stream_path {
650            addresses.push(ListenAddress::Unix(socket_stream_path.into()));
651        }
652
653        if let Some(pipe_name) = &self.pipe_name {
654            addresses.push(ListenAddress::named_pipe_with_input_buffer_size(
655                pipe_name,
656                &self.windows_pipe_security_descriptor,
657                self.buffer_size as u32,
658            ));
659        }
660
661        addresses
662    }
663
664    fn uds_origin_detection_unsupported_on_platform(&self, addresses: &[ListenAddress]) -> bool {
665        self.origin_enrichment.enabled
666            && cfg!(not(target_os = "linux"))
667            && addresses
668                .iter()
669                .any(|address| matches!(address, ListenAddress::Unixgram(_) | ListenAddress::Unix(_)))
670    }
671
672    fn warn_if_uds_origin_detection_unsupported(&self, addresses: &[ListenAddress]) {
673        if self.uds_origin_detection_unsupported_on_platform(addresses) {
674            warn!(
675                "DogStatsD UDS origin detection is enabled, but PID-based Unix socket credentials are unsupported on \
676                 this platform. Metrics are accepted without PID-based origin enrichment."
677            );
678        }
679    }
680
681    /// Resolves the IP that UDP and TCP listeners bind to, if `bind_host` applies.
682    ///
683    /// The lookup is skipped when `non_local_traffic` is set, since `bind_host` is then unused, so an unresolvable
684    /// hostname that wouldn't be used doesn't stop the source from starting.
685    async fn resolve_bind_ip(&self) -> Result<Option<std::net::IpAddr>, Error> {
686        match &self.bind_host {
687            Some(host) if !self.non_local_traffic => Ok(Some(resolve_host(host).await?)),
688            _ => Ok(None),
689        }
690    }
691
692    /// Handles `error`, raised because `listeners` can't be created.
693    ///
694    /// The error is not propagated if the listener requirement skips failed listeners.
695    fn handle_listener_failure(&self, error: Error, listeners: &str) -> Result<(), Error> {
696        // A key registered as a different type of resource is a programming error, not a listener that couldn't be
697        // created, so it's never skipped.
698        let is_key_collision = matches!(
699            error,
700            Error::FailedToAcquireListener {
701                source: AcquireError::TypeMismatch { .. },
702                ..
703            }
704        );
705        let fail = match self.listener_requirement {
706            ListenerRequirement::All => true,
707            ListenerRequirement::AtLeastOne | ListenerRequirement::Optional => false,
708        };
709        if is_key_collision || fail {
710            return Err(error);
711        }
712
713        error!(error = %error, "Can't create DogStatsD {}.", listeners);
714        Ok(())
715    }
716
717    /// Acquires the appropriate `Listener` objects from the resource registry.
718    ///
719    /// The listeners are leased, not owned: the registry keeps the bound sockets when this source is torn down, so a
720    /// rebuilt source gets the same sockets back rather than racing to re-bind addresses it just released.
721    ///
722    /// The listener requirement decides whether a listener that can't be created is skipped, and whether ending up with
723    /// no listener at all is an error.
724    async fn build_listeners(&self, context: &BuildContext) -> Result<Vec<ResourceLease<Listener>>, Error> {
725        let addresses = match self.resolve_bind_ip().await {
726            Ok(bind_ip) => self.build_addresses(bind_ip),
727            Err(e) => {
728                // An unresolvable `bind_host` only prevents the UDP and TCP listeners.
729                self.handle_listener_failure(e, "UDP and TCP listeners")?;
730                self.build_addresses(None)
731                    .into_iter()
732                    .filter(|address| !matches!(address, ListenAddress::Udp(_) | ListenAddress::Tcp(_)))
733                    .collect()
734            }
735        };
736
737        self.warn_if_uds_origin_detection_unsupported(&addresses);
738
739        let mut listeners = Vec::new();
740        let socket_receive_buffer_size =
741            (self.socket_receive_buffer_size != 0).then_some(self.socket_receive_buffer_size);
742        let udp_streams_to_yield = self.udp_streams_to_yield();
743        for address in addresses {
744            let listener_type = address.listener_type();
745            let listener_streams = matches!(address, ListenAddress::Udp(_))
746                .then_some(udp_streams_to_yield)
747                .flatten();
748            let spec = SocketSpecification::new(address)
749                .with_udp_streams(listener_streams)
750                .with_receive_buffer_size(socket_receive_buffer_size);
751            match context.acquire_resource(spec).await {
752                Ok(listener) => listeners.push(listener),
753                Err(source) => self.handle_listener_failure(
754                    Error::FailedToAcquireListener { listener_type, source },
755                    &format!("{listener_type} listener"),
756                )?,
757            }
758        }
759
760        if listeners.is_empty() {
761            match self.listener_requirement {
762                ListenerRequirement::All | ListenerRequirement::AtLeastOne => return Err(Error::NoListeners),
763                // The server process keeps running. The source runs without listeners until shutdown.
764                ListenerRequirement::Optional => error!(error = %Error::NoListeners, "Could not start DogStatsD."),
765            }
766        }
767        Ok(listeners)
768    }
769}
770
771#[async_trait]
772impl SourceBuilder for DogStatsDConfiguration {
773    async fn build(&self, context: BuildContext) -> Result<Box<dyn Source + Send>, GenericError> {
774        let listeners = self.build_listeners(&context).await?;
775
776        // Every listener requires at least one I/O buffer to ensure that all listeners can be serviced without
777        // deadlocking any of the others. Multi-socket connectionless listeners require one buffer per yielded socket.
778        let min_buffers: usize = listeners.iter().map(|listener| listener.min_buffer_reservation()).sum();
779        let max_buffers = self.effective_max_buffer_count();
780        // Without listeners, `min_buffers` is zero, so the check below can't catch a capacity of zero. The datagram
781        // queue needs room for at least one buffer either way.
782        if max_buffers == 0 {
783            return Err(generic_error!(
784                "The maximum I/O buffer count must be greater than zero."
785            ));
786        }
787        if max_buffers < min_buffers {
788            return Err(generic_error!(
789                "The maximum I/O buffer count ({}) must be at least {} to service all configured listeners.",
790                max_buffers,
791                min_buffers,
792            ));
793        }
794
795        let origin_detection_enabled = self.origin_enrichment.enabled;
796        // Single CapturedTaggerHandle is cloned to both the resolver (reader of the captured store) and the replay
797        // control surface (writer). Both sides reference the same atomic slot.
798        let captured_tagger = CapturedTaggerHandle::new();
799
800        let maybe_origin_tags_resolver = self.workload_provider.clone().map(|provider| {
801            DogStatsDOriginTagResolver::new(self.origin_enrichment.clone(), provider, captured_tagger.clone())
802        });
803        let context_resolvers = ContextResolvers::new(self, context.component_context(), maybe_origin_tags_resolver)
804            .error_context("Failed to create context resolvers.")?;
805
806        let codec_config = DogStatsDCodecConfiguration::default()
807            .with_timestamps(self.no_aggregation_pipeline_support)
808            .with_permissive_mode(self.permissive_decoding)
809            .with_minimum_sample_rate(self.minimum_sample_rate)
810            .with_client_origin_detection(self.origin_enrichment.origin_detection_client);
811
812        let codec = DogStatsDCodec::from_configuration(codec_config);
813        let eol_required = self.eol_required();
814
815        let enable_payloads_filter = EnablePayloadsFilter::default()
816            .with_allow_series(self.enable_payloads.series)
817            .with_allow_sketches(self.enable_payloads.sketches)
818            .with_allow_events(self.enable_payloads.events)
819            .with_allow_service_checks(self.enable_payloads.service_checks);
820        let traffic_capture = TrafficCapture::with_workload_provider(
821            self.capture_path.clone(),
822            self.capture_depth,
823            self.workload_provider.clone(),
824        );
825        self.capture_control.bind(traffic_capture.clone());
826        let packet_forwarder_target = self.packet_forwarder_target();
827
828        self.replay_control.bind(captured_tagger);
829
830        // The pool allocates `buffer_count` buffers up front and may grow on demand up to `max_buffers`. The effective
831        // maximum is never below the baseline, so configs that only raise `dogstatsd_buffer_count` keep their full
832        // capacity instead of being silently reduced to the `dogstatsd_buffer_count_max` default.
833        let (io_buffer_pool, io_buffer_pool_shrinker) =
834            build_io_buffer_pool(self.buffer_count, max_buffers, self.buffer_size);
835        Ok(Box::new(DogStatsD {
836            listeners,
837            decoder_worker_count: self.decoder_worker_count(),
838            io_buffer_pool,
839            io_buffer_queue_capacity: max_buffers,
840            io_buffer_pool_shrinker: Box::pin(io_buffer_pool_shrinker),
841            codec,
842            context_resolvers,
843            default_hostname: self.default_hostname.clone(),
844            enabled_filter: enable_payloads_filter,
845            origin_detection_enabled,
846            origin_telemetry_enabled: self.origin_telemetry_enabled,
847            stream_log_too_big: self.stream_log_too_big,
848            disable_verbose_logs: self.disable_verbose_logs,
849            eol_required,
850            additional_tags: self.additional_tags().into(),
851            capture_entity_resolver: self.capture_entity_resolver.clone(),
852            traffic_capture,
853            packet_forwarder_target,
854        }))
855    }
856
857    fn outputs(&self) -> &[OutputDefinition<EventType>] {
858        static OUTPUTS: LazyLock<Vec<OutputDefinition<EventType>>> = LazyLock::new(|| {
859            vec![
860                OutputDefinition::named_output("metrics", EventType::Metric),
861                OutputDefinition::named_output("events", EventType::EventD),
862                OutputDefinition::named_output("service_checks", EventType::ServiceCheck),
863            ]
864        });
865        &OUTPUTS
866    }
867}
868
869impl MemoryBounds for DogStatsDConfiguration {
870    fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
871        let additional_buffers = self.effective_max_buffer_count().saturating_sub(self.buffer_count);
872        let adjusted_buffer_size = get_adjusted_buffer_size(self.buffer_size);
873
874        builder
875            .minimum()
876            // Capture the size of the heap allocation when the component is built.
877            .with_single_value::<DogStatsD>("source struct")
878            // We allocate the baseline buffer pool up front.
879            .with_expr(UsageExpr::product(
880                "buffers",
881                UsageExpr::config("dogstatsd_buffer_count", self.buffer_count),
882                UsageExpr::config("dogstatsd_buffer_size", adjusted_buffer_size),
883            ))
884            // We also allocate the backing storage for the string interner up front, which is used by our context
885            // resolver.
886            .with_expr(UsageExpr::config(
887                "dogstatsd_string_interner_size_bytes",
888                self.effective_context_string_interner_bytes().as_u64() as usize,
889            ));
890
891        // The pool can grow on demand up to its maximum, so account for the additional headroom as firm usage.
892        builder.firm().with_expr(UsageExpr::product(
893            "elastic buffers",
894            UsageExpr::constant("dogstatsd_buffer_count_max_extra", additional_buffers),
895            UsageExpr::config("dogstatsd_buffer_size", adjusted_buffer_size),
896        ));
897    }
898}
899
900/// DogStatsD source.
901pub struct DogStatsD {
902    listeners: Vec<ResourceLease<Listener>>,
903    decoder_worker_count: NonZeroUsize,
904    io_buffer_pool: ElasticObjectPool<BytesBuffer>,
905    io_buffer_queue_capacity: usize,
906    io_buffer_pool_shrinker: Pin<Box<dyn Future<Output = ()> + Send>>,
907    codec: DogStatsDCodec,
908    context_resolvers: ContextResolvers,
909    default_hostname: MetaString,
910    enabled_filter: EnablePayloadsFilter,
911    origin_detection_enabled: bool,
912    origin_telemetry_enabled: bool,
913    stream_log_too_big: bool,
914    disable_verbose_logs: bool,
915    eol_required: EolRequired,
916    additional_tags: Arc<[String]>,
917    capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
918    traffic_capture: TrafficCapture,
919    packet_forwarder_target: Option<PacketForwarderTarget>,
920}
921
922struct ListenerContext {
923    listener: ResourceLease<Listener>,
924    datagram_sender: mpsc::Sender<QueuedDatagram>,
925    io_buffer_pool: ElasticObjectPool<BytesBuffer>,
926    origin_telemetry_enabled: bool,
927    eol_required: EolRequired,
928    decoder_context: DecoderContext,
929    capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
930    packet_forwarder_target: Option<PacketForwarderTarget>,
931}
932
933#[derive(Clone)]
934struct HandlerContext {
935    listen_addr: ListenAddress,
936    eol_required: bool,
937    io_buffer_pool: ElasticObjectPool<BytesBuffer>,
938    datagram_sender: mpsc::Sender<QueuedDatagram>,
939    datagram_context: Option<Arc<DatagramSocketContext>>,
940    metrics: Metrics,
941    decoder_context: DecoderContext,
942    capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
943    packet_forwarder: Option<PacketForwarder>,
944}
945
946#[derive(Clone)]
947struct DecoderContext {
948    codec: DogStatsDCodec,
949    context_resolvers: ContextResolvers,
950    default_hostname: MetaString,
951    enabled_filter: EnablePayloadsFilter,
952    origin_detection_enabled: bool,
953    stream_log_too_big: bool,
954    disable_verbose_logs: bool,
955    additional_tags: Arc<[String]>,
956    traffic_capture: TrafficCapture,
957}
958
959struct DatagramSocketContext {
960    listen_addr: ListenAddress,
961    eol_required: bool,
962    metrics: Metrics,
963    packet_forwarder: Option<PacketForwarder>,
964}
965
966struct QueuedDatagram {
967    result: io::Result<ReceivedBuffer>,
968    socket_context: Arc<DatagramSocketContext>,
969}
970
971struct DogStatsDDecoder {
972    source_context: SourceContext,
973    codec: DogStatsDCodec,
974    context_resolvers: ContextResolvers,
975    default_hostname: MetaString,
976    enabled_filter: EnablePayloadsFilter,
977    origin_detection_enabled: bool,
978    stream_log_too_big: bool,
979    disable_verbose_logs: bool,
980    additional_tags: Arc<[String]>,
981    traffic_capture: TrafficCapture,
982    event_buffer: Option<EventsBuffer>,
983}
984
985#[derive(Clone, Copy)]
986enum BufferDecodeMode {
987    Connectionless,
988    Connected,
989}
990
991impl BufferDecodeMode {
992    fn is_eof(self, bytes_read: usize) -> bool {
993        match self {
994            Self::Connectionless => true,
995            Self::Connected => bytes_read == 0,
996        }
997    }
998
999    fn should_stop_on_eof(self, eof: bool) -> bool {
1000        matches!(self, Self::Connected) && eof
1001    }
1002
1003    fn should_stop_on_framing_error(self) -> bool {
1004        matches!(self, Self::Connected)
1005    }
1006}
1007
1008struct BufferDecodeContext<'a> {
1009    listen_addr: &'a ListenAddress,
1010    metrics: &'a Metrics,
1011    packet_forwarder: Option<&'a PacketForwarder>,
1012    mode: BufferDecodeMode,
1013    framer: DsdFramer,
1014    stream_capture: StreamCaptureState,
1015}
1016
1017impl<'a> BufferDecodeContext<'a> {
1018    fn new(
1019        listen_addr: &'a ListenAddress, eol_required: bool, metrics: &'a Metrics,
1020        packet_forwarder: Option<&'a PacketForwarder>, mode: BufferDecodeMode,
1021    ) -> Self {
1022        Self {
1023            listen_addr,
1024            metrics,
1025            packet_forwarder,
1026            mode,
1027            framer: get_framer(listen_addr, eol_required),
1028            stream_capture: StreamCaptureState::new(),
1029        }
1030    }
1031}
1032
1033#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1034enum DecodeOutcome {
1035    Continue,
1036    Stop,
1037}
1038
1039#[async_trait]
1040impl Source for DogStatsD {
1041    async fn run(mut self: Box<Self>, mut context: SourceContext) -> Result<(), GenericError> {
1042        let global_shutdown = context.take_shutdown_handle();
1043        pin!(global_shutdown);
1044
1045        let mut health = context.take_health_handle();
1046
1047        // This source spawns everything below as its own children. Thus, the source does not finish until they finish.
1048        // When the body of the source returns, the children receive the shutdown signal.
1049        //
1050        // Brutal: the shrinker loops forever with no terminal condition of its own, so waiting for it would hold
1051        // every shutdown open until its timeout expired.
1052        runtime::worker("io_buffer_pool_shrinker", self.io_buffer_pool_shrinker)
1053            .with_shutdown_strategy(ShutdownStrategy::Brutal)
1054            .spawn();
1055
1056        let (datagram_sender, datagram_receiver) = mpsc::channel(self.io_buffer_queue_capacity);
1057        let datagram_receiver = Arc::new(Mutex::new(datagram_receiver));
1058        let decoder_context = DecoderContext {
1059            codec: self.codec.clone(),
1060            context_resolvers: self.context_resolvers.clone(),
1061            default_hostname: self.default_hostname.clone(),
1062            enabled_filter: self.enabled_filter,
1063            origin_detection_enabled: self.origin_detection_enabled,
1064            stream_log_too_big: self.stream_log_too_big,
1065            disable_verbose_logs: self.disable_verbose_logs,
1066            additional_tags: self.additional_tags.clone(),
1067            traffic_capture: self.traffic_capture.clone(),
1068        };
1069
1070        // Decoders must drain their queue to completion, so they deliberately ignore the shutdown signal and stop only
1071        // once the datagram channel closes -- which happens when every listener and stream handler has dropped its
1072        // sender. This makes shutdown lossless. The listeners stop when the body of this source returns. The decoders
1073        // can see the close only after the listeners and their stream handlers stop. Then the decoders drain the
1074        // datagrams that remain.
1075        //
1076        // When dispatch is backpressured, the drain can take longer than five seconds, the default timeout of a
1077        // closure-based child. Thus, the decoders have no timeout of their own, and are halted with the source if the
1078        // component's shutdown timeout expires.
1079        for worker_id in 0..self.decoder_worker_count.get() {
1080            let datagram_receiver = datagram_receiver.clone();
1081            let decoder_source_context = context.clone();
1082            let decoder_context = decoder_context.clone();
1083
1084            runtime::worker(
1085                format!("datagram_decoder_{worker_id}"),
1086                process_datagram_decoder(datagram_receiver, decoder_source_context, decoder_context),
1087            )
1088            .with_shutdown_timeout(Duration::MAX)
1089            .spawn();
1090        }
1091        drop(datagram_receiver);
1092
1093        // For each listener, spawn a dedicated task to run it. There may be none, when no listener could be created and
1094        // the listener requirement is `Optional`. The source then only waits for shutdown.
1095        for listener in self.listeners {
1096            let task_name = format!("listener_{}", listener.listen_address().listener_type());
1097            let listener_source_context = context.clone();
1098
1099            // TODO: Create a health handle for each listener.
1100            //
1101            // We need to rework `HealthRegistry` to look a little more like `ComponentRegistry` so that we can have it
1102            // already be scoped properly, otherwise all we can do here at present is either have a relative name, like
1103            // `uds-stream`, or try and hardcode the full component name, which we will inevitably forget to update if
1104            // we tweak the topology configuration, etc.
1105            let listener_context = ListenerContext {
1106                listener,
1107                datagram_sender: datagram_sender.clone(),
1108                io_buffer_pool: self.io_buffer_pool.clone(),
1109                origin_telemetry_enabled: self.origin_telemetry_enabled,
1110                eol_required: self.eol_required,
1111                decoder_context: decoder_context.clone(),
1112                capture_entity_resolver: self.capture_entity_resolver.clone(),
1113                packet_forwarder_target: self.packet_forwarder_target.clone(),
1114            };
1115
1116            // Each listener is needed. A fatal accept error stops a listener without a request to stop. Then the source
1117            // has nothing to listen on, so the listener stops the source. The source does not continue to run.
1118            runtime::supervisable(ListenerWorker::new(
1119                task_name,
1120                listener_source_context,
1121                listener_context,
1122            ))
1123            .needed()
1124            .spawn();
1125        }
1126        drop(datagram_sender);
1127
1128        health.mark_ready();
1129        debug!("DogStatsD source started.");
1130
1131        // Wait for the global shutdown signal, then notify listeners to shutdown.
1132        //
1133        // We also handle liveness here, which doesn't really matter for _this_ task, since the real work is happening
1134        // in the listeners, but we need to satisfy the health checker.
1135        loop {
1136            select! {
1137                _ = &mut global_shutdown => {
1138                    debug!("Received shutdown signal.");
1139                    break
1140                },
1141                _ = health.live() => continue,
1142            }
1143        }
1144
1145        // This return stops the listeners, and after they stop, the decoders drain. The source finishes when all of its
1146        // children finish.
1147        debug!("Stopping DogStatsD source...");
1148
1149        Ok(())
1150    }
1151}
1152
1153fn build_io_buffer_pool(
1154    min_buffers: usize, max_buffers: usize, buffer_size: usize,
1155) -> (ElasticObjectPool<BytesBuffer>, impl Future<Output = ()> + Send) {
1156    saluki_antithesis::always_le!(
1157        buffer_size,
1158        usize::MAX - 4,
1159        "dogstatsd buffer size add does not overflow",
1160        { "buffer_size": buffer_size }
1161    );
1162    let adjusted_buffer_size = get_adjusted_buffer_size(buffer_size);
1163    ElasticObjectPool::with_builder("dsd_packet_bufs", min_buffers, max_buffers, move || {
1164        FixedSizeVec::with_capacity(adjusted_buffer_size)
1165    })
1166}
1167
1168fn is_connectionless_listen_address(listen_addr: &ListenAddress) -> bool {
1169    match listen_addr {
1170        ListenAddress::Udp(_) => true,
1171        #[cfg(unix)]
1172        ListenAddress::Unixgram(_) => true,
1173        _ => false,
1174    }
1175}
1176
1177/// A DogStatsD listener, as a [`Supervisable`].
1178///
1179/// An accept loop has no terminal condition of its own -- it runs until something tells it to stop -- so unlike the
1180/// other children this source spawns, it genuinely needs the supervisor's shutdown signal. That is what makes it a
1181/// `Supervisable` rather than a plain worker. See [`process_listener`] for the two paths that end the loop.
1182struct ListenerWorker {
1183    name: String,
1184    inner: StdMutex<Option<(SourceContext, ListenerContext)>>,
1185}
1186
1187impl ListenerWorker {
1188    fn new(name: String, source_context: SourceContext, listener_context: ListenerContext) -> Self {
1189        Self {
1190            name,
1191            inner: StdMutex::new(Some((source_context, listener_context))),
1192        }
1193    }
1194}
1195
1196#[async_trait]
1197impl Supervisable for ListenerWorker {
1198    fn name(&self) -> &str {
1199        &self.name
1200    }
1201
1202    fn shutdown_strategy(&self) -> ShutdownStrategy {
1203        // The listener has no shutdown timeout of its own. The topology shutdown timeout applies to the source and to
1204        // everything that the source spawned.
1205        ShutdownStrategy::Graceful(Duration::MAX)
1206    }
1207
1208    fn can_restart(&self) -> bool {
1209        // The first start takes the listener, so there is nothing left to listen on a second time.
1210        false
1211    }
1212
1213    async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
1214        let (source_context, listener_context) = self
1215            .inner
1216            .lock()
1217            .expect("listener worker mutex poisoned")
1218            .take()
1219            .ok_or_else(|| InitializationError::from(generic_error!("listener worker already initialized")))?;
1220
1221        Ok(Box::pin(async move {
1222            process_listener(source_context, listener_context, process_shutdown).await;
1223            Ok(())
1224        }))
1225    }
1226}
1227
1228async fn process_listener(
1229    source_context: SourceContext, listener_context: ListenerContext, process_shutdown: ShutdownHandle,
1230) {
1231    let ListenerContext {
1232        mut listener,
1233        datagram_sender,
1234        io_buffer_pool,
1235        origin_telemetry_enabled,
1236        eol_required,
1237        decoder_context,
1238        capture_entity_resolver,
1239        packet_forwarder_target,
1240    } = listener_context;
1241
1242    pin!(process_shutdown);
1243
1244    let listen_addr = listener.listen_address().clone();
1245    let metrics = build_metrics(
1246        &listen_addr,
1247        source_context.component_context(),
1248        origin_telemetry_enabled,
1249    );
1250    let packet_forwarder = packet_forwarder_target
1251        .as_ref()
1252        .map(|target| target.to_forwarder(metrics.clone()));
1253    if let Some(packet_forwarder) = &packet_forwarder {
1254        packet_forwarder.spawn();
1255    }
1256    let datagram_context = is_connectionless_listen_address(&listen_addr).then(|| {
1257        Arc::new(DatagramSocketContext {
1258            listen_addr: listen_addr.clone(),
1259            eol_required: eol_required.for_listener(&listen_addr),
1260            metrics: metrics.clone(),
1261            packet_forwarder: packet_forwarder.clone(),
1262        })
1263    });
1264
1265    info!(%listen_addr, "DogStatsD listener started.");
1266
1267    loop {
1268        select! {
1269            // This signal fires when the body of the source returns. That return closes the scope that this listener
1270            // belongs to.
1271            _ = &mut process_shutdown => {
1272                debug!(%listen_addr, "Received shutdown signal. Waiting for existing stream handlers to finish...");
1273                break;
1274            }
1275            result = listener.accept() => match result {
1276                Ok(stream) => {
1277                    debug!(%listen_addr, "Spawning new stream handler.");
1278
1279                    let handler_context = HandlerContext {
1280                        listen_addr: listen_addr.clone(),
1281                        eol_required: eol_required.for_listener(&listen_addr),
1282                        io_buffer_pool: io_buffer_pool.clone(),
1283                        datagram_sender: datagram_sender.clone(),
1284                        datagram_context: datagram_context.clone(),
1285                        metrics: metrics.clone(),
1286                        decoder_context: decoder_context.clone(),
1287                        capture_entity_resolver: capture_entity_resolver.clone(),
1288                        packet_forwarder: packet_forwarder.clone(),
1289                    };
1290
1291                    let task_name = format!("conn_{}", listen_addr.listener_type());
1292
1293                    // Each handler is a child of the listener. Thus, the listener does not finish until its stream
1294                    // handlers finish. The handlers receive the shutdown signal when the listener no longer accepts
1295                    // connections.
1296                    let handler_source_context = source_context.clone();
1297                    runtime::worker_with_shutdown(task_name, move |shutdown| {
1298                        process_stream(stream, handler_source_context, handler_context, shutdown)
1299                    })
1300                    .spawn();
1301                }
1302                Err(e) => {
1303                    error!(%listen_addr, error = %e, "Failed to accept connection. Stopping listener.");
1304                    break
1305                }
1306            }
1307        }
1308    }
1309
1310    info!(%listen_addr, "DogStatsD listener stopped accepting connections.");
1311}
1312
1313async fn process_stream(
1314    stream: Stream, source_context: SourceContext, handler_context: HandlerContext, shutdown_handle: ShutdownHandle,
1315) {
1316    select! {
1317        _ = shutdown_handle => {
1318            debug!("Stream handler received shutdown signal.");
1319        },
1320        _ = drive_stream(stream, source_context, handler_context) => {},
1321    }
1322}
1323
1324fn origin_detection_error_for_telemetry(
1325    origin_detection_enabled: bool, bytes_read: usize, peer_addr: &ConnectionAddress,
1326) -> Option<ProcessCredentialsError> {
1327    match peer_addr {
1328        ConnectionAddress::ProcessLike(identity @ ProcessIdentity::Error(error))
1329            if origin_detection_enabled && bytes_read > 0 && identity.is_telemetry_error() =>
1330        {
1331            Some(*error)
1332        }
1333        _ => None,
1334    }
1335}
1336
1337struct ReceivedBuffer {
1338    buffer: Option<BytesBuffer>,
1339    bytes_read: usize,
1340    peer_addr: ConnectionAddress,
1341    process_origin: Option<ProcessOrigin>,
1342    buffer_sender: Option<oneshot::Sender<BytesBuffer>>,
1343}
1344
1345impl ReceivedBuffer {
1346    fn with_return(
1347        buffer: BytesBuffer, bytes_read: usize, peer_addr: ConnectionAddress, process_origin: Option<ProcessOrigin>,
1348    ) -> (Self, oneshot::Receiver<BytesBuffer>) {
1349        let (buffer_sender, returned_buffer) = oneshot::channel();
1350        (
1351            Self {
1352                buffer: Some(buffer),
1353                bytes_read,
1354                peer_addr,
1355                process_origin,
1356                buffer_sender: Some(buffer_sender),
1357            },
1358            returned_buffer,
1359        )
1360    }
1361
1362    fn without_return(
1363        buffer: BytesBuffer, bytes_read: usize, peer_addr: ConnectionAddress, process_origin: Option<ProcessOrigin>,
1364    ) -> Self {
1365        Self {
1366            buffer: Some(buffer),
1367            bytes_read,
1368            peer_addr,
1369            process_origin,
1370            buffer_sender: None,
1371        }
1372    }
1373
1374    fn parts_mut(&mut self) -> (&mut BytesBuffer, usize, &ConnectionAddress, Option<&ProcessOrigin>) {
1375        let Self {
1376            buffer,
1377            bytes_read,
1378            peer_addr,
1379            process_origin,
1380            ..
1381        } = self;
1382        (
1383            buffer.as_mut().expect("Received buffer already taken."),
1384            *bytes_read,
1385            peer_addr,
1386            process_origin.as_ref(),
1387        )
1388    }
1389
1390    #[cfg(all(test, unix))]
1391    fn buffer(&self) -> &BytesBuffer {
1392        self.buffer.as_ref().expect("Received buffer already taken.")
1393    }
1394
1395    #[cfg(all(test, unix))]
1396    fn buffer_mut(&mut self) -> &mut BytesBuffer {
1397        self.buffer.as_mut().expect("Received buffer already taken.")
1398    }
1399}
1400
1401impl Drop for ReceivedBuffer {
1402    fn drop(&mut self) {
1403        if let Some(sender) = self.buffer_sender.take() {
1404            let buffer = self.buffer.take().expect("Received buffer already taken.");
1405            let _ = sender.send(buffer);
1406        }
1407    }
1408}
1409
1410struct BufferedStreamReader {
1411    receiver: Option<mpsc::Receiver<io::Result<ReceivedBuffer>>>,
1412
1413    // Held but never fired explicitly, so that dropping this reader signals the worker to stop.
1414    _shutdown_coordinator: ShutdownCoordinator,
1415}
1416
1417impl BufferedStreamReader {
1418    fn new(
1419        stream: Stream, io_buffer_pool: ElasticObjectPool<BytesBuffer>, memory_limiter: MemoryLimiter,
1420        origin_detection_enabled: bool, traffic_capture: TrafficCapture,
1421        capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
1422    ) -> Self {
1423        debug_assert!(!stream.is_connectionless());
1424        let (packets_tx, receiver) = mpsc::channel(1);
1425        let (shutdown_coordinator, shutdown_handle) = ShutdownHandle::paired();
1426
1427        let reader = receive_connected_stream(
1428            stream,
1429            io_buffer_pool,
1430            memory_limiter,
1431            origin_detection_enabled,
1432            traffic_capture,
1433            capture_entity_resolver,
1434            packets_tx,
1435        );
1436
1437        runtime::worker("stream_reader", async move {
1438            select! {
1439                _ = shutdown_handle => debug!("Stream reader cancelled."),
1440                _ = reader => {},
1441            }
1442        })
1443        .spawn();
1444
1445        Self {
1446            receiver: Some(receiver),
1447            _shutdown_coordinator: shutdown_coordinator,
1448        }
1449    }
1450
1451    fn take_receiver(&mut self) -> mpsc::Receiver<io::Result<ReceivedBuffer>> {
1452        self.receiver.take().expect("Buffered stream receiver already taken")
1453    }
1454}
1455
1456async fn receive_connected_stream(
1457    mut stream: Stream, io_buffer_pool: ElasticObjectPool<BytesBuffer>, memory_limiter: MemoryLimiter,
1458    origin_detection_enabled: bool, traffic_capture: TrafficCapture,
1459    capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
1460    packets_tx: mpsc::Sender<io::Result<ReceivedBuffer>>,
1461) {
1462    debug!("Stream reader started.");
1463
1464    let mut retained_buffer: Option<BytesBuffer> = None;
1465    loop {
1466        memory_limiter.wait_for_capacity().await;
1467
1468        let mut buffer = match retained_buffer.take() {
1469            Some(mut buffer) => {
1470                buffer.collapse();
1471                buffer
1472            }
1473            None => acquire_io_buffer(&io_buffer_pool).await,
1474        };
1475        let (bytes_read, peer_addr) = match stream.receive(&mut buffer).await {
1476            Ok(received) => received,
1477            Err(error) => {
1478                let _ = packets_tx.send(Err(error)).await;
1479                break;
1480            }
1481        };
1482        let process_origin = resolve_process_origin_if_needed(
1483            origin_detection_enabled,
1484            &traffic_capture,
1485            capture_entity_resolver.as_deref(),
1486            &peer_addr,
1487        );
1488
1489        let (received, returned_buffer) = ReceivedBuffer::with_return(buffer, bytes_read, peer_addr, process_origin);
1490
1491        if packets_tx.send(Ok(received)).await.is_err() {
1492            debug!("Failed to enqueue DogStatsD packet for decoding: receiver dropped.");
1493            break;
1494        }
1495
1496        match returned_buffer.await {
1497            Ok(buffer) if buffer.has_remaining() => retained_buffer = Some(buffer),
1498            Ok(buffer) => drop(buffer),
1499            Err(_) => break,
1500        }
1501    }
1502
1503    debug!("Stream reader stopped.");
1504}
1505
1506async fn receive_connectionless_stream(
1507    mut stream: Stream, io_buffer_pool: ElasticObjectPool<BytesBuffer>, memory_limiter: MemoryLimiter,
1508    origin_detection_enabled: bool, traffic_capture: TrafficCapture,
1509    capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
1510    datagram_sender: mpsc::Sender<QueuedDatagram>, socket_context: Arc<DatagramSocketContext>,
1511) {
1512    debug!(listen_addr = %socket_context.listen_addr, "Datagram reader started.");
1513
1514    loop {
1515        memory_limiter.wait_for_capacity().await;
1516
1517        let mut buffer = acquire_io_buffer(&io_buffer_pool).await;
1518        let result = match stream.receive(&mut buffer).await {
1519            Ok((bytes_read, peer_addr)) => {
1520                let process_origin = resolve_process_origin_if_needed(
1521                    origin_detection_enabled,
1522                    &traffic_capture,
1523                    capture_entity_resolver.as_deref(),
1524                    &peer_addr,
1525                );
1526                Ok(ReceivedBuffer::without_return(
1527                    buffer,
1528                    bytes_read,
1529                    peer_addr,
1530                    process_origin,
1531                ))
1532            }
1533            Err(error) => Err(error),
1534        };
1535
1536        let receive_failed = result.is_err();
1537        let queued = QueuedDatagram {
1538            result,
1539            socket_context: socket_context.clone(),
1540        };
1541        if datagram_sender.send(queued).await.is_err() {
1542            debug!(
1543                listen_addr = %socket_context.listen_addr,
1544                "Failed to enqueue DogStatsD packet for decoding: receiver dropped."
1545            );
1546            break;
1547        }
1548        if receive_failed {
1549            continue;
1550        }
1551    }
1552
1553    debug!(listen_addr = %socket_context.listen_addr, "Datagram reader stopped.");
1554}
1555
1556async fn acquire_io_buffer(io_buffer_pool: &ElasticObjectPool<BytesBuffer>) -> BytesBuffer {
1557    let buffer = io_buffer_pool.acquire().await;
1558    trace!(
1559        remaining = buffer.remaining(),
1560        capacity = buffer.capacity(),
1561        "Acquired new buffer from pool."
1562    );
1563    buffer
1564}
1565
1566async fn process_datagram_decoder(
1567    datagram_receiver: Arc<Mutex<mpsc::Receiver<QueuedDatagram>>>, source_context: SourceContext,
1568    decoder_context: DecoderContext,
1569) {
1570    drive_datagram_decoder(datagram_receiver, source_context, decoder_context).await;
1571    debug!("Datagram decoder drained its queue.");
1572}
1573
1574impl DogStatsDDecoder {
1575    fn new(source_context: SourceContext, decoder_context: DecoderContext) -> Self {
1576        let DecoderContext {
1577            codec,
1578            context_resolvers,
1579            default_hostname,
1580            enabled_filter,
1581            origin_detection_enabled,
1582            stream_log_too_big,
1583            disable_verbose_logs,
1584            additional_tags,
1585            traffic_capture,
1586        } = decoder_context;
1587
1588        Self {
1589            source_context,
1590            codec,
1591            context_resolvers,
1592            default_hostname,
1593            enabled_filter,
1594            origin_detection_enabled,
1595            stream_log_too_big,
1596            disable_verbose_logs,
1597            additional_tags,
1598            traffic_capture,
1599            event_buffer: None,
1600        }
1601    }
1602
1603    async fn decode_buffer(
1604        &mut self, context: &mut BufferDecodeContext<'_>, mut received: ReceivedBuffer,
1605    ) -> DecodeOutcome {
1606        let (buffer, bytes_read, peer_addr, process_origin) = received.parts_mut();
1607        self.decode_buffer_contents(context, buffer, bytes_read, peer_addr, process_origin)
1608            .await
1609    }
1610
1611    async fn decode_buffer_contents(
1612        &mut self, context: &mut BufferDecodeContext<'_>, io_buffer: &mut BytesBuffer, bytes_read: usize,
1613        peer_addr: &ConnectionAddress, process_origin: Option<&ProcessOrigin>,
1614    ) -> DecodeOutcome {
1615        let listen_addr = context.listen_addr;
1616        let metrics = context.metrics;
1617        let packet_forwarder = context.packet_forwarder;
1618        let mode = context.mode;
1619
1620        let payload = received_payload(io_buffer, bytes_read);
1621        capture_unixgram_traffic(listen_addr, &self.traffic_capture, peer_addr, process_origin, payload);
1622
1623        let capture_stream_frames = matches!(listen_addr, ListenAddress::Unix(_)) && self.traffic_capture.is_ongoing();
1624        if capture_stream_frames {
1625            context.stream_capture.update_peer_metadata(peer_addr);
1626        }
1627        context.framer.set_retain_outer_frames(capture_stream_frames);
1628
1629        metrics.bytes_received().increment(bytes_read as u64);
1630        metrics.bytes_received_size().record(bytes_read as f64);
1631        let origin_detection_errors =
1632            origin_detection_error_for_telemetry(self.origin_detection_enabled, bytes_read, peer_addr)
1633                .and_then(|error| metrics.origin_detection_errors(error));
1634
1635        if matches!(mode, BufferDecodeMode::Connectionless) {
1636            metrics.packet_receive_success().increment(1);
1637            if let Some(origin_detection_errors) = origin_detection_errors {
1638                origin_detection_errors.increment(1);
1639            }
1640        }
1641
1642        let eof = mode.is_eof(bytes_read);
1643        trace!(
1644            buffer_len = io_buffer.remaining(),
1645            buffer_cap = io_buffer.remaining_mut(),
1646            %listen_addr,
1647            %peer_addr,
1648            eof,
1649            "Received {} bytes from socket.",
1650            bytes_read
1651        );
1652
1653        if should_drop_oversized_named_pipe_frame(listen_addr, io_buffer) {
1654            metrics.framing_errors().increment(1);
1655            debug!(%listen_addr, %peer_addr, "DogStatsD named pipe frame exceeded the configured buffer size. Dropping frame.");
1656            io_buffer.clear();
1657            return DecodeOutcome::Continue;
1658        }
1659
1660        loop {
1661            let frame_result = context.framer.next_frame(io_buffer, eof);
1662            for outer_frame in context.framer.drain_outer_frames() {
1663                let _ = self.traffic_capture.enqueue(build_capture_record(
1664                    context.stream_capture.last_pid,
1665                    process_origin,
1666                    &outer_frame,
1667                ));
1668            }
1669            let completed_outer_frames = context.framer.take_completed_outer_frames();
1670            if completed_outer_frames > 0 {
1671                metrics
1672                    .packet_receive_success()
1673                    .increment(completed_outer_frames as u64);
1674                if let Some(origin_detection_errors) = origin_detection_errors {
1675                    origin_detection_errors.increment(completed_outer_frames as u64);
1676                }
1677            }
1678
1679            match frame_result {
1680                Ok(Some(frame)) => {
1681                    if matches!(listen_addr, ListenAddress::NamedPipe { .. }) {
1682                        capture_named_pipe_frame(&self.traffic_capture, &frame);
1683                        metrics.packet_receive_success().increment(1);
1684                    }
1685                    self.decode_frame(frame, listen_addr, peer_addr, process_origin, metrics, packet_forwarder)
1686                        .await;
1687                }
1688                Ok(None) => {
1689                    if mode.should_stop_on_eof(eof) {
1690                        debug!(%listen_addr, %peer_addr, "Stream received EOF. Shutting down handler.");
1691                        return DecodeOutcome::Stop;
1692                    }
1693                    return DecodeOutcome::Continue;
1694                }
1695                Err(error) => {
1696                    metrics.framing_errors().increment(1);
1697                    if should_warn_stream_log_too_big(listen_addr, &error, self.stream_log_too_big) {
1698                        warn!(
1699                            %listen_addr,
1700                            %peer_addr,
1701                            error = %error,
1702                            "DogStatsD stream frame exceeded the configured buffer size."
1703                        );
1704                    }
1705
1706                    if mode.should_stop_on_framing_error() {
1707                        debug!(%listen_addr, %peer_addr, %error, "Error decoding frame. Stopping stream.");
1708                        return DecodeOutcome::Stop;
1709                    }
1710
1711                    debug!(%listen_addr, %peer_addr, %error, "Error decoding datagram frame. Continuing listener.");
1712                    return DecodeOutcome::Continue;
1713                }
1714            }
1715        }
1716    }
1717
1718    async fn decode_frame(
1719        &mut self, frame: Bytes, listen_addr: &ListenAddress, peer_addr: &ConnectionAddress,
1720        process_origin: Option<&ProcessOrigin>, metrics: &Metrics, packet_forwarder: Option<&PacketForwarder>,
1721    ) {
1722        trace!(%listen_addr, %peer_addr, ?frame, "Decoded frame.");
1723        if let Some(forwarder) = packet_forwarder {
1724            forwarder.forward(frame.clone()).await;
1725        }
1726
1727        match handle_frame(
1728            &frame[..],
1729            &self.codec,
1730            &mut self.context_resolvers,
1731            metrics,
1732            self.origin_detection_enabled,
1733            process_origin,
1734            self.enabled_filter,
1735            &self.additional_tags,
1736            &self.default_hostname,
1737        ) {
1738            Ok(Some(event)) => {
1739                if let Some(event_buffer) = self.buffer_event(event) {
1740                    debug!(%listen_addr, %peer_addr, "Event buffer is full. Forwarding events.");
1741                    dispatch_events(event_buffer, &self.source_context).await;
1742                }
1743            }
1744            Ok(None) => {}
1745            Err(error) => {
1746                log_parse_failure(self.disable_verbose_logs, listen_addr, peer_addr, &frame, &error);
1747            }
1748        }
1749    }
1750
1751    fn buffer_event(&mut self, event: Event) -> Option<EventsBuffer> {
1752        let event_buffer = self.event_buffer.get_or_insert_default();
1753        match event_buffer.try_push(event) {
1754            Some(event) => {
1755                let full_event_buffer = mem::take(event_buffer);
1756                assert!(
1757                    event_buffer.try_push(event).is_none(),
1758                    "New event buffer is unexpectedly full."
1759                );
1760                Some(full_event_buffer)
1761            }
1762            None => None,
1763        }
1764    }
1765
1766    async fn flush_events(&mut self) {
1767        if let Some(event_buffer) = self.event_buffer.take() {
1768            dispatch_events(event_buffer, &self.source_context).await;
1769        }
1770    }
1771}
1772
1773async fn drive_datagram_decoder(
1774    datagram_receiver: Arc<Mutex<mpsc::Receiver<QueuedDatagram>>>, source_context: SourceContext,
1775    decoder_context: DecoderContext,
1776) {
1777    let mut decoder = DogStatsDDecoder::new(source_context, decoder_context);
1778    let mut buffer_flush = interval(Duration::from_millis(100));
1779    buffer_flush.set_missed_tick_behavior(MissedTickBehavior::Delay);
1780
1781    loop {
1782        select! {
1783            maybe_datagram = async {
1784                datagram_receiver.lock().await.recv().await
1785            } => {
1786                let Some(QueuedDatagram { result, socket_context }) = maybe_datagram else {
1787                    break;
1788                };
1789                {
1790                    let DatagramSocketContext {
1791                        listen_addr,
1792                        eol_required,
1793                        metrics,
1794                        packet_forwarder,
1795                    } = socket_context.as_ref();
1796
1797                    let received = match result {
1798                        Ok(received) => received,
1799                        Err(error) => {
1800                            metrics.packet_receive_failure().increment(1);
1801                            warn!(%listen_addr, %error, "I/O error while reading datagram. Continuing listener.");
1802                            continue;
1803                        }
1804                    };
1805
1806                    let mut buffer_decode_context = BufferDecodeContext::new(
1807                        listen_addr,
1808                        *eol_required,
1809                        metrics,
1810                        packet_forwarder.as_ref(),
1811                        BufferDecodeMode::Connectionless,
1812                    );
1813                    let outcome = decoder.decode_buffer(&mut buffer_decode_context, received).await;
1814                    debug_assert_eq!(outcome, DecodeOutcome::Continue);
1815                }
1816            }
1817            _ = buffer_flush.tick() => {
1818                decoder.flush_events().await;
1819            }
1820        }
1821    }
1822
1823    decoder.flush_events().await;
1824}
1825
1826async fn drive_stream(stream: Stream, source_context: SourceContext, handler_context: HandlerContext) {
1827    if stream.is_connectionless() {
1828        let memory_limiter = source_context.topology_context().memory_limiter().clone();
1829        let socket_context = handler_context
1830            .datagram_context
1831            .clone()
1832            .expect("connectionless stream must have a datagram context");
1833        receive_connectionless_stream(
1834            stream,
1835            handler_context.io_buffer_pool,
1836            memory_limiter,
1837            handler_context.decoder_context.origin_detection_enabled,
1838            handler_context.decoder_context.traffic_capture.clone(),
1839            handler_context.capture_entity_resolver,
1840            handler_context.datagram_sender,
1841            socket_context,
1842        )
1843        .await;
1844        return;
1845    }
1846
1847    drive_connected_stream(stream, source_context, handler_context).await;
1848}
1849
1850async fn drive_connected_stream(stream: Stream, source_context: SourceContext, handler_context: HandlerContext) {
1851    let listen_addr = handler_context.listen_addr.clone();
1852    let metrics = handler_context.metrics.clone();
1853    let memory_limiter = source_context.topology_context().memory_limiter().clone();
1854    let mut stream_reader = BufferedStreamReader::new(
1855        stream,
1856        handler_context.io_buffer_pool.clone(),
1857        memory_limiter,
1858        handler_context.decoder_context.origin_detection_enabled,
1859        handler_context.decoder_context.traffic_capture.clone(),
1860        handler_context.capture_entity_resolver.clone(),
1861    );
1862    let receiver = stream_reader.take_receiver();
1863
1864    debug!(%listen_addr, "Stream handler started.");
1865
1866    metrics.connections_active().increment(1);
1867    drive_decoder(receiver, source_context, handler_context).await;
1868    metrics.connections_active().decrement(1);
1869
1870    debug!(%listen_addr, "Stream handler stopped.");
1871}
1872
1873async fn drive_decoder(
1874    mut stream_receiver: mpsc::Receiver<io::Result<ReceivedBuffer>>, source_context: SourceContext,
1875    handler_context: HandlerContext,
1876) {
1877    let HandlerContext {
1878        listen_addr,
1879        eol_required,
1880        metrics,
1881        decoder_context,
1882        packet_forwarder,
1883        ..
1884    } = handler_context;
1885    let mut decoder = DogStatsDDecoder::new(source_context, decoder_context);
1886    // Set a buffer flush interval of 100ms, which will ensure we always flush buffered events at least every 100ms if
1887    // we're otherwise idle and not receiving packets from the client.
1888    let mut buffer_flush = interval(Duration::from_millis(100));
1889    buffer_flush.set_missed_tick_behavior(MissedTickBehavior::Delay);
1890    let mut buffer_decode_context = BufferDecodeContext::new(
1891        &listen_addr,
1892        eol_required,
1893        &metrics,
1894        packet_forwarder.as_ref(),
1895        BufferDecodeMode::Connected,
1896    );
1897
1898    'read: loop {
1899        select! {
1900            // We read from the stream.
1901            maybe_read_result = stream_receiver.recv() => match maybe_read_result {
1902                Some(Ok(received)) => {
1903                    let outcome = decoder.decode_buffer(&mut buffer_decode_context, received).await;
1904                    if outcome == DecodeOutcome::Stop {
1905                        break 'read;
1906                    }
1907                },
1908                Some(Err(e)) => {
1909                    metrics.packet_receive_failure().increment(1);
1910                    warn!(%listen_addr, error = %e, "I/O error while decoding. Stopping stream.");
1911                    break 'read;
1912                },
1913                None => {
1914                    warn!(%listen_addr, "Buffered stream reader stopped unexpectedly. Stopping stream.");
1915                    break 'read;
1916                }
1917            },
1918
1919            _ = buffer_flush.tick() => {
1920                decoder.flush_events().await;
1921            },
1922
1923        }
1924    }
1925
1926    decoder.flush_events().await;
1927}
1928
1929fn should_drop_oversized_named_pipe_frame(listen_addr: &ListenAddress, buffer: &BytesBuffer) -> bool {
1930    matches!(listen_addr, ListenAddress::NamedPipe { .. })
1931        && buffer.remaining_mut() == 0
1932        && memchr::memchr(b'\n', buffer.chunk()).is_none()
1933}
1934
1935fn should_warn_stream_log_too_big(listen_addr: &ListenAddress, error: &FramingError, stream_log_too_big: bool) -> bool {
1936    stream_log_too_big
1937        && matches!(listen_addr, ListenAddress::Unix(_))
1938        && matches!(error, FramingError::InvalidFrame { .. })
1939}
1940
1941fn log_parse_failure(
1942    disable_verbose_logs: bool, listen_addr: &ListenAddress, peer_addr: &ConnectionAddress, frame: &[u8],
1943    error: &ParseError,
1944) {
1945    let frame = String::from_utf8_lossy(frame);
1946    if disable_verbose_logs {
1947        debug!(%listen_addr, %peer_addr, %frame, %error, "Failed to parse frame.");
1948    } else {
1949        warn!(%listen_addr, %peer_addr, %frame, %error, "Failed to parse frame.");
1950    }
1951}
1952
1953fn capture_named_pipe_frame(traffic_capture: &TrafficCapture, frame: &[u8]) {
1954    if !frame.is_empty() && traffic_capture.is_ongoing() {
1955        let _ = traffic_capture.enqueue(build_capture_record(None, None, frame));
1956    }
1957}
1958
1959fn capture_unixgram_traffic(
1960    listen_addr: &ListenAddress, traffic_capture: &TrafficCapture, peer_addr: &ConnectionAddress,
1961    process_origin: Option<&ProcessOrigin>, payload: &[u8],
1962) {
1963    if payload.is_empty() || !matches!(listen_addr, ListenAddress::Unixgram(_)) || !traffic_capture.is_ongoing() {
1964        return;
1965    }
1966
1967    let _ = traffic_capture.enqueue(build_capture_record(
1968        process_id_from_peer_addr(peer_addr),
1969        process_origin,
1970        payload,
1971    ));
1972}
1973
1974/// Per-connection metadata used when capturing Unix stream traffic.
1975struct StreamCaptureState {
1976    last_pid: Option<i32>,
1977}
1978
1979impl StreamCaptureState {
1980    fn new() -> Self {
1981        Self { last_pid: None }
1982    }
1983
1984    fn update_peer_metadata(&mut self, peer_addr: &ConnectionAddress) {
1985        if let Some(process_id) = process_id_from_peer_addr(peer_addr) {
1986            self.last_pid = Some(process_id);
1987        }
1988    }
1989}
1990
1991fn build_capture_record(
1992    process_id: Option<i32>, process_origin: Option<&ProcessOrigin>, payload: &[u8],
1993) -> CaptureRecord {
1994    CaptureRecord {
1995        timestamp_ns: capture_timestamp_ns(),
1996        payload: payload.to_vec(),
1997        pid: process_id,
1998        ancillary: Vec::new(),
1999        container_id: process_origin
2000            .and_then(ProcessOrigin::container_entity_id)
2001            .map(ToString::to_string),
2002    }
2003}
2004
2005fn process_id_from_peer_addr(peer_addr: &ConnectionAddress) -> Option<i32> {
2006    match peer_addr {
2007        ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(creds)) => Some(creds.pid),
2008        _ => None,
2009    }
2010}
2011
2012fn resolve_process_origin(
2013    capture_entity_resolver: Option<&(dyn CaptureEntityResolver + Send + Sync)>, peer_addr: &ConnectionAddress,
2014) -> Option<ProcessOrigin> {
2015    let creds = peer_addr.process_credentials()?;
2016    if creds.gid == REPLAY_CREDENTIALS_GID {
2017        return Some(ProcessOrigin::Replay(creds.uid));
2018    }
2019
2020    let process_id = u32::try_from(creds.pid).ok()?;
2021    Some(match capture_entity_resolver {
2022        Some(resolver) => ProcessOrigin::Pinned(resolver.resolve_container_entity_for_live_pid(process_id)),
2023        None => ProcessOrigin::Unpinned(process_id),
2024    })
2025}
2026
2027fn resolve_process_origin_if_needed(
2028    origin_detection_enabled: bool, traffic_capture: &TrafficCapture,
2029    capture_entity_resolver: Option<&(dyn CaptureEntityResolver + Send + Sync)>, peer_addr: &ConnectionAddress,
2030) -> Option<ProcessOrigin> {
2031    if !origin_detection_enabled && !traffic_capture.is_ongoing() {
2032        return None;
2033    }
2034
2035    resolve_process_origin(capture_entity_resolver, peer_addr)
2036}
2037
2038fn received_payload(buffer: &BytesBuffer, bytes_read: usize) -> &[u8] {
2039    let chunk = buffer.chunk();
2040    let start = chunk.len().saturating_sub(bytes_read);
2041    &chunk[start..]
2042}
2043
2044fn capture_timestamp_ns() -> i64 {
2045    SystemTime::now()
2046        .duration_since(UNIX_EPOCH)
2047        .map(|duration| duration.as_nanos().min(i64::MAX as u128) as i64)
2048        .unwrap_or_default()
2049}
2050
2051#[allow(clippy::too_many_arguments)]
2052fn handle_frame(
2053    frame: &[u8], codec: &DogStatsDCodec, context_resolvers: &mut ContextResolvers, source_metrics: &Metrics,
2054    origin_detection_enabled: bool, process_origin: Option<&ProcessOrigin>, enabled_filter: EnablePayloadsFilter,
2055    additional_tags: &[String], default_hostname: &MetaString,
2056) -> Result<Option<Event>, ParseError> {
2057    let resolve_telemetry_origin = || {
2058        (source_metrics.origin_telemetry_enabled() && origin_detection_enabled)
2059            .then(|| {
2060                process_origin
2061                    .and_then(ProcessOrigin::container_entity_id)
2062                    .map(ToString::to_string)
2063            })
2064            .flatten()
2065    };
2066
2067    let parsed = match codec.decode_packet(frame) {
2068        Ok(parsed) => parsed,
2069        Err(e) => {
2070            // Try and determine what the message type was, if possible, to increment the correct error counter.
2071            match parse_message_type(frame) {
2072                MessageType::MetricSample => {
2073                    source_metrics.record_metric_parse_failed(resolve_telemetry_origin().as_deref())
2074                }
2075                MessageType::Event => source_metrics.event_decode_failed().increment(1),
2076                MessageType::ServiceCheck => source_metrics.service_check_decode_failed().increment(1),
2077            }
2078
2079            return Err(e);
2080        }
2081    };
2082
2083    let event = match parsed {
2084        ParsedPacket::Metric(metric_packet) => {
2085            if metric_packet.num_points == 0 {
2086                return Ok(None);
2087            }
2088            let events_len = metric_packet.num_points;
2089            if !enabled_filter.allow_metric(&metric_packet) {
2090                trace!(
2091                    metric.name = metric_packet.metric_name,
2092                    "Skipping metric due to filter configuration."
2093                );
2094                return Ok(None);
2095            }
2096
2097            match handle_metric_packet(
2098                metric_packet,
2099                context_resolvers,
2100                process_origin,
2101                additional_tags,
2102                default_hostname,
2103            ) {
2104                Some(metric) => {
2105                    source_metrics.record_metrics_received(events_len, resolve_telemetry_origin().as_deref());
2106                    Event::Metric(metric)
2107                }
2108                None => {
2109                    // We can only fail to get a metric back if we failed to resolve the context.
2110                    source_metrics.failed_context_resolve_total().increment(1);
2111                    return Ok(None);
2112                }
2113            }
2114        }
2115        ParsedPacket::Event(event) => {
2116            if !enabled_filter.allow_event(&event) {
2117                trace!("Skipping event {} due to filter configuration.", event.title);
2118                return Ok(None);
2119            }
2120            match handle_event_packet(event, context_resolvers, process_origin, additional_tags) {
2121                Some(event) => {
2122                    source_metrics.events_received().increment(1);
2123                    Event::EventD(event)
2124                }
2125                None => {
2126                    source_metrics.failed_context_resolve_total().increment(1);
2127                    return Ok(None);
2128                }
2129            }
2130        }
2131        ParsedPacket::ServiceCheck(service_check) => {
2132            if !enabled_filter.allow_service_check(&service_check) {
2133                trace!(
2134                    "Skipping service check {} due to filter configuration.",
2135                    service_check.name
2136                );
2137                return Ok(None);
2138            }
2139            match handle_service_check_packet(service_check, context_resolvers, process_origin, additional_tags) {
2140                Some(service_check) => {
2141                    source_metrics.service_checks_received().increment(1);
2142                    Event::ServiceCheck(service_check)
2143                }
2144                None => {
2145                    source_metrics.failed_context_resolve_total().increment(1);
2146                    return Ok(None);
2147                }
2148            }
2149        }
2150    };
2151
2152    Ok(Some(event))
2153}
2154
2155fn handle_metric_packet(
2156    packet: MetricPacket, context_resolvers: &mut ContextResolvers, process_origin: Option<&ProcessOrigin>,
2157    additional_tags: &[String], default_hostname: &MetaString,
2158) -> Option<Metric> {
2159    let well_known_tags = WellKnownTags::from_raw_tags(&packet.tags);
2160
2161    let origin = origin_from_metric_packet(&packet, &well_known_tags);
2162    let origin_tags = context_resolvers.resolve_origin_tags(origin, process_origin);
2163
2164    // Choose the right context resolver based on whether or not this metric is pre-aggregated.
2165    let context_resolver = if packet.timestamp.is_some() {
2166        context_resolvers.no_agg()
2167    } else {
2168        context_resolvers.primary()
2169    };
2170
2171    let tags = get_filtered_tags_iterator(&packet.tags, additional_tags);
2172
2173    let hostname = well_known_tags.hostname.unwrap_or(default_hostname);
2174
2175    // Try to resolve the context for this metric.
2176    let maybe_context =
2177        context_resolver.resolve_with_host_and_origin_tags(packet.metric_name, hostname, tags, origin_tags);
2178
2179    match maybe_context {
2180        Some(context) => {
2181            let metric_origin = well_known_tags
2182                .jmx_check_name
2183                .map(MetricOrigin::jmx_check)
2184                .unwrap_or_else(MetricOrigin::dogstatsd);
2185            let metadata = MetricMetadata::default()
2186                .with_origin(metric_origin)
2187                .with_unit(packet.unit.map_or_else(MetaString::empty, MetaString::from_static));
2188
2189            Some(Metric::from_parts(context, packet.values, metadata))
2190        }
2191        // We failed to resolve the context, likely due to not having enough interner capacity.
2192        None => None,
2193    }
2194}
2195
2196fn handle_event_packet(
2197    packet: EventPacket, context_resolvers: &mut ContextResolvers, process_origin: Option<&ProcessOrigin>,
2198    additional_tags: &[String],
2199) -> Option<EventD> {
2200    let well_known_tags = WellKnownTags::from_raw_tags(&packet.tags);
2201
2202    let origin = origin_from_event_packet(&packet, &well_known_tags);
2203    let origin_tags = context_resolvers.resolve_origin_tags(origin, process_origin);
2204
2205    let tags = get_filtered_tags_iterator(&packet.tags, additional_tags);
2206    let tags_resolver = context_resolvers.tags();
2207    let tags = tags_resolver.create_tag_set(tags)?;
2208
2209    // When no d: field is present, backfill the current time—matching the stock Datadog Agent's
2210    // behavior in pkg/aggregator/aggregator.go (addEvent), which sets e.Ts = time.Now().Unix()
2211    // for any event with Ts == 0.
2212    let timestamp = packet
2213        .timestamp
2214        .or_else(|| SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()));
2215
2216    let eventd = EventD::new(packet.title, packet.text)
2217        .with_timestamp(timestamp)
2218        .with_hostname(packet.hostname.map(|s| s.into()))
2219        .with_aggregation_key(packet.aggregation_key.map(|s| s.into()))
2220        .with_alert_type(packet.alert_type)
2221        .with_priority(packet.priority)
2222        // When no source type is provided, default to "api"—the same default the stock Datadog
2223        // Agent applies when serializing DogStatsD events to the intake JSON format. The agent
2224        // groups events by source type name and uses "api" as the key for events without an
2225        // explicit `s:` field. See: pkg/serializer/internal/metrics/events.go (writeItem).
2226        .with_source_type_name(Some(
2227            packet
2228                .source_type_name
2229                .map(|s| s.into())
2230                .unwrap_or_else(|| "api".into()),
2231        ))
2232        .with_alert_type(packet.alert_type)
2233        .with_tags(tags)
2234        .with_origin_tags(origin_tags);
2235
2236    Some(eventd)
2237}
2238
2239fn handle_service_check_packet(
2240    packet: ServiceCheckPacket, context_resolvers: &mut ContextResolvers, process_origin: Option<&ProcessOrigin>,
2241    additional_tags: &[String],
2242) -> Option<ServiceCheck> {
2243    let well_known_tags = WellKnownTags::from_raw_tags(&packet.tags);
2244
2245    let origin = origin_from_service_check_packet(&packet, &well_known_tags);
2246    let origin_tags = context_resolvers.resolve_origin_tags(origin, process_origin);
2247
2248    let tags = get_filtered_tags_iterator(&packet.tags, additional_tags);
2249    let tags_resolver = context_resolvers.tags();
2250    let tags = tags_resolver.create_tag_set(tags)?;
2251
2252    // When no d: field is present, backfill the current time—matching the stock Datadog Agent's
2253    // behavior, which sets the timestamp to time.Now().Unix() for any service check with a zero
2254    // timestamp.
2255    let timestamp = packet
2256        .timestamp
2257        .or_else(|| SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()));
2258
2259    let service_check = ServiceCheck::new(packet.name, packet.status)
2260        .with_timestamp(timestamp)
2261        .with_hostname(packet.hostname.map(|s| s.into()))
2262        .with_tags(tags)
2263        .with_origin_tags(origin_tags)
2264        .with_message(packet.message.map(|s| s.into()));
2265
2266    Some(service_check)
2267}
2268
2269fn get_filtered_tags_iterator<'a>(
2270    raw_tags: &'a RawTags<'_>, additional_tags: &'a [String],
2271) -> impl Iterator<Item = &'a str> + Clone {
2272    // This filters out "well-known" tags from the raw tags in the DogStatsD packet, and then chains on any additional tags
2273    // that were configured on the source.
2274    RawTagsFilter::exclude(raw_tags, WellKnownTagsFilterPredicate).chain(additional_tags.iter().map(|s| s.as_str()))
2275}
2276
2277async fn dispatch_events(mut event_buffer: EventsBuffer, source_context: &SourceContext) {
2278    debug!(events_len = event_buffer.len(), "Forwarding events.");
2279
2280    // TODO: This is maybe a little dicey because if we fail to dispatch the events, we may not have iterated over all of
2281    // them, so there might still be eventd events when get to the service checks point, and eventd events and/or service
2282    // check events when we get to the metrics point, and so on.
2283    //
2284    // There's probably something to be said for erroring out fully if this happens, since we should only fail to
2285    // dispatch if the downstream component fails entirely... and unless we have a way to restart the component, then
2286    // we're going to continue to fail to dispatch any more events until the process is restarted anyways.
2287
2288    // Dispatch any eventd events, if present.
2289    if event_buffer.has_event_type(EventType::EventD) {
2290        let eventd_events = event_buffer.extract(Event::is_eventd);
2291        let events_output = source_context.dispatcher().buffered_named("events");
2292
2293        // The `events` output is always wired in the DSD topology, so a missing output is an invariant violation that
2294        // crashes this component.
2295        if events_output.is_err() {
2296            saluki_antithesis::unreachable!("dsd 'events' output missing at dispatch");
2297        }
2298
2299        if let Err(e) = events_output
2300            .expect("events output should always exist")
2301            .send_all(eventd_events)
2302            .await
2303        {
2304            error!(error = %e, "Failed to dispatch eventd events.");
2305
2306            saluki_antithesis::unreachable!("dsd dispatch failed mid-buffer", { "stream": "events" });
2307        }
2308    }
2309
2310    // Dispatch any service check events, if present.
2311    if event_buffer.has_event_type(EventType::ServiceCheck) {
2312        let service_check_events = event_buffer.extract(Event::is_service_check);
2313        let service_checks_output = source_context.dispatcher().buffered_named("service_checks");
2314
2315        if service_checks_output.is_err() {
2316            saluki_antithesis::unreachable!("dsd 'service_checks' output missing at dispatch");
2317        }
2318
2319        if let Err(e) = service_checks_output
2320            .expect("service checks output should always exist")
2321            .send_all(service_check_events)
2322            .await
2323        {
2324            error!(error = %e, "Failed to dispatch service check events.");
2325
2326            saluki_antithesis::unreachable!("dsd dispatch failed mid-buffer", { "stream": "service_checks" });
2327        }
2328    }
2329
2330    // Finally, if there are events left, they'll be metrics, so dispatch them.
2331    if !event_buffer.is_empty() {
2332        if let Err(e) = source_context
2333            .dispatcher()
2334            .dispatch_named("metrics", event_buffer)
2335            .await
2336        {
2337            error!(error = %e, "Failed to dispatch metric events.");
2338
2339            saluki_antithesis::unreachable!("dsd dispatch failed mid-buffer", { "stream": "metrics" });
2340        }
2341    }
2342}
2343
2344const fn get_adjusted_buffer_size(buffer_size: usize) -> usize {
2345    // This is a little goofy, but hear me out:
2346    //
2347    // In the Datadog Agent, the way the UDS listener works is that if it's in stream mode, it will do a standalone
2348    // socket read to get _just_ the length delimiter, which is 4 bytes. After that, it will do a read to get the packet
2349    // data itself, up to the limit of `dogstatsd_buffer_size`. This means that a _full_ UDS stream packet can be up to
2350    // `dogstatsd_buffer_size + 4` bytes.
2351    //
2352    // 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
2353    // able to get an entire frame in a single buffer for the purpose of decoding the frame. Rather than rewriting our
2354    // read loop such that we have to change the logic depending on UDP/UDS datagram vs UDS stream, we simply increase
2355    // the buffer size by 4 bytes to account for the length delimiter.
2356    //
2357    // We do it this way so that we don't have to change the buffer size in the configuration, since if you just ported
2358    // over a Datadog Agent configuration, the value would be too small, and vise versa.
2359    buffer_size + 4
2360}
2361
2362#[cfg(test)]
2363mod tests {
2364    use std::{
2365        collections::HashMap,
2366        net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
2367        path::PathBuf,
2368        sync::{
2369            atomic::{AtomicUsize, Ordering},
2370            Arc, Mutex as StdMutex,
2371        },
2372        time::{Duration, Instant},
2373    };
2374
2375    use bytes::{Buf as _, BufMut as _};
2376    use bytesize::ByteSize;
2377    use metrics::{Key, Label};
2378    #[cfg(unix)]
2379    use saluki_core::accounting::MemoryLimiter;
2380    use saluki_core::{
2381        components::{
2382            sources::{SourceBuilder as _, SourceContext},
2383            test_util::{ContextParts, Outputs, TestComponentDriver},
2384            ComponentContext,
2385        },
2386        data_model::event::metric::context::{ContextResolverBuilder, TagsResolverBuilder},
2387        pooling::{helpers::get_pooled_object_via_builder, ObjectPool as _},
2388        topology::EventsBuffer,
2389    };
2390    #[cfg(target_os = "linux")]
2391    use saluki_env::workload::providers::TestWorkloadProvider;
2392    use saluki_env::workload::{CaptureEntityResolver, EntityId};
2393    #[cfg(unix)]
2394    use saluki_io::net::Stream;
2395    use saluki_io::{
2396        buf::{BytesBuffer, FixedSizeVec},
2397        deser::codec::dogstatsd::{DogStatsDCodec, DogStatsDCodecConfiguration, ParsedPacket},
2398        net::{ConnectionAddress, ListenAddress, ProcessCredentials, ProcessIdentity},
2399    };
2400    use saluki_metrics::test::TestRecorder;
2401    use stringtheory::MetaString;
2402    #[cfg(unix)]
2403    use tokio::{
2404        io::AsyncWriteExt as _,
2405        net::{UnixDatagram, UnixStream},
2406    };
2407    use tokio::{
2408        sync::{mpsc, Mutex},
2409        task::yield_now,
2410        time::timeout,
2411    };
2412
2413    use super::{
2414        build_io_buffer_pool, capture_named_pipe_frame, default_decoder_worker_count, filters::EnablePayloadsFilter,
2415        handle_frame, handle_metric_packet, metrics::build_metrics, origin_detection_error_for_telemetry,
2416        resolve_process_origin, resolve_process_origin_if_needed, BufferDecodeContext, BufferDecodeMode,
2417        ContextResolvers, DatagramSocketContext, DecodeOutcome, DecoderContext, DogStatsDConfiguration,
2418        DogStatsDDecoder, OriginEnrichmentConfiguration, ProcessOrigin, QueuedDatagram, ReceivedBuffer, TrafficCapture,
2419        TrafficCaptureReader,
2420    };
2421    #[cfg(unix)]
2422    use super::{receive_connected_stream, receive_connectionless_stream, received_payload};
2423    #[cfg(target_os = "linux")]
2424    use super::{DogStatsDOriginTagResolver, Listener};
2425    use crate::sources::dogstatsd::PacketForwarder;
2426
2427    const TEST_WINDOWS_PIPE_SECURITY_DESCRIPTOR: &str = "D:AI(A;;GA;;;WD)";
2428    const TEST_BUFFER_SIZE: usize = 8192;
2429
2430    fn test_component_context() -> ComponentContext {
2431        ComponentContext::test_source("dogstatsd_test")
2432    }
2433
2434    fn test_datagram_socket_context(listen_addr: ListenAddress) -> Arc<DatagramSocketContext> {
2435        Arc::new(DatagramSocketContext {
2436            metrics: build_metrics(&listen_addr, &test_component_context(), false),
2437            listen_addr,
2438            eol_required: false,
2439            packet_forwarder: None,
2440        })
2441    }
2442
2443    #[derive(Default)]
2444    struct CaptureTestEntityResolver {
2445        pid_map: StdMutex<HashMap<u32, EntityId>>,
2446        resolution_count: AtomicUsize,
2447    }
2448
2449    impl CaptureTestEntityResolver {
2450        fn with_pid_mapping(process_id: u32, entity_id: EntityId) -> Self {
2451            let mut pid_map = HashMap::new();
2452            pid_map.insert(process_id, entity_id);
2453            Self {
2454                pid_map: StdMutex::new(pid_map),
2455                resolution_count: AtomicUsize::new(0),
2456            }
2457        }
2458
2459        fn resolution_count(&self) -> usize {
2460            self.resolution_count.load(Ordering::Relaxed)
2461        }
2462
2463        #[cfg(target_os = "linux")]
2464        fn set_pid_mapping(&self, process_id: u32, entity_id: EntityId) {
2465            self.pid_map
2466                .lock()
2467                .expect("PID map lock should not be poisoned")
2468                .insert(process_id, entity_id);
2469        }
2470    }
2471
2472    impl CaptureEntityResolver for CaptureTestEntityResolver {
2473        fn resolve_container_entity_for_live_pid(&self, process_id: u32) -> Option<EntityId> {
2474            self.resolution_count.fetch_add(1, Ordering::Relaxed);
2475            self.pid_map
2476                .lock()
2477                .expect("PID map lock should not be poisoned")
2478                .get(&process_id)
2479                .cloned()
2480        }
2481    }
2482
2483    fn processed_metric_key(listener_type: &'static str, origin: Option<&str>) -> Key {
2484        let mut labels = vec![
2485            Label::from_static_parts("component_id", "dogstatsd_test"),
2486            Label::from_static_parts("component_type", "source"),
2487            Label::from_static_parts("listener_type", listener_type),
2488            Label::from_static_parts("message_type", "metrics"),
2489        ];
2490        if let Some(origin) = origin {
2491            labels.push(Label::new("origin", origin.to_string()));
2492        }
2493
2494        Key::from_parts("component_events_received_total", labels)
2495    }
2496
2497    fn test_context_resolvers() -> ContextResolvers {
2498        let tags_resolver = TagsResolverBuilder::for_tests().build();
2499        let context_resolver = ContextResolverBuilder::for_tests()
2500            .with_tags_resolver(Some(tags_resolver.clone()))
2501            .build();
2502        ContextResolvers::manual(context_resolver.clone(), context_resolver, tags_resolver)
2503    }
2504
2505    /// Builds the context of the source, along with its outputs.
2506    ///
2507    /// Only useful for tests that drive pieces of the source directly. Hold the returned parts for as long as the test
2508    /// runs, since they keep the outputs open. A test that runs the source itself goes through the driver, as the tests
2509    /// in the `supervision` module do.
2510    fn test_source_context() -> ContextParts<SourceContext, (), Outputs<EventsBuffer>> {
2511        TestComponentDriver::options()
2512            .with_component_id("dogstatsd_test")
2513            .source_context(DogStatsDConfiguration::for_test().outputs())
2514            .expect("the source's outputs should be valid")
2515    }
2516
2517    fn test_decoder_context(origin_detection_enabled: bool) -> DecoderContext {
2518        DecoderContext {
2519            codec: DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default()),
2520            context_resolvers: test_context_resolvers(),
2521            default_hostname: MetaString::from_static("default-hostname"),
2522            enabled_filter: EnablePayloadsFilter::default(),
2523            origin_detection_enabled,
2524            stream_log_too_big: false,
2525            disable_verbose_logs: false,
2526            additional_tags: Vec::<String>::new().into(),
2527            traffic_capture: TrafficCapture::new(PathBuf::new(), 1),
2528        }
2529    }
2530
2531    fn test_io_buffer(payload: &[u8], capacity: usize) -> BytesBuffer {
2532        let mut buffer: BytesBuffer = get_pooled_object_via_builder(|| FixedSizeVec::with_capacity(capacity));
2533        buffer.put_slice(payload);
2534        buffer
2535    }
2536
2537    #[test]
2538    fn named_pipe_frames_are_written_to_an_active_capture() {
2539        let capture_directory = tempfile::tempdir().expect("temporary capture directory should be created");
2540        let capture = TrafficCapture::new(capture_directory.path().to_path_buf(), 1);
2541        let capture_path = capture
2542            .start_capture(None, Duration::from_secs(30), false)
2543            .expect("capture should start");
2544
2545        capture_named_pipe_frame(&capture, b"captured.named_pipe.one:1|c");
2546        capture_named_pipe_frame(&capture, b"captured.named_pipe.two:1|c");
2547        capture.stop_capture();
2548
2549        let deadline = Instant::now() + Duration::from_secs(2);
2550        while capture.is_ongoing() && Instant::now() < deadline {
2551            std::thread::sleep(Duration::from_millis(10));
2552        }
2553        assert!(!capture.is_ongoing(), "capture should stop after its sender is dropped");
2554
2555        let mut reader = TrafficCaptureReader::from_path(&capture_path).expect("capture should be readable");
2556        let first_record = reader
2557            .read_next()
2558            .expect("first capture record should decode")
2559            .expect("first named-pipe frame should be captured");
2560        let second_record = reader
2561            .read_next()
2562            .expect("second capture record should decode")
2563            .expect("second named-pipe frame should be captured");
2564        assert_eq!(first_record.payload, b"captured.named_pipe.one:1|c");
2565        assert_eq!(first_record.pid, 0);
2566        assert_eq!(second_record.payload, b"captured.named_pipe.two:1|c");
2567        assert_eq!(second_record.pid, 0);
2568        assert!(reader.read_next().expect("capture should terminate cleanly").is_none());
2569    }
2570
2571    #[tokio::test]
2572    async fn connectionless_decoder_dispatches_full_and_flushed_buffers_and_forwards_frames() {
2573        let recorder = TestRecorder::default();
2574        let _recorder_guard = metrics::set_default_local_recorder(&recorder);
2575        let listen_addr = ListenAddress::Unixgram("/tmp/dsd.sock".into());
2576        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Unavailable);
2577        let metrics = build_metrics(&listen_addr, &test_component_context(), false);
2578        let mut source = test_source_context();
2579        let mut metrics_rx = source.outputs.take_named_output("metrics");
2580        let mut decoder = DogStatsDDecoder::new(source.context, test_decoder_context(false));
2581        let event_buffer_capacity = EventsBuffer::default().capacity();
2582        let (packet_forwarder, mut packets_rx) = PacketForwarder::for_tests(event_buffer_capacity + 1);
2583        let mut buffer_decode_context = BufferDecodeContext::new(
2584            &listen_addr,
2585            false,
2586            &metrics,
2587            Some(&packet_forwarder),
2588            BufferDecodeMode::Connectionless,
2589        );
2590
2591        let mut payload = b"decoder.metric:1|c\n".repeat(event_buffer_capacity);
2592        payload.extend_from_slice(b"decoder.metric:1|c");
2593        let io_buffer = test_io_buffer(&payload, payload.len());
2594        let (received, returned_buffer) = ReceivedBuffer::with_return(io_buffer, payload.len(), peer_addr, None);
2595        let outcome = decoder.decode_buffer(&mut buffer_decode_context, received).await;
2596
2597        assert_eq!(outcome, DecodeOutcome::Continue);
2598        let io_buffer = returned_buffer
2599            .await
2600            .expect("connectionless decoder should return the I/O buffer");
2601        assert_eq!(io_buffer.remaining(), 0);
2602        let full_buffer = metrics_rx.next().await.expect("metrics output should remain connected");
2603        assert_eq!(full_buffer.len(), event_buffer_capacity);
2604        assert!(
2605            timeout(Duration::from_secs(1), packets_rx.recv())
2606                .await
2607                .expect("forwarded packet should not time out")
2608                .is_some(),
2609            "forwarded packet should be queued"
2610        );
2611        assert_eq!(packets_rx.len(), event_buffer_capacity);
2612
2613        decoder.flush_events().await;
2614        let flushed_buffer = metrics_rx.next().await.expect("metrics output should remain connected");
2615        assert_eq!(flushed_buffer.len(), 1);
2616        decoder.flush_events().await;
2617        assert!(
2618            metrics_rx.try_next().is_none(),
2619            "empty flush should not dispatch another buffer"
2620        );
2621
2622        assert_eq!(
2623            recorder.counter((
2624                "component_packets_received_total",
2625                &[
2626                    ("component_id", "dogstatsd_test"),
2627                    ("component_type", "source"),
2628                    ("listener_type", "unixgram"),
2629                    ("state", "ok"),
2630                ],
2631            )),
2632            Some(1)
2633        );
2634        assert_eq!(
2635            recorder.counter((
2636                "component_bytes_received_total",
2637                &[
2638                    ("component_id", "dogstatsd_test"),
2639                    ("component_type", "source"),
2640                    ("listener_type", "unixgram"),
2641                ],
2642            )),
2643            Some(payload.len() as u64)
2644        );
2645        assert_eq!(
2646            recorder.counter(processed_metric_key("unixgram", None)),
2647            Some((event_buffer_capacity + 1) as u64)
2648        );
2649    }
2650
2651    #[tokio::test]
2652    async fn connected_decoder_waits_for_complete_outer_frame_and_stops_on_eof() {
2653        let recorder = TestRecorder::default();
2654        let _recorder_guard = metrics::set_default_local_recorder(&recorder);
2655        let listen_addr = ListenAddress::Unix("/tmp/dsd.socket".into());
2656        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Error(
2657            saluki_io::net::ProcessCredentialsError::InvalidCredentials,
2658        ));
2659        let metrics = build_metrics(&listen_addr, &test_component_context(), false);
2660        let mut source = test_source_context();
2661        let mut metrics_rx = source.outputs.take_named_output("metrics");
2662        let mut decoder = DogStatsDDecoder::new(source.context, test_decoder_context(true));
2663        let (packet_forwarder, mut packets_rx) = PacketForwarder::for_tests(1);
2664        let mut buffer_decode_context = BufferDecodeContext::new(
2665            &listen_addr,
2666            false,
2667            &metrics,
2668            Some(&packet_forwarder),
2669            BufferDecodeMode::Connected,
2670        );
2671
2672        let frame = b"stream.metric:1|c\n";
2673        let mut payload = Vec::with_capacity(frame.len() + 4);
2674        payload.extend_from_slice(&(frame.len() as u32).to_le_bytes());
2675        payload.extend_from_slice(frame);
2676        let io_buffer = test_io_buffer(&payload[..2], payload.len() + 16);
2677        let (received, returned_buffer) = ReceivedBuffer::with_return(io_buffer, 2, peer_addr.clone(), None);
2678
2679        let partial_outcome = decoder.decode_buffer(&mut buffer_decode_context, received).await;
2680        assert_eq!(partial_outcome, DecodeOutcome::Continue);
2681        let mut io_buffer = returned_buffer
2682            .await
2683            .expect("connected decoder should return the partial I/O buffer");
2684        assert_eq!(io_buffer.remaining(), 2);
2685        assert_eq!(
2686            recorder.counter((
2687                "component_packets_received_total",
2688                &[
2689                    ("component_id", "dogstatsd_test"),
2690                    ("component_type", "source"),
2691                    ("listener_type", "unix"),
2692                    ("state", "ok"),
2693                ],
2694            )),
2695            Some(0)
2696        );
2697        assert_eq!(
2698            recorder.counter((
2699                "component_errors_total",
2700                &[
2701                    ("component_id", "dogstatsd_test"),
2702                    ("component_type", "source"),
2703                    ("error_type", "origin_detection"),
2704                    ("reason", "invalid_credentials"),
2705                ],
2706            )),
2707            Some(0)
2708        );
2709
2710        io_buffer.put_slice(&payload[2..]);
2711        let (received, returned_buffer) =
2712            ReceivedBuffer::with_return(io_buffer, payload.len() - 2, peer_addr.clone(), None);
2713        let complete_outcome = decoder.decode_buffer(&mut buffer_decode_context, received).await;
2714        assert_eq!(complete_outcome, DecodeOutcome::Continue);
2715        let io_buffer = returned_buffer
2716            .await
2717            .expect("connected decoder should return the consumed I/O buffer");
2718        assert_eq!(io_buffer.remaining(), 0);
2719        assert!(
2720            timeout(Duration::from_secs(1), packets_rx.recv())
2721                .await
2722                .expect("forwarded stream packet should not time out")
2723                .is_some(),
2724            "complete outer frame should be forwarded"
2725        );
2726
2727        let (received, returned_buffer) = ReceivedBuffer::with_return(io_buffer, 0, peer_addr, None);
2728        let eof_outcome = decoder.decode_buffer(&mut buffer_decode_context, received).await;
2729        assert_eq!(eof_outcome, DecodeOutcome::Stop);
2730        let returned_buffer = returned_buffer
2731            .await
2732            .expect("stopped decoder should return the I/O buffer");
2733        assert!(!returned_buffer.has_remaining());
2734
2735        decoder.flush_events().await;
2736        let flushed_buffer = metrics_rx.next().await.expect("metrics output should remain connected");
2737        assert_eq!(flushed_buffer.len(), 1);
2738        assert_eq!(
2739            recorder.counter((
2740                "component_packets_received_total",
2741                &[
2742                    ("component_id", "dogstatsd_test"),
2743                    ("component_type", "source"),
2744                    ("listener_type", "unix"),
2745                    ("state", "ok"),
2746                ],
2747            )),
2748            Some(1)
2749        );
2750        assert_eq!(
2751            recorder.counter((
2752                "component_bytes_received_total",
2753                &[
2754                    ("component_id", "dogstatsd_test"),
2755                    ("component_type", "source"),
2756                    ("listener_type", "unix"),
2757                ],
2758            )),
2759            Some(payload.len() as u64)
2760        );
2761        assert_eq!(
2762            recorder.counter((
2763                "component_errors_total",
2764                &[
2765                    ("component_id", "dogstatsd_test"),
2766                    ("component_type", "source"),
2767                    ("error_type", "origin_detection"),
2768                    ("reason", "invalid_credentials"),
2769                ],
2770            )),
2771            Some(1)
2772        );
2773        assert_eq!(recorder.counter(processed_metric_key("unix", None)), Some(1));
2774    }
2775
2776    fn length_delimited(payload: &[u8]) -> Vec<u8> {
2777        let mut frame = (payload.len() as u32).to_le_bytes().to_vec();
2778        frame.extend_from_slice(payload);
2779        frame
2780    }
2781
2782    /// Feeds `data` through the decoder the same way the connected stream reader does: each read fills whatever
2783    /// space is left in the I/O buffer after collapsing any partial frame retained from the previous read.
2784    async fn feed_connected_stream(
2785        decoder: &mut DogStatsDDecoder, context: &mut BufferDecodeContext<'_>, mut io_buffer: BytesBuffer,
2786        mut data: &[u8], peer_addr: &ConnectionAddress,
2787    ) -> BytesBuffer {
2788        use saluki_io::buf::CollapsibleReadWriteIoBuffer as _;
2789
2790        while !data.is_empty() {
2791            io_buffer.collapse();
2792            let bytes_read = io_buffer.remaining_mut().min(data.len());
2793            io_buffer.put_slice(&data[..bytes_read]);
2794            data = &data[bytes_read..];
2795
2796            let (received, returned_buffer) =
2797                ReceivedBuffer::with_return(io_buffer, bytes_read, peer_addr.clone(), None);
2798            let outcome = decoder.decode_buffer(context, received).await;
2799            assert_eq!(outcome, DecodeOutcome::Continue);
2800            io_buffer = returned_buffer.await.expect("decoder should return the I/O buffer");
2801        }
2802        io_buffer
2803    }
2804
2805    async fn finish_capture(capture: &TrafficCapture, capture_path: &std::path::Path) -> Vec<(Vec<u8>, i32)> {
2806        capture.stop_capture();
2807        timeout(Duration::from_secs(2), async {
2808            while capture.is_ongoing() {
2809                yield_now().await;
2810            }
2811        })
2812        .await
2813        .expect("capture should stop");
2814
2815        let mut reader = TrafficCaptureReader::from_path(capture_path).expect("capture should be readable");
2816        let mut records = Vec::new();
2817        while let Some(record) = reader.read_next().expect("capture record should decode") {
2818            records.push((record.payload, record.pid));
2819        }
2820        records
2821    }
2822
2823    fn stream_test_payloads(count: usize) -> Vec<Vec<u8>> {
2824        // 57 lines of 26 bytes gives 1,486-byte frames, which never align with the 8,192-byte I/O buffer.
2825        (0..count).map(|_| b"capture.stream.metric:1|c\n".repeat(57)).collect()
2826    }
2827
2828    #[tokio::test]
2829    async fn uds_stream_capture_records_every_outer_frame_across_reads() {
2830        let listen_addr = ListenAddress::Unix("/tmp/dsd.socket".into());
2831        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(ProcessCredentials {
2832            pid: 42,
2833            uid: 0,
2834            gid: 0,
2835        }));
2836        let capture_directory = tempfile::tempdir().expect("temporary capture directory should be created");
2837        let capture = TrafficCapture::new(capture_directory.path().to_path_buf(), 1);
2838        let metrics = build_metrics(&listen_addr, &test_component_context(), false);
2839        // The metrics output holds more buffers than the test dispatches, so nothing needs to drain it.
2840        let source = test_source_context();
2841        let decoder_context = DecoderContext {
2842            traffic_capture: capture.clone(),
2843            ..test_decoder_context(false)
2844        };
2845        let mut decoder = DogStatsDDecoder::new(source.context, decoder_context);
2846        let mut context = BufferDecodeContext::new(&listen_addr, false, &metrics, None, BufferDecodeMode::Connected);
2847
2848        let capture_path = capture
2849            .start_capture(None, Duration::from_secs(30), false)
2850            .expect("capture should start");
2851        let payloads = stream_test_payloads(40);
2852        let stream = payloads
2853            .iter()
2854            .flat_map(|payload| length_delimited(payload))
2855            .collect::<Vec<_>>();
2856        let io_buffer = test_io_buffer(&[], TEST_BUFFER_SIZE);
2857        let io_buffer = feed_connected_stream(&mut decoder, &mut context, io_buffer, &stream, &peer_addr).await;
2858        assert_eq!(io_buffer.remaining(), 0);
2859
2860        let records = finish_capture(&capture, &capture_path).await;
2861        assert_eq!(records.len(), payloads.len());
2862        for ((payload, pid), expected) in records.iter().zip(&payloads) {
2863            assert_eq!(payload, expected);
2864            assert_eq!(*pid, 42);
2865        }
2866    }
2867
2868    #[tokio::test]
2869    async fn uds_stream_capture_starting_mid_frame_captures_from_next_whole_frame_and_stops_cleanly() {
2870        let listen_addr = ListenAddress::Unix("/tmp/dsd.socket".into());
2871        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Unavailable);
2872        let capture_directory = tempfile::tempdir().expect("temporary capture directory should be created");
2873        let capture = TrafficCapture::new(capture_directory.path().to_path_buf(), 1);
2874        let metrics = build_metrics(&listen_addr, &test_component_context(), false);
2875        // The metrics output holds more buffers than the test dispatches, so nothing needs to drain it.
2876        let source = test_source_context();
2877        let decoder_context = DecoderContext {
2878            traffic_capture: capture.clone(),
2879            ..test_decoder_context(false)
2880        };
2881        let mut decoder = DogStatsDDecoder::new(source.context, decoder_context);
2882        let mut context = BufferDecodeContext::new(&listen_addr, false, &metrics, None, BufferDecodeMode::Connected);
2883
2884        let payloads = stream_test_payloads(20);
2885        let stream = payloads
2886            .iter()
2887            .flat_map(|payload| length_delimited(payload))
2888            .collect::<Vec<_>>();
2889        let frame_len = stream.len() / payloads.len();
2890
2891        // Before capture starts, deliver the first frame plus half of the second.
2892        let split = frame_len + frame_len / 2;
2893        let io_buffer = test_io_buffer(&[], TEST_BUFFER_SIZE);
2894        let io_buffer =
2895            feed_connected_stream(&mut decoder, &mut context, io_buffer, &stream[..split], &peer_addr).await;
2896        assert_eq!(io_buffer.remaining(), frame_len / 2);
2897
2898        let capture_path = capture
2899            .start_capture(None, Duration::from_secs(30), false)
2900            .expect("capture should start");
2901        let io_buffer =
2902            feed_connected_stream(&mut decoder, &mut context, io_buffer, &stream[split..], &peer_addr).await;
2903        let records = finish_capture(&capture, &capture_path).await;
2904
2905        assert_eq!(records.len(), payloads.len() - 1);
2906        for ((payload, _), expected) in records.iter().zip(&payloads[1..]) {
2907            assert_eq!(payload, expected);
2908        }
2909
2910        // Once capture has stopped, the connection keeps decoding without retaining any frames for capture.
2911        let more = length_delimited(&payloads[0]);
2912        let io_buffer = feed_connected_stream(&mut decoder, &mut context, io_buffer, &more, &peer_addr).await;
2913        assert_eq!(io_buffer.remaining(), 0);
2914        assert_eq!(context.framer.drain_outer_frames().count(), 0);
2915    }
2916
2917    #[tokio::test]
2918    async fn connectionless_decoder_counts_origin_detection_errors_by_reason() {
2919        let recorder = TestRecorder::default();
2920        let _recorder_guard = metrics::set_default_local_recorder(&recorder);
2921        let listen_addr = ListenAddress::Unixgram("/tmp/dsd.sock".into());
2922        let metrics = build_metrics(&listen_addr, &test_component_context(), false);
2923        let source = test_source_context();
2924        let mut decoder = DogStatsDDecoder::new(source.context, test_decoder_context(true));
2925        let mut buffer_decode_context =
2926            BufferDecodeContext::new(&listen_addr, false, &metrics, None, BufferDecodeMode::Connectionless);
2927
2928        // Each datagram counts as a single origin detection error, regardless of how many metrics it holds, and is
2929        // tagged with the reason that origin detection failed.
2930        let payload = b"decoder.metric:1|c\ndecoder.metric:2|c";
2931        for (error, datagrams) in [
2932            (saluki_io::net::ProcessCredentialsError::ZeroPid, 2),
2933            (saluki_io::net::ProcessCredentialsError::InvalidCredentials, 1),
2934        ] {
2935            for _ in 0..datagrams {
2936                let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Error(error));
2937                let io_buffer = test_io_buffer(payload, payload.len());
2938                let (received, _returned_buffer) =
2939                    ReceivedBuffer::with_return(io_buffer, payload.len(), peer_addr, None);
2940                let outcome = decoder.decode_buffer(&mut buffer_decode_context, received).await;
2941                assert_eq!(outcome, DecodeOutcome::Continue);
2942            }
2943        }
2944
2945        for (reason, expected) in [("zero_pid", 2), ("invalid_credentials", 1)] {
2946            assert_eq!(
2947                recorder.counter((
2948                    "component_errors_total",
2949                    &[
2950                        ("component_id", "dogstatsd_test"),
2951                        ("component_type", "source"),
2952                        ("error_type", "origin_detection"),
2953                        ("reason", reason),
2954                    ],
2955                )),
2956                Some(expected),
2957                "unexpected origin detection error count for reason `{reason}`"
2958            );
2959        }
2960    }
2961
2962    #[tokio::test]
2963    async fn decoder_continues_connectionless_but_stops_connected_on_framing_error() {
2964        let recorder = TestRecorder::default();
2965        let _recorder_guard = metrics::set_default_local_recorder(&recorder);
2966        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Unavailable);
2967        let source = test_source_context();
2968        let mut decoder = DogStatsDDecoder::new(source.context, test_decoder_context(false));
2969
2970        let datagram_addr = ListenAddress::Unixgram("/tmp/dsd.sock".into());
2971        let datagram_metrics = build_metrics(&datagram_addr, &test_component_context(), false);
2972        let payload = b"missing.newline:1|c";
2973        let datagram_buffer = test_io_buffer(payload, payload.len());
2974        let mut datagram_context = BufferDecodeContext::new(
2975            &datagram_addr,
2976            true,
2977            &datagram_metrics,
2978            None,
2979            BufferDecodeMode::Connectionless,
2980        );
2981        let (received, _returned_datagram_buffer) =
2982            ReceivedBuffer::with_return(datagram_buffer, payload.len(), peer_addr.clone(), None);
2983        let datagram_outcome = decoder.decode_buffer(&mut datagram_context, received).await;
2984        assert_eq!(datagram_outcome, DecodeOutcome::Continue);
2985
2986        let stream_addr = ListenAddress::Unix("/tmp/dsd.socket".into());
2987        let stream_metrics = build_metrics(&stream_addr, &test_component_context(), false);
2988        let stream_buffer_capacity = 64;
2989        let oversized_frame = (stream_buffer_capacity as u32).to_le_bytes();
2990        let stream_buffer = test_io_buffer(&oversized_frame, stream_buffer_capacity);
2991        let mut stream_context =
2992            BufferDecodeContext::new(&stream_addr, false, &stream_metrics, None, BufferDecodeMode::Connected);
2993        let (received, _returned_stream_buffer) =
2994            ReceivedBuffer::with_return(stream_buffer, oversized_frame.len(), peer_addr, None);
2995        let stream_outcome = decoder.decode_buffer(&mut stream_context, received).await;
2996        assert_eq!(stream_outcome, DecodeOutcome::Stop);
2997
2998        for listener_type in ["unixgram", "unix"] {
2999            assert_eq!(
3000                recorder.counter((
3001                    "component_errors_total",
3002                    &[
3003                        ("component_id", "dogstatsd_test"),
3004                        ("component_type", "source"),
3005                        ("listener_type", listener_type),
3006                        ("error_type", "framing"),
3007                    ],
3008                )),
3009                Some(1)
3010            );
3011        }
3012        assert_eq!(
3013            recorder.counter((
3014                "component_packets_received_total",
3015                &[
3016                    ("component_id", "dogstatsd_test"),
3017                    ("component_type", "source"),
3018                    ("listener_type", "unixgram"),
3019                    ("state", "ok"),
3020                ],
3021            )),
3022            Some(1)
3023        );
3024        assert_eq!(
3025            recorder.counter((
3026                "component_packets_received_total",
3027                &[
3028                    ("component_id", "dogstatsd_test"),
3029                    ("component_type", "source"),
3030                    ("listener_type", "unix"),
3031                    ("state", "ok"),
3032                ],
3033            )),
3034            Some(0)
3035        );
3036    }
3037
3038    #[test]
3039    fn origin_telemetry_does_not_resolve_origin_when_origin_detection_is_disabled() {
3040        let recorder = TestRecorder::default();
3041        let _recorder_guard = metrics::set_default_local_recorder(&recorder);
3042        let listen_addr = ListenAddress::Unixgram("/tmp/dsd.sock".into());
3043        let context = test_component_context();
3044        let metrics = build_metrics(&listen_addr, &context, true);
3045        let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
3046        let mut context_resolvers = test_context_resolvers();
3047        let capture_entity_resolver = CaptureTestEntityResolver::with_pid_mapping(
3048            42,
3049            EntityId::from_local_data("ci-pid-container").expect("container entity"),
3050        );
3051        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(ProcessCredentials {
3052            pid: 42,
3053            uid: 0,
3054            gid: 0,
3055        }));
3056        let process_origin = resolve_process_origin(Some(&capture_entity_resolver), &peer_addr);
3057
3058        let event = handle_frame(
3059            b"test_metric:1|c",
3060            &codec,
3061            &mut context_resolvers,
3062            &metrics,
3063            false,
3064            process_origin.as_ref(),
3065            EnablePayloadsFilter::default(),
3066            &[],
3067            &MetaString::from_static("default-host"),
3068        )
3069        .expect("frame should parse");
3070
3071        assert!(event.is_some());
3072        assert_eq!(
3073            recorder.counter(processed_metric_key("unixgram", Some("container_id://pid-container"))),
3074            None
3075        );
3076        assert_eq!(recorder.counter(processed_metric_key("unixgram", Some(""))), Some(1));
3077    }
3078
3079    #[test]
3080    fn origin_telemetry_records_resolved_origin_when_origin_detection_is_enabled() {
3081        let recorder = TestRecorder::default();
3082        let _recorder_guard = metrics::set_default_local_recorder(&recorder);
3083        let listen_addr = ListenAddress::Unixgram("/tmp/dsd.sock".into());
3084        let context = test_component_context();
3085        let metrics = build_metrics(&listen_addr, &context, true);
3086        let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
3087        let mut context_resolvers = test_context_resolvers();
3088        let capture_entity_resolver = CaptureTestEntityResolver::with_pid_mapping(
3089            42,
3090            EntityId::from_local_data("ci-pid-container").expect("container entity"),
3091        );
3092        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(ProcessCredentials {
3093            pid: 42,
3094            uid: 0,
3095            gid: 0,
3096        }));
3097        let process_origin = resolve_process_origin(Some(&capture_entity_resolver), &peer_addr);
3098
3099        let event = handle_frame(
3100            b"test_metric:1|c",
3101            &codec,
3102            &mut context_resolvers,
3103            &metrics,
3104            true,
3105            process_origin.as_ref(),
3106            EnablePayloadsFilter::default(),
3107            &[],
3108            &MetaString::from_static("default-host"),
3109        )
3110        .expect("frame should parse");
3111
3112        assert!(event.is_some());
3113        assert_eq!(
3114            recorder.counter(processed_metric_key("unixgram", Some("container_id://pid-container"))),
3115            Some(1)
3116        );
3117        assert_eq!(recorder.counter(processed_metric_key("unixgram", Some(""))), Some(0));
3118    }
3119
3120    #[test]
3121    fn no_metrics_when_interner_full_allocations_disallowed() {
3122        // We're specifically testing here that when we don't allow outside allocations, we should not be able to
3123        // resolve a context if the interner is full. A no-op interner has the smallest possible size, so that's going
3124        // to assure we can't intern anything... but we also need a string (name or one of the tags) that can't be
3125        // _inlined_ either, since that will get around the interner being full.
3126        //
3127        // We set our metric name to be longer than 31 bytes (the inlining limit) to ensure this.
3128
3129        let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
3130        let tags_resolver = TagsResolverBuilder::for_tests().build();
3131        let context_resolver = ContextResolverBuilder::for_tests()
3132            .with_heap_allocations(false)
3133            .with_tags_resolver(Some(tags_resolver.clone()))
3134            .build();
3135        let mut context_resolvers = ContextResolvers::manual(context_resolver.clone(), context_resolver, tags_resolver);
3136        let input = "big_metric_name_that_cant_possibly_be_inlined:1|c|#tag1:value1,tag2:value2,tag3:value3";
3137
3138        let Ok(ParsedPacket::Metric(packet)) = codec.decode_packet(input.as_bytes()) else {
3139            panic!("Failed to parse packet.");
3140        };
3141
3142        let maybe_metric = handle_metric_packet(
3143            packet,
3144            &mut context_resolvers,
3145            None,
3146            &[],
3147            &MetaString::from_static("default-host"),
3148        );
3149        assert!(maybe_metric.is_none());
3150    }
3151
3152    #[test]
3153    fn metric_host_tag_disambiguates_contexts_without_remaining_tag() {
3154        let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
3155        let mut context_resolvers = test_context_resolvers();
3156        let default_hostname = MetaString::from_static("default-host");
3157
3158        let packets = [
3159            ("unset", b"test_metric_name:1|g".as_slice(), "default-host"),
3160            ("empty", b"test_metric_name:2|g|#host:".as_slice(), ""),
3161            (
3162                "explicit_default",
3163                b"test_metric_name:3|g|#host:default-host".as_slice(),
3164                "default-host",
3165            ),
3166            (
3167                "custom",
3168                b"test_metric_name:4|g|#host:custom-host".as_slice(),
3169                "custom-host",
3170            ),
3171        ];
3172
3173        let mut metrics = Vec::new();
3174        for (case, raw, expected_host) in packets {
3175            let Ok(ParsedPacket::Metric(packet)) = codec.decode_packet(raw) else {
3176                panic!("Failed to parse {case} packet.");
3177            };
3178            let metric = handle_metric_packet(packet, &mut context_resolvers, None, &[], &default_hostname)
3179                .unwrap_or_else(|| panic!("{case} metric should resolve"));
3180
3181            assert_eq!(metric.context().host(), Some(expected_host), "{case} context host");
3182            assert!(metric.context().tags().into_iter().all(|tag| tag.name() != "host"));
3183            metrics.push(metric);
3184        }
3185
3186        assert_eq!(metrics[0].context(), metrics[2].context());
3187        assert_ne!(metrics[0].context(), metrics[1].context());
3188        assert_ne!(metrics[0].context(), metrics[3].context());
3189        assert_ne!(metrics[1].context(), metrics[3].context());
3190    }
3191
3192    #[test]
3193    fn additional_tags_are_sorted_and_deduplicated() {
3194        let config = DogStatsDConfiguration {
3195            additional_tags: vec![
3196                "dogstatsd:configured".to_string(),
3197                "env:prod".to_string(),
3198                "provider_kind:autopilot".to_string(),
3199                "kube_distribution:eks".to_string(),
3200                "env:prod".to_string(),
3201            ],
3202            ..DogStatsDConfiguration::for_test()
3203        };
3204
3205        assert_eq!(
3206            config.additional_tags(),
3207            [
3208                "dogstatsd:configured",
3209                "env:prod",
3210                "kube_distribution:eks",
3211                "provider_kind:autopilot",
3212            ]
3213        );
3214    }
3215
3216    #[test]
3217    fn metric_with_additional_tags() {
3218        let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
3219        let tags_resolver = TagsResolverBuilder::for_tests().build();
3220        let context_resolver = ContextResolverBuilder::for_tests()
3221            .with_heap_allocations(false)
3222            .with_tags_resolver(Some(tags_resolver.clone()))
3223            .build();
3224        let mut context_resolvers = ContextResolvers::manual(context_resolver.clone(), context_resolver, tags_resolver);
3225        let existing_tags = ["tag1:value1", "tag2:value2", "tag3:value3"];
3226        let existing_tags_str = existing_tags.join(",");
3227
3228        let input = format!("test_metric_name:1|c|#{}", existing_tags_str);
3229        let additional_tags = [
3230            "tag4:value4".to_string(),
3231            "tag5:value5".to_string(),
3232            "tag6:value6".to_string(),
3233        ];
3234
3235        let Ok(ParsedPacket::Metric(packet)) = codec.decode_packet(input.as_bytes()) else {
3236            panic!("Failed to parse packet.");
3237        };
3238        let maybe_metric = handle_metric_packet(
3239            packet,
3240            &mut context_resolvers,
3241            None,
3242            &additional_tags,
3243            &MetaString::from_static("default-host"),
3244        );
3245        assert!(maybe_metric.is_some());
3246
3247        let metric = maybe_metric.unwrap();
3248        let context = metric.context();
3249
3250        for tag in existing_tags {
3251            assert!(context.tags().has_tag(tag));
3252        }
3253
3254        for tag in additional_tags {
3255            assert!(context.tags().has_tag(tag));
3256        }
3257    }
3258
3259    fn udp_listen_address() -> ListenAddress {
3260        ListenAddress::udp_loopback(8125)
3261    }
3262
3263    fn tcp_listen_address() -> ListenAddress {
3264        ListenAddress::tcp_loopback(8125)
3265    }
3266
3267    fn named_pipe_listen_address() -> ListenAddress {
3268        ListenAddress::named_pipe_with_input_buffer_size(
3269            "datadog-dogstatsd",
3270            TEST_WINDOWS_PIPE_SECURITY_DESCRIPTOR,
3271            TEST_BUFFER_SIZE as u32,
3272        )
3273    }
3274
3275    #[test]
3276    fn build_addresses_includes_named_pipe_when_configured() {
3277        let config = DogStatsDConfiguration {
3278            port: 0,
3279            pipe_name: Some("datadog-dogstatsd".to_string()),
3280            ..DogStatsDConfiguration::for_test()
3281        };
3282
3283        let addresses = config.build_addresses(None);
3284
3285        assert_eq!(addresses, vec![named_pipe_listen_address()]);
3286    }
3287
3288    #[test]
3289    fn build_addresses_uses_buffer_size_for_named_pipe_input_buffer() {
3290        let config = DogStatsDConfiguration {
3291            port: 0,
3292            pipe_name: Some("datadog-dogstatsd".to_string()),
3293            buffer_size: 16384,
3294            ..DogStatsDConfiguration::for_test()
3295        };
3296
3297        let addresses = config.build_addresses(None);
3298
3299        let [ListenAddress::NamedPipe { input_buffer_size, .. }] = addresses.as_slice() else {
3300            panic!("expected only a named pipe listen address, got {addresses:?}");
3301        };
3302        assert_eq!(*input_buffer_size, Some(16_384));
3303    }
3304
3305    #[test]
3306    fn eol_required_matches_named_pipe_listener_type() {
3307        let config = DogStatsDConfiguration {
3308            eol_required: vec!["named_pipe".to_string()],
3309            ..DogStatsDConfiguration::for_test()
3310        };
3311        let eol_required = config.eol_required();
3312
3313        assert!(eol_required.for_listener(&named_pipe_listen_address()));
3314        assert!(!eol_required.for_listener(&udp_listen_address()));
3315        assert!(!eol_required.for_listener(&tcp_listen_address()));
3316    }
3317
3318    #[test]
3319    fn statsd_forward_no_host_disabled() {
3320        let config = DogStatsDConfiguration {
3321            statsd_forward_port: 9125,
3322            ..DogStatsDConfiguration::for_test()
3323        };
3324        assert!(config.statsd_forward_target().is_none());
3325    }
3326
3327    #[test]
3328    fn statsd_forward_zero_port_disabled() {
3329        let config = DogStatsDConfiguration {
3330            statsd_forward_host: Some(MetaString::from("127.0.0.1")),
3331            statsd_forward_port: 0,
3332            ..DogStatsDConfiguration::for_test()
3333        };
3334        assert!(config.statsd_forward_target().is_none());
3335    }
3336
3337    #[test]
3338    fn statsd_forward_host_and_port_enabled() {
3339        let config = DogStatsDConfiguration {
3340            statsd_forward_host: Some(MetaString::from("127.0.0.1")),
3341            statsd_forward_port: 9125,
3342            ..DogStatsDConfiguration::for_test()
3343        };
3344        let (host, port) = config.statsd_forward_target().expect("forwarding should be enabled");
3345        assert_eq!(host.as_ref(), "127.0.0.1");
3346        assert_eq!(port, 9125);
3347    }
3348
3349    #[test]
3350    fn statsd_forward_invalid_target_still_builds_forwarder_handle() {
3351        let config = DogStatsDConfiguration {
3352            statsd_forward_host: Some(MetaString::from("not a valid host")),
3353            statsd_forward_port: 9125,
3354            ..DogStatsDConfiguration::for_test()
3355        };
3356        assert!(config.packet_forwarder_target().is_some());
3357    }
3358
3359    #[test]
3360    fn unsupported_platform_process_credentials_do_not_count_as_origin_detection_telemetry_errors() {
3361        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Error(
3362            saluki_io::net::ProcessCredentialsError::UnsupportedPlatform,
3363        ));
3364
3365        assert!(origin_detection_error_for_telemetry(true, 1, &peer_addr).is_none());
3366    }
3367
3368    #[test]
3369    fn invalid_process_credentials_count_as_origin_detection_telemetry_errors() {
3370        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Error(
3371            saluki_io::net::ProcessCredentialsError::InvalidCredentials,
3372        ));
3373
3374        assert!(matches!(
3375            origin_detection_error_for_telemetry(true, 1, &peer_addr),
3376            Some(saluki_io::net::ProcessCredentialsError::InvalidCredentials)
3377        ));
3378    }
3379
3380    #[test]
3381    fn zero_pid_process_credentials_count_as_origin_detection_telemetry_errors() {
3382        let peer_addr =
3383            ConnectionAddress::ProcessLike(ProcessIdentity::Error(saluki_io::net::ProcessCredentialsError::ZeroPid));
3384
3385        assert!(matches!(
3386            origin_detection_error_for_telemetry(true, 1, &peer_addr),
3387            Some(saluki_io::net::ProcessCredentialsError::ZeroPid)
3388        ));
3389    }
3390
3391    #[test]
3392    fn origin_detection_telemetry_errors_require_origin_detection_and_received_bytes() {
3393        let peer_addr =
3394            ConnectionAddress::ProcessLike(ProcessIdentity::Error(saluki_io::net::ProcessCredentialsError::ZeroPid));
3395
3396        assert!(origin_detection_error_for_telemetry(false, 1, &peer_addr).is_none());
3397        assert!(origin_detection_error_for_telemetry(true, 0, &peer_addr).is_none());
3398    }
3399
3400    #[test]
3401    fn autoscaling_disabled_yields_a_single_udp_stream() {
3402        let config = DogStatsDConfiguration {
3403            autoscale_udp_listeners: false,
3404            ..DogStatsDConfiguration::for_test()
3405        };
3406        assert!(config.udp_streams_to_yield().is_none());
3407    }
3408
3409    #[test]
3410    fn effective_max_buffer_count_never_below_baseline() {
3411        fn buffer_counts(buffer_count: usize, buffer_count_max: usize) -> DogStatsDConfiguration {
3412            DogStatsDConfiguration {
3413                buffer_count,
3414                buffer_count_max,
3415                ..DogStatsDConfiguration::for_test()
3416            }
3417        }
3418
3419        // A config that only raised the baseline keeps its full capacity rather than being capped to the maximum.
3420        assert_eq!(buffer_counts(65536, 32_768).effective_max_buffer_count(), 65536);
3421
3422        // An explicit maximum above the baseline is honored as-is.
3423        assert_eq!(buffer_counts(128, 512).effective_max_buffer_count(), 512);
3424
3425        // A maximum below the baseline is treated as equal to the baseline.
3426        assert_eq!(buffer_counts(200, 64).effective_max_buffer_count(), 200);
3427    }
3428
3429    #[test]
3430    fn decoder_worker_count_matches_core_agent_defaults() {
3431        assert_eq!(default_decoder_worker_count(1), 2);
3432        assert_eq!(default_decoder_worker_count(4), 2);
3433        assert_eq!(default_decoder_worker_count(8), 6);
3434    }
3435
3436    #[test]
3437    fn decoder_worker_count_honors_explicit_override() {
3438        let config = DogStatsDConfiguration {
3439            workers_count: 1,
3440            ..DogStatsDConfiguration::for_test()
3441        };
3442
3443        assert_eq!(config.decoder_worker_count().get(), 1);
3444    }
3445
3446    #[tokio::test]
3447    async fn global_datagram_receiver_distributes_packets_to_workers() {
3448        let (sender, receiver) = mpsc::channel(2);
3449        let receiver = Arc::new(Mutex::new(receiver));
3450        let first_worker = receiver.clone();
3451        let second_worker = receiver;
3452        let socket_context = test_datagram_socket_context(udp_listen_address());
3453
3454        sender
3455            .send(QueuedDatagram {
3456                result: Err(std::io::Error::other("first")),
3457                socket_context: socket_context.clone(),
3458            })
3459            .await
3460            .expect("first packet should be queued");
3461        sender
3462            .send(QueuedDatagram {
3463                result: Err(std::io::Error::other("second")),
3464                socket_context,
3465            })
3466            .await
3467            .expect("second packet should be queued");
3468
3469        let (first, second) = tokio::join!(async { first_worker.lock().await.recv().await }, async {
3470            second_worker.lock().await.recv().await
3471        },);
3472
3473        assert!(first.expect("first worker should receive a packet").result.is_err());
3474        assert!(second.expect("second worker should receive a packet").result.is_err());
3475    }
3476
3477    #[tokio::test]
3478    async fn dogstatsd_io_buffer_pool_grows_on_demand_until_limit() {
3479        let min_buffers = 2;
3480        let max_buffers = 3;
3481        let (pool, shrinker) = build_io_buffer_pool(min_buffers, max_buffers, TEST_BUFFER_SIZE);
3482
3483        let mut initial_buffers = Vec::with_capacity(min_buffers);
3484        for _ in 0..min_buffers {
3485            initial_buffers.push(
3486                timeout(Duration::from_secs(1), pool.acquire())
3487                    .await
3488                    .expect("initial buffer should be available"),
3489            );
3490        }
3491        let on_demand_buffer = timeout(Duration::from_secs(1), pool.acquire())
3492            .await
3493            .expect("pool should grow on demand before hitting the limit");
3494
3495        let capped_acquire = timeout(Duration::from_millis(25), pool.acquire()).await;
3496        assert!(capped_acquire.is_err(), "pool should wait once it reaches the limit");
3497
3498        drop(initial_buffers.pop().expect("initial buffer should still be held"));
3499        timeout(Duration::from_secs(1), pool.acquire())
3500            .await
3501            .expect("returned buffer should unblock acquisition");
3502
3503        drop(on_demand_buffer);
3504        drop(shrinker);
3505    }
3506
3507    #[cfg(unix)]
3508    #[tokio::test]
3509    async fn uds_datagram_reader_is_bounded_by_io_buffer_pool() {
3510        let temp_dir = tempfile::tempdir().expect("temp directory should be created");
3511        let socket_path = temp_dir.path().join("dogstatsd.socket");
3512        let receiver = UnixDatagram::bind(&socket_path).expect("receiver should bind");
3513        let sender = UnixDatagram::unbound().expect("sender should be created");
3514        let (pool, shrinker) = build_io_buffer_pool(2, 2, TEST_BUFFER_SIZE);
3515        let (packets_tx, mut packets_rx) = mpsc::channel(3);
3516        let listen_addr = ListenAddress::Unixgram(socket_path.clone());
3517        let socket_context = Arc::new(DatagramSocketContext {
3518            metrics: build_metrics(&listen_addr, &test_component_context(), false),
3519            listen_addr,
3520            eol_required: false,
3521            packet_forwarder: None,
3522        });
3523        let reader = tokio::spawn(receive_connectionless_stream(
3524            Stream::from(receiver),
3525            pool,
3526            MemoryLimiter::noop(),
3527            false,
3528            TrafficCapture::new(PathBuf::new(), 1),
3529            None,
3530            packets_tx,
3531            socket_context,
3532        ));
3533        let payloads: [&[u8]; 3] = [b"first", b"second", b"third"];
3534
3535        for payload in payloads {
3536            sender
3537                .send_to(payload, &socket_path)
3538                .await
3539                .expect("payload should send");
3540        }
3541
3542        timeout(Duration::from_secs(1), async {
3543            while packets_rx.len() < 2 {
3544                yield_now().await;
3545            }
3546        })
3547        .await
3548        .expect("reader should fill the two-buffer pool");
3549        assert_eq!(packets_rx.len(), 2);
3550
3551        let first = packets_rx
3552            .recv()
3553            .await
3554            .expect("first packet should be queued")
3555            .result
3556            .expect("first receive should succeed");
3557        assert_eq!(received_payload(first.buffer(), first.bytes_read), payloads[0]);
3558        drop(first);
3559
3560        timeout(Duration::from_secs(1), async {
3561            while packets_rx.len() < 2 {
3562                yield_now().await;
3563            }
3564        })
3565        .await
3566        .expect("returning a buffer should allow the third packet to be read");
3567
3568        for expected in &payloads[1..] {
3569            let received = packets_rx
3570                .recv()
3571                .await
3572                .expect("packet should be queued")
3573                .result
3574                .expect("receive should succeed");
3575            assert_eq!(received_payload(received.buffer(), received.bytes_read), *expected);
3576        }
3577
3578        drop(packets_rx);
3579        sender
3580            .send_to(b"shutdown", &socket_path)
3581            .await
3582            .expect("shutdown payload should send");
3583        timeout(Duration::from_secs(1), reader)
3584            .await
3585            .expect("reader should stop after observing the closed queue")
3586            .expect("reader task should not panic");
3587        drop(shrinker);
3588    }
3589
3590    #[cfg(target_os = "linux")]
3591    #[tokio::test]
3592    async fn uds_datagram_reader_pins_origin_before_decode() {
3593        let temp_dir = tempfile::tempdir().expect("temp directory should be created");
3594        let socket_path = temp_dir.path().join("dogstatsd.socket");
3595        let listen_addr = ListenAddress::Unixgram(socket_path.clone());
3596        let mut listener = Listener::from_listen_address(listen_addr.clone(), None)
3597            .await
3598            .expect("listener should bind");
3599        let stream = listener.accept().await.expect("listener should yield its socket");
3600        let sender = UnixDatagram::unbound().expect("sender should be created");
3601        let process_id = std::process::id();
3602        let original_entity = EntityId::from_local_data("ci-original-container").expect("container entity");
3603        let reused_entity = EntityId::from_local_data("ci-reused-container").expect("container entity");
3604        let capture_entity_resolver = Arc::new(CaptureTestEntityResolver::with_pid_mapping(
3605            process_id,
3606            original_entity.clone(),
3607        ));
3608        let (pool, shrinker) = build_io_buffer_pool(1, 1, TEST_BUFFER_SIZE);
3609        let (packets_tx, mut packets_rx) = mpsc::channel(1);
3610        let reader = tokio::spawn(receive_connectionless_stream(
3611            stream,
3612            pool,
3613            MemoryLimiter::noop(),
3614            true,
3615            TrafficCapture::new(PathBuf::new(), 1),
3616            Some(capture_entity_resolver.clone()),
3617            packets_tx,
3618            test_datagram_socket_context(listen_addr),
3619        ));
3620
3621        sender
3622            .send_to(b"test.metric:1|c", &socket_path)
3623            .await
3624            .expect("payload should send");
3625        let received = timeout(Duration::from_secs(1), packets_rx.recv())
3626            .await
3627            .expect("packet should be received")
3628            .expect("reader should remain active")
3629            .result
3630            .expect("receive should succeed");
3631        assert_eq!(
3632            received.process_origin,
3633            Some(ProcessOrigin::Pinned(Some(original_entity.clone())))
3634        );
3635        assert_eq!(capture_entity_resolver.resolution_count(), 1);
3636
3637        // Simulate the sender exiting and its PID being reused before a decoder worker reaches the queued packet.
3638        capture_entity_resolver.set_pid_mapping(process_id, reused_entity.clone());
3639
3640        let mut workload_provider = TestWorkloadProvider::new();
3641        workload_provider.add_entity(original_entity, &["container:original"]);
3642        workload_provider.add_entity(reused_entity, &["container:reused"]);
3643        let origin_config = OriginEnrichmentConfiguration {
3644            enabled: true,
3645            ..OriginEnrichmentConfiguration::for_test()
3646        };
3647        let origin_resolver = DogStatsDOriginTagResolver::new(
3648            origin_config,
3649            Arc::new(workload_provider),
3650            super::CapturedTaggerHandle::new(),
3651        );
3652        let tags_resolver = TagsResolverBuilder::for_tests().build();
3653        let context_resolver = ContextResolverBuilder::for_tests()
3654            .with_tags_resolver(Some(tags_resolver.clone()))
3655            .build();
3656        let mut context_resolvers = ContextResolvers::manual_with_origin(
3657            context_resolver.clone(),
3658            context_resolver,
3659            tags_resolver,
3660            origin_resolver,
3661        );
3662        let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
3663        let Ok(ParsedPacket::Metric(packet)) = codec.decode_packet(b"test.metric:1|c") else {
3664            panic!("metric should parse");
3665        };
3666        let metric = handle_metric_packet(
3667            packet,
3668            &mut context_resolvers,
3669            received.process_origin.as_ref(),
3670            &[],
3671            &MetaString::from_static("default-host"),
3672        )
3673        .expect("metric context should resolve");
3674
3675        assert!(metric.context().origin_tags().has_tag("container:original"));
3676        assert!(!metric.context().origin_tags().has_tag("container:reused"));
3677
3678        drop(received);
3679        drop(packets_rx);
3680        sender
3681            .send_to(b"shutdown", &socket_path)
3682            .await
3683            .expect("shutdown payload should send");
3684        timeout(Duration::from_secs(1), reader)
3685            .await
3686            .expect("reader should stop after observing the closed queue")
3687            .expect("reader task should not panic");
3688        drop(shrinker);
3689    }
3690
3691    #[cfg(unix)]
3692    #[tokio::test]
3693    async fn connection_oriented_reader_preserves_partial_frames() {
3694        let (mut sender, receiver) = UnixStream::pair().expect("stream pair should be created");
3695        let (pool, shrinker) = build_io_buffer_pool(1, 1, TEST_BUFFER_SIZE);
3696        let (packets_tx, mut packets_rx) = mpsc::channel(1);
3697        let reader = tokio::spawn(receive_connected_stream(
3698            Stream::from(receiver),
3699            pool,
3700            MemoryLimiter::noop(),
3701            false,
3702            TrafficCapture::new(PathBuf::new(), 1),
3703            None,
3704            packets_tx,
3705        ));
3706
3707        sender.write_all(b"partial").await.expect("first payload should send");
3708        let first = timeout(Duration::from_secs(1), packets_rx.recv())
3709            .await
3710            .expect("first read should finish")
3711            .expect("reader should remain active")
3712            .expect("first read should succeed");
3713        assert_eq!(first.buffer().chunk(), b"partial");
3714        drop(first);
3715
3716        sender.write_all(b"-frame").await.expect("second payload should send");
3717        let second = timeout(Duration::from_secs(1), packets_rx.recv())
3718            .await
3719            .expect("second read should finish")
3720            .expect("reader should remain active")
3721            .expect("second read should succeed");
3722        assert_eq!(second.bytes_read, b"-frame".len());
3723        assert_eq!(second.buffer().chunk(), b"partial-frame");
3724        drop(second);
3725
3726        drop(packets_rx);
3727        sender
3728            .write_all(b"shutdown")
3729            .await
3730            .expect("shutdown payload should send");
3731        timeout(Duration::from_secs(1), reader)
3732            .await
3733            .expect("reader should stop after observing the closed queue")
3734            .expect("reader task should not panic");
3735        drop(shrinker);
3736    }
3737
3738    #[cfg(unix)]
3739    #[tokio::test]
3740    async fn connection_oriented_reader_releases_drained_buffer_before_reacquiring() {
3741        let (mut sender, receiver) = UnixStream::pair().expect("stream pair should be created");
3742        let (pool, shrinker) = build_io_buffer_pool(1, 1, TEST_BUFFER_SIZE);
3743        let (packets_tx, mut packets_rx) = mpsc::channel(1);
3744        let reader = tokio::spawn(receive_connected_stream(
3745            Stream::from(receiver),
3746            pool,
3747            MemoryLimiter::noop(),
3748            false,
3749            TrafficCapture::new(PathBuf::new(), 1),
3750            None,
3751            packets_tx,
3752        ));
3753
3754        sender.write_all(b"first").await.expect("first payload should send");
3755        let mut first = timeout(Duration::from_secs(1), packets_rx.recv())
3756            .await
3757            .expect("first read should finish")
3758            .expect("reader should remain active")
3759            .expect("first read should succeed");
3760        let bytes_read = first.bytes_read;
3761        first.buffer_mut().advance(bytes_read);
3762        drop(first);
3763
3764        sender.write_all(b"second").await.expect("second payload should send");
3765        let mut second = timeout(Duration::from_secs(1), packets_rx.recv())
3766            .await
3767            .expect("reader should reacquire the released buffer")
3768            .expect("reader should remain active")
3769            .expect("second read should succeed");
3770        let bytes_read = second.bytes_read;
3771        assert_eq!(received_payload(second.buffer(), bytes_read), b"second");
3772        second.buffer_mut().advance(bytes_read);
3773        drop(second);
3774
3775        drop(packets_rx);
3776        sender
3777            .write_all(b"shutdown")
3778            .await
3779            .expect("shutdown payload should send");
3780        timeout(Duration::from_secs(1), reader)
3781            .await
3782            .expect("reader should stop after observing the closed queue")
3783            .expect("reader task should not panic");
3784        drop(shrinker);
3785    }
3786
3787    #[test]
3788    #[cfg(target_os = "linux")]
3789    fn autoscale_udp_listeners_yields_multiple_streams_on_linux() {
3790        let config = DogStatsDConfiguration {
3791            autoscale_udp_listeners: true,
3792            ..DogStatsDConfiguration::for_test()
3793        };
3794
3795        let streams = config
3796            .udp_streams_to_yield()
3797            .expect("autoscale yields at least 1 stream");
3798        let n = streams.get();
3799        assert!(
3800            (1..=4).contains(&n),
3801            "expected 1..=4 streams from vCPU formula, got {n}"
3802        );
3803    }
3804
3805    #[test]
3806    #[cfg(not(target_os = "linux"))]
3807    fn warns_for_uds_origin_detection_on_non_linux() {
3808        let config = DogStatsDConfiguration {
3809            port: 0,
3810            socket_path: Some("/tmp/dsd.sock".to_string()),
3811            origin_enrichment: OriginEnrichmentConfiguration {
3812                enabled: true,
3813                ..OriginEnrichmentConfiguration::for_test()
3814            },
3815            ..DogStatsDConfiguration::for_test()
3816        };
3817        let addresses = config.build_addresses(None);
3818
3819        assert!(config.uds_origin_detection_unsupported_on_platform(&addresses));
3820    }
3821
3822    #[test]
3823    #[cfg(not(target_os = "linux"))]
3824    fn does_not_warn_for_udp_origin_detection_on_non_linux() {
3825        let config = DogStatsDConfiguration {
3826            origin_enrichment: OriginEnrichmentConfiguration {
3827                enabled: true,
3828                ..OriginEnrichmentConfiguration::for_test()
3829            },
3830            ..DogStatsDConfiguration::for_test()
3831        };
3832        let addresses = config.build_addresses(None);
3833
3834        assert!(!config.uds_origin_detection_unsupported_on_platform(&addresses));
3835    }
3836
3837    #[test]
3838    #[cfg(not(target_os = "linux"))]
3839    fn autoscale_udp_listeners_yields_a_single_stream_on_non_linux() {
3840        let config = DogStatsDConfiguration {
3841            autoscale_udp_listeners: true,
3842            ..DogStatsDConfiguration::for_test()
3843        };
3844
3845        assert_eq!(None, config.udp_streams_to_yield());
3846    }
3847
3848    #[test]
3849    fn no_eol_required_listener_types_requires_no_newline() {
3850        let config = DogStatsDConfiguration {
3851            eol_required: Vec::new(),
3852            ..DogStatsDConfiguration::for_test()
3853        };
3854        let eol_required = config.eol_required();
3855
3856        assert!(!eol_required.for_listener(&udp_listen_address()));
3857        assert!(!eol_required.for_listener(&tcp_listen_address()));
3858    }
3859
3860    #[test]
3861    fn eol_required_matches_configured_listener_types() {
3862        let config = DogStatsDConfiguration {
3863            eol_required: vec!["udp".to_string(), "uds".to_string()],
3864            ..DogStatsDConfiguration::for_test()
3865        };
3866        let eol_required = config.eol_required();
3867
3868        assert!(eol_required.for_listener(&udp_listen_address()));
3869        assert!(!eol_required.for_listener(&tcp_listen_address()));
3870
3871        #[cfg(unix)]
3872        {
3873            assert!(eol_required.for_listener(&ListenAddress::Unixgram("/tmp/dsd.sock".into())));
3874            assert!(eol_required.for_listener(&ListenAddress::Unix("/tmp/dsd-stream.sock".into())));
3875        }
3876    }
3877
3878    #[test]
3879    fn eol_required_ignores_unrecognized_listener_types() {
3880        let config = DogStatsDConfiguration {
3881            eol_required: vec!["udp".to_string(), "carrier_pigeon".to_string()],
3882            ..DogStatsDConfiguration::for_test()
3883        };
3884        let eol_required = config.eol_required();
3885
3886        assert!(eol_required.for_listener(&udp_listen_address()));
3887        assert!(!eol_required.for_listener(&named_pipe_listen_address()));
3888    }
3889
3890    #[test]
3891    fn drops_full_named_pipe_buffer_without_newline() {
3892        let named_pipe_stream = named_pipe_listen_address();
3893        let mut buffer = get_pooled_object_via_builder::<_, BytesBuffer>(|| FixedSizeVec::with_capacity(8));
3894        buffer.put_slice(b"12345678");
3895
3896        assert!(super::should_drop_oversized_named_pipe_frame(
3897            &named_pipe_stream,
3898            &buffer
3899        ));
3900    }
3901
3902    #[test]
3903    fn keeps_named_pipe_partial_frame_when_buffer_has_capacity() {
3904        let named_pipe_stream = named_pipe_listen_address();
3905        let mut buffer = get_pooled_object_via_builder::<_, BytesBuffer>(|| FixedSizeVec::with_capacity(9));
3906        buffer.put_slice(b"12345678");
3907
3908        assert!(!super::should_drop_oversized_named_pipe_frame(
3909            &named_pipe_stream,
3910            &buffer
3911        ));
3912    }
3913
3914    #[test]
3915    fn keeps_full_named_pipe_buffer_with_newline() {
3916        let named_pipe_stream = named_pipe_listen_address();
3917        let mut buffer = get_pooled_object_via_builder::<_, BytesBuffer>(|| FixedSizeVec::with_capacity(8));
3918        buffer.put_slice(b"1234567\n");
3919
3920        assert!(!super::should_drop_oversized_named_pipe_frame(
3921            &named_pipe_stream,
3922            &buffer
3923        ));
3924    }
3925
3926    #[test]
3927    fn stream_log_too_big_warns_for_enabled_length_delimited_stream_invalid_frames() {
3928        let uds_stream = ListenAddress::Unix("/tmp/dsd-stream.sock".into());
3929        let named_pipe_stream = named_pipe_listen_address();
3930        let tcp_stream = ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8125)));
3931        let error = saluki_io::deser::framing::FramingError::InvalidFrame {
3932            frame_len: 8193,
3933            reason: "frame length exceeds buffer capacity",
3934        };
3935
3936        assert!(super::should_warn_stream_log_too_big(&uds_stream, &error, true));
3937        assert!(!super::should_warn_stream_log_too_big(&uds_stream, &error, false));
3938        assert!(!super::should_warn_stream_log_too_big(&named_pipe_stream, &error, true));
3939        assert!(!super::should_warn_stream_log_too_big(&tcp_stream, &error, true));
3940    }
3941
3942    #[test]
3943    fn interner_size_from_entry_count() {
3944        // A Core Agent migration config with entry count 4096 should yield 2 MiB, not 4096 bytes.
3945        let config = DogStatsDConfiguration {
3946            context_string_interner_entry_count: 4096,
3947            ..DogStatsDConfiguration::for_test()
3948        };
3949        assert_eq!(config.effective_context_string_interner_bytes(), ByteSize::mib(2));
3950    }
3951
3952    #[test]
3953    fn interner_size_from_explicit_bytes() {
3954        let config = DogStatsDConfiguration {
3955            context_string_interner_size_bytes: Some(ByteSize::b(4194304)),
3956            ..DogStatsDConfiguration::for_test()
3957        };
3958        assert_eq!(config.effective_context_string_interner_bytes(), ByteSize::b(4194304));
3959    }
3960
3961    #[test]
3962    fn interner_size_explicit_bytes_takes_priority() {
3963        let config = DogStatsDConfiguration {
3964            context_string_interner_entry_count: 4096,
3965            context_string_interner_size_bytes: Some(ByteSize::b(8388608)),
3966            ..DogStatsDConfiguration::for_test()
3967        };
3968        // The explicit byte size (8 MiB) takes priority over the entry count.
3969        assert_eq!(config.effective_context_string_interner_bytes(), ByteSize::b(8388608));
3970    }
3971
3972    #[test]
3973    fn interner_size_custom_entry_count() {
3974        let config = DogStatsDConfiguration {
3975            context_string_interner_entry_count: 8192,
3976            ..DogStatsDConfiguration::for_test()
3977        };
3978        // 8192 entries * 512 bytes = 4 MiB
3979        assert_eq!(config.effective_context_string_interner_bytes(), ByteSize::mib(4));
3980    }
3981
3982    /// Asserts that two lists of ListenAddress are equivalent.
3983    fn address_list_eq(expected: &mut [ListenAddress], actual: &mut [ListenAddress]) -> Result<(), String> {
3984        if expected.len() != actual.len() {
3985            return Err(format!(
3986                "length mismatch: expected {} addresses, got {}",
3987                expected.len(),
3988                actual.len()
3989            ));
3990        }
3991
3992        expected.sort_by_key(|a| a.to_string());
3993        actual.sort_by_key(|a| a.to_string());
3994
3995        for (e, a) in expected.iter().zip(actual.iter()) {
3996            let (es, as_) = (e.to_string(), a.to_string());
3997            if es != as_ {
3998                return Err(format!("address mismatch: expected {}, got {}", es, as_));
3999            }
4000        }
4001
4002        Ok(())
4003    }
4004
4005    /// This test verifies that we didn't accidentally break the `build_addresses_no_listeners` helper function which
4006    /// would render all further tests useless.
4007    #[test]
4008    fn build_addresses_assertion_function_works() {
4009        let config = DogStatsDConfiguration {
4010            port: 0,
4011            tcp_port: 123,
4012            socket_path: None,
4013            socket_stream_path: None,
4014            non_local_traffic: false,
4015            ..DogStatsDConfiguration::for_test()
4016        };
4017        let mut expected = vec![ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(
4018            // Close, but not quite! This is intentionally *not* 127.0.0.1 to test that the assertion will fail
4019            Ipv4Addr::new(127, 0, 0, 2),
4020            123,
4021        )))];
4022        let mut actual = config.build_addresses(None);
4023        assert!(address_list_eq(&mut expected, &mut actual).is_err())
4024    }
4025
4026    /// With all four listener gates off, `build_addresses` returns an empty Vec.
4027    #[test]
4028    fn build_addresses_no_listeners() {
4029        let config = DogStatsDConfiguration {
4030            port: 0,
4031            tcp_port: 0,
4032            socket_path: None,
4033            socket_stream_path: None,
4034            non_local_traffic: false,
4035            ..DogStatsDConfiguration::for_test()
4036        };
4037        let mut expected = vec![];
4038        let mut actual = config.build_addresses(None);
4039        address_list_eq(&mut expected, &mut actual).unwrap();
4040    }
4041
4042    /// UDP port set, `non_local_traffic=false` -> UDP listener bound to `127.0.0.1`.
4043    #[test]
4044    fn build_addresses_udp_local_only() {
4045        let config = DogStatsDConfiguration {
4046            port: 8125,
4047            tcp_port: 0,
4048            socket_path: None,
4049            socket_stream_path: None,
4050            non_local_traffic: false,
4051            ..DogStatsDConfiguration::for_test()
4052        };
4053        let mut expected = vec![ListenAddress::udp_loopback(8125)];
4054        let mut actual = config.build_addresses(None);
4055        address_list_eq(&mut expected, &mut actual).unwrap();
4056    }
4057
4058    /// UDP port set, `non_local_traffic=true` -> UDP listener bound to `0.0.0.0`.
4059    #[test]
4060    fn build_addresses_udp_non_local_only() {
4061        let config = DogStatsDConfiguration {
4062            port: 8125,
4063            tcp_port: 0,
4064            socket_path: None,
4065            socket_stream_path: None,
4066            non_local_traffic: true,
4067            ..DogStatsDConfiguration::for_test()
4068        };
4069        let mut expected = vec![ListenAddress::udp_any(8125)];
4070        let mut actual = config.build_addresses(None);
4071        address_list_eq(&mut expected, &mut actual).unwrap();
4072    }
4073
4074    /// TCP port set, `non_local_traffic=false` -> TCP listener bound to `127.0.0.1`.
4075    #[test]
4076    fn build_addresses_tcp_local_only() {
4077        let config = DogStatsDConfiguration {
4078            port: 0,
4079            tcp_port: 9000,
4080            socket_path: None,
4081            socket_stream_path: None,
4082            non_local_traffic: false,
4083            ..DogStatsDConfiguration::for_test()
4084        };
4085        let mut expected = vec![ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(
4086            Ipv4Addr::new(127, 0, 0, 1),
4087            9000,
4088        )))];
4089        let mut actual = config.build_addresses(None);
4090        address_list_eq(&mut expected, &mut actual).unwrap();
4091    }
4092
4093    /// TCP port set, `non_local_traffic=true` -> TCP listener bound to `0.0.0.0`.
4094    #[test]
4095    fn build_addresses_tcp_non_local_only() {
4096        let config = DogStatsDConfiguration {
4097            port: 0,
4098            tcp_port: 9000,
4099            socket_path: None,
4100            socket_stream_path: None,
4101            non_local_traffic: true,
4102            ..DogStatsDConfiguration::for_test()
4103        };
4104        let mut expected = vec![ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(
4105            Ipv4Addr::new(0, 0, 0, 0),
4106            9000,
4107        )))];
4108        let mut actual = config.build_addresses(None);
4109        address_list_eq(&mut expected, &mut actual).unwrap();
4110    }
4111
4112    /// `socket_path` set -> a `Unixgram` address is produced with that path.
4113    #[test]
4114    fn build_addresses_unixgram_only() {
4115        let config = DogStatsDConfiguration {
4116            port: 0,
4117            tcp_port: 0,
4118            socket_path: Some("/tmp/dsd.sock".to_string()),
4119            socket_stream_path: None,
4120            non_local_traffic: false,
4121            ..DogStatsDConfiguration::for_test()
4122        };
4123        let mut expected = vec![ListenAddress::Unixgram("/tmp/dsd.sock".into())];
4124        let mut actual = config.build_addresses(None);
4125        address_list_eq(&mut expected, &mut actual).unwrap();
4126    }
4127
4128    /// `socket_stream_path` set -> a `Unix` (stream) address is produced with that path.
4129    #[test]
4130    fn build_addresses_unix_stream_only() {
4131        let config = DogStatsDConfiguration {
4132            port: 0,
4133            tcp_port: 0,
4134            socket_path: None,
4135            socket_stream_path: Some("/tmp/dsd-stream.sock".to_string()),
4136            non_local_traffic: false,
4137            ..DogStatsDConfiguration::for_test()
4138        };
4139        let mut expected = vec![ListenAddress::Unix("/tmp/dsd-stream.sock".into())];
4140        let mut actual = config.build_addresses(None);
4141        address_list_eq(&mut expected, &mut actual).unwrap();
4142    }
4143
4144    /// All four listener types enabled at once, with `non_local_traffic=true`.
4145    #[test]
4146    fn build_addresses_all_four_non_local() {
4147        let config = DogStatsDConfiguration {
4148            port: 8125,
4149            tcp_port: 9000,
4150            socket_path: Some("/tmp/dsd.sock".to_string()),
4151            socket_stream_path: Some("/tmp/dsd-stream.sock".to_string()),
4152            non_local_traffic: true,
4153            ..DogStatsDConfiguration::for_test()
4154        };
4155        let mut expected = vec![
4156            ListenAddress::udp_any(8125),
4157            ListenAddress::tcp_any(9000),
4158            ListenAddress::Unixgram("/tmp/dsd.sock".into()),
4159            ListenAddress::Unix("/tmp/dsd-stream.sock".into()),
4160        ];
4161        let mut actual = config.build_addresses(None);
4162        address_list_eq(&mut expected, &mut actual).unwrap();
4163    }
4164
4165    /// All four listener types enabled at once, with `non_local_traffic=false`.
4166    #[test]
4167    fn build_addresses_all_four_local() {
4168        let config = DogStatsDConfiguration {
4169            port: 8125,
4170            tcp_port: 9000,
4171            socket_path: Some("/tmp/dsd.sock".to_string()),
4172            socket_stream_path: Some("/tmp/dsd-stream.sock".to_string()),
4173            non_local_traffic: false,
4174            ..DogStatsDConfiguration::for_test()
4175        };
4176        let mut expected = vec![
4177            ListenAddress::udp_loopback(8125),
4178            ListenAddress::tcp_loopback(9000),
4179            ListenAddress::Unixgram("/tmp/dsd.sock".into()),
4180            ListenAddress::Unix("/tmp/dsd-stream.sock".into()),
4181        ];
4182        let mut actual = config.build_addresses(None);
4183        address_list_eq(&mut expected, &mut actual).unwrap();
4184    }
4185
4186    /// Passing `Some(ip)` to `build_addresses` with `non_local_traffic=false` -> both UDP and TCP
4187    /// bind to that IP. Includes a UDS datagram socket to confirm `bind_host` doesn't affect it.
4188    #[test]
4189    fn build_addresses_bind_host_applies_to_udp_and_tcp() {
4190        let config = DogStatsDConfiguration {
4191            port: 8125,
4192            tcp_port: 9000,
4193            socket_path: Some("/tmp/dsd.sock".to_string()),
4194            socket_stream_path: None,
4195            non_local_traffic: false,
4196            ..DogStatsDConfiguration::for_test()
4197        };
4198        let bind_host = Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50)));
4199        let mut expected = vec![
4200            ListenAddress::Udp(([192, 168, 1, 50], 8125).into()),
4201            ListenAddress::Tcp(([192, 168, 1, 50], 9000).into()),
4202            ListenAddress::Unixgram("/tmp/dsd.sock".into()),
4203        ];
4204        let mut actual = config.build_addresses(bind_host);
4205        address_list_eq(&mut expected, &mut actual).unwrap();
4206    }
4207
4208    /// Passing `Some(ip)` to `build_addresses` with `non_local_traffic=true` -> both UDP and TCP
4209    /// bind to `0.0.0.0`; the `bind_host` parameter is ignored (precedence matches the Agent).
4210    /// Includes a UDS stream socket to confirm `bind_host` doesn't affect it.
4211    #[test]
4212    fn build_addresses_non_local_clobbers_bind_host() {
4213        let config = DogStatsDConfiguration {
4214            port: 8125,
4215            tcp_port: 9000,
4216            socket_path: None,
4217            socket_stream_path: Some("/tmp/dsd-stream.sock".to_string()),
4218            non_local_traffic: true,
4219            ..DogStatsDConfiguration::for_test()
4220        };
4221        let bind_host = Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50)));
4222        let mut expected = vec![
4223            ListenAddress::udp_any(8125),
4224            ListenAddress::tcp_any(9000),
4225            ListenAddress::Unix("/tmp/dsd-stream.sock".into()),
4226        ];
4227        let mut actual = config.build_addresses(bind_host);
4228        address_list_eq(&mut expected, &mut actual).unwrap();
4229    }
4230
4231    #[test]
4232    fn non_finite_metric_values_are_silently_dropped() {
4233        // The Datadog Agent sends NaN gauges (for example, encode_ms.avg computed as 0.0/0.0 in Go).
4234        // FloatIter skips non-finite values with a debug log, so decode_packet returns Ok with
4235        // num_points == 0. handle_frame then returns Ok(None) for zero-point packets, which is
4236        // the existing silent-drop path (no warning emitted).
4237        let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
4238        for input in &[b"my.gauge:NaN|g" as &[u8], b"my.gauge:inf|g", b"my.gauge:-inf|g"] {
4239            match codec.decode_packet(input).expect("should decode without error") {
4240                ParsedPacket::Metric(packet) => assert_eq!(
4241                    packet.num_points, 0,
4242                    "non-finite value should be dropped, leaving 0 valid points"
4243                ),
4244                _ => panic!("expected Metric packet"),
4245            }
4246        }
4247    }
4248
4249    #[test]
4250    fn resolve_process_origin_pins_live_entity() {
4251        let capture_entity_resolver = CaptureTestEntityResolver::with_pid_mapping(
4252            42,
4253            EntityId::from_local_data("ci-pid-container").expect("container entity"),
4254        );
4255        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(ProcessCredentials {
4256            pid: 42,
4257            uid: 0,
4258            gid: 0,
4259        }));
4260
4261        assert_eq!(
4262            resolve_process_origin(Some(&capture_entity_resolver), &peer_addr),
4263            Some(ProcessOrigin::Pinned(Some(
4264                EntityId::from_local_data("ci-pid-container").expect("container entity")
4265            )))
4266        );
4267    }
4268
4269    #[test]
4270    fn resolve_process_origin_skips_live_lookup_when_unused() {
4271        let capture_entity_resolver = CaptureTestEntityResolver::with_pid_mapping(
4272            42,
4273            EntityId::from_local_data("ci-pid-container").expect("container entity"),
4274        );
4275        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(ProcessCredentials {
4276            pid: 42,
4277            uid: 0,
4278            gid: 0,
4279        }));
4280        let traffic_capture = TrafficCapture::new(PathBuf::new(), 1);
4281
4282        assert_eq!(
4283            resolve_process_origin_if_needed(false, &traffic_capture, Some(&capture_entity_resolver), &peer_addr),
4284            None
4285        );
4286        assert_eq!(capture_entity_resolver.resolution_count(), 0);
4287    }
4288
4289    #[tokio::test]
4290    async fn resolve_process_origin_pins_live_entity_during_capture() {
4291        let capture_entity_resolver = CaptureTestEntityResolver::with_pid_mapping(
4292            42,
4293            EntityId::from_local_data("ci-pid-container").expect("container entity"),
4294        );
4295        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(ProcessCredentials {
4296            pid: 42,
4297            uid: 0,
4298            gid: 0,
4299        }));
4300        let capture_dir = tempfile::tempdir().expect("capture directory should be created");
4301        let traffic_capture = TrafficCapture::new(capture_dir.path().to_path_buf(), 1);
4302        traffic_capture
4303            .start_capture(None, Duration::from_secs(30), false)
4304            .expect("capture should start");
4305
4306        assert_eq!(
4307            resolve_process_origin_if_needed(false, &traffic_capture, Some(&capture_entity_resolver), &peer_addr),
4308            Some(ProcessOrigin::Pinned(Some(
4309                EntityId::from_local_data("ci-pid-container").expect("container entity")
4310            )))
4311        );
4312        assert_eq!(capture_entity_resolver.resolution_count(), 1);
4313
4314        traffic_capture.stop_capture();
4315        timeout(Duration::from_secs(1), async {
4316            while traffic_capture.is_ongoing() {
4317                yield_now().await;
4318            }
4319        })
4320        .await
4321        .expect("capture should stop");
4322    }
4323
4324    #[test]
4325    fn build_capture_record_ignores_payload_local_data() {
4326        let record = super::build_capture_record(None, None, b"test.metric:1|c|c:ci-local-container\n");
4327
4328        assert_eq!(record.container_id, None);
4329        assert!(record.ancillary.is_empty());
4330    }
4331
4332    #[test]
4333    fn stream_capture_state_preserves_last_pid_without_new_creds() {
4334        let mut stream_capture = super::StreamCaptureState::new();
4335
4336        stream_capture.update_peer_metadata(&ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(
4337            ProcessCredentials {
4338                pid: 42,
4339                uid: 0,
4340                gid: 0,
4341            },
4342        )));
4343        stream_capture.update_peer_metadata(&ConnectionAddress::ProcessLike(ProcessIdentity::Unavailable));
4344
4345        assert_eq!(stream_capture.last_pid, Some(42));
4346    }
4347
4348    #[test]
4349    fn resolve_process_origin_preserves_live_pid_without_entity_resolver() {
4350        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(ProcessCredentials {
4351            pid: 12345,
4352            uid: 1000,
4353            gid: 1000,
4354        }));
4355
4356        assert_eq!(
4357            resolve_process_origin(None, &peer_addr),
4358            Some(ProcessOrigin::Unpinned(12345))
4359        );
4360    }
4361
4362    #[test]
4363    fn resolve_process_origin_unpacks_captured_pid_when_replay_gid_present() {
4364        let captured_pid: u32 = 99887766;
4365        let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(ProcessCredentials {
4366            pid: 12345,        // our PID (irrelevant for replay)
4367            uid: captured_pid, // captured PID packed by the sender
4368            gid: super::REPLAY_CREDENTIALS_GID,
4369        }));
4370
4371        assert_eq!(
4372            resolve_process_origin(None, &peer_addr),
4373            Some(ProcessOrigin::Replay(captured_pid))
4374        );
4375    }
4376}
4377
4378/// Tests covering the source's background work running as supervised children.
4379///
4380/// The `tests` module above exercises the decode path in isolation. Here, the wiring is the important part. These tests
4381/// check that `run` puts the pool shrinker, datagram decoders, listeners, and per-connection handlers below the source
4382/// as its own children. They also check that the whole subtree stops cleanly and does not drop queued work.
4383#[cfg(test)]
4384mod supervision {
4385    use std::net::SocketAddr;
4386    use std::num::NonZeroUsize;
4387    use std::path::PathBuf;
4388    use std::time::Duration;
4389
4390    use bytes::BytesMut;
4391    use saluki_core::{
4392        components::test_util::{
4393            ComponentControl, DriverOptions, NoInput, Outputs, PrebuiltSource, TestComponentDriver,
4394        },
4395        data_model::event::metric::context::{ContextResolverBuilder, TagsResolverBuilder},
4396        runtime::{state::ResourceRegistry, NodeSnapshot, NodeState, SupervisorError},
4397        support::SubsystemIdentifier,
4398        topology::EventsBuffer,
4399    };
4400    use saluki_io::net::listener::Listener;
4401    use saluki_io::net::{BoundListenAddress, ListenAddress};
4402    use stringtheory::MetaString;
4403    use tokio::net::UdpSocket;
4404    use tokio::time::timeout;
4405
4406    use super::*;
4407
4408    /// Bound on socket operations, so a hang fails rather than stalling the suite.
4409    const RUN_TIMEOUT: Duration = Duration::from_secs(10);
4410
4411    /// ID of the source under test, which names its worker `sources.dogstatsd` in the supervision tree.
4412    const COMPONENT_ID: &str = "dogstatsd";
4413
4414    /// Decoder workers the test source runs with.
4415    ///
4416    /// More than one, so the drain covers the real case of several decoders sharing the queue.
4417    const DECODER_WORKERS: usize = 2;
4418
4419    /// How long to let datagrams make their way through to the (single-slot) output before asserting on backpressure.
4420    const BACKPRESSURE_SETTLE: Duration = Duration::from_millis(250);
4421
4422    /// How long a drain test holds a decoder mid-dispatch after shutdown.
4423    ///
4424    /// This is longer than five seconds, the default timeout of a closure-based child. Thus, the drain survives the hold
4425    /// only if the decoders have no timeout of their own.
4426    const DRAIN_HOLD: Duration = Duration::from_secs(6);
4427
4428    /// Shutdown timeout of the component in a drain test. It must be longer than [`DRAIN_HOLD`].
4429    const DRAIN_COMPONENT_TIMEOUT: Duration = Duration::from_secs(30);
4430
4431    /// Children a running source with one UDP listener should have.
4432    ///
4433    /// These children are:
4434    ///
4435    /// - the pool shrinker
4436    /// - one child for each decoder worker
4437    /// - the listener
4438    /// - one stream handler, below the listener
4439    ///
4440    /// For a connectionless listener, `accept` returns its socket immediately. Thus, its handler exists from the start,
4441    /// and not only after a peer appears.
4442    const fn udp_child_count() -> usize {
4443        1 + DECODER_WORKERS + 1 + 1
4444    }
4445
4446    /// Processes that run below the component's supervisor while a source with one UDP listener runs.
4447    ///
4448    /// These processes are the source itself and its children.
4449    const fn udp_descendant_count() -> usize {
4450        1 + udp_child_count()
4451    }
4452
4453    /// Runs `source` through the driver with `options`, declaring the outputs that its configuration declares.
4454    async fn run_source(
4455        options: DriverOptions, source: Box<DogStatsD>,
4456    ) -> (ComponentControl<NoInput>, Outputs<EventsBuffer>) {
4457        let outputs = DogStatsDConfiguration::for_test().outputs().to_vec();
4458        options
4459            .with_component_id(COMPONENT_ID)
4460            .source(PrebuiltSource::new(source, outputs))
4461            .await
4462            .expect("the pre-built source should build")
4463    }
4464
4465    /// Counts the running processes below `node`.
4466    fn running_beneath(node: &NodeSnapshot) -> usize {
4467        node.children
4468            .iter()
4469            .map(|child| usize::from(child.state == NodeState::Running) + running_beneath(child))
4470            .sum()
4471    }
4472
4473    /// Finds the first node with the name `name` below `node`, in depth-first order.
4474    fn find_node<'a>(node: &'a NodeSnapshot, name: &str) -> Option<&'a NodeSnapshot> {
4475        node.children.iter().find_map(|child| {
4476            if child.name == name {
4477                Some(child)
4478            } else {
4479                find_node(child, name)
4480            }
4481        })
4482    }
4483
4484    /// Builds a source with a single UDP listener bound to an ephemeral port, and returns it with that port's address.
4485    ///
4486    /// The source is built by hand, since its configuration can't ask for an ephemeral port: `build_addresses` treats
4487    /// port 0 as disabled.
4488    async fn build_source() -> (Box<DogStatsD>, SocketAddr) {
4489        let listener = leased_listener(ListenAddress::udp_loopback(0)).await;
4490        let listen_addr = match listener.bound_listen_address() {
4491            BoundListenAddress::Udp(addr) => addr,
4492            other => panic!("expected a bound UDP address, got {other:?}"),
4493        };
4494
4495        let io_buffer_count = 16;
4496        let (io_buffer_pool, io_buffer_pool_shrinker) = build_io_buffer_pool(
4497            io_buffer_count,
4498            io_buffer_count,
4499            DogStatsDConfiguration::for_test().buffer_size,
4500        );
4501
4502        let tags_resolver = TagsResolverBuilder::for_tests().build();
4503        let context_resolver = ContextResolverBuilder::for_tests()
4504            .with_tags_resolver(Some(tags_resolver.clone()))
4505            .build();
4506
4507        let source = Box::new(DogStatsD {
4508            listeners: vec![listener],
4509            decoder_worker_count: NonZeroUsize::new(DECODER_WORKERS).expect("decoder count should be non-zero"),
4510            io_buffer_pool,
4511            io_buffer_queue_capacity: 16,
4512            io_buffer_pool_shrinker: Box::pin(io_buffer_pool_shrinker),
4513            codec: DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default()),
4514            context_resolvers: ContextResolvers::manual(context_resolver.clone(), context_resolver, tags_resolver),
4515            default_hostname: MetaString::from_static("test-host"),
4516            enabled_filter: EnablePayloadsFilter::default(),
4517            origin_detection_enabled: false,
4518            origin_telemetry_enabled: false,
4519            stream_log_too_big: false,
4520            disable_verbose_logs: false,
4521            eol_required: EolRequired::default(),
4522            additional_tags: Vec::<String>::new().into(),
4523            capture_entity_resolver: None,
4524            traffic_capture: TrafficCapture::new(PathBuf::new(), 1),
4525            packet_forwarder_target: None,
4526        });
4527
4528        (source, listen_addr)
4529    }
4530
4531    /// Acquires a listener the way a real build does, so the source holds a lease rather than a bare listener.
4532    ///
4533    /// A registry per call is enough for a test that only needs one holder: a `ResourceLease` keeps its own clone of
4534    /// the registry, so the registry outlives the lease without the caller holding it.
4535    async fn leased_listener(address: ListenAddress) -> ResourceLease<Listener> {
4536        leased_listener_from(&ResourceRegistry::new(), address).await
4537    }
4538
4539    /// Acquires a listener from a specific registry, for a test that cares about what the registry does afterwards.
4540    async fn leased_listener_from(registry: &ResourceRegistry, address: ListenAddress) -> ResourceLease<Listener> {
4541        registry
4542            .acquire(
4543                &SubsystemIdentifier::from_segments(["test", COMPONENT_ID]),
4544                SocketSpecification::new(address),
4545            )
4546            .await
4547            .expect("listener should bind")
4548    }
4549
4550    #[tokio::test]
4551    async fn background_work_runs_as_the_sources_own_children() {
4552        // The pool shrinker, each datagram decoder, and each listener are children of the source. They are not detached
4553        // tasks or siblings. Thus, all of them appear below the source while it runs, and none of them remain after it
4554        // stops.
4555        let (source, _) = build_source().await;
4556        let (control, _outputs) = run_source(TestComponentDriver::options(), source).await;
4557        control
4558            .supervisor()
4559            .wait_for_running_descendants(udp_descendant_count())
4560            .await;
4561
4562        let root = control.supervisor().tree_handle().snapshot().root;
4563        let source_node = find_node(&root, "sources.dogstatsd").expect("the source is running");
4564        assert_eq!(running_beneath(source_node), udp_child_count());
4565
4566        // If the error is `ShutdownTimedOut`, a child ignored shutdown and was aborted. The pool shrinker is a deliberate
4567        // exception. It stops brutally, and a brutal stop does not count as forced.
4568        control
4569            .shutdown()
4570            .await
4571            .expect("every child should have stopped on its own");
4572    }
4573
4574    #[tokio::test]
4575    async fn source_does_not_finish_until_the_decoders_have_drained() {
4576        // The drain guarantee that matters: work already queued when shutdown is signalled must still be decoded and
4577        // dispatched. The decoders deliberately ignore shutdown for exactly this reason. They exit only after the
4578        // listeners drop their senders and the queue is empty. The source owns the decoders, and it does not finish
4579        // until they exit.
4580        //
4581        // Rather than race a datagram against shutdown, this pins a decoder open: the metrics output holds a single
4582        // buffer, so one dispatched buffer fills it and the next blocks a decoder mid-dispatch. The source must not
4583        // finish while that is true, even after the test signals shutdown. The test holds the decoder there for longer
4584        // than the default timeout of a closure-based child, but for less than the shutdown timeout of the component.
4585        let (source, listen_addr) = build_source().await;
4586        let options = TestComponentDriver::options()
4587            .with_shutdown_timeout(DRAIN_COMPONENT_TIMEOUT)
4588            .with_interconnect_capacity(NonZeroUsize::MIN);
4589        let (mut control, mut outputs) = run_source(options, source).await;
4590        let mut metrics = outputs.take_named_output("metrics");
4591        control
4592            .supervisor()
4593            .wait_for_running_descendants(udp_descendant_count())
4594            .await;
4595
4596        let client = UdpSocket::bind("127.0.0.1:0").await.expect("client should bind");
4597
4598        // Establish that the pipeline works, and leave the output empty again.
4599        client
4600            .send_to(b"first.metric:1|c", listen_addr)
4601            .await
4602            .expect("client should send");
4603        let buffer = metrics.next().await.expect("the metrics output should stay open");
4604        assert_eq!(buffer.len(), 1);
4605
4606        // Now back the output up: the first of these fills the single slot, the second blocks a decoder.
4607        for payload in [b"second.metric:1|c".as_slice(), b"third.metric:1|c".as_slice()] {
4608            client.send_to(payload, listen_addr).await.expect("client should send");
4609        }
4610        tokio::time::sleep(BACKPRESSURE_SETTLE).await;
4611
4612        // From here on, the test does not wait on the socket. Thus, it can pause the clock, and the hold below then
4613        // takes no real time.
4614        tokio::time::pause();
4615        control.signal_shutdown();
4616
4617        // The body of the source already returned. But the source still waits for its blocked decoder, and its
4618        // supervisor also waits. Without the drain, the source and the supervisor finish at this point. If the
4619        // decoders had the default timeout of a closure-based child, the scope of the source aborts them during the
4620        // hold.
4621        tokio::time::sleep(DRAIN_HOLD).await;
4622        assert!(
4623            !control.supervisor().is_finished(),
4624            "the source finished while a decoder was still mid-dispatch"
4625        );
4626
4627        // The test drains the output, and this unblocks the decoder. Then the drain completes, and the source also
4628        // completes. The output closes after the source and all of its decoders stop.
4629        let drained = 1 + metrics.collect().await.iter().map(EventsBuffer::len).sum::<usize>();
4630
4631        // Nothing accepted before shutdown should have been dropped on the way out.
4632        assert_eq!(drained, 3, "every queued datagram should have been dispatched");
4633
4634        control
4635            .wait()
4636            .await
4637            .expect("every child should have stopped on its own");
4638    }
4639
4640    #[tokio::test]
4641    async fn a_source_ignoring_shutdown_is_halted_with_its_children() {
4642        // In the orderly path, the body of the source returns when it receives the shutdown signal, and this stops its
4643        // children. This test covers the other path: a source that never sees the signal is halted along with
4644        // everything below it.
4645        //
4646        // This cannot occur in production, because there the shutdown signal of a source *is* the signal of its
4647        // worker, so the test withholds the signal from the source.
4648        let (source, _) = build_source().await;
4649        let options = TestComponentDriver::options()
4650            .with_shutdown_timeout(Duration::from_millis(500))
4651            .with_shutdown_withheld();
4652        let (control, _outputs) = run_source(options, source).await;
4653        control
4654            .supervisor()
4655            .wait_for_running_descendants(udp_descendant_count())
4656            .await;
4657
4658        let error = control
4659            .shutdown()
4660            .await
4661            .expect_err("the source should be halted once its timeout runs out");
4662        match error.downcast_ref::<SupervisorError>() {
4663            // The body never returned, so every child is still running when the source is halted. All are counted
4664            // except the brutal pool shrinker.
4665            Some(SupervisorError::ShutdownTimedOut { aborted }) => assert_eq!(*aborted, udp_descendant_count() - 1),
4666            other => panic!("expected the source to be halted and counted, got {other:?}"),
4667        }
4668    }
4669
4670    #[tokio::test]
4671    async fn stream_handlers_are_the_listeners_own_children() {
4672        // Per-connection handlers are also supervised, with one fixed name for each listener type. Each handler belongs
4673        // to the listener that accepted its connection. When the listener accepts a connection, it adds a handler. When
4674        // the source stops, its handlers stop with everything else.
4675
4676        // A TCP listener, so there are real connections to accept.
4677        let tcp_listener = leased_listener(ListenAddress::Tcp("127.0.0.1:0".parse().unwrap())).await;
4678        let listen_addr = match tcp_listener.bound_listen_address() {
4679            BoundListenAddress::Tcp(addr) => addr,
4680            other => panic!("expected a bound TCP address, got {other:?}"),
4681        };
4682
4683        let (mut source, _) = build_source().await;
4684        source.listeners = vec![tcp_listener];
4685        let (control, _outputs) = run_source(TestComponentDriver::options(), source).await;
4686
4687        // The baseline is the source, the pool shrinker, the decoders, and the listener.
4688        let baseline = 1 + 1 + DECODER_WORKERS + 1;
4689        control.supervisor().wait_for_running_descendants(baseline).await;
4690
4691        let _client = tokio::net::TcpStream::connect(listen_addr)
4692            .await
4693            .expect("client should connect");
4694
4695        // Each connection adds two processes. One is the handler, below the listener. The other is the stream reader
4696        // that the handler starts below itself.
4697        control.supervisor().wait_for_running_descendants(baseline + 2).await;
4698        let root = control.supervisor().tree_handle().snapshot().root;
4699        let listener_node = find_node(&root, "listener_tcp").expect("the listener is running");
4700        let handler_node = find_node(listener_node, "conn_tcp")
4701            .expect("the handler should hang beneath the listener that accepted its connection");
4702        assert!(
4703            find_node(handler_node, "stream_reader").is_some(),
4704            "the stream reader should hang beneath the handler that started it"
4705        );
4706
4707        control
4708            .shutdown()
4709            .await
4710            .expect("every child should have stopped on its own");
4711    }
4712
4713    #[tokio::test]
4714    async fn the_registry_keeps_the_listener_bound_after_the_source_stops() {
4715        // The property the whole migration is for. The source only ever borrows its listener, so stopping it hands the
4716        // socket back to the registry still bound, rather than releasing the port to the operating system and leaving
4717        // a rebuilt source to race whatever else wants it.
4718
4719        // A registry the test keeps, so it can ask for the same address once the source is gone.
4720        //
4721        // Port 0 is what makes the assertion below mean anything: the specification is keyed on the address as asked
4722        // for, so re-acquiring it can only come back on the same port if the registry really held the socket. A fixed
4723        // port could be re-bound from scratch and look identical.
4724        let registry = ResourceRegistry::new();
4725        let address = ListenAddress::udp_loopback(0);
4726
4727        let (mut source, _) = build_source().await;
4728        source.listeners = vec![leased_listener_from(&registry, address.clone()).await];
4729        let bound = source.listeners[0].bound_listen_address();
4730        let listen_addr = match bound {
4731            BoundListenAddress::Udp(addr) => addr,
4732            other => panic!("expected a bound UDP address, got {other:?}"),
4733        };
4734
4735        let (control, _outputs) = run_source(TestComponentDriver::options(), source).await;
4736        control
4737            .supervisor()
4738            .wait_for_running_descendants(udp_descendant_count())
4739            .await;
4740        control.shutdown().await.expect("the source should stop cleanly");
4741
4742        // The same socket, not a lucky re-bind: the registry never let go of it.
4743        let mut listener = leased_listener_from(&registry, address).await;
4744        assert_eq!(listener.bound_listen_address(), bound);
4745
4746        // And it is still usable. A listener whose socket had been moved out by the previous holder rather than
4747        // borrowed would come back exhausted and hang here instead of yielding anything.
4748        let mut stream = timeout(RUN_TIMEOUT, listener.accept())
4749            .await
4750            .expect("accept should not hang once the source has released its lease")
4751            .expect("listener should yield its socket");
4752
4753        let client = UdpSocket::bind("127.0.0.1:0").await.expect("client should bind");
4754        client
4755            .send_to(b"after.teardown:1|c", listen_addr)
4756            .await
4757            .expect("client should send");
4758
4759        let mut buf = BytesMut::with_capacity(64);
4760        let (read, _) = timeout(RUN_TIMEOUT, stream.receive(&mut buf))
4761            .await
4762            .expect("receive should not time out")
4763            .expect("receive should succeed");
4764        assert_eq!(&buf[..read], b"after.teardown:1|c");
4765    }
4766
4767    /// Socket paths for a DogStatsD source, in a temporary directory unique to the test.
4768    ///
4769    /// Socket paths rather than ports: the configuration has to name the address concretely for the build to ask
4770    /// for the same one, and a path in a temporary directory is unique by construction, so there is no ephemeral
4771    /// port to discover and race over.
4772    #[cfg(unix)]
4773    struct ListenerPaths {
4774        _temp_dir: tempfile::TempDir,
4775        datagram: PathBuf,
4776        stream: PathBuf,
4777    }
4778
4779    #[cfg(unix)]
4780    impl ListenerPaths {
4781        fn new() -> Self {
4782            let temp_dir = tempfile::tempdir().expect("temp directory should be created");
4783            Self {
4784                datagram: temp_dir.path().join("dogstatsd.socket"),
4785                stream: temp_dir.path().join("dogstatsd-stream.socket"),
4786                _temp_dir: temp_dir,
4787            }
4788        }
4789
4790        /// Configures a source to listen on both socket paths.
4791        fn configure(&self, requirement: ListenerRequirement) -> DogStatsDConfiguration {
4792            let mut config = listenerless_config(requirement);
4793            config.socket_path = Some(self.datagram.to_string_lossy().into_owned());
4794            config.socket_stream_path = Some(self.stream.to_string_lossy().into_owned());
4795            config
4796        }
4797    }
4798
4799    #[cfg(unix)]
4800    const LISTENER_REQUIREMENTS: [ListenerRequirement; 3] = [
4801        ListenerRequirement::All,
4802        ListenerRequirement::AtLeastOne,
4803        ListenerRequirement::Optional,
4804    ];
4805
4806    #[cfg(unix)]
4807    fn listenerless_config(requirement: ListenerRequirement) -> DogStatsDConfiguration {
4808        let mut config = DogStatsDConfiguration::for_test();
4809        // The fixture defaults to the real DogStatsD port, which these tests have no business binding.
4810        config.port = 0;
4811        config.listener_requirement = requirement;
4812        config
4813    }
4814
4815    #[cfg(unix)]
4816    type DriveResult = Result<(ComponentControl<NoInput>, Outputs<EventsBuffer>), GenericError>;
4817
4818    #[cfg(unix)]
4819    async fn drive_source(registry: ResourceRegistry, config: DogStatsDConfiguration) -> DriveResult {
4820        TestComponentDriver::options()
4821            .with_component_id(COMPONENT_ID)
4822            .with_resource_registry(registry)
4823            .source(config)
4824            .await
4825    }
4826
4827    #[cfg(unix)]
4828    async fn assert_runs_idle(result: DriveResult, requirement: ListenerRequirement) {
4829        let (control, _outputs) = result
4830            .unwrap_or_else(|error| panic!("the source should build without listeners ({requirement:?}): {error:#}"));
4831        control.wait_until_ready().await;
4832        assert!(
4833            !control.is_finished(),
4834            "the source should keep running without listeners ({requirement:?})"
4835        );
4836        control.shutdown().await.expect("the source should stop cleanly");
4837    }
4838
4839    #[cfg(unix)]
4840    fn assert_fails_to_start(result: DriveResult, requirement: ListenerRequirement, expected: &str) {
4841        let Err(error) = result else {
4842            panic!("the component build should fail ({requirement:?})");
4843        };
4844        let error = format!("{error:#}");
4845        assert!(error.contains(expected), "unexpected error ({requirement:?}): {error}");
4846    }
4847
4848    #[cfg(unix)]
4849    const NO_LISTENERS: &str = "No listeners could be created";
4850    #[cfg(unix)]
4851    const ALREADY_LEASED: &str = "already leased";
4852
4853    #[cfg(unix)]
4854    #[tokio::test]
4855    async fn zero_io_buffer_capacity_fails_to_start_without_listeners() {
4856        // Without listeners, no listener reserves a buffer, so the reservation check alone would accept a capacity of
4857        // zero, and the datagram queue can't be created with no capacity.
4858        let mut config = listenerless_config(ListenerRequirement::Optional);
4859        config.buffer_count = 0;
4860        config.buffer_count_max = 0;
4861
4862        let result = drive_source(ResourceRegistry::new(), config).await;
4863        assert_fails_to_start(
4864            result,
4865            ListenerRequirement::Optional,
4866            "maximum I/O buffer count must be greater than zero",
4867        );
4868    }
4869
4870    #[cfg(unix)]
4871    #[tokio::test]
4872    async fn no_configured_listener_runs_idle_only_when_listeners_are_optional() {
4873        for requirement in LISTENER_REQUIREMENTS {
4874            let result = drive_source(ResourceRegistry::new(), listenerless_config(requirement)).await;
4875            match requirement {
4876                ListenerRequirement::All | ListenerRequirement::AtLeastOne => {
4877                    assert_fails_to_start(result, requirement, NO_LISTENERS)
4878                }
4879                ListenerRequirement::Optional => assert_runs_idle(result, requirement).await,
4880            }
4881        }
4882    }
4883
4884    #[cfg(unix)]
4885    #[tokio::test]
4886    async fn no_created_listener_runs_idle_only_when_listeners_are_optional() {
4887        // Every configured address is already leased, so the build can create none of them.
4888        for requirement in LISTENER_REQUIREMENTS {
4889            let paths = ListenerPaths::new();
4890            let registry = ResourceRegistry::new();
4891            let _held_datagram = leased_listener_from(&registry, ListenAddress::Unixgram(paths.datagram.clone())).await;
4892            let _held_stream = leased_listener_from(&registry, ListenAddress::Unix(paths.stream.clone())).await;
4893
4894            let result = drive_source(registry, paths.configure(requirement)).await;
4895            match requirement {
4896                ListenerRequirement::All => assert_fails_to_start(result, requirement, ALREADY_LEASED),
4897                ListenerRequirement::AtLeastOne => assert_fails_to_start(result, requirement, NO_LISTENERS),
4898                ListenerRequirement::Optional => assert_runs_idle(result, requirement).await,
4899            }
4900        }
4901    }
4902
4903    #[cfg(unix)]
4904    #[tokio::test]
4905    async fn listener_that_cannot_be_created_is_skipped_unless_all_are_required() {
4906        // The stream address is already leased, but the datagram listener can still be created.
4907        for requirement in LISTENER_REQUIREMENTS {
4908            let paths = ListenerPaths::new();
4909            let registry = ResourceRegistry::new();
4910            let _held_stream = leased_listener_from(&registry, ListenAddress::Unix(paths.stream.clone())).await;
4911
4912            let result = drive_source(registry, paths.configure(requirement)).await;
4913            if requirement == ListenerRequirement::All {
4914                assert_fails_to_start(result, requirement, ALREADY_LEASED);
4915                continue;
4916            }
4917
4918            let (control, mut outputs) = result.unwrap_or_else(|error| {
4919                panic!("the source should build with the listener it could create ({requirement:?}): {error:#}")
4920            });
4921            control.wait_until_ready().await;
4922            assert_datagram_listener_receives(&mut outputs, &paths.datagram).await;
4923            control.shutdown().await.expect("the source should stop cleanly");
4924        }
4925    }
4926
4927    #[cfg(unix)]
4928    #[tokio::test]
4929    async fn unresolvable_bind_host_skips_only_udp_and_tcp_unless_all_are_required() {
4930        // An unresolvable `bind_host` prevents the UDP listener, but the datagram socket listener can still be created.
4931        for requirement in LISTENER_REQUIREMENTS {
4932            let paths = ListenerPaths::new();
4933            let mut config = listenerless_config(requirement);
4934            config.port = 8125;
4935            config.bind_host = Some("unresolvable.invalid".to_string());
4936            config.socket_path = Some(paths.datagram.to_string_lossy().into_owned());
4937
4938            let result = drive_source(ResourceRegistry::new(), config).await;
4939            if requirement == ListenerRequirement::All {
4940                assert_fails_to_start(result, requirement, "Could not resolve bind_host");
4941                continue;
4942            }
4943
4944            let (control, mut outputs) = result.unwrap_or_else(|error| {
4945                panic!("the source should build with the socket listener ({requirement:?}): {error:#}")
4946            });
4947            control.wait_until_ready().await;
4948            assert_datagram_listener_receives(&mut outputs, &paths.datagram).await;
4949            control.shutdown().await.expect("the source should stop cleanly");
4950        }
4951    }
4952
4953    /// Sends a metric to the datagram socket at `path`, and asserts that the source emits it.
4954    #[cfg(unix)]
4955    async fn assert_datagram_listener_receives(outputs: &mut Outputs<EventsBuffer>, path: &std::path::Path) {
4956        let sender = std::os::unix::net::UnixDatagram::unbound().expect("sender should be created");
4957        sender
4958            .send_to(b"survivor:1|c", path)
4959            .expect("the datagram listener should accept a metric");
4960        let events = timeout(RUN_TIMEOUT, outputs.named_output("metrics").next())
4961            .await
4962            .expect("the metric should arrive")
4963            .expect("the metrics output should stay open");
4964        let metric = events
4965            .into_iter()
4966            .next()
4967            .and_then(|event| event.try_into_metric())
4968            .expect("the output should carry the metric");
4969        assert_eq!(metric.context().name(), "survivor");
4970    }
4971}