1use std::{
10 collections::VecDeque,
11 future::Future,
12 num::NonZeroUsize,
13 path::PathBuf,
14 pin::Pin,
15 sync::{Arc, LazyLock},
16 time::{Duration, SystemTime, UNIX_EPOCH},
17};
18
19use async_trait::async_trait;
20use bytes::{Buf, BufMut};
21use bytesize::ByteSize;
22use resource_accounting::{MemoryBounds, MemoryBoundsBuilder, UsageExpr};
23use saluki_common::{
24 sync::shutdown::{ShutdownCoordinator, ShutdownHandle},
25 task::spawn_traced_named,
26};
27use saluki_config::{deserialize_space_separated_or_seq, GenericConfiguration};
28use saluki_context::{
29 origin::RawOrigin,
30 tags::{RawTags, RawTagsFilter},
31 TagsResolver,
32};
33use saluki_core::data_model::event::{
34 eventd::EventD,
35 metric::{Metric, MetricMetadata, MetricOrigin},
36 service_check::ServiceCheck,
37 Event, EventType,
38};
39use saluki_core::{
40 components::{sources::*, ComponentContext},
41 pooling::ElasticObjectPool,
42 topology::{interconnect::EventBufferManager, EventsBuffer, OutputDefinition},
43};
44use saluki_env::{workload::CaptureEntityResolver, WorkloadProvider};
45use saluki_error::{generic_error, ErrorContext as _, GenericError};
46use saluki_io::{
47 buf::{BytesBuffer, ClearableIoBuffer as _, FixedSizeVec},
48 deser::{
49 codec::dogstatsd::*,
50 framing::{Framer as _, FramingError, LengthDelimitedFramer},
51 },
52 net::{
53 listener::{Listener, ListenerError},
54 ConnectionAddress, ListenAddress, ProcessCredentials, ProcessIdentity, Stream,
55 },
56};
57use serde::{Deserialize, Deserializer};
58use serde_with::{serde_as, NoneAsEmptyString};
59use snafu::{ResultExt as _, Snafu};
60use stringtheory::MetaString;
61use tokio::{
62 pin, select,
63 time::{interval, MissedTickBehavior},
64};
65use tracing::{debug, error, info, trace, warn};
66
67mod forwarder;
68use self::forwarder::{PacketForwarder, PacketForwarderTarget};
69
70mod framer;
71use self::framer::{get_framer, DsdFramer};
72use crate::sources::dogstatsd::tags::{WellKnownTags, WellKnownTagsFilterPredicate};
73
74mod filters;
75use self::filters::EnablePayloadsFilter;
76
77mod io_buffer;
78use self::io_buffer::IoBufferManager;
79
80mod metrics;
81use self::metrics::{build_metrics, Metrics};
82
83mod replay;
84use self::replay::{CaptureRecord, CapturedTaggerHandle, TrafficCapture};
85pub use self::replay::{
86 DogStatsDCaptureAPIHandler, DogStatsDCaptureControl, DogStatsDReplayAPIHandler, DogStatsDReplayControl,
87 ReplaySession, TimestampResolution, TrafficCaptureReader, DEFAULT_REPLAY_LOOPS, REPLAY_CREDENTIALS_GID,
88};
89
90mod origin;
91use self::origin::{
92 mark_replay_process_id, origin_from_event_packet, origin_from_metric_packet, origin_from_service_check_packet,
93 DogStatsDOriginTagResolver, OriginEnrichmentConfiguration,
94};
95
96mod resolver;
97use self::resolver::ContextResolvers;
98
99mod tags;
100
101#[derive(Debug, Snafu)]
102#[snafu(context(suffix(false)))]
103enum Error {
104 #[snafu(display("Failed to create {} listener: {}", listener_type, source))]
105 FailedToCreateListener {
106 listener_type: &'static str,
107 source: ListenerError,
108 },
109
110 #[snafu(display("No listeners configured. Please specify a port (`dogstatsd_port`) or a socket path (`dogstatsd_socket` or `dogstatsd_stream_socket`) to enable a listener."))]
111 NoListenersConfigured,
112
113 #[snafu(display("Could not resolve bind_host '{}': {}", host, source))]
114 UnresolvableBindHost { host: String, source: std::io::Error },
115
116 #[snafu(display("bind_host '{}' resolved to zero IP addresses.", host))]
117 BindHostHasNoAddresses { host: String },
118}
119
120const INTERNER_BASELINE_BYTES_PER_ENTRY: u64 = 512;
125
126const fn default_buffer_size() -> usize {
127 8192
128}
129
130const fn default_buffer_count() -> usize {
131 128
132}
133
134const fn default_buffer_count_max() -> usize {
135 256
136}
137
138const fn default_port() -> u16 {
139 8125
140}
141
142const fn default_tcp_port() -> u16 {
143 0
144}
145
146const fn default_statsd_forward_port() -> u16 {
147 0
148}
149
150const fn default_socket_receive_buffer_size() -> usize {
151 0
152}
153
154const fn default_allow_context_heap_allocations() -> bool {
155 true
156}
157
158const fn default_no_aggregation_pipeline_support() -> bool {
159 true
160}
161
162const fn default_context_string_interner_entry_count() -> u64 {
163 4096
164}
165
166const fn default_cached_contexts_limit() -> usize {
167 500_000
168}
169
170const fn default_cached_tagsets_limit() -> usize {
171 500_000
172}
173
174const fn default_context_expiry_seconds() -> u64 {
175 20
176}
177
178const fn default_dogstatsd_permissive_decoding() -> bool {
179 true
180}
181
182const fn default_dogstatsd_minimum_sample_rate() -> f64 {
183 0.000000003845
184}
185
186const fn default_true() -> bool {
187 true
188}
189
190const fn default_windows_pipe_security_descriptor() -> &'static str {
192 "D:AI(A;;GA;;;WD)"
193}
194
195fn default_windows_pipe_security_descriptor_string() -> String {
196 default_windows_pipe_security_descriptor().to_string()
197}
198
199#[derive(Deserialize)]
201#[cfg_attr(test, derive(PartialEq, serde::Serialize))]
202pub struct EnablePayloadsConfiguration {
203 #[serde(default = "default_true")]
207 pub series: bool,
208
209 #[serde(default = "default_true")]
213 pub sketches: bool,
214
215 #[serde(default = "default_true")]
219 pub events: bool,
220
221 #[serde(default = "default_true")]
225 pub service_checks: bool,
226}
227
228impl Default for EnablePayloadsConfiguration {
229 fn default() -> Self {
230 Self {
231 series: true,
232 sketches: true,
233 events: true,
234 service_checks: true,
235 }
236 }
237}
238
239const MIN_CAPTURE_DEPTH: usize = 1024;
240
241const fn default_capture_depth() -> usize {
242 MIN_CAPTURE_DEPTH
243}
244
245const DOGSTATSD_CAPTURE_DIR: &str = "dsd_capture";
246
247fn deserialize_empty_metastring_as_none<'de, D>(deserializer: D) -> Result<Option<MetaString>, D::Error>
248where
249 D: Deserializer<'de>,
250{
251 let value = Option::<MetaString>::deserialize(deserializer)?;
252 Ok(value.filter(|host| !host.is_empty()))
253}
254
255#[derive(Deserialize, Default)]
256#[cfg_attr(test, derive(PartialEq, serde::Serialize))]
257struct DogStatsDTelemetryConfiguration {
258 #[serde(default)]
265 dogstatsd_origin: bool,
266}
267
268#[serde_as]
272#[derive(Deserialize, Default)]
273#[cfg_attr(test, derive(derive_where::DeriveWhere, serde::Serialize))]
274#[cfg_attr(test, derive_where(PartialEq))]
275pub struct DogStatsDConfiguration {
276 #[serde(rename = "dogstatsd_buffer_size", default = "default_buffer_size")]
282 buffer_size: usize,
283
284 #[serde(rename = "dogstatsd_buffer_count", default = "default_buffer_count")]
292 buffer_count: usize,
293
294 #[serde(rename = "dogstatsd_buffer_count_max", default = "default_buffer_count_max")]
306 buffer_count_max: usize,
307
308 #[serde(rename = "dogstatsd_port", default = "default_port")]
314 port: u16,
315
316 #[serde(rename = "dogstatsd_so_rcvbuf", default = "default_socket_receive_buffer_size")]
322 socket_receive_buffer_size: usize,
323
324 #[serde(rename = "dogstatsd_tcp_port", default = "default_tcp_port")]
330 tcp_port: u16,
331
332 #[serde(
339 rename = "statsd_forward_host",
340 default,
341 deserialize_with = "deserialize_empty_metastring_as_none"
342 )]
343 statsd_forward_host: Option<MetaString>,
344
345 #[serde(rename = "statsd_forward_port", default = "default_statsd_forward_port")]
351 statsd_forward_port: u16,
352
353 #[serde(rename = "dogstatsd_socket", default)]
359 #[serde_as(as = "NoneAsEmptyString")]
360 socket_path: Option<String>,
361
362 #[serde(rename = "dogstatsd_stream_socket", default)]
368 #[serde_as(as = "NoneAsEmptyString")]
369 socket_stream_path: Option<String>,
370
371 #[serde(rename = "dogstatsd_stream_log_too_big", default)]
380 stream_log_too_big: bool,
381
382 #[serde(rename = "dogstatsd_pipe_name", default)]
389 #[serde_as(as = "NoneAsEmptyString")]
390 pipe_name: Option<String>,
391
392 #[serde(
398 rename = "dogstatsd_windows_pipe_security_descriptor",
399 default = "default_windows_pipe_security_descriptor_string"
400 )]
401 windows_pipe_security_descriptor: String,
402
403 #[serde(rename = "dogstatsd_disable_verbose_logs", default)]
411 disable_verbose_logs: bool,
412
413 #[serde(
421 rename = "dogstatsd_eol_required",
422 default,
423 deserialize_with = "deserialize_space_separated_or_seq"
424 )]
425 eol_required: Vec<String>,
426
427 #[serde(rename = "bind_host", default)]
435 #[serde_as(as = "NoneAsEmptyString")]
436 bind_host: Option<String>,
437
438 #[serde(rename = "dogstatsd_non_local_traffic", default)]
445 non_local_traffic: bool,
446
447 #[serde(rename = "dogstatsd_autoscale_udp_listeners", default)]
462 autoscale_udp_listeners: bool,
463
464 #[serde(
474 rename = "dogstatsd_allow_context_heap_allocs",
475 default = "default_allow_context_heap_allocations"
476 )]
477 allow_context_heap_allocations: bool,
478
479 #[serde(
487 rename = "dogstatsd_no_aggregation_pipeline",
488 default = "default_no_aggregation_pipeline_support"
489 )]
490 no_aggregation_pipeline_support: bool,
491
492 #[serde(
500 rename = "dogstatsd_string_interner_size",
501 default = "default_context_string_interner_entry_count"
502 )]
503 context_string_interner_entry_count: u64,
504
505 #[serde(rename = "dogstatsd_string_interner_size_bytes", default)]
511 context_string_interner_size_bytes: Option<ByteSize>,
512
513 #[serde(
521 rename = "dogstatsd_cached_contexts_limit",
522 default = "default_cached_contexts_limit"
523 )]
524 cached_contexts_limit: usize,
525
526 #[serde(rename = "dogstatsd_cached_tagsets_limit", default = "default_cached_tagsets_limit")]
534 cached_tagsets_limit: usize,
535
536 #[serde(
542 rename = "dogstatsd_context_expiry_seconds",
543 default = "default_context_expiry_seconds"
544 )]
545 context_expiry_seconds: u64,
546
547 #[serde(
554 rename = "dogstatsd_permissive_decoding",
555 default = "default_dogstatsd_permissive_decoding"
556 )]
557 permissive_decoding: bool,
558
559 #[serde(
569 rename = "dogstatsd_minimum_sample_rate",
570 default = "default_dogstatsd_minimum_sample_rate"
571 )]
572 minimum_sample_rate: f64,
573
574 #[serde(rename = "enable_payloads", default)]
576 enable_payloads: EnablePayloadsConfiguration,
577
578 #[serde(flatten, default)]
580 origin_enrichment: OriginEnrichmentConfiguration,
581
582 #[serde(default)]
584 telemetry: DogStatsDTelemetryConfiguration,
585
586 #[serde(skip)]
588 #[cfg_attr(test, derive_where(skip))]
589 workload_provider: Option<Arc<dyn WorkloadProvider + Send + Sync>>,
590
591 #[serde(skip, default)]
593 #[cfg_attr(test, derive_where(skip))]
594 capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
595
596 #[serde(rename = "dogstatsd_tags", default)]
598 additional_tags: Vec<String>,
599
600 #[serde(rename = "dogstatsd_capture_path", default)]
608 capture_path: PathBuf,
609
610 #[serde(rename = "dogstatsd_capture_depth", default = "default_capture_depth")]
618 capture_depth: usize,
619
620 #[serde(skip, default)]
621 #[cfg_attr(test, derive_where(skip))]
622 capture_control: DogStatsDCaptureControl,
623
624 #[serde(skip, default)]
625 #[cfg_attr(test, derive_where(skip))]
626 replay_control: DogStatsDReplayControl,
627
628 #[serde(default)]
635 provider_kind: String,
636}
637
638#[derive(Clone, Copy, Default)]
639struct EolRequired {
640 udp: bool,
641 uds: bool,
642 named_pipe: bool,
643}
644
645impl EolRequired {
646 fn from_config_values(values: &[String]) -> Self {
647 let mut eol_required = Self::default();
648
649 for value in values {
650 match value.as_str() {
651 "udp" => eol_required.udp = true,
652 "uds" => eol_required.uds = true,
653 "named_pipe" => eol_required.named_pipe = true,
654 _ => warn!(
655 value,
656 "Invalid dogstatsd_eol_required value. Expected 'udp', 'uds', or 'named_pipe'."
657 ),
658 }
659 }
660
661 eol_required
662 }
663
664 fn for_listener(&self, listen_addr: &ListenAddress) -> bool {
665 match listen_addr {
666 ListenAddress::Udp(_) => self.udp,
667 ListenAddress::Tcp(_) => false,
668 ListenAddress::Unixgram(_) | ListenAddress::Unix(_) => self.uds,
669 ListenAddress::NamedPipe { .. } => self.named_pipe,
670 }
671 }
672}
673
674async fn resolve_bind_host(host: &str) -> Result<std::net::IpAddr, Error> {
680 let mut addrs = tokio::net::lookup_host((host, 0u16))
681 .await
682 .context(UnresolvableBindHost { host: host.to_string() })?;
683 addrs
684 .next()
685 .map(|sa| sa.ip())
686 .ok_or_else(|| Error::BindHostHasNoAddresses { host: host.to_string() })
687}
688
689impl DogStatsDConfiguration {
690 pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
692 let mut dogstatsd_config: Self = config.as_typed()?;
693 dogstatsd_config.fix_empty_capture_path(config);
694 dogstatsd_config.fix_capture_depth();
695 Ok(dogstatsd_config)
696 }
697
698 fn additional_tags(&self) -> Vec<String> {
700 if self.provider_kind.is_empty() {
701 return self.additional_tags.clone();
702 }
703
704 let mut tags = self.additional_tags.clone();
705 tags.push(format!("provider_kind:{}", self.provider_kind.clone()));
706 tags
707 }
708
709 fn fix_capture_depth(&mut self) {
710 self.capture_depth = self.capture_depth.max(MIN_CAPTURE_DEPTH);
711 }
712
713 fn effective_context_string_interner_bytes(&self) -> ByteSize {
719 match self.context_string_interner_size_bytes {
720 Some(explicit_bytes) => explicit_bytes,
721 None => {
722 saluki_antithesis::always_le!(
723 self.context_string_interner_entry_count,
724 u64::MAX / INTERNER_BASELINE_BYTES_PER_ENTRY,
725 "dogstatsd interner byte-size multiply does not overflow",
726 { "entry_count": self.context_string_interner_entry_count }
727 );
728 ByteSize::b(
729 self.context_string_interner_entry_count
730 .saturating_mul(INTERNER_BASELINE_BYTES_PER_ENTRY),
731 )
732 }
733 }
734 }
735
736 fn eol_required(&self) -> EolRequired {
737 EolRequired::from_config_values(&self.eol_required)
738 }
739
740 fn statsd_forward_target(&self) -> Option<(&MetaString, u16)> {
741 let host = self.statsd_forward_host.as_ref()?;
742 if self.statsd_forward_port == 0 {
743 return None;
744 }
745
746 Some((host, self.statsd_forward_port))
747 }
748
749 fn packet_forwarder_target(&self) -> Option<PacketForwarderTarget> {
750 let (host, port) = self.statsd_forward_target()?;
751 Some(PacketForwarderTarget::new(host.clone(), port))
752 }
753
754 fn udp_streams_to_yield(&self) -> Option<NonZeroUsize> {
760 if !self.autoscale_udp_listeners {
761 return None;
762 }
763
764 #[cfg(not(target_os = "linux"))]
765 if self.autoscale_udp_listeners {
766 warn!("UDP stream handler autoscaling not supported on non-Linux platforms. Default to single stream handler.");
767 return None;
768 }
769
770 let vcpus = std::thread::available_parallelism().map(NonZeroUsize::get).unwrap_or(1);
771 let streams = (1 + vcpus / 8).min(4);
772 NonZeroUsize::new(streams)
773 }
774
775 fn effective_max_buffer_count(&self) -> usize {
781 self.buffer_count_max.max(self.buffer_count)
782 }
783
784 pub fn with_workload_provider<W>(mut self, workload_provider: W) -> Self
790 where
791 W: WorkloadProvider + Send + Sync + 'static,
792 {
793 self.workload_provider = Some(Arc::new(workload_provider));
794 self
795 }
796
797 pub fn with_capture_entity_resolver<R>(mut self, capture_entity_resolver: R) -> Self
804 where
805 R: CaptureEntityResolver + Send + Sync + 'static,
806 {
807 self.capture_entity_resolver = Some(Arc::new(capture_entity_resolver));
808 self
809 }
810
811 pub fn capture_control(&self) -> DogStatsDCaptureControl {
813 self.capture_control.clone()
814 }
815
816 pub fn capture_api_handler(&self) -> DogStatsDCaptureAPIHandler {
818 DogStatsDCaptureAPIHandler::new(self.capture_control.clone())
819 }
820
821 pub fn replay_control(&self) -> DogStatsDReplayControl {
823 self.replay_control.clone()
824 }
825
826 pub fn replay_api_handler(&self) -> DogStatsDReplayAPIHandler {
828 DogStatsDReplayAPIHandler::new(self.replay_control.clone())
829 }
830
831 fn fix_empty_capture_path(&mut self, config: &GenericConfiguration) {
832 if self.capture_path.parent().is_some() {
833 return;
834 }
835
836 let capture_path = match config.try_get_typed::<PathBuf>("run_path") {
837 Ok(Some(mut run_path)) => {
838 run_path.push(DOGSTATSD_CAPTURE_DIR);
839 run_path
840 }
841 Ok(None) => {
842 debug!(
843 "`dogstatsd_capture_path` and `run_path` were empty. Default DogStatsD capture path is unavailable."
844 );
845 return;
846 }
847 Err(e) => {
848 debug!(
849 error = %e,
850 "Failed to read `run_path` from configuration. Default DogStatsD capture path is unavailable."
851 );
852 return;
853 }
854 };
855
856 self.capture_path = capture_path;
857 }
858
859 fn build_addresses(&self, bind_host: Option<std::net::IpAddr>) -> Vec<ListenAddress> {
869 let bind_ip: std::net::IpAddr = if self.non_local_traffic {
870 [0, 0, 0, 0].into()
871 } else {
872 bind_host.unwrap_or_else(|| [127, 0, 0, 1].into())
873 };
874
875 let mut addresses: Vec<ListenAddress> = Vec::new();
876
877 if self.port != 0 {
878 addresses.push(ListenAddress::Udp(std::net::SocketAddr::new(bind_ip, self.port)));
879 }
880
881 if self.tcp_port != 0 {
882 addresses.push(ListenAddress::Tcp(std::net::SocketAddr::new(bind_ip, self.tcp_port)));
883 }
884
885 if let Some(socket_path) = &self.socket_path {
886 addresses.push(ListenAddress::Unixgram(socket_path.into()));
887 }
888
889 if let Some(socket_stream_path) = &self.socket_stream_path {
890 addresses.push(ListenAddress::Unix(socket_stream_path.into()));
891 }
892
893 if let Some(pipe_name) = &self.pipe_name {
894 addresses.push(ListenAddress::named_pipe_with_input_buffer_size(
895 pipe_name,
896 &self.windows_pipe_security_descriptor,
897 self.buffer_size as u32,
898 ));
899 }
900
901 addresses
902 }
903
904 fn uds_origin_detection_unsupported_on_platform(&self, addresses: &[ListenAddress]) -> bool {
905 self.origin_enrichment.enabled()
906 && cfg!(not(target_os = "linux"))
907 && addresses
908 .iter()
909 .any(|address| matches!(address, ListenAddress::Unixgram(_) | ListenAddress::Unix(_)))
910 }
911
912 fn warn_if_uds_origin_detection_unsupported(&self, addresses: &[ListenAddress]) {
913 if self.uds_origin_detection_unsupported_on_platform(addresses) {
914 warn!(
915 "DogStatsD UDS origin detection is enabled, but PID-based Unix socket credentials are unsupported on \
916 this platform. Metrics are accepted without PID-based origin enrichment."
917 );
918 }
919 }
920
921 async fn build_listeners(&self) -> Result<Vec<Listener>, Error> {
923 let bind_host: Option<std::net::IpAddr> = if self.non_local_traffic {
927 None
928 } else {
929 match &self.bind_host {
930 Some(host) => Some(resolve_bind_host(host).await?),
931 None => None,
932 }
933 };
934
935 let addresses = self.build_addresses(bind_host);
936 self.warn_if_uds_origin_detection_unsupported(&addresses);
937 let mut listeners = Vec::new();
938 let socket_receive_buffer_size =
939 (self.socket_receive_buffer_size != 0).then_some(self.socket_receive_buffer_size);
940 let udp_streams_to_yield = self.udp_streams_to_yield();
941 for address in addresses {
942 let listener_type = address.listener_type();
943 let listener_streams = matches!(address, ListenAddress::Udp(_))
944 .then_some(udp_streams_to_yield)
945 .flatten();
946 let listener = Listener::from_listen_address(address, listener_streams)
947 .await
948 .context(FailedToCreateListener { listener_type })?
949 .with_receive_buffer_size(socket_receive_buffer_size);
950
951 listeners.push(listener);
952 }
953 Ok(listeners)
954 }
955}
956
957#[async_trait]
958impl SourceBuilder for DogStatsDConfiguration {
959 async fn build(&self, context: ComponentContext) -> Result<Box<dyn Source + Send>, GenericError> {
960 let listeners = self.build_listeners().await?;
961 if listeners.is_empty() {
962 return Err(Error::NoListenersConfigured.into());
963 }
964
965 let min_buffers: usize = listeners.iter().map(Listener::min_buffer_reservation).sum();
969 let max_buffers = self.effective_max_buffer_count();
970 if max_buffers < min_buffers {
971 return Err(generic_error!(
972 "The maximum I/O buffer count ({}) must be at least {} to service all configured listeners.",
973 max_buffers,
974 min_buffers,
975 ));
976 }
977
978 let origin_detection_enabled = self.origin_enrichment.enabled();
979 let captured_tagger = CapturedTaggerHandle::new();
982
983 let maybe_origin_tags_resolver = self.workload_provider.clone().map(|provider| {
984 DogStatsDOriginTagResolver::new(self.origin_enrichment.clone(), provider, captured_tagger.clone())
985 });
986 let context_resolvers = ContextResolvers::new(self, &context, maybe_origin_tags_resolver)
987 .error_context("Failed to create context resolvers.")?;
988
989 let codec_config = DogStatsDCodecConfiguration::default()
990 .with_timestamps(self.no_aggregation_pipeline_support)
991 .with_permissive_mode(self.permissive_decoding)
992 .with_minimum_sample_rate(self.minimum_sample_rate)
993 .with_client_origin_detection(self.origin_enrichment.origin_detection_client);
994
995 let codec = DogStatsDCodec::from_configuration(codec_config);
996 let eol_required = self.eol_required();
997
998 let enable_payloads_filter = EnablePayloadsFilter::default()
999 .with_allow_series(self.enable_payloads.series)
1000 .with_allow_sketches(self.enable_payloads.sketches)
1001 .with_allow_events(self.enable_payloads.events)
1002 .with_allow_service_checks(self.enable_payloads.service_checks);
1003 let traffic_capture = TrafficCapture::with_workload_provider(
1004 self.capture_path.clone(),
1005 self.capture_depth.max(MIN_CAPTURE_DEPTH),
1006 self.workload_provider.clone(),
1007 );
1008 self.capture_control.bind(traffic_capture.clone());
1009 let packet_forwarder_target = self.packet_forwarder_target();
1010
1011 self.replay_control.bind(captured_tagger);
1012
1013 let (io_buffer_pool, io_buffer_pool_shrinker) =
1017 build_io_buffer_pool(self.buffer_count, max_buffers, self.buffer_size);
1018
1019 Ok(Box::new(DogStatsD {
1020 listeners,
1021 io_buffer_pool,
1022 io_buffer_pool_shrinker: Box::pin(io_buffer_pool_shrinker),
1023 codec,
1024 context_resolvers,
1025 enabled_filter: enable_payloads_filter,
1026 origin_detection_enabled,
1027 origin_telemetry_enabled: self.telemetry.dogstatsd_origin,
1028 stream_log_too_big: self.stream_log_too_big,
1029 disable_verbose_logs: self.disable_verbose_logs,
1030 eol_required,
1031 additional_tags: self.additional_tags().into(),
1032 capture_entity_resolver: self.capture_entity_resolver.clone(),
1033 traffic_capture,
1034 packet_forwarder_target,
1035 }))
1036 }
1037
1038 fn outputs(&self) -> &[OutputDefinition<EventType>] {
1039 static OUTPUTS: LazyLock<Vec<OutputDefinition<EventType>>> = LazyLock::new(|| {
1040 vec![
1041 OutputDefinition::named_output("metrics", EventType::Metric),
1042 OutputDefinition::named_output("events", EventType::EventD),
1043 OutputDefinition::named_output("service_checks", EventType::ServiceCheck),
1044 ]
1045 });
1046 &OUTPUTS
1047 }
1048}
1049
1050impl MemoryBounds for DogStatsDConfiguration {
1051 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
1052 let additional_buffers = self.effective_max_buffer_count().saturating_sub(self.buffer_count);
1053 let adjusted_buffer_size = get_adjusted_buffer_size(self.buffer_size);
1054
1055 builder
1056 .minimum()
1057 .with_single_value::<DogStatsD>("source struct")
1059 .with_expr(UsageExpr::product(
1061 "buffers",
1062 UsageExpr::config("dogstatsd_buffer_count", self.buffer_count),
1063 UsageExpr::config("dogstatsd_buffer_size", adjusted_buffer_size),
1064 ))
1065 .with_expr(UsageExpr::config(
1068 "dogstatsd_string_interner_size_bytes",
1069 self.effective_context_string_interner_bytes().as_u64() as usize,
1070 ));
1071
1072 builder.firm().with_expr(UsageExpr::product(
1074 "elastic buffers",
1075 UsageExpr::constant("dogstatsd_buffer_count_max_extra", additional_buffers),
1076 UsageExpr::config("dogstatsd_buffer_size", adjusted_buffer_size),
1077 ));
1078 }
1079}
1080
1081pub struct DogStatsD {
1083 listeners: Vec<Listener>,
1084 io_buffer_pool: ElasticObjectPool<BytesBuffer>,
1085 io_buffer_pool_shrinker: Pin<Box<dyn Future<Output = ()> + Send>>,
1086 codec: DogStatsDCodec,
1087 context_resolvers: ContextResolvers,
1088 enabled_filter: EnablePayloadsFilter,
1089 origin_detection_enabled: bool,
1090 origin_telemetry_enabled: bool,
1091 stream_log_too_big: bool,
1092 disable_verbose_logs: bool,
1093 eol_required: EolRequired,
1094 additional_tags: Arc<[String]>,
1095 capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
1096 traffic_capture: TrafficCapture,
1097 packet_forwarder_target: Option<PacketForwarderTarget>,
1098}
1099
1100struct ListenerContext {
1101 shutdown_handle: ShutdownHandle,
1102 listener: Listener,
1103 io_buffer_pool: ElasticObjectPool<BytesBuffer>,
1104 codec: DogStatsDCodec,
1105 context_resolvers: ContextResolvers,
1106 origin_detection_enabled: bool,
1107 origin_telemetry_enabled: bool,
1108 stream_log_too_big: bool,
1109 disable_verbose_logs: bool,
1110 eol_required: EolRequired,
1111 additional_tags: Arc<[String]>,
1112 capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
1113 traffic_capture: TrafficCapture,
1114 packet_forwarder_target: Option<PacketForwarderTarget>,
1115}
1116
1117struct HandlerContext {
1118 listen_addr: ListenAddress,
1119 framer: DsdFramer,
1120 codec: DogStatsDCodec,
1121 io_buffer_pool: ElasticObjectPool<BytesBuffer>,
1122 metrics: Metrics,
1123 context_resolvers: ContextResolvers,
1124 origin_detection_enabled: bool,
1125 stream_log_too_big: bool,
1126 disable_verbose_logs: bool,
1127 additional_tags: Arc<[String]>,
1128 capture_entity_resolver: Option<Arc<dyn CaptureEntityResolver + Send + Sync>>,
1129 traffic_capture: TrafficCapture,
1130 packet_forwarder: Option<PacketForwarder>,
1131}
1132
1133#[async_trait]
1134impl Source for DogStatsD {
1135 async fn run(mut self: Box<Self>, mut context: SourceContext) -> Result<(), GenericError> {
1136 let global_shutdown = context.take_shutdown_handle();
1137 pin!(global_shutdown);
1138
1139 let mut health = context.take_health_handle();
1140
1141 let mut listener_shutdown_coordinator = ShutdownCoordinator::default();
1142 spawn_traced_named(
1143 "dogstatsd-io-buffer-pool-shrinker",
1144 process_io_buffer_pool_shrinker(self.io_buffer_pool_shrinker, listener_shutdown_coordinator.register()),
1145 );
1146
1147 for listener in self.listeners {
1149 let task_name = format!("dogstatsd-listener-{}", listener.listen_address().listener_type());
1150
1151 let listener_context = ListenerContext {
1158 shutdown_handle: listener_shutdown_coordinator.register(),
1159 listener,
1160 io_buffer_pool: self.io_buffer_pool.clone(),
1161 codec: self.codec.clone(),
1162 context_resolvers: self.context_resolvers.clone(),
1163 origin_detection_enabled: self.origin_detection_enabled,
1164 origin_telemetry_enabled: self.origin_telemetry_enabled,
1165 stream_log_too_big: self.stream_log_too_big,
1166 disable_verbose_logs: self.disable_verbose_logs,
1167 eol_required: self.eol_required,
1168 additional_tags: self.additional_tags.clone(),
1169 capture_entity_resolver: self.capture_entity_resolver.clone(),
1170 traffic_capture: self.traffic_capture.clone(),
1171 packet_forwarder_target: self.packet_forwarder_target.clone(),
1172 };
1173
1174 spawn_traced_named(
1175 task_name,
1176 process_listener(context.clone(), listener_context, self.enabled_filter),
1177 );
1178 }
1179
1180 health.mark_ready();
1181 debug!("DogStatsD source started.");
1182
1183 loop {
1188 select! {
1189 _ = &mut global_shutdown => {
1190 debug!("Received shutdown signal.");
1191 break
1192 },
1193 _ = health.live() => continue,
1194 }
1195 }
1196
1197 debug!("Stopping DogStatsD source...");
1198
1199 listener_shutdown_coordinator.shutdown_and_wait().await;
1200
1201 debug!("DogStatsD source stopped.");
1202
1203 Ok(())
1204 }
1205}
1206
1207async fn process_io_buffer_pool_shrinker(
1208 io_buffer_pool_shrinker: Pin<Box<dyn Future<Output = ()> + Send>>, shutdown_handle: ShutdownHandle,
1209) {
1210 pin!(shutdown_handle);
1211
1212 select! {
1213 _ = &mut shutdown_handle => {
1214 debug!("I/O buffer pool shrinker received shutdown signal.");
1215 },
1216 _ = io_buffer_pool_shrinker => {
1217 debug!("I/O buffer pool shrinker stopped.");
1218 },
1219 }
1220}
1221
1222fn build_io_buffer_pool(
1223 min_buffers: usize, max_buffers: usize, buffer_size: usize,
1224) -> (ElasticObjectPool<BytesBuffer>, impl Future<Output = ()> + Send) {
1225 saluki_antithesis::always_le!(
1226 buffer_size,
1227 usize::MAX - 4,
1228 "dogstatsd buffer size add does not overflow",
1229 { "buffer_size": buffer_size }
1230 );
1231 let adjusted_buffer_size = get_adjusted_buffer_size(buffer_size);
1232 ElasticObjectPool::with_builder("dsd_packet_bufs", min_buffers, max_buffers, move || {
1233 FixedSizeVec::with_capacity(adjusted_buffer_size)
1234 })
1235}
1236
1237async fn process_listener(
1238 source_context: SourceContext, listener_context: ListenerContext, enabled_filter: EnablePayloadsFilter,
1239) {
1240 let ListenerContext {
1241 shutdown_handle,
1242 mut listener,
1243 io_buffer_pool,
1244 codec,
1245 context_resolvers,
1246 origin_detection_enabled,
1247 origin_telemetry_enabled,
1248 stream_log_too_big,
1249 disable_verbose_logs,
1250 eol_required,
1251 additional_tags,
1252 capture_entity_resolver,
1253 traffic_capture,
1254 packet_forwarder_target,
1255 } = listener_context;
1256
1257 pin!(shutdown_handle);
1258
1259 let listen_addr = listener.listen_address().clone();
1260 let metrics = build_metrics(
1261 &listen_addr,
1262 source_context.component_context(),
1263 origin_telemetry_enabled,
1264 );
1265 let packet_forwarder = packet_forwarder_target
1266 .as_ref()
1267 .map(|target| target.to_forwarder(metrics.clone()));
1268 if let Some(packet_forwarder) = &packet_forwarder {
1269 packet_forwarder.spawn_connect();
1270 }
1271
1272 let mut stream_shutdown_coordinator = ShutdownCoordinator::default();
1273
1274 info!(%listen_addr, "DogStatsD listener started.");
1275
1276 loop {
1277 select! {
1278 _ = &mut shutdown_handle => {
1279 debug!(%listen_addr, "Received shutdown signal. Waiting for existing stream handlers to finish...");
1280 break;
1281 }
1282 result = listener.accept() => match result {
1283 Ok(stream) => {
1284 debug!(%listen_addr, "Spawning new stream handler.");
1285
1286 let handler_context = HandlerContext {
1287 listen_addr: listen_addr.clone(),
1288 framer: get_framer(&listen_addr, eol_required.for_listener(&listen_addr)),
1289 codec: codec.clone(),
1290 io_buffer_pool: io_buffer_pool.clone(),
1291 metrics: metrics.clone(),
1292 context_resolvers: context_resolvers.clone(),
1293 origin_detection_enabled,
1294 stream_log_too_big,
1295 disable_verbose_logs,
1296 additional_tags: additional_tags.clone(),
1297 capture_entity_resolver: capture_entity_resolver.clone(),
1298 traffic_capture: traffic_capture.clone(),
1299 packet_forwarder: packet_forwarder.clone(),
1300 };
1301
1302 let task_name = format!(
1303 "dogstatsd-stream-handler-{}",
1304 listen_addr.listener_type(),
1305 );
1306 spawn_traced_named(task_name, process_stream(stream, source_context.clone(), handler_context, stream_shutdown_coordinator.register(), enabled_filter));
1307 }
1308 Err(e) => {
1309 error!(%listen_addr, error = %e, "Failed to accept connection. Stopping listener.");
1310 break
1311 }
1312 }
1313 }
1314 }
1315
1316 stream_shutdown_coordinator.shutdown_and_wait().await;
1317
1318 info!(%listen_addr, "DogStatsD listener stopped.");
1319}
1320
1321async fn process_stream(
1322 stream: Stream, source_context: SourceContext, handler_context: HandlerContext, shutdown_handle: ShutdownHandle,
1323 enabled_filter: EnablePayloadsFilter,
1324) {
1325 select! {
1326 _ = shutdown_handle => {
1327 debug!("Stream handler received shutdown signal.");
1328 },
1329 _ = drive_stream(stream, source_context, handler_context, enabled_filter) => {},
1330 }
1331}
1332
1333fn origin_detection_failed_for_telemetry(
1334 origin_detection_enabled: bool, bytes_read: usize, peer_addr: &ConnectionAddress,
1335) -> bool {
1336 origin_detection_enabled && bytes_read > 0 && peer_addr.has_process_credential_telemetry_error()
1337}
1338
1339async fn drive_stream(
1340 mut stream: Stream, source_context: SourceContext, handler_context: HandlerContext,
1341 enabled_filter: EnablePayloadsFilter,
1342) {
1343 let HandlerContext {
1344 listen_addr,
1345 mut framer,
1346 codec,
1347 io_buffer_pool,
1348 metrics,
1349 mut context_resolvers,
1350 origin_detection_enabled,
1351 stream_log_too_big,
1352 disable_verbose_logs,
1353 additional_tags,
1354 capture_entity_resolver,
1355 traffic_capture,
1356 packet_forwarder,
1357 } = handler_context;
1358
1359 debug!(%listen_addr, "Stream handler started.");
1360
1361 if !stream.is_connectionless() {
1362 metrics.connections_active().increment(1);
1363 }
1364
1365 let mut stream_capture = StreamCaptureState::new();
1366 let mut buffer_flush = interval(Duration::from_millis(100));
1369 buffer_flush.set_missed_tick_behavior(MissedTickBehavior::Delay);
1370
1371 let mut event_buffer_manager = EventBufferManager::default();
1372 let mut io_buffer_manager = IoBufferManager::new(&io_buffer_pool, &stream);
1373 let memory_limiter = source_context.topology_context().memory_limiter();
1374
1375 'read: loop {
1376 let mut eof = false;
1377
1378 let mut io_buffer = io_buffer_manager.get_buffer_mut().await;
1379
1380 memory_limiter.wait_for_capacity().await;
1381
1382 select! {
1383 read_result = stream.receive(&mut io_buffer) => match read_result {
1385 Ok((bytes_read, peer_addr)) => {
1386 if bytes_read == 0 {
1387 eof = true;
1388 }
1389
1390 let is_connectionless = stream.is_connectionless();
1391 let payload = received_payload(io_buffer, bytes_read);
1392
1393 capture_uds_traffic(
1394 &listen_addr,
1395 &traffic_capture,
1396 capture_entity_resolver.as_deref(),
1397 &peer_addr,
1398 payload,
1399 &mut stream_capture,
1400 );
1401
1402 if is_connectionless {
1403 metrics.packet_receive_success().increment(1);
1404 }
1405 metrics.bytes_received().increment(bytes_read as u64);
1406 metrics.bytes_received_size().record(bytes_read as f64);
1407 let origin_detection_failed =
1408 origin_detection_failed_for_telemetry(origin_detection_enabled, bytes_read, &peer_addr);
1409 if origin_detection_failed && is_connectionless {
1410 metrics.origin_detection_errors().increment(1);
1411 }
1412
1413 let reached_eof = eof || is_connectionless;
1419
1420 trace!(
1421 buffer_len = io_buffer.remaining(),
1422 buffer_cap = io_buffer.remaining_mut(),
1423 eof = reached_eof,
1424 %listen_addr,
1425 %peer_addr,
1426 "Received {} bytes from stream.",
1427 bytes_read
1428 );
1429
1430 if should_drop_oversized_named_pipe_frame(&listen_addr, io_buffer) {
1431 metrics.framing_errors().increment(1);
1432 debug!(%listen_addr, %peer_addr, "DogStatsD named pipe frame exceeded the configured buffer size. Dropping frame.");
1433 io_buffer.clear();
1434 continue 'read;
1435 }
1436
1437 'frame: loop {
1438 let frame_result = framer.next_frame(io_buffer, reached_eof);
1439 let completed_outer_frames = framer.take_completed_outer_frames();
1440 if !is_connectionless && completed_outer_frames > 0 {
1441 metrics.packet_receive_success().increment(completed_outer_frames as u64);
1442 }
1443 if origin_detection_failed && completed_outer_frames > 0 {
1444 metrics.origin_detection_errors().increment(completed_outer_frames as u64);
1445 }
1446
1447 match frame_result {
1448 Ok(Some(frame)) => {
1449 if matches!(listen_addr, ListenAddress::NamedPipe { .. }) {
1450 metrics.packet_receive_success().increment(1);
1451 }
1452 trace!(%listen_addr, %peer_addr, ?frame, "Decoded frame.");
1453 if let Some(forwarder) = &packet_forwarder {
1454 forwarder.forward(frame.clone()).await;
1455 }
1456 match handle_frame(
1457 &frame[..],
1458 &codec,
1459 &mut context_resolvers,
1460 &metrics,
1461 capture_entity_resolver.as_deref(),
1462 origin_detection_enabled,
1463 &peer_addr,
1464 enabled_filter,
1465 &additional_tags,
1466 ) {
1467 Ok(Some(event)) => {
1468 if let Some(event_buffer) = event_buffer_manager.try_push(event) {
1469 debug!(%listen_addr, %peer_addr, "Event buffer is full. Forwarding events.");
1470 dispatch_events(event_buffer, &source_context, &listen_addr).await;
1471 }
1472 },
1473 Ok(None) => {
1474 continue
1479 },
1480 Err(e) => {
1481 log_parse_failure(disable_verbose_logs, &listen_addr, &peer_addr, &frame, &e);
1482 },
1483 }
1484 }
1485 Err(e) => {
1486 metrics.framing_errors().increment(1);
1487 if should_warn_stream_log_too_big(&listen_addr, &e, stream_log_too_big) {
1488 warn!(
1489 %listen_addr,
1490 %peer_addr,
1491 error = %e,
1492 "DogStatsD stream frame exceeded the configured buffer size."
1493 );
1494 }
1495
1496 if stream.is_connectionless() {
1497 io_buffer.clear();
1498 debug!(%listen_addr, %peer_addr, error = %e, "Error decoding frame. Continuing stream.");
1501 continue 'read;
1502 } else {
1503 debug!(%listen_addr, %peer_addr, error = %e, "Error decoding frame. Stopping stream.");
1504 break 'read;
1505 }
1506 }
1507 Ok(None) => {
1508 trace!(%listen_addr, %peer_addr, "Not enough data to decode another frame.");
1509 if eof && !stream.is_connectionless() {
1510 debug!(%listen_addr, %peer_addr, "Stream received EOF. Shutting down handler.");
1511 break 'read;
1512 } else {
1513 break 'frame;
1514 }
1515 }
1516 }
1517 }
1518 },
1519 Err(e) => {
1520 metrics.packet_receive_failure().increment(1);
1521
1522 if stream.is_connectionless() {
1523 warn!(%listen_addr, error = %e, "I/O error while decoding. Continuing stream.");
1526 continue 'read;
1527 } else {
1528 warn!(%listen_addr, error = %e, "I/O error while decoding. Stopping stream.");
1529 break 'read;
1530 }
1531 }
1532 },
1533
1534 _ = buffer_flush.tick() => {
1535 if let Some(event_buffer) = event_buffer_manager.consume() {
1536 dispatch_events(event_buffer, &source_context, &listen_addr).await;
1537 }
1538 },
1539
1540 }
1541 }
1542
1543 if let Some(event_buffer) = event_buffer_manager.consume() {
1544 dispatch_events(event_buffer, &source_context, &listen_addr).await;
1545 }
1546
1547 metrics.connections_active().decrement(1);
1548
1549 debug!(%listen_addr, "Stream handler stopped.");
1550}
1551
1552fn should_drop_oversized_named_pipe_frame(listen_addr: &ListenAddress, buffer: &BytesBuffer) -> bool {
1553 matches!(listen_addr, ListenAddress::NamedPipe { .. })
1554 && buffer.remaining_mut() == 0
1555 && memchr::memchr(b'\n', buffer.chunk()).is_none()
1556}
1557
1558fn should_warn_stream_log_too_big(listen_addr: &ListenAddress, error: &FramingError, stream_log_too_big: bool) -> bool {
1559 stream_log_too_big
1560 && matches!(listen_addr, ListenAddress::Unix(_))
1561 && matches!(error, FramingError::InvalidFrame { .. })
1562}
1563
1564fn log_parse_failure(
1565 disable_verbose_logs: bool, listen_addr: &ListenAddress, peer_addr: &ConnectionAddress, frame: &[u8],
1566 error: &ParseError,
1567) {
1568 let frame = String::from_utf8_lossy(frame);
1569 if disable_verbose_logs {
1570 debug!(%listen_addr, %peer_addr, %frame, %error, "Failed to parse frame.");
1571 } else {
1572 warn!(%listen_addr, %peer_addr, %frame, %error, "Failed to parse frame.");
1573 }
1574}
1575
1576fn capture_uds_traffic(
1577 listen_addr: &ListenAddress, traffic_capture: &TrafficCapture,
1578 capture_entity_resolver: Option<&(dyn CaptureEntityResolver + Send + Sync)>, peer_addr: &ConnectionAddress,
1579 payload: &[u8], stream_capture: &mut StreamCaptureState,
1580) {
1581 if payload.is_empty() || !traffic_capture.is_ongoing() {
1582 return;
1583 }
1584
1585 match listen_addr {
1586 ListenAddress::Unixgram(_) => {
1587 let _ = traffic_capture.enqueue(build_capture_record(
1588 capture_entity_resolver,
1589 process_id_from_peer_addr(peer_addr),
1590 payload,
1591 ));
1592 }
1593 ListenAddress::Unix(_) => {
1594 stream_capture.update_peer_metadata(peer_addr);
1595 stream_capture.pending.extend(payload);
1596
1597 while let Ok(Some(outer_payload)) = stream_capture
1598 .outer_framer
1599 .next_frame(&mut stream_capture.pending, false)
1600 {
1601 let _ = traffic_capture.enqueue(build_capture_record(
1602 capture_entity_resolver,
1603 stream_capture.last_pid,
1604 &outer_payload,
1605 ));
1606 }
1607 }
1608 _ => {}
1609 }
1610}
1611
1612struct StreamCaptureState {
1613 outer_framer: LengthDelimitedFramer,
1614 pending: VecDeque<u8>,
1615 last_pid: Option<i32>,
1616}
1617
1618impl StreamCaptureState {
1619 fn new() -> Self {
1620 Self {
1621 outer_framer: LengthDelimitedFramer,
1622 pending: VecDeque::new(),
1623 last_pid: None,
1624 }
1625 }
1626
1627 fn update_peer_metadata(&mut self, peer_addr: &ConnectionAddress) {
1628 if let Some(process_id) = process_id_from_peer_addr(peer_addr) {
1629 self.last_pid = Some(process_id);
1630 }
1631 }
1632}
1633
1634fn build_capture_record(
1635 capture_entity_resolver: Option<&(dyn CaptureEntityResolver + Send + Sync)>, process_id: Option<i32>,
1636 payload: &[u8],
1637) -> CaptureRecord {
1638 CaptureRecord {
1639 timestamp_ns: capture_timestamp_ns(),
1640 payload: payload.to_vec(),
1641 pid: process_id,
1642 ancillary: Vec::new(),
1643 container_id: resolve_capture_container_id(capture_entity_resolver, process_id),
1644 }
1645}
1646
1647fn resolve_capture_container_id(
1648 capture_entity_resolver: Option<&(dyn CaptureEntityResolver + Send + Sync)>, process_id: Option<i32>,
1649) -> Option<String> {
1650 let process_id = u32::try_from(process_id?).ok()?;
1651 capture_entity_resolver
1652 .and_then(|resolver| resolver.resolve_container_entity_for_live_pid(process_id))
1653 .map(|entity_id| entity_id.to_string())
1654}
1655
1656fn process_id_from_peer_addr(peer_addr: &ConnectionAddress) -> Option<i32> {
1657 match peer_addr {
1658 ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(creds)) => Some(creds.pid),
1659 _ => None,
1660 }
1661}
1662
1663fn apply_credentials_to_origin(origin: &mut RawOrigin<'_>, creds: &ProcessCredentials) {
1669 if creds.gid == REPLAY_CREDENTIALS_GID {
1670 origin.set_process_id(mark_replay_process_id(creds.uid));
1671 } else {
1672 origin.set_process_id(creds.pid as u32);
1673 }
1674}
1675
1676fn received_payload(buffer: &BytesBuffer, bytes_read: usize) -> &[u8] {
1677 let chunk = buffer.chunk();
1678 let start = chunk.len().saturating_sub(bytes_read);
1679 &chunk[start..]
1680}
1681
1682fn capture_timestamp_ns() -> i64 {
1683 SystemTime::now()
1684 .duration_since(UNIX_EPOCH)
1685 .map(|duration| duration.as_nanos().min(i64::MAX as u128) as i64)
1686 .unwrap_or_default()
1687}
1688
1689#[allow(clippy::too_many_arguments)]
1690fn handle_frame(
1691 frame: &[u8], codec: &DogStatsDCodec, context_resolvers: &mut ContextResolvers, source_metrics: &Metrics,
1692 capture_entity_resolver: Option<&(dyn CaptureEntityResolver + Send + Sync)>, origin_detection_enabled: bool,
1693 peer_addr: &ConnectionAddress, enabled_filter: EnablePayloadsFilter, additional_tags: &[String],
1694) -> Result<Option<Event>, ParseError> {
1695 let resolve_telemetry_origin = || {
1698 (source_metrics.origin_telemetry_enabled() && origin_detection_enabled)
1699 .then(|| resolve_capture_container_id(capture_entity_resolver, process_id_from_peer_addr(peer_addr)))
1700 .flatten()
1701 };
1702
1703 let parsed = match codec.decode_packet(frame) {
1704 Ok(parsed) => parsed,
1705 Err(e) => {
1706 match parse_message_type(frame) {
1708 MessageType::MetricSample => {
1709 source_metrics.record_metric_parse_failed(resolve_telemetry_origin().as_deref())
1710 }
1711 MessageType::Event => source_metrics.event_decode_failed().increment(1),
1712 MessageType::ServiceCheck => source_metrics.service_check_decode_failed().increment(1),
1713 }
1714
1715 return Err(e);
1716 }
1717 };
1718
1719 let event = match parsed {
1720 ParsedPacket::Metric(metric_packet) => {
1721 if metric_packet.num_points == 0 {
1722 return Ok(None);
1723 }
1724 let events_len = metric_packet.num_points;
1725 if !enabled_filter.allow_metric(&metric_packet) {
1726 trace!(
1727 metric.name = metric_packet.metric_name,
1728 "Skipping metric due to filter configuration."
1729 );
1730 return Ok(None);
1731 }
1732
1733 match handle_metric_packet(metric_packet, context_resolvers, peer_addr, additional_tags) {
1734 Some(metric) => {
1735 source_metrics.record_metrics_received(events_len, resolve_telemetry_origin().as_deref());
1736 Event::Metric(metric)
1737 }
1738 None => {
1739 source_metrics.failed_context_resolve_total().increment(1);
1741 return Ok(None);
1742 }
1743 }
1744 }
1745 ParsedPacket::Event(event) => {
1746 if !enabled_filter.allow_event(&event) {
1747 trace!("Skipping event {} due to filter configuration.", event.title);
1748 return Ok(None);
1749 }
1750 let tags_resolver = context_resolvers.tags();
1751 match handle_event_packet(event, tags_resolver, peer_addr, additional_tags) {
1752 Some(event) => {
1753 source_metrics.events_received().increment(1);
1754 Event::EventD(event)
1755 }
1756 None => {
1757 source_metrics.failed_context_resolve_total().increment(1);
1758 return Ok(None);
1759 }
1760 }
1761 }
1762 ParsedPacket::ServiceCheck(service_check) => {
1763 if !enabled_filter.allow_service_check(&service_check) {
1764 trace!(
1765 "Skipping service check {} due to filter configuration.",
1766 service_check.name
1767 );
1768 return Ok(None);
1769 }
1770 let tags_resolver = context_resolvers.tags();
1771 match handle_service_check_packet(service_check, tags_resolver, peer_addr, additional_tags) {
1772 Some(service_check) => {
1773 source_metrics.service_checks_received().increment(1);
1774 Event::ServiceCheck(service_check)
1775 }
1776 None => {
1777 source_metrics.failed_context_resolve_total().increment(1);
1778 return Ok(None);
1779 }
1780 }
1781 }
1782 };
1783
1784 Ok(Some(event))
1785}
1786
1787fn handle_metric_packet(
1788 packet: MetricPacket, context_resolvers: &mut ContextResolvers, peer_addr: &ConnectionAddress,
1789 additional_tags: &[String],
1790) -> Option<Metric> {
1791 let well_known_tags = WellKnownTags::from_raw_tags(packet.tags.clone());
1792
1793 let mut origin = origin_from_metric_packet(&packet, &well_known_tags);
1794 if let Some(creds) = peer_addr.process_credentials() {
1795 apply_credentials_to_origin(&mut origin, creds);
1796 }
1797
1798 let context_resolver = if packet.timestamp.is_some() {
1800 context_resolvers.no_agg()
1801 } else {
1802 context_resolvers.primary()
1803 };
1804
1805 let tags = get_filtered_tags_iterator(packet.tags, additional_tags);
1806
1807 match context_resolver.resolve(packet.metric_name, tags, Some(origin)) {
1809 Some(context) => {
1810 let metric_origin = well_known_tags
1811 .jmx_check_name
1812 .map(MetricOrigin::jmx_check)
1813 .unwrap_or_else(MetricOrigin::dogstatsd);
1814 let metadata = MetricMetadata::default()
1815 .with_origin(metric_origin)
1816 .with_hostname(well_known_tags.hostname.map(Arc::from))
1817 .with_unit(packet.unit.map_or_else(MetaString::empty, MetaString::from_static));
1818
1819 Some(Metric::from_parts(context, packet.values, metadata))
1820 }
1821 None => None,
1823 }
1824}
1825
1826fn handle_event_packet(
1827 packet: EventPacket, tags_resolver: &mut TagsResolver, peer_addr: &ConnectionAddress, additional_tags: &[String],
1828) -> Option<EventD> {
1829 let well_known_tags = WellKnownTags::from_raw_tags(packet.tags.clone());
1830
1831 let mut origin = origin_from_event_packet(&packet, &well_known_tags);
1832 if let Some(creds) = peer_addr.process_credentials() {
1833 apply_credentials_to_origin(&mut origin, creds);
1834 }
1835 let origin_tags = tags_resolver.resolve_origin_tags(Some(origin));
1836
1837 let tags = get_filtered_tags_iterator(packet.tags, additional_tags);
1838 let tags = tags_resolver.create_tag_set(tags)?;
1839
1840 let timestamp = packet
1844 .timestamp
1845 .or_else(|| SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()));
1846
1847 let eventd = EventD::new(packet.title, packet.text)
1848 .with_timestamp(timestamp)
1849 .with_hostname(packet.hostname.map(|s| s.into()))
1850 .with_aggregation_key(packet.aggregation_key.map(|s| s.into()))
1851 .with_alert_type(packet.alert_type)
1852 .with_priority(packet.priority)
1853 .with_source_type_name(Some(
1858 packet
1859 .source_type_name
1860 .map(|s| s.into())
1861 .unwrap_or_else(|| "api".into()),
1862 ))
1863 .with_alert_type(packet.alert_type)
1864 .with_tags(tags)
1865 .with_origin_tags(origin_tags);
1866
1867 Some(eventd)
1868}
1869
1870fn handle_service_check_packet(
1871 packet: ServiceCheckPacket, tags_resolver: &mut TagsResolver, peer_addr: &ConnectionAddress,
1872 additional_tags: &[String],
1873) -> Option<ServiceCheck> {
1874 let well_known_tags = WellKnownTags::from_raw_tags(packet.tags.clone());
1875
1876 let mut origin = origin_from_service_check_packet(&packet, &well_known_tags);
1877 if let Some(creds) = peer_addr.process_credentials() {
1878 apply_credentials_to_origin(&mut origin, creds);
1879 }
1880 let origin_tags = tags_resolver.resolve_origin_tags(Some(origin));
1881
1882 let tags = get_filtered_tags_iterator(packet.tags, additional_tags);
1883 let tags = tags_resolver.create_tag_set(tags)?;
1884
1885 let timestamp = packet
1889 .timestamp
1890 .or_else(|| SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()));
1891
1892 let service_check = ServiceCheck::new(packet.name, packet.status)
1893 .with_timestamp(timestamp)
1894 .with_hostname(packet.hostname.map(|s| s.into()))
1895 .with_tags(tags)
1896 .with_origin_tags(origin_tags)
1897 .with_message(packet.message.map(|s| s.into()));
1898
1899 Some(service_check)
1900}
1901
1902fn get_filtered_tags_iterator<'a>(
1903 raw_tags: RawTags<'a>, additional_tags: &'a [String],
1904) -> impl Iterator<Item = &'a str> + Clone {
1905 RawTagsFilter::exclude(raw_tags, WellKnownTagsFilterPredicate).chain(additional_tags.iter().map(|s| s.as_str()))
1908}
1909
1910async fn dispatch_events(mut event_buffer: EventsBuffer, source_context: &SourceContext, listen_addr: &ListenAddress) {
1911 debug!(%listen_addr, events_len = event_buffer.len(), "Forwarding events.");
1912
1913 if event_buffer.has_event_type(EventType::EventD) {
1923 let eventd_events = event_buffer.extract(Event::is_eventd);
1924 let events_output = source_context.dispatcher().buffered_named("events");
1925
1926 if events_output.is_err() {
1929 saluki_antithesis::unreachable!("dsd 'events' output missing at dispatch");
1930 }
1931
1932 if let Err(e) = events_output
1933 .expect("events output should always exist")
1934 .send_all(eventd_events)
1935 .await
1936 {
1937 error!(%listen_addr, error = %e, "Failed to dispatch eventd events.");
1938
1939 saluki_antithesis::sometimes!(
1942 true,
1943 "dsd dispatch failed mid-buffer",
1944 { "stream": "events" }
1945 );
1946 }
1947 }
1948
1949 if event_buffer.has_event_type(EventType::ServiceCheck) {
1951 let service_check_events = event_buffer.extract(Event::is_service_check);
1952 let service_checks_output = source_context.dispatcher().buffered_named("service_checks");
1953
1954 if service_checks_output.is_err() {
1955 saluki_antithesis::unreachable!("dsd 'service_checks' output missing at dispatch");
1956 }
1957
1958 if let Err(e) = service_checks_output
1959 .expect("service checks output should always exist")
1960 .send_all(service_check_events)
1961 .await
1962 {
1963 error!(%listen_addr, error = %e, "Failed to dispatch service check events.");
1964
1965 saluki_antithesis::sometimes!(
1966 true,
1967 "dsd dispatch failed mid-buffer",
1968 { "stream": "service_checks" }
1969 );
1970 }
1971 }
1972
1973 if !event_buffer.is_empty() {
1975 if let Err(e) = source_context
1976 .dispatcher()
1977 .dispatch_named("metrics", event_buffer)
1978 .await
1979 {
1980 error!(%listen_addr, error = %e, "Failed to dispatch metric events.");
1981
1982 saluki_antithesis::sometimes!(
1983 true,
1984 "dsd dispatch failed mid-buffer",
1985 { "stream": "metrics" }
1986 );
1987 }
1988 }
1989}
1990
1991const fn get_adjusted_buffer_size(buffer_size: usize) -> usize {
1992 buffer_size + 4
2007}
2008
2009#[cfg(test)]
2010mod tests {
2011 use std::{
2012 collections::HashMap,
2013 io::ErrorKind,
2014 net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
2015 path::PathBuf,
2016 sync::{Arc, OnceLock},
2017 time::Duration,
2018 };
2019
2020 use bytes::{BufMut as _, Bytes};
2021 use bytesize::ByteSize;
2022 use metrics::{Key, Label};
2023 use saluki_config::ConfigurationLoader;
2024 use saluki_context::{origin::RawOrigin, ContextResolverBuilder, TagsResolverBuilder};
2025 use saluki_core::{
2026 components::ComponentContext,
2027 pooling::{helpers::get_pooled_object_via_builder, ObjectPool as _},
2028 topology::ComponentId,
2029 };
2030 use saluki_env::workload::{CaptureEntityResolver, EntityId};
2031 use saluki_io::{
2032 buf::{BytesBuffer, FixedSizeVec},
2033 deser::codec::dogstatsd::{DogStatsDCodec, DogStatsDCodecConfiguration, ParsedPacket},
2034 net::{ConnectionAddress, ListenAddress, ProcessCredentials, ProcessIdentity},
2035 };
2036 use saluki_metrics::test::TestRecorder;
2037 use serde_json::json;
2038 use stringtheory::MetaString;
2039 use tokio::{net::UdpSocket, sync::mpsc, time::timeout};
2040
2041 use super::{
2042 build_io_buffer_pool, default_buffer_size, default_windows_pipe_security_descriptor,
2043 filters::EnablePayloadsFilter,
2044 forwarder::{
2045 ConnectedPacketForwarder, ForwardPacket, PacketForwarder, PacketForwarderTarget, FORWARDER_QUEUE_CAPACITY,
2046 },
2047 handle_frame, handle_metric_packet,
2048 metrics::build_metrics,
2049 origin_detection_failed_for_telemetry, resolve_capture_container_id, ContextResolvers, DogStatsDConfiguration,
2050 DOGSTATSD_CAPTURE_DIR, MIN_CAPTURE_DEPTH,
2051 };
2052
2053 const LINUX_EAFNOSUPPORT: i32 = 97;
2054 const MACOS_EAFNOSUPPORT: i32 = 47;
2055
2056 fn is_ipv6_unavailable_error(error: &std::io::Error) -> bool {
2057 matches!(error.kind(), ErrorKind::AddrNotAvailable | ErrorKind::Unsupported)
2058 || matches!(error.raw_os_error(), Some(LINUX_EAFNOSUPPORT | MACOS_EAFNOSUPPORT))
2059 }
2060
2061 #[derive(Default)]
2062 struct CaptureTestEntityResolver {
2063 pid_map: HashMap<u32, EntityId>,
2064 }
2065
2066 impl CaptureTestEntityResolver {
2067 fn with_pid_mapping(process_id: u32, entity_id: EntityId) -> Self {
2068 let mut pid_map = HashMap::new();
2069 pid_map.insert(process_id, entity_id);
2070 Self { pid_map }
2071 }
2072 }
2073
2074 impl CaptureEntityResolver for CaptureTestEntityResolver {
2075 fn resolve_container_entity_for_live_pid(&self, process_id: u32) -> Option<EntityId> {
2076 self.pid_map.get(&process_id).cloned()
2077 }
2078 }
2079
2080 fn packet_forwarder_from_sender(
2081 target_port: u16, packets_tx: mpsc::Sender<ForwardPacket>, metrics: super::metrics::Metrics,
2082 ) -> PacketForwarder {
2083 let mut forwarder =
2084 PacketForwarderTarget::new(MetaString::from_static("127.0.0.1"), target_port).to_forwarder(metrics);
2085 forwarder.connected = Arc::new(OnceLock::from(packets_tx));
2086 forwarder
2087 }
2088
2089 fn processed_metric_key(listener_type: &'static str, origin: Option<&str>) -> Key {
2090 let mut labels = vec![
2091 Label::from_static_parts("component_id", "dogstatsd_test"),
2092 Label::from_static_parts("component_type", "source"),
2093 Label::from_static_parts("listener_type", listener_type),
2094 Label::from_static_parts("message_type", "metrics"),
2095 ];
2096 if let Some(origin) = origin {
2097 labels.push(Label::new("origin", origin.to_string()));
2098 }
2099
2100 Key::from_parts("component_events_received_total", labels)
2101 }
2102
2103 fn test_context_resolvers() -> ContextResolvers {
2104 let tags_resolver = TagsResolverBuilder::for_tests().build();
2105 let context_resolver = ContextResolverBuilder::for_tests()
2106 .with_tags_resolver(Some(tags_resolver.clone()))
2107 .build();
2108 ContextResolvers::manual(context_resolver.clone(), context_resolver, tags_resolver)
2109 }
2110
2111 #[test]
2112 fn origin_telemetry_does_not_resolve_origin_when_origin_detection_is_disabled() {
2113 let recorder = TestRecorder::default();
2114 let _recorder_guard = metrics::set_default_local_recorder(&recorder);
2115 let listen_addr = ListenAddress::Unixgram("/tmp/dsd.sock".into());
2116 let context = ComponentContext::source(ComponentId::try_from("dogstatsd_test").expect("valid component ID"));
2117 let metrics = build_metrics(&listen_addr, &context, true);
2118 let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
2119 let mut context_resolvers = test_context_resolvers();
2120 let capture_entity_resolver = CaptureTestEntityResolver::with_pid_mapping(
2121 42,
2122 EntityId::from_local_data("ci-pid-container").expect("container entity"),
2123 );
2124 let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(ProcessCredentials {
2125 pid: 42,
2126 uid: 0,
2127 gid: 0,
2128 }));
2129
2130 let event = handle_frame(
2131 b"test_metric:1|c",
2132 &codec,
2133 &mut context_resolvers,
2134 &metrics,
2135 Some(&capture_entity_resolver),
2136 false,
2137 &peer_addr,
2138 EnablePayloadsFilter::default(),
2139 &[],
2140 )
2141 .expect("frame should parse");
2142
2143 assert!(event.is_some());
2144 assert_eq!(
2145 recorder.counter(processed_metric_key("unixgram", Some("container_id://pid-container"))),
2146 None
2147 );
2148 assert_eq!(recorder.counter(processed_metric_key("unixgram", Some(""))), Some(1));
2149 }
2150
2151 #[test]
2152 fn origin_telemetry_records_resolved_origin_when_origin_detection_is_enabled() {
2153 let recorder = TestRecorder::default();
2154 let _recorder_guard = metrics::set_default_local_recorder(&recorder);
2155 let listen_addr = ListenAddress::Unixgram("/tmp/dsd.sock".into());
2156 let context = ComponentContext::source(ComponentId::try_from("dogstatsd_test").expect("valid component ID"));
2157 let metrics = build_metrics(&listen_addr, &context, true);
2158 let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
2159 let mut context_resolvers = test_context_resolvers();
2160 let capture_entity_resolver = CaptureTestEntityResolver::with_pid_mapping(
2161 42,
2162 EntityId::from_local_data("ci-pid-container").expect("container entity"),
2163 );
2164 let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(ProcessCredentials {
2165 pid: 42,
2166 uid: 0,
2167 gid: 0,
2168 }));
2169
2170 let event = handle_frame(
2171 b"test_metric:1|c",
2172 &codec,
2173 &mut context_resolvers,
2174 &metrics,
2175 Some(&capture_entity_resolver),
2176 true,
2177 &peer_addr,
2178 EnablePayloadsFilter::default(),
2179 &[],
2180 )
2181 .expect("frame should parse");
2182
2183 assert!(event.is_some());
2184 assert_eq!(
2185 recorder.counter(processed_metric_key("unixgram", Some("container_id://pid-container"))),
2186 Some(1)
2187 );
2188 assert_eq!(recorder.counter(processed_metric_key("unixgram", Some(""))), Some(0));
2189 }
2190
2191 #[test]
2192 fn no_metrics_when_interner_full_allocations_disallowed() {
2193 let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
2201 let tags_resolver = TagsResolverBuilder::for_tests().build();
2202 let context_resolver = ContextResolverBuilder::for_tests()
2203 .with_heap_allocations(false)
2204 .with_tags_resolver(Some(tags_resolver.clone()))
2205 .build();
2206 let mut context_resolvers = ContextResolvers::manual(context_resolver.clone(), context_resolver, tags_resolver);
2207 let peer_addr = ConnectionAddress::from("1.1.1.1:1234".parse::<SocketAddr>().unwrap());
2208
2209 let input = "big_metric_name_that_cant_possibly_be_inlined:1|c|#tag1:value1,tag2:value2,tag3:value3";
2210
2211 let Ok(ParsedPacket::Metric(packet)) = codec.decode_packet(input.as_bytes()) else {
2212 panic!("Failed to parse packet.");
2213 };
2214
2215 let maybe_metric = handle_metric_packet(packet, &mut context_resolvers, &peer_addr, &[]);
2216 assert!(maybe_metric.is_none());
2217 }
2218
2219 #[test]
2220 fn metric_with_additional_tags() {
2221 let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
2222 let tags_resolver = TagsResolverBuilder::for_tests().build();
2223 let context_resolver = ContextResolverBuilder::for_tests()
2224 .with_heap_allocations(false)
2225 .with_tags_resolver(Some(tags_resolver.clone()))
2226 .build();
2227 let mut context_resolvers = ContextResolvers::manual(context_resolver.clone(), context_resolver, tags_resolver);
2228 let peer_addr = ConnectionAddress::from("1.1.1.1:1234".parse::<SocketAddr>().unwrap());
2229
2230 let existing_tags = ["tag1:value1", "tag2:value2", "tag3:value3"];
2231 let existing_tags_str = existing_tags.join(",");
2232
2233 let input = format!("test_metric_name:1|c|#{}", existing_tags_str);
2234 let additional_tags = [
2235 "tag4:value4".to_string(),
2236 "tag5:value5".to_string(),
2237 "tag6:value6".to_string(),
2238 ];
2239
2240 let Ok(ParsedPacket::Metric(packet)) = codec.decode_packet(input.as_bytes()) else {
2241 panic!("Failed to parse packet.");
2242 };
2243 let maybe_metric = handle_metric_packet(packet, &mut context_resolvers, &peer_addr, &additional_tags);
2244 assert!(maybe_metric.is_some());
2245
2246 let metric = maybe_metric.unwrap();
2247 let context = metric.context();
2248
2249 for tag in existing_tags {
2250 assert!(context.tags().has_tag(tag));
2251 }
2252
2253 for tag in additional_tags {
2254 assert!(context.tags().has_tag(tag));
2255 }
2256 }
2257
2258 fn deser_config(json: &str) -> DogStatsDConfiguration {
2259 serde_json::from_str(json).expect("failed to deserialize config")
2260 }
2261
2262 fn udp_listen_address() -> ListenAddress {
2263 ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8125)))
2264 }
2265
2266 fn tcp_listen_address() -> ListenAddress {
2267 ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8125)))
2268 }
2269
2270 fn named_pipe_listen_address() -> ListenAddress {
2271 ListenAddress::named_pipe_with_input_buffer_size(
2272 "datadog-dogstatsd",
2273 default_windows_pipe_security_descriptor(),
2274 default_buffer_size() as u32,
2275 )
2276 }
2277
2278 #[test]
2279 fn build_addresses_includes_named_pipe_when_configured() {
2280 let config = deser_config(
2281 r#"{
2282 "dogstatsd_port": 0,
2283 "dogstatsd_pipe_name": "datadog-dogstatsd"
2284 }"#,
2285 );
2286
2287 let addresses = config.build_addresses(None);
2288
2289 assert_eq!(addresses, vec![named_pipe_listen_address()]);
2290 }
2291
2292 #[test]
2293 fn build_addresses_uses_dogstatsd_buffer_size_for_named_pipe_input_buffer() {
2294 let config = deser_config(
2295 r#"{
2296 "dogstatsd_port": 0,
2297 "dogstatsd_pipe_name": "datadog-dogstatsd",
2298 "dogstatsd_buffer_size": 16384
2299 }"#,
2300 );
2301
2302 let addresses = config.build_addresses(None);
2303
2304 let [ListenAddress::NamedPipe { input_buffer_size, .. }] = addresses.as_slice() else {
2305 panic!("expected only a named pipe listen address, got {addresses:?}");
2306 };
2307 assert_eq!(*input_buffer_size, Some(16_384));
2308 }
2309
2310 #[test]
2311 fn eol_required_matches_named_pipe_listener_type() {
2312 let config = deser_config(r#"{"dogstatsd_eol_required": ["named_pipe"]}"#);
2313 let eol_required = config.eol_required();
2314
2315 assert!(eol_required.for_listener(&named_pipe_listen_address()));
2316 assert!(!eol_required.for_listener(&udp_listen_address()));
2317 assert!(!eol_required.for_listener(&tcp_listen_address()));
2318 }
2319
2320 #[test]
2321 fn interner_size_defaults_to_2mib() {
2322 let config = deser_config("{}");
2323 assert_eq!(config.effective_context_string_interner_bytes(), ByteSize::mib(2));
2324 }
2325
2326 #[test]
2327 fn socket_receive_buffer_size_defaults_to_zero() {
2328 let config = deser_config("{}");
2329 assert_eq!(config.socket_receive_buffer_size, 0);
2330 }
2331
2332 #[test]
2333 fn socket_receive_buffer_size_from_config() {
2334 let config = deser_config(r#"{"dogstatsd_so_rcvbuf": 131072}"#);
2335 assert_eq!(config.socket_receive_buffer_size, 131_072);
2336 }
2337
2338 #[test]
2339 fn stream_log_too_big_defaults_to_false() {
2340 let config = deser_config("{}");
2341 assert!(!config.stream_log_too_big);
2342 }
2343
2344 #[test]
2345 fn stream_log_too_big_from_config() {
2346 let config = deser_config(r#"{"dogstatsd_stream_log_too_big": true}"#);
2347 assert!(config.stream_log_too_big);
2348 }
2349
2350 #[test]
2351 fn disable_verbose_logs_defaults_to_false() {
2352 let config = deser_config("{}");
2353 assert!(!config.disable_verbose_logs);
2354 }
2355
2356 #[test]
2357 fn disable_verbose_logs_from_config() {
2358 let config = deser_config(r#"{"dogstatsd_disable_verbose_logs": true}"#);
2359 assert!(config.disable_verbose_logs);
2360 }
2361
2362 #[test]
2363 fn statsd_forward_defaults_disabled() {
2364 let config = deser_config("{}");
2365 assert!(config.statsd_forward_host.is_none());
2366 assert_eq!(config.statsd_forward_port, 0);
2367 assert!(config.statsd_forward_target().is_none());
2368 }
2369
2370 #[test]
2371 fn statsd_forward_empty_host_disabled() {
2372 let config = deser_config(r#"{"statsd_forward_host": "", "statsd_forward_port": 9125}"#);
2373 assert!(config.statsd_forward_host.is_none());
2374 assert!(config.statsd_forward_target().is_none());
2375 }
2376
2377 #[test]
2378 fn statsd_forward_zero_port_disabled() {
2379 let config = deser_config(r#"{"statsd_forward_host": "127.0.0.1", "statsd_forward_port": 0}"#);
2380 assert_eq!(config.statsd_forward_host.as_deref(), Some("127.0.0.1"));
2381 assert!(config.statsd_forward_target().is_none());
2382 }
2383
2384 #[test]
2385 fn statsd_forward_host_and_port_enabled() {
2386 let config = deser_config(r#"{"statsd_forward_host": "127.0.0.1", "statsd_forward_port": 9125}"#);
2387 let (host, port) = config.statsd_forward_target().expect("forwarding should be enabled");
2388 assert_eq!(host.as_ref(), "127.0.0.1");
2389 assert_eq!(port, 9125);
2390 }
2391
2392 #[test]
2393 fn statsd_forward_invalid_target_still_builds_forwarder_handle() {
2394 let config = deser_config(r#"{"statsd_forward_host": "not a valid host", "statsd_forward_port": 9125}"#);
2395 assert!(config.packet_forwarder_target().is_some());
2396 }
2397
2398 #[tokio::test]
2399 async fn packet_forwarder_sends_payload_bytes() {
2400 let receiver = UdpSocket::bind("127.0.0.1:0").await.expect("receiver should bind");
2401 let receiver_addr = receiver.local_addr().expect("receiver should have an address");
2402 let forwarder = ConnectedPacketForwarder::connect("127.0.0.1", receiver_addr.port())
2403 .await
2404 .expect("forwarder should connect");
2405 let payload = b"daemon:666|g|#sometag1:somevalue1,sometag2:somevalue2";
2406
2407 let recorder = TestRecorder::default();
2408 let _recorder_guard = metrics::set_default_local_recorder(&recorder);
2409 let listen_addr = ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8125)));
2410 let context = ComponentContext::source(ComponentId::try_from("dogstatsd_test").expect("valid component ID"));
2411 let metrics = build_metrics(&listen_addr, &context, false);
2412 let (packets_tx, packets_rx) = mpsc::channel(1);
2413 let worker = tokio::spawn(forwarder.run(packets_rx, metrics.clone()));
2414 let packet_forwarder = packet_forwarder_from_sender(receiver_addr.port(), packets_tx, metrics);
2415
2416 packet_forwarder.forward(Bytes::copy_from_slice(payload)).await;
2417
2418 let mut actual = [0u8; 128];
2419 let (received_len, _) = timeout(Duration::from_secs(1), receiver.recv_from(&mut actual))
2420 .await
2421 .expect("receive should not time out")
2422 .expect("receiver should receive payload");
2423
2424 assert_eq!(&actual[..received_len], payload);
2425 assert_eq!(
2426 recorder.counter((
2427 "component_packets_forwarded_total",
2428 &[
2429 ("component_id", "dogstatsd_test"),
2430 ("component_type", "source"),
2431 ("listener_type", "udp"),
2432 ("state", "ok"),
2433 ]
2434 )),
2435 Some(1)
2436 );
2437 assert_eq!(
2438 recorder.counter((
2439 "component_bytes_forwarded_total",
2440 &[
2441 ("component_id", "dogstatsd_test"),
2442 ("component_type", "source"),
2443 ("listener_type", "udp"),
2444 ]
2445 )),
2446 Some(payload.len() as u64)
2447 );
2448 worker.abort();
2449 }
2450
2451 #[tokio::test]
2452 async fn packet_forwarder_sends_payload_bytes_to_ipv6_target() {
2453 let receiver = match UdpSocket::bind("[::1]:0").await {
2454 Ok(receiver) => receiver,
2455 Err(e) if is_ipv6_unavailable_error(&e) => return,
2456 Err(e) => panic!("receiver should bind: {e}"),
2457 };
2458 let receiver_addr = receiver.local_addr().expect("receiver should have an address");
2459 let forwarder = ConnectedPacketForwarder::connect("::1", receiver_addr.port())
2460 .await
2461 .expect("forwarder should connect");
2462 let payload = b"daemon:666|g|#ip:6";
2463
2464 let recorder = TestRecorder::default();
2465 let _recorder_guard = metrics::set_default_local_recorder(&recorder);
2466 let listen_addr = ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8125)));
2467 let context = ComponentContext::source(ComponentId::try_from("dogstatsd_test").expect("valid component ID"));
2468 let metrics = build_metrics(&listen_addr, &context, false);
2469 let (packets_tx, packets_rx) = mpsc::channel(1);
2470 let worker = tokio::spawn(forwarder.run(packets_rx, metrics.clone()));
2471 let packet_forwarder = packet_forwarder_from_sender(receiver_addr.port(), packets_tx, metrics);
2472
2473 packet_forwarder.forward(Bytes::copy_from_slice(payload)).await;
2474
2475 let mut actual = [0u8; 128];
2476 let (received_len, _) = timeout(Duration::from_secs(1), receiver.recv_from(&mut actual))
2477 .await
2478 .expect("receive should not time out")
2479 .expect("receiver should receive payload");
2480
2481 assert_eq!(&actual[..received_len], payload);
2482 worker.abort();
2483 }
2484
2485 #[tokio::test]
2486 async fn packet_forwarder_waits_when_queue_is_full() {
2487 let recorder = TestRecorder::default();
2488 let _recorder_guard = metrics::set_default_local_recorder(&recorder);
2489 let listen_addr = ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8125)));
2490 let context = ComponentContext::source(ComponentId::try_from("dogstatsd_test").expect("valid component ID"));
2491 let metrics = build_metrics(&listen_addr, &context, false);
2492 let (packets_tx, _packets_rx) = mpsc::channel(FORWARDER_QUEUE_CAPACITY);
2493 let packet_forwarder = packet_forwarder_from_sender(9125, packets_tx, metrics);
2494
2495 for _ in 0..FORWARDER_QUEUE_CAPACITY {
2496 packet_forwarder.forward(Bytes::from_static(b"queued:1|c")).await;
2497 }
2498
2499 assert!(
2500 timeout(
2501 Duration::from_millis(100),
2502 packet_forwarder.forward(Bytes::from_static(b"blocked:1|c")),
2503 )
2504 .await
2505 .is_err(),
2506 "forwarding should wait for queue capacity instead of dropping"
2507 );
2508 }
2509
2510 #[tokio::test]
2511 async fn packet_forwarder_send_error_increments_error_telemetry() {
2512 let recorder = TestRecorder::default();
2513 let _recorder_guard = metrics::set_default_local_recorder(&recorder);
2514 let listen_addr = ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8125)));
2515 let context = ComponentContext::source(ComponentId::try_from("dogstatsd_test").expect("valid component ID"));
2516 let metrics = build_metrics(&listen_addr, &context, false);
2517 let socket = UdpSocket::bind("127.0.0.1:0").await.expect("socket should bind");
2518 let forwarder = ConnectedPacketForwarder {
2519 socket,
2520 target: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9125)),
2521 };
2522 let (packets_tx, packets_rx) = mpsc::channel(1);
2523 let worker = tokio::spawn(forwarder.run(packets_rx, metrics.clone()));
2524 let packet_forwarder = packet_forwarder_from_sender(9125, packets_tx, metrics);
2525
2526 packet_forwarder.forward(Bytes::from_static(b"daemon:666|g")).await;
2527
2528 let deadline = tokio::time::Instant::now() + Duration::from_secs(1);
2529 loop {
2530 if recorder.counter((
2531 "component_packets_forwarded_total",
2532 &[
2533 ("component_id", "dogstatsd_test"),
2534 ("component_type", "source"),
2535 ("listener_type", "udp"),
2536 ("state", "error"),
2537 ],
2538 )) == Some(1)
2539 {
2540 break;
2541 }
2542
2543 assert!(
2544 tokio::time::Instant::now() < deadline,
2545 "forwarding error telemetry should be recorded"
2546 );
2547 tokio::time::sleep(Duration::from_millis(10)).await;
2548 }
2549 worker.abort();
2550 }
2551
2552 #[test]
2553 fn unsupported_platform_process_credentials_do_not_count_as_origin_detection_telemetry_errors() {
2554 let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Error(
2555 saluki_io::net::ProcessCredentialsError::UnsupportedPlatform,
2556 ));
2557
2558 assert!(!origin_detection_failed_for_telemetry(true, 1, &peer_addr));
2559 }
2560
2561 #[test]
2562 fn invalid_process_credentials_count_as_origin_detection_telemetry_errors() {
2563 let peer_addr = ConnectionAddress::ProcessLike(ProcessIdentity::Error(
2564 saluki_io::net::ProcessCredentialsError::InvalidCredentials,
2565 ));
2566
2567 assert!(origin_detection_failed_for_telemetry(true, 1, &peer_addr));
2568 }
2569
2570 #[test]
2571 fn autoscale_udp_listeners_defaults_to_false() {
2572 let config = deser_config("{}");
2573 assert!(!config.autoscale_udp_listeners);
2574 assert!(config.udp_streams_to_yield().is_none());
2575 }
2576
2577 #[test]
2578 fn effective_max_buffer_count_never_below_baseline() {
2579 let legacy = deser_config(r#"{"dogstatsd_buffer_count": 1024}"#);
2582 assert_eq!(legacy.effective_max_buffer_count(), 1024);
2583
2584 let explicit = deser_config(r#"{"dogstatsd_buffer_count": 128, "dogstatsd_buffer_count_max": 512}"#);
2586 assert_eq!(explicit.effective_max_buffer_count(), 512);
2587
2588 let below = deser_config(r#"{"dogstatsd_buffer_count": 200, "dogstatsd_buffer_count_max": 64}"#);
2590 assert_eq!(below.effective_max_buffer_count(), 200);
2591 }
2592
2593 #[tokio::test]
2594 async fn dogstatsd_io_buffer_pool_grows_on_demand_until_limit() {
2595 let min_buffers = 2;
2596 let max_buffers = 3;
2597 let (pool, shrinker) = build_io_buffer_pool(min_buffers, max_buffers, default_buffer_size());
2598
2599 let mut initial_buffers = Vec::with_capacity(min_buffers);
2600 for _ in 0..min_buffers {
2601 initial_buffers.push(
2602 timeout(Duration::from_secs(1), pool.acquire())
2603 .await
2604 .expect("initial buffer should be available"),
2605 );
2606 }
2607 let on_demand_buffer = timeout(Duration::from_secs(1), pool.acquire())
2608 .await
2609 .expect("pool should grow on demand before hitting the limit");
2610
2611 let capped_acquire = timeout(Duration::from_millis(25), pool.acquire()).await;
2612 assert!(capped_acquire.is_err(), "pool should wait once it reaches the limit");
2613
2614 drop(initial_buffers.pop().expect("initial buffer should still be held"));
2615 timeout(Duration::from_secs(1), pool.acquire())
2616 .await
2617 .expect("returned buffer should unblock acquisition");
2618
2619 drop(on_demand_buffer);
2620 drop(shrinker);
2621 }
2622
2623 #[test]
2624 #[cfg(target_os = "linux")]
2625 fn autoscale_udp_listeners_from_config_linux() {
2626 let config = deser_config(r#"{"dogstatsd_autoscale_udp_listeners": true}"#);
2627 assert!(config.autoscale_udp_listeners);
2628
2629 let streams = config
2630 .udp_streams_to_yield()
2631 .expect("autoscale yields at least 1 stream");
2632 let n = streams.get();
2633 assert!(
2634 (1..=4).contains(&n),
2635 "expected 1..=4 streams from vCPU formula, got {n}"
2636 );
2637 }
2638
2639 #[test]
2640 #[cfg(not(target_os = "linux"))]
2641 fn warns_for_uds_origin_detection_on_non_linux() {
2642 let config = deser_config(
2643 r#"{
2644 "dogstatsd_origin_detection": true,
2645 "dogstatsd_port": 0,
2646 "dogstatsd_socket": "/tmp/dsd.sock"
2647 }"#,
2648 );
2649 let addresses = config.build_addresses(None);
2650
2651 assert!(config.uds_origin_detection_unsupported_on_platform(&addresses));
2652 }
2653
2654 #[test]
2655 #[cfg(not(target_os = "linux"))]
2656 fn does_not_warn_for_udp_origin_detection_on_non_linux() {
2657 let config = deser_config(r#"{"dogstatsd_origin_detection": true}"#);
2658 let addresses = config.build_addresses(None);
2659
2660 assert!(!config.uds_origin_detection_unsupported_on_platform(&addresses));
2661 }
2662
2663 #[test]
2664 #[cfg(not(target_os = "linux"))]
2665 fn autoscale_udp_listeners_from_config_non_linux() {
2666 let config = deser_config(r#"{"dogstatsd_autoscale_udp_listeners": true}"#);
2667 assert!(config.autoscale_udp_listeners);
2668
2669 assert_eq!(None, config.udp_streams_to_yield());
2670 }
2671
2672 #[test]
2673 fn eol_required_defaults_to_no_listeners() {
2674 let config = deser_config("{}");
2675 let eol_required = config.eol_required();
2676
2677 assert!(!eol_required.for_listener(&udp_listen_address()));
2678 assert!(!eol_required.for_listener(&tcp_listen_address()));
2679 }
2680
2681 #[test]
2682 fn eol_required_matches_configured_listener_types() {
2683 let config = deser_config(r#"{"dogstatsd_eol_required": ["udp", "uds"]}"#);
2684 let eol_required = config.eol_required();
2685
2686 assert!(eol_required.for_listener(&udp_listen_address()));
2687 assert!(!eol_required.for_listener(&tcp_listen_address()));
2688
2689 #[cfg(unix)]
2690 {
2691 assert!(eol_required.for_listener(&ListenAddress::Unixgram("/tmp/dsd.sock".into())));
2692 assert!(eol_required.for_listener(&ListenAddress::Unix("/tmp/dsd-stream.sock".into())));
2693 }
2694 }
2695
2696 #[test]
2697 fn eol_required_accepts_space_separated_string() {
2698 let config = deser_config(r#"{"dogstatsd_eol_required": "udp uds"}"#);
2699 let eol_required = config.eol_required();
2700
2701 assert!(eol_required.for_listener(&udp_listen_address()));
2702 }
2703
2704 #[test]
2705 fn drops_full_named_pipe_buffer_without_newline() {
2706 let named_pipe_stream = named_pipe_listen_address();
2707 let mut buffer = get_pooled_object_via_builder::<_, BytesBuffer>(|| FixedSizeVec::with_capacity(8));
2708 buffer.put_slice(b"12345678");
2709
2710 assert!(super::should_drop_oversized_named_pipe_frame(
2711 &named_pipe_stream,
2712 &buffer
2713 ));
2714 }
2715
2716 #[test]
2717 fn keeps_named_pipe_partial_frame_when_buffer_has_capacity() {
2718 let named_pipe_stream = named_pipe_listen_address();
2719 let mut buffer = get_pooled_object_via_builder::<_, BytesBuffer>(|| FixedSizeVec::with_capacity(9));
2720 buffer.put_slice(b"12345678");
2721
2722 assert!(!super::should_drop_oversized_named_pipe_frame(
2723 &named_pipe_stream,
2724 &buffer
2725 ));
2726 }
2727
2728 #[test]
2729 fn keeps_full_named_pipe_buffer_with_newline() {
2730 let named_pipe_stream = named_pipe_listen_address();
2731 let mut buffer = get_pooled_object_via_builder::<_, BytesBuffer>(|| FixedSizeVec::with_capacity(8));
2732 buffer.put_slice(b"1234567\n");
2733
2734 assert!(!super::should_drop_oversized_named_pipe_frame(
2735 &named_pipe_stream,
2736 &buffer
2737 ));
2738 }
2739
2740 #[test]
2741 fn stream_log_too_big_warns_for_enabled_length_delimited_stream_invalid_frames() {
2742 let uds_stream = ListenAddress::Unix("/tmp/dsd-stream.sock".into());
2743 let named_pipe_stream = named_pipe_listen_address();
2744 let tcp_stream = ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8125)));
2745 let error = saluki_io::deser::framing::FramingError::InvalidFrame {
2746 frame_len: 8193,
2747 reason: "frame length exceeds buffer capacity",
2748 };
2749
2750 assert!(super::should_warn_stream_log_too_big(&uds_stream, &error, true));
2751 assert!(!super::should_warn_stream_log_too_big(&uds_stream, &error, false));
2752 assert!(!super::should_warn_stream_log_too_big(&named_pipe_stream, &error, true));
2753 assert!(!super::should_warn_stream_log_too_big(&tcp_stream, &error, true));
2754 }
2755
2756 #[test]
2757 fn interner_size_from_entry_count() {
2758 let config = deser_config(r#"{"dogstatsd_string_interner_size": 4096}"#);
2760 assert_eq!(config.effective_context_string_interner_bytes(), ByteSize::mib(2));
2761 }
2762
2763 #[test]
2764 fn interner_size_from_explicit_bytes() {
2765 let config = deser_config(r#"{"dogstatsd_string_interner_size_bytes": 4194304}"#);
2766 assert_eq!(config.effective_context_string_interner_bytes(), ByteSize::b(4194304));
2767 }
2768
2769 #[test]
2770 fn interner_size_explicit_bytes_takes_priority() {
2771 let config = deser_config(
2772 r#"{"dogstatsd_string_interner_size": 4096, "dogstatsd_string_interner_size_bytes": 8388608}"#,
2773 );
2774 assert_eq!(config.effective_context_string_interner_bytes(), ByteSize::b(8388608));
2776 }
2777
2778 #[test]
2779 fn interner_size_custom_entry_count() {
2780 let config = deser_config(r#"{"dogstatsd_string_interner_size": 8192}"#);
2781 assert_eq!(config.effective_context_string_interner_bytes(), ByteSize::mib(4));
2783 }
2784
2785 fn address_list_eq(expected: &mut [ListenAddress], actual: &mut [ListenAddress]) -> Result<(), String> {
2787 if expected.len() != actual.len() {
2788 return Err(format!(
2789 "length mismatch: expected {} addresses, got {}",
2790 expected.len(),
2791 actual.len()
2792 ));
2793 }
2794
2795 expected.sort_by_key(|a| a.to_string());
2796 actual.sort_by_key(|a| a.to_string());
2797
2798 for (e, a) in expected.iter().zip(actual.iter()) {
2799 let (es, as_) = (e.to_string(), a.to_string());
2800 if es != as_ {
2801 return Err(format!("address mismatch: expected {}, got {}", es, as_));
2802 }
2803 }
2804
2805 Ok(())
2806 }
2807
2808 #[test]
2811 fn build_addresses_assertion_function_works() {
2812 let config = DogStatsDConfiguration {
2813 port: 0,
2814 tcp_port: 123,
2815 socket_path: None,
2816 socket_stream_path: None,
2817 non_local_traffic: false,
2818 ..Default::default()
2819 };
2820 let mut expected = vec![ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(
2821 Ipv4Addr::new(127, 0, 0, 2),
2823 123,
2824 )))];
2825 let mut actual = config.build_addresses(None);
2826 assert!(address_list_eq(&mut expected, &mut actual).is_err())
2827 }
2828
2829 #[test]
2831 fn build_addresses_no_listeners() {
2832 let config = DogStatsDConfiguration {
2833 port: 0,
2834 tcp_port: 0,
2835 socket_path: None,
2836 socket_stream_path: None,
2837 non_local_traffic: false,
2838 ..Default::default()
2839 };
2840 let mut expected = vec![];
2841 let mut actual = config.build_addresses(None);
2842 address_list_eq(&mut expected, &mut actual).unwrap();
2843 }
2844
2845 #[test]
2847 fn build_addresses_udp_local_only() {
2848 let config = DogStatsDConfiguration {
2849 port: 8125,
2850 tcp_port: 0,
2851 socket_path: None,
2852 socket_stream_path: None,
2853 non_local_traffic: false,
2854 ..Default::default()
2855 };
2856 let mut expected = vec![ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(
2857 Ipv4Addr::new(127, 0, 0, 1),
2858 8125,
2859 )))];
2860 let mut actual = config.build_addresses(None);
2861 address_list_eq(&mut expected, &mut actual).unwrap();
2862 }
2863
2864 #[test]
2866 fn build_addresses_udp_non_local_only() {
2867 let config = DogStatsDConfiguration {
2868 port: 8125,
2869 tcp_port: 0,
2870 socket_path: None,
2871 socket_stream_path: None,
2872 non_local_traffic: true,
2873 ..Default::default()
2874 };
2875 let mut expected = vec![ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(
2876 Ipv4Addr::new(0, 0, 0, 0),
2877 8125,
2878 )))];
2879 let mut actual = config.build_addresses(None);
2880 address_list_eq(&mut expected, &mut actual).unwrap();
2881 }
2882
2883 #[test]
2885 fn build_addresses_tcp_local_only() {
2886 let config = DogStatsDConfiguration {
2887 port: 0,
2888 tcp_port: 9000,
2889 socket_path: None,
2890 socket_stream_path: None,
2891 non_local_traffic: false,
2892 ..Default::default()
2893 };
2894 let mut expected = vec![ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(
2895 Ipv4Addr::new(127, 0, 0, 1),
2896 9000,
2897 )))];
2898 let mut actual = config.build_addresses(None);
2899 address_list_eq(&mut expected, &mut actual).unwrap();
2900 }
2901
2902 #[test]
2904 fn build_addresses_tcp_non_local_only() {
2905 let config = DogStatsDConfiguration {
2906 port: 0,
2907 tcp_port: 9000,
2908 socket_path: None,
2909 socket_stream_path: None,
2910 non_local_traffic: true,
2911 ..Default::default()
2912 };
2913 let mut expected = vec![ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(
2914 Ipv4Addr::new(0, 0, 0, 0),
2915 9000,
2916 )))];
2917 let mut actual = config.build_addresses(None);
2918 address_list_eq(&mut expected, &mut actual).unwrap();
2919 }
2920
2921 #[test]
2923 fn build_addresses_unixgram_only() {
2924 let config = DogStatsDConfiguration {
2925 port: 0,
2926 tcp_port: 0,
2927 socket_path: Some("/tmp/dsd.sock".to_string()),
2928 socket_stream_path: None,
2929 non_local_traffic: false,
2930 ..Default::default()
2931 };
2932 let mut expected = vec![ListenAddress::Unixgram("/tmp/dsd.sock".into())];
2933 let mut actual = config.build_addresses(None);
2934 address_list_eq(&mut expected, &mut actual).unwrap();
2935 }
2936
2937 #[test]
2939 fn build_addresses_unix_stream_only() {
2940 let config = DogStatsDConfiguration {
2941 port: 0,
2942 tcp_port: 0,
2943 socket_path: None,
2944 socket_stream_path: Some("/tmp/dsd-stream.sock".to_string()),
2945 non_local_traffic: false,
2946 ..Default::default()
2947 };
2948 let mut expected = vec![ListenAddress::Unix("/tmp/dsd-stream.sock".into())];
2949 let mut actual = config.build_addresses(None);
2950 address_list_eq(&mut expected, &mut actual).unwrap();
2951 }
2952
2953 #[test]
2955 fn build_addresses_all_four_non_local() {
2956 let config = DogStatsDConfiguration {
2957 port: 8125,
2958 tcp_port: 9000,
2959 socket_path: Some("/tmp/dsd.sock".to_string()),
2960 socket_stream_path: Some("/tmp/dsd-stream.sock".to_string()),
2961 non_local_traffic: true,
2962 ..Default::default()
2963 };
2964 let mut expected = vec![
2965 ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 8125))),
2966 ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 9000))),
2967 ListenAddress::Unixgram("/tmp/dsd.sock".into()),
2968 ListenAddress::Unix("/tmp/dsd-stream.sock".into()),
2969 ];
2970 let mut actual = config.build_addresses(None);
2971 address_list_eq(&mut expected, &mut actual).unwrap();
2972 }
2973
2974 #[test]
2976 fn build_addresses_all_four_local() {
2977 let config = DogStatsDConfiguration {
2978 port: 8125,
2979 tcp_port: 9000,
2980 socket_path: Some("/tmp/dsd.sock".to_string()),
2981 socket_stream_path: Some("/tmp/dsd-stream.sock".to_string()),
2982 non_local_traffic: false,
2983 ..Default::default()
2984 };
2985 let mut expected = vec![
2986 ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 8125))),
2987 ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 9000))),
2988 ListenAddress::Unixgram("/tmp/dsd.sock".into()),
2989 ListenAddress::Unix("/tmp/dsd-stream.sock".into()),
2990 ];
2991 let mut actual = config.build_addresses(None);
2992 address_list_eq(&mut expected, &mut actual).unwrap();
2993 }
2994
2995 #[test]
2998 fn build_addresses_bind_host_applies_to_udp_and_tcp() {
2999 let config = DogStatsDConfiguration {
3000 port: 8125,
3001 tcp_port: 9000,
3002 socket_path: Some("/tmp/dsd.sock".to_string()),
3003 socket_stream_path: None,
3004 non_local_traffic: false,
3005 ..Default::default()
3006 };
3007 let bind_host = Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50)));
3008 let mut expected = vec![
3009 ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 50), 8125))),
3010 ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 50), 9000))),
3011 ListenAddress::Unixgram("/tmp/dsd.sock".into()),
3012 ];
3013 let mut actual = config.build_addresses(bind_host);
3014 address_list_eq(&mut expected, &mut actual).unwrap();
3015 }
3016
3017 #[test]
3021 fn build_addresses_non_local_clobbers_bind_host() {
3022 let config = DogStatsDConfiguration {
3023 port: 8125,
3024 tcp_port: 9000,
3025 socket_path: None,
3026 socket_stream_path: Some("/tmp/dsd-stream.sock".to_string()),
3027 non_local_traffic: true,
3028 ..Default::default()
3029 };
3030 let bind_host = Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50)));
3031 let mut expected = vec![
3032 ListenAddress::Udp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 8125))),
3033 ListenAddress::Tcp(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 9000))),
3034 ListenAddress::Unix("/tmp/dsd-stream.sock".into()),
3035 ];
3036 let mut actual = config.build_addresses(bind_host);
3037 address_list_eq(&mut expected, &mut actual).unwrap();
3038 }
3039
3040 #[test]
3041 fn non_finite_metric_values_are_silently_dropped() {
3042 let codec = DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default());
3047 for input in &[b"my.gauge:NaN|g" as &[u8], b"my.gauge:inf|g", b"my.gauge:-inf|g"] {
3048 match codec.decode_packet(input).expect("should decode without error") {
3049 ParsedPacket::Metric(packet) => assert_eq!(
3050 packet.num_points, 0,
3051 "non-finite value should be dropped, leaving 0 valid points"
3052 ),
3053 _ => panic!("expected Metric packet"),
3054 }
3055 }
3056 }
3057
3058 #[tokio::test]
3059 async fn fix_empty_capture_path_sets_path_from_run_path() {
3060 const RUN_PATH: &str = "/my/little/run_path";
3061
3062 let base_config_values = json!({ "run_path": RUN_PATH });
3063 let (config, _) = ConfigurationLoader::for_tests(Some(base_config_values), None, false).await;
3064
3065 let dogstatsd_config = DogStatsDConfiguration::from_configuration(&config).expect("should deserialize");
3066
3067 let expected = PathBuf::from(RUN_PATH).join(DOGSTATSD_CAPTURE_DIR);
3068 assert_eq!(expected, dogstatsd_config.capture_path);
3069 }
3070
3071 #[tokio::test]
3072 async fn fix_empty_capture_path_keeps_explicit_path() {
3073 const RUN_PATH: &str = "/my/little/run_path";
3074 const CAPTURE_PATH: &str = "/custom/path/to/capture";
3075
3076 let base_config_values = json!({ "run_path": RUN_PATH, "dogstatsd_capture_path": CAPTURE_PATH });
3077 let (config, _) = ConfigurationLoader::for_tests(Some(base_config_values), None, false).await;
3078
3079 let dogstatsd_config = DogStatsDConfiguration::from_configuration(&config).expect("should deserialize");
3080
3081 assert_eq!(PathBuf::from(CAPTURE_PATH), dogstatsd_config.capture_path);
3082 }
3083
3084 #[tokio::test]
3085 async fn from_configuration_normalizes_capture_depth() {
3086 let cases = [
3087 (json!({}), MIN_CAPTURE_DEPTH),
3088 (json!({ "dogstatsd_capture_depth": 0 }), MIN_CAPTURE_DEPTH),
3089 (json!({ "dogstatsd_capture_depth": 2048 }), 2048),
3090 ];
3091
3092 for (base_config_values, expected_depth) in cases {
3093 let (config, _) = ConfigurationLoader::for_tests(Some(base_config_values), None, false).await;
3094 let dogstatsd_config = DogStatsDConfiguration::from_configuration(&config).expect("should deserialize");
3095
3096 assert_eq!(expected_depth, dogstatsd_config.capture_depth);
3097 }
3098 }
3099
3100 #[test]
3101 fn capture_entity_resolver_is_configured_separately_from_workload_provider() {
3102 let config =
3103 DogStatsDConfiguration::default().with_capture_entity_resolver(CaptureTestEntityResolver::default());
3104
3105 assert!(config.capture_entity_resolver.is_some());
3106 assert!(config.workload_provider.is_none());
3107 }
3108
3109 #[test]
3110 fn resolve_capture_container_id_uses_live_pid_mapping() {
3111 let capture_entity_resolver = CaptureTestEntityResolver::with_pid_mapping(
3112 42,
3113 EntityId::from_local_data("ci-pid-container").expect("container entity"),
3114 );
3115
3116 assert_eq!(
3117 resolve_capture_container_id(Some(&capture_entity_resolver), Some(42)),
3118 Some("container_id://pid-container".to_string())
3119 );
3120 }
3121
3122 #[test]
3123 fn build_capture_record_ignores_payload_local_data() {
3124 let record = super::build_capture_record(None, None, b"test.metric:1|c|c:ci-local-container\n");
3125
3126 assert_eq!(record.container_id, None);
3127 assert!(record.ancillary.is_empty());
3128 }
3129
3130 #[test]
3131 fn stream_capture_state_preserves_last_pid_without_new_creds() {
3132 let mut stream_capture = super::StreamCaptureState::new();
3133
3134 stream_capture.update_peer_metadata(&ConnectionAddress::ProcessLike(ProcessIdentity::Credentials(
3135 ProcessCredentials {
3136 pid: 42,
3137 uid: 0,
3138 gid: 0,
3139 },
3140 )));
3141 stream_capture.update_peer_metadata(&ConnectionAddress::ProcessLike(ProcessIdentity::Unavailable));
3142
3143 assert_eq!(stream_capture.last_pid, Some(42));
3144 }
3145
3146 #[test]
3147 fn apply_credentials_uses_live_pid_for_normal_packet() {
3148 let mut origin = RawOrigin::default();
3149 let creds = ProcessCredentials {
3150 pid: 12345,
3151 uid: 1000,
3152 gid: 1000,
3153 };
3154 super::apply_credentials_to_origin(&mut origin, &creds);
3155
3156 assert_eq!(origin.process_id(), Some(12345));
3157 }
3158
3159 #[test]
3160 fn apply_credentials_unpacks_captured_pid_when_replay_gid_present() {
3161 let mut origin = RawOrigin::default();
3162 let captured_pid: u32 = 99887766;
3163 let creds = ProcessCredentials {
3164 pid: 12345, uid: captured_pid, gid: super::REPLAY_CREDENTIALS_GID,
3167 };
3168 super::apply_credentials_to_origin(&mut origin, &creds);
3169
3170 assert_eq!(
3171 origin.process_id(),
3172 Some(super::origin::mark_replay_process_id(captured_pid))
3173 );
3174 }
3175}
3176
3177#[cfg(test)]
3178mod config_smoke {
3179 use datadog_agent_config_testing::config_registry::structs;
3180 use datadog_agent_config_testing::run_config_smoke_tests;
3181 use serde_json::json;
3182
3183 use super::DogStatsDConfiguration;
3184 use crate::config::{DatadogRemapper, KEY_ALIASES};
3185
3186 #[tokio::test]
3187 async fn smoke_test() {
3188 run_config_smoke_tests(
3189 structs::DOGSTATSD_CONFIGURATION,
3190 &[],
3191 json!({}),
3192 |cfg| {
3193 cfg.as_typed::<DogStatsDConfiguration>()
3194 .expect("DogStatsDConfiguration should deserialize")
3195 },
3196 KEY_ALIASES,
3197 DatadogRemapper::new,
3198 )
3199 .await
3200 }
3201}