1use std::{
2 collections::BTreeMap,
3 net::{SocketAddr, UdpSocket},
4 sync::Mutex,
5};
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
14pub struct StatEvent {
15 pub name: String,
16 pub tags: BTreeMap<String, String>,
18 pub value: f64,
19 pub kind: StatKind,
20}
21
22#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
23pub enum StatKind {
24 Counter,
26 Gauge,
28 TimerMs,
30}
31
32impl StatEvent {
33 pub fn counter(name: &str, tags: &[(&str, &str)], value: f64) -> Self {
34 Self::new(name, tags, value, StatKind::Counter)
35 }
36
37 pub fn gauge(name: &str, tags: &[(&str, &str)], value: f64) -> Self {
38 Self::new(name, tags, value, StatKind::Gauge)
39 }
40
41 pub fn timer_ms(name: &str, tags: &[(&str, &str)], value: f64) -> Self {
42 Self::new(name, tags, value, StatKind::TimerMs)
43 }
44
45 fn new(name: &str, tags: &[(&str, &str)], value: f64, kind: StatKind) -> Self {
46 Self {
47 name: sanitize(name),
48 tags: tags
49 .iter()
50 .map(|(k, v)| (sanitize(k), sanitize(v)))
51 .collect(),
52 value,
53 kind,
54 }
55 }
56
57 pub fn dogstatsd_line(&self) -> String {
59 let kind = match self.kind {
60 StatKind::Counter => "c",
61 StatKind::Gauge => "g",
62 StatKind::TimerMs => "ms",
63 };
64 let mut line = format!("{}:{}|{}", sanitize(&self.name), self.value, kind);
65 if !self.tags.is_empty() {
66 line.push_str("|#");
67 let mut first = true;
68 for (k, v) in &self.tags {
69 if !first {
70 line.push(',');
71 }
72 first = false;
73 line.push_str(&sanitize(k));
74 line.push(':');
75 line.push_str(&sanitize(v));
76 }
77 }
78 line
79 }
80}
81
82fn sanitize(s: &str) -> String {
84 s.replace([':', '|', '#', ',', '\n', '@', '='], "_")
85}
86
87const MAX_DATAGRAM: usize = 1400;
89
90pub struct StatsdUdpSink {
93 sock: UdpSocket,
94 dest: SocketAddr,
95}
96
97impl StatsdUdpSink {
98 pub fn new(dest: SocketAddr) -> std::io::Result<Self> {
99 let bind: SocketAddr = if dest.is_ipv4() {
100 "0.0.0.0:0".parse().unwrap()
101 } else {
102 "[::]:0".parse().unwrap()
103 };
104 let sock = UdpSocket::bind(bind)?;
105 sock.set_nonblocking(true)?;
106 Ok(Self { sock, dest })
107 }
108
109 pub fn send_one(&self, event: &StatEvent) {
110 let _ = self
111 .sock
112 .send_to(event.dogstatsd_line().as_bytes(), self.dest);
113 }
114
115 pub fn send_many<'a>(&self, events: impl IntoIterator<Item = &'a StatEvent>) {
117 let mut buf = String::new();
118 for event in events {
119 let line = event.dogstatsd_line();
120 if !buf.is_empty() && buf.len() + 1 + line.len() > MAX_DATAGRAM {
121 let _ = self.sock.send_to(buf.as_bytes(), self.dest);
122 buf.clear();
123 }
124 if !buf.is_empty() {
125 buf.push('\n');
126 }
127 buf.push_str(&line);
128 }
129 if !buf.is_empty() {
130 let _ = self.sock.send_to(buf.as_bytes(), self.dest);
131 }
132 }
133}
134
135#[derive(Default)]
139pub struct StatBatcher {
140 inner: Mutex<BatcherInner>,
141}
142
143#[derive(Default)]
144struct BatcherInner {
145 counters: BTreeMap<(String, BTreeMap<String, String>), f64>,
146 gauges: BTreeMap<(String, BTreeMap<String, String>), f64>,
147 timers: Vec<StatEvent>,
148}
149
150impl StatBatcher {
151 pub fn new() -> Self {
152 Self::default()
153 }
154
155 pub fn push(&self, event: StatEvent) {
156 let mut inner = self.inner.lock().unwrap();
157 match event.kind {
158 StatKind::Counter => {
159 *inner
160 .counters
161 .entry((event.name, event.tags))
162 .or_insert(0.0) += event.value;
163 }
164 StatKind::Gauge => {
165 inner.gauges.insert((event.name, event.tags), event.value);
166 }
167 StatKind::TimerMs => inner.timers.push(event),
168 }
169 }
170
171 pub fn counter(&self, name: &str, tags: &[(&str, &str)], value: f64) {
172 self.push(StatEvent::counter(name, tags, value));
173 }
174
175 pub fn gauge(&self, name: &str, tags: &[(&str, &str)], value: f64) {
176 self.push(StatEvent::gauge(name, tags, value));
177 }
178
179 pub fn timer_ms(&self, name: &str, tags: &[(&str, &str)], value: f64) {
180 self.push(StatEvent::timer_ms(name, tags, value));
181 }
182
183 pub fn drain(&self) -> Vec<StatEvent> {
185 let mut inner = self.inner.lock().unwrap();
186 let counters = std::mem::take(&mut inner.counters);
187 let gauges = std::mem::take(&mut inner.gauges);
188 let timers = std::mem::take(&mut inner.timers);
189 counters
190 .into_iter()
191 .map(|((name, tags), value)| StatEvent {
192 name,
193 tags,
194 value,
195 kind: StatKind::Counter,
196 })
197 .chain(gauges.into_iter().map(|((name, tags), value)| StatEvent {
198 name,
199 tags,
200 value,
201 kind: StatKind::Gauge,
202 }))
203 .chain(timers)
204 .collect()
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn dogstatsd_line_formats_counter_with_tags() {
214 let event = StatEvent::counter(
215 "bridge_bytes",
216 &[("pool", "hk"), ("country", "CN")],
217 1048576.0,
218 );
219 assert_eq!(
220 event.dogstatsd_line(),
221 "bridge_bytes:1048576|c|#country:CN,pool:hk"
222 );
223 }
224
225 #[test]
226 fn dogstatsd_line_formats_untagged_gauge() {
227 let event = StatEvent::gauge("plus", &[], 17.0);
228 assert_eq!(event.dogstatsd_line(), "plus:17|g");
229 }
230
231 #[test]
232 fn dogstatsd_line_formats_timer() {
233 let event = StatEvent::timer_ms("broker_rpc_calls", &[("method", "get_exits")], 12.5);
234 assert_eq!(
235 event.dogstatsd_line(),
236 "broker_rpc_calls:12.5|ms|#method:get_exits"
237 );
238 }
239
240 #[test]
241 fn sanitize_strips_protocol_characters() {
242 let event = StatEvent::gauge("we|ird:name", &[("ta#g", "va,lue")], 1.0);
243 assert_eq!(event.dogstatsd_line(), "we_ird_name:1|g|#ta_g:va_lue");
244 }
245
246 #[test]
247 fn batcher_sums_counters_and_overwrites_gauges() {
248 let batcher = StatBatcher::new();
249 batcher.counter("bytes", &[("pool", "a")], 10.0);
250 batcher.counter("bytes", &[("pool", "a")], 5.0);
251 batcher.counter("bytes", &[("pool", "b")], 1.0);
252 batcher.gauge("load", &[], 0.5);
253 batcher.gauge("load", &[], 0.7);
254
255 let mut drained = batcher.drain();
256 drained.sort_by(|a, b| (&a.name, &a.tags).cmp(&(&b.name, &b.tags)));
257 assert_eq!(drained.len(), 3);
258 assert_eq!(drained[0].value, 15.0);
259 assert_eq!(drained[1].value, 1.0);
260 assert_eq!(drained[2].value, 0.7);
261 assert!(batcher.drain().is_empty());
262 }
263
264 #[test]
265 fn batcher_keeps_individual_timers() {
266 let batcher = StatBatcher::new();
267 batcher.timer_ms("lat", &[], 1.0);
268 batcher.timer_ms("lat", &[], 2.0);
269 assert_eq!(batcher.drain().len(), 2);
270 }
271
272 #[test]
273 fn send_many_packs_multiple_lines() {
274 let receiver = UdpSocket::bind("127.0.0.1:0").unwrap();
275 let sink = StatsdUdpSink::new(receiver.local_addr().unwrap()).unwrap();
276 sink.send_many(&[
277 StatEvent::gauge("a", &[], 1.0),
278 StatEvent::gauge("b", &[], 2.0),
279 ]);
280 let mut buf = [0u8; 2048];
281 let (n, _) = receiver.recv_from(&mut buf).unwrap();
282 assert_eq!(std::str::from_utf8(&buf[..n]).unwrap(), "a:1|g\nb:2|g");
283 }
284}