saluki_io/deser/codec/dogstatsd/
mod.rs

1use std::fmt;
2
3mod event;
4pub use self::event::EventPacket;
5
6mod helpers;
7pub use self::helpers::{parse_message_type, MessageType};
8
9mod metric;
10pub use self::metric::MetricPacket;
11
12mod service_check;
13pub use self::service_check::ServiceCheckPacket;
14
15type NomParserError<'a> = nom::Err<nom::error::Error<&'a [u8]>>;
16
17// This is the lowest sample rate that we consider to be "safe" with typical DogStatsD default settings.
18//
19// Our logic here is:
20// - DogStatsD payloads are limited to 8KiB by default
21// - a valid distribution metric could have a multi-value payload with ~4093 values (value of `1`, when factoring for
22//   protocol overhead)
23// - to avoid overflow in resulting sketch, total count of all values must be less than or equal to 2^64
24// - 2^64 / 4093 = 4.5069006e+15.. which is really big
25// - our DDSketch implementation we write into, however, is effectively capped at ~270M (4096 bins max, `u16` for bin
26//   count, so 4096 * 2^16 = 268,435,456)
27// - we take 260M to be safe, which when calculating the sample rate, gives us 1 / 260,000,000, or 0.000000003845
28const MINIMUM_SAFE_DEFAULT_SAMPLE_RATE: f64 = 0.000000003845;
29
30/// Parser error.
31#[derive(Debug)]
32pub struct ParseError {
33    kind: nom::error::ErrorKind,
34    data: String,
35}
36
37impl fmt::Display for ParseError {
38    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
39        write!(
40            f,
41            "encountered error '{:?}' while processing message '{}'",
42            self.kind, self.data
43        )
44    }
45}
46
47impl std::error::Error for ParseError {}
48
49impl<'a> From<NomParserError<'a>> for ParseError {
50    fn from(err: NomParserError<'a>) -> Self {
51        match err {
52            nom::Err::Error(e) | nom::Err::Failure(e) => Self {
53                kind: e.code,
54                data: String::from_utf8_lossy(e.input).to_string(),
55            },
56            nom::Err::Incomplete(_) => {
57                // The DSD codec uses complete parsers, so `Incomplete` is structurally impossible. Surface it to
58                // Antithesis before the panic guards the invariant.
59                saluki_antithesis::unreachable!("DogStatsD codec received Incomplete from a complete parser");
60                unreachable!("DogStatsD codec only supports complete payloads")
61            }
62        }
63    }
64}
65
66/// A DogStatsD packet.
67pub enum ParsedPacket<'a> {
68    /// Metric.
69    Metric(MetricPacket<'a>),
70
71    /// Event.
72    Event(EventPacket<'a>),
73
74    /// Service check.
75    ServiceCheck(ServiceCheckPacket<'a>),
76}
77
78/// DogStatsD codec configuration.
79#[derive(Clone, Debug)]
80pub struct DogStatsDCodecConfiguration {
81    permissive: bool,
82    maximum_tag_length: usize,
83    maximum_tag_count: usize,
84    timestamps: bool,
85    minimum_sample_rate: f64,
86    client_origin_detection: bool,
87}
88
89impl DogStatsDCodecConfiguration {
90    /// Sets whether or not the codec should operate in permissive mode.
91    ///
92    /// In permissive mode, the codec will attempt to parse as much of the input as possible, relying solely on
93    /// structural markers (specific delimiting characters) to determine the boundaries of different parts of the
94    /// payload. This allows for decoding payloads with invalid contents (for example, characters that are valid UTF-8, but
95    /// aren't within ASCII bounds, etc) such that the data plane can attempt to process them further.
96    ///
97    /// Permissive mode doesn't allow for decoding payloads with structural errors (for example, missing delimiters, etc) or
98    /// that can't be safely handled internally (for example, invalid UTF-8 characters for the metric name or tags).
99    ///
100    /// Defaults to `false`.
101    pub fn with_permissive_mode(mut self, permissive: bool) -> Self {
102        self.permissive = permissive;
103        self
104    }
105
106    /// Sets the maximum tag length.
107    ///
108    /// This controls the number of bytes that are allowed for a single tag. If a tag exceeds this limit, it's
109    /// truncated to the closest previous UTF-8 character boundary, in order to preserve UTF-8 validity.
110    ///
111    /// Defaults to no limit.
112    pub fn with_maximum_tag_length(mut self, maximum_tag_length: usize) -> Self {
113        self.maximum_tag_length = maximum_tag_length;
114        self
115    }
116
117    /// Sets the maximum tag count.
118    ///
119    /// This is the maximum number of tags allowed for a single metric. If the number of tags exceeds this limit,
120    /// remaining tags are simply ignored.
121    ///
122    /// Defaults to no limit.
123    pub fn with_maximum_tag_count(mut self, maximum_tag_count: usize) -> Self {
124        self.maximum_tag_count = maximum_tag_count;
125        self
126    }
127
128    /// Sets whether or not timestamps are read from metrics.
129    ///
130    /// This is generally used in conjunction with aggregating metrics pipelines to control whether or not metrics are
131    /// able to specify their own timestamp in order to be forwarded immediately without aggregation.
132    ///
133    /// Defaults to `true`.
134    pub fn with_timestamps(mut self, timestamps: bool) -> Self {
135        self.timestamps = timestamps;
136        self
137    }
138
139    /// Sets the minimum sample rate.
140    ///
141    /// This is the minimum sample rate that's allowed for a metric payload. If the sample rate is less than this limit,
142    /// the sample rate is clamped to this value and a log message is emitted.
143    ///
144    /// Defaults to `0.000000003845`.
145    pub fn with_minimum_sample_rate(mut self, minimum_sample_rate: f64) -> Self {
146        self.minimum_sample_rate = minimum_sample_rate;
147        self
148    }
149
150    /// Sets whether client-provided origin detection fields are parsed.
151    ///
152    /// When disabled, the `c:` (Local Data), `e:` (External Data), and `card:` (Cardinality) fields are ignored even if
153    /// present in the payload.
154    ///
155    /// Defaults to `false`.
156    pub fn with_client_origin_detection(mut self, enabled: bool) -> Self {
157        self.client_origin_detection = enabled;
158        self
159    }
160}
161
162impl Default for DogStatsDCodecConfiguration {
163    fn default() -> Self {
164        Self {
165            maximum_tag_length: usize::MAX,
166            maximum_tag_count: usize::MAX,
167            timestamps: true,
168            permissive: false,
169            minimum_sample_rate: MINIMUM_SAFE_DEFAULT_SAMPLE_RATE,
170            client_origin_detection: false,
171        }
172    }
173}
174
175/// A [DogStatsD][dsd] codec.
176///
177/// This codec is used to parse the DogStatsD protocol, which is a superset of the StatsD protocol. DogStatsD adds a
178/// number of additional features, such as the ability to specify tags, send histograms directly, send service checks
179/// and events (Datadog-specific), and more.
180///
181/// [dsd]: https://docs.datadoghq.com/developers/dogstatsd/
182#[derive(Clone, Debug)]
183pub struct DogStatsDCodec {
184    config: DogStatsDCodecConfiguration,
185}
186
187impl DogStatsDCodec {
188    /// Sets the given configuration for the codec.
189    ///
190    /// Different aspects of the codec's behavior (such as tag length, tag count, and timestamp parsing) can be
191    /// controlled through its configuration. See [`DogStatsDCodecConfiguration`] for more information.
192    pub fn from_configuration(config: DogStatsDCodecConfiguration) -> Self {
193        Self { config }
194    }
195
196    /// Decodes a DogStatsD packet from the given raw data.
197    ///
198    /// # Errors
199    ///
200    /// If the raw data isn't a valid DogStatsD packet, an error is returned.
201    pub fn decode_packet<'a>(&self, data: &'a [u8]) -> Result<ParsedPacket<'a>, ParseError> {
202        match parse_message_type(data) {
203            MessageType::Event => self.decode_event(data).map(ParsedPacket::Event),
204            MessageType::ServiceCheck => self.decode_service_check(data).map(ParsedPacket::ServiceCheck),
205            MessageType::MetricSample => self.decode_metric(data).map(ParsedPacket::Metric),
206        }
207    }
208
209    fn decode_metric<'a>(&self, data: &'a [u8]) -> Result<MetricPacket<'a>, ParseError> {
210        // Decode the payload and get the representative parts of the metric.
211        // TODO: Can probably assert remaining is empty now.
212        let (_remaining, metric) = self::metric::parse_dogstatsd_metric(data, &self.config)?;
213        Ok(metric)
214    }
215
216    fn decode_event<'a>(&self, data: &'a [u8]) -> Result<EventPacket<'a>, ParseError> {
217        let (_remaining, event) = self::event::parse_dogstatsd_event(data, &self.config)?;
218        Ok(event)
219    }
220
221    fn decode_service_check<'a>(&self, data: &'a [u8]) -> Result<ServiceCheckPacket<'a>, ParseError> {
222        let (_remaining, service_check) = self::service_check::parse_dogstatsd_service_check(data, &self.config)?;
223        Ok(service_check)
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    fn codec() -> DogStatsDCodec {
232        DogStatsDCodec::from_configuration(DogStatsDCodecConfiguration::default())
233    }
234
235    #[test]
236    fn parse_message_type_routes_by_leading_marker() {
237        // Only the exact `_e{` and `_sc|` prefixes select the event/service-check parsers; everything else — including
238        // payloads that merely start with `_e`/`_sc` but lack the structural marker — is treated as a metric sample.
239        assert!(matches!(parse_message_type(b"_e{5,4}:title|text"), MessageType::Event));
240        assert!(matches!(parse_message_type(b"_sc|svc|0"), MessageType::ServiceCheck));
241
242        assert!(matches!(
243            parse_message_type(b"page.views:1|c"),
244            MessageType::MetricSample
245        ));
246        // `_events` / `_scope` share a leading substring with the markers but aren't the markers themselves.
247        assert!(matches!(parse_message_type(b"_events:1|c"), MessageType::MetricSample));
248        assert!(matches!(parse_message_type(b"_scope:1|c"), MessageType::MetricSample));
249        assert!(matches!(parse_message_type(b""), MessageType::MetricSample));
250    }
251
252    #[test]
253    fn decode_packet_dispatches_to_the_matching_parser() {
254        let codec = codec();
255
256        // A metric sample is routed to the metric parser.
257        match codec.decode_packet(b"page.views:1|c").expect("metric should decode") {
258            ParsedPacket::Metric(metric) => assert_eq!(metric.metric_name, "page.views"),
259            _ => panic!("expected a metric packet"),
260        }
261
262        // An `_e{`-prefixed payload is routed to the event parser.
263        match codec.decode_packet(b"_e{5,4}:title|text").expect("event should decode") {
264            ParsedPacket::Event(event) => {
265                assert_eq!(&*event.title, "title");
266                assert_eq!(&*event.text, "text");
267            }
268            _ => panic!("expected an event packet"),
269        }
270
271        // An `_sc|`-prefixed payload is routed to the service-check parser.
272        match codec
273            .decode_packet(b"_sc|my.check|0")
274            .expect("service check should decode")
275        {
276            ParsedPacket::ServiceCheck(service_check) => assert_eq!(&*service_check.name, "my.check"),
277            _ => panic!("expected a service-check packet"),
278        }
279    }
280
281    #[test]
282    fn decode_packet_propagates_parser_errors_from_the_selected_path() {
283        // Dispatch happens before parsing, so a payload with the event marker but an invalid body is routed to the
284        // event parser and surfaces that parser's error rather than being silently retried as a metric.
285        let codec = codec();
286        match codec.decode_packet(b"_e{0,4}:|text") {
287            Err(err) => assert_eq!(err.kind, nom::error::ErrorKind::Verify),
288            Ok(_) => panic!("empty-title event must fail to decode"),
289        }
290    }
291}