relay_statsd/lib.rs
1//! A high-level StatsD metric client built on cadence.
2//!
3//! ## Defining Metrics
4//!
5//! In order to use metrics, one needs to first define one of the metric traits on a custom enum.
6//! The following types of metrics are available: `counter`, `timer`, `gauge`, `distribution`, and
7//! `set`. For explanations on what that means see [Metric Types].
8//!
9//! The metric traits serve only to provide a type safe metric name. All metric types have exactly
10//! the same form, they are different only to ensure that a metric can only be used for the type for
11//! which it was defined, (e.g. a counter metric cannot be used as a timer metric). See the traits
12//! for more detailed examples.
13//!
14//! ## Initializing the Client
15//!
16//! Metrics can be used without initializing a statsd client. In that case, invoking `with_client`
17//! or the [`metric!`] macro will become a noop. Only when configured, metrics will actually be
18//! collected.
19//!
20//! To initialize the client, use [`init`] to create a default client with known arguments:
21//!
22//! ```no_run
23//! # use std::collections::BTreeMap;
24//! # use relay_statsd::MetricsConfig;
25//!
26//! relay_statsd::init(MetricsConfig {
27//! prefix: "myprefix".to_owned(),
28//! host: "localhost:8125".to_owned(),
29//! buffer_size: None,
30//! default_tags: BTreeMap::new(),
31//! });
32//! ```
33//!
34//! ## Macro Usage
35//!
36//! The recommended way to record metrics is by using the [`metric!`] macro. See the trait docs
37//! for more information on how to record each type of metric.
38//!
39//! ```
40//! use relay_statsd::{metric, CounterMetric};
41//!
42//! struct MyCounter;
43//!
44//! impl CounterMetric for MyCounter {
45//! fn name(&self) -> &'static str {
46//! "counter"
47//! }
48//! }
49//!
50//! metric!(counter(MyCounter) += 1);
51//! ```
52//! [Metric Types]: https://github.com/statsd/statsd/blob/master/docs/metric_types.md
53use metrics_exporter_dogstatsd::{AggregationMode, BuildError, DogStatsDBuilder};
54
55use std::{collections::BTreeMap, fmt};
56
57use crate::mock::MockRecorder;
58
59mod mock;
60
61#[doc(hidden)]
62pub mod _metrics {
63 pub use ::metrics::*;
64}
65
66/// Client configuration used for initialization of the metrics sub-system.
67#[derive(Debug)]
68pub struct MetricsConfig {
69 /// Prefix which is appended to all metric names.
70 pub prefix: String,
71 /// Host of the metrics upstream.
72 pub host: String,
73 /// The buffer size to use for the socket.
74 pub buffer_size: Option<usize>,
75 /// Tags that are added to all metrics.
76 pub default_tags: BTreeMap<String, String>,
77}
78
79/// Error returned from [`init`].
80#[derive(Debug)]
81pub struct Error(BuildError);
82
83impl fmt::Display for Error {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 self.0.fmt(f)
86 }
87}
88
89impl std::error::Error for Error {}
90
91impl From<BuildError> for Error {
92 fn from(value: BuildError) -> Self {
93 Self(value)
94 }
95}
96
97/// Set a test client for the period of the called function (only affects the current thread).
98pub fn with_capturing_test_client(f: impl FnOnce()) -> Vec<String> {
99 let recorder = MockRecorder::default();
100 metrics::with_local_recorder(&recorder, f);
101 recorder.consume()
102}
103
104/// Tell the metrics system to report to statsd.
105pub fn init(config: MetricsConfig) -> Result<(), Error> {
106 relay_log::info!("reporting metrics to statsd at {}", config.host);
107
108 let default_labels = config
109 .default_tags
110 .into_iter()
111 .map(|(key, value)| metrics::Label::new(key, value))
112 .collect();
113
114 let mut statsd = DogStatsDBuilder::default()
115 .with_remote_address(&config.host)?
116 .with_telemetry(true)
117 .with_aggregation_mode(AggregationMode::Aggressive)
118 .send_histograms_as_distributions(true)
119 .with_histogram_sampling(true)
120 .set_global_prefix(config.prefix)
121 .with_global_labels(default_labels);
122
123 if let Some(buffer_size) = config.buffer_size {
124 statsd = statsd.with_maximum_payload_length(buffer_size)?;
125 };
126
127 statsd.install()?;
128
129 Ok(())
130}
131
132/// A metric for capturing timings.
133///
134/// Timings are a positive number of milliseconds between a start and end time. Examples include
135/// time taken to render a web page or time taken for a database call to return.
136///
137/// ## Example
138///
139/// ```
140/// use relay_statsd::{metric, TimerMetric};
141///
142/// enum MyTimer {
143/// ProcessA,
144/// ProcessB,
145/// }
146///
147/// impl TimerMetric for MyTimer {
148/// fn name(&self) -> &'static str {
149/// match self {
150/// Self::ProcessA => "process_a",
151/// Self::ProcessB => "process_b",
152/// }
153/// }
154/// }
155///
156/// # fn process_a() {}
157///
158/// // measure time by explicitly setting a std::timer::Duration
159/// # use std::time::Instant;
160/// let start_time = Instant::now();
161/// process_a();
162/// metric!(timer(MyTimer::ProcessA) = start_time.elapsed());
163///
164/// // provide tags to a timer
165/// metric!(
166/// timer(MyTimer::ProcessA) = start_time.elapsed(),
167/// server = "server1",
168/// host = "host1",
169/// );
170///
171/// // measure time implicitly by enclosing a code block in a metric
172/// metric!(timer(MyTimer::ProcessA), {
173/// process_a();
174/// });
175///
176/// // measure block and also provide tags
177/// metric!(
178/// timer(MyTimer::ProcessB),
179/// server = "server1",
180/// host = "host1",
181/// {
182/// process_a();
183/// }
184/// );
185/// ```
186pub trait TimerMetric {
187 /// Returns the timer metric name that will be sent to statsd.
188 fn name(&self) -> &'static str;
189}
190
191/// A metric for capturing counters.
192///
193/// Counters are simple values incremented or decremented by a client. The rates at which these
194/// events occur or average values will be determined by the server receiving them. Examples of
195/// counter uses include number of logins to a system or requests received.
196///
197/// ## Example
198///
199/// ```
200/// use relay_statsd::{metric, CounterMetric};
201///
202/// enum MyCounter {
203/// TotalRequests,
204/// TotalBytes,
205/// }
206///
207/// impl CounterMetric for MyCounter {
208/// fn name(&self) -> &'static str {
209/// match self {
210/// Self::TotalRequests => "total_requests",
211/// Self::TotalBytes => "total_bytes",
212/// }
213/// }
214/// }
215///
216/// # let buffer = &[(), ()];
217///
218/// // add to the counter
219/// metric!(counter(MyCounter::TotalRequests) += 1);
220/// metric!(counter(MyCounter::TotalBytes) += buffer.len() as u64);
221///
222/// // add to the counter and provide tags
223/// metric!(
224/// counter(MyCounter::TotalRequests) += 1,
225/// server = "s1",
226/// host = "h1"
227/// );
228/// ```
229pub trait CounterMetric {
230 /// Returns the counter metric name that will be sent to statsd.
231 fn name(&self) -> &'static str;
232}
233
234/// A metric for capturing distributions.
235///
236/// A distribution is often similar to timers. Distributions can be thought of as a
237/// more general (not limited to timing things) form of timers.
238///
239/// ## Example
240///
241/// ```
242/// use relay_statsd::{metric, DistributionMetric};
243///
244/// struct QueueSize;
245///
246/// impl DistributionMetric for QueueSize {
247/// fn name(&self) -> &'static str {
248/// "queue_size"
249/// }
250/// }
251///
252/// # use std::collections::VecDeque;
253/// let queue = VecDeque::new();
254/// # let _hint: &VecDeque<()> = &queue;
255///
256/// // record a distribution value (uses global sample rate)
257/// metric!(distribution(QueueSize) = queue.len() as u64);
258///
259/// // record with tags
260/// metric!(
261/// distribution(QueueSize) = queue.len() as u64,
262/// server = "server1",
263/// host = "host1",
264/// );
265/// ```
266pub trait DistributionMetric {
267 /// Returns the distribution metric name that will be sent to statsd.
268 fn name(&self) -> &'static str;
269}
270
271/// A metric for capturing gauges.
272///
273/// Gauge values are an instantaneous measurement of a value determined by the client. They do not
274/// change unless changed by the client. Examples include things like load average or how many
275/// connections are active.
276///
277/// ## Example
278///
279/// ```
280/// use relay_statsd::{metric, GaugeMetric};
281///
282/// struct QueueSize;
283///
284/// impl GaugeMetric for QueueSize {
285/// fn name(&self) -> &'static str {
286/// "queue_size"
287/// }
288/// }
289///
290/// # use std::collections::VecDeque;
291/// let queue = VecDeque::new();
292/// # let _hint: &VecDeque<()> = &queue;
293///
294/// // a simple gauge value
295/// metric!(gauge(QueueSize) = queue.len() as u64);
296///
297/// // a gauge with tags
298/// metric!(
299/// gauge(QueueSize) = queue.len() as u64,
300/// server = "server1",
301/// host = "host1"
302/// );
303///
304/// // subtract from the gauge
305/// metric!(gauge(QueueSize) -= 1);
306///
307/// // subtract from the gauge and provide tags
308/// metric!(
309/// gauge(QueueSize) -= 1,
310/// server = "s1",
311/// host = "h1"
312/// );
313/// ```
314pub trait GaugeMetric {
315 /// Returns the gauge metric name that will be sent to statsd.
316 fn name(&self) -> &'static str;
317}
318
319#[doc(hidden)]
320#[macro_export]
321macro_rules! key_var {
322 ($id:expr $(,)*) => {{
323 let name = $crate::_metrics::KeyName::from_const_str($id);
324 $crate::_metrics::Key::from_static_labels(name, &[])
325 }};
326 ($id:expr $(, $k:expr => $v:expr)* $(,)?) => {{
327 let name = $crate::_metrics::KeyName::from_const_str($id);
328 let labels = ::std::vec![
329 $($crate::_metrics::Label::new(
330 $crate::_metrics::SharedString::const_str($k),
331 $crate::_metrics::SharedString::from_owned($v.into())
332 )),*
333 ];
334
335 $crate::_metrics::Key::from_parts(name, labels)
336 }};
337}
338
339/// Emits a metric.
340///
341/// See [crate-level documentation](self) for examples.
342#[macro_export]
343macro_rules! metric {
344 // counter increment
345 (counter($id:expr) += $value:expr $(, $($k:ident).* = $v:expr)* $(,)?) => {{
346 match $value {
347 value if value != 0 => {
348 let key = $crate::key_var!($crate::CounterMetric::name(&$id) $(, stringify!($($k).*) => $v)*);
349 let metadata = $crate::_metrics::metadata_var!(::std::module_path!(), $crate::_metrics::Level::INFO);
350 $crate::_metrics::with_recorder(|recorder| recorder.register_counter(&key, metadata))
351 .increment(value);
352 }
353 _ => {}
354 }
355 }};
356
357 // gauge set
358 (gauge($id:expr) = $value:expr $(, $($k:ident).* = $v:expr)* $(,)?) => {{
359 let key = $crate::key_var!($crate::GaugeMetric::name(&$id) $(, stringify!($($k).*) => $v)*);
360 let metadata = $crate::_metrics::metadata_var!(::std::module_path!(), $crate::_metrics::Level::INFO);
361 $crate::_metrics::with_recorder(|recorder| recorder.register_gauge(&key, metadata))
362 .set($value as f64);
363 }};
364 // gauge increment
365 (gauge($id:expr) += $value:expr $(, $($k:ident).* = $v:expr)* $(,)?) => {{
366 let key = $crate::key_var!($crate::GaugeMetric::name(&$id) $(, stringify!($($k).*) => $v)*);
367 let metadata = $crate::_metrics::metadata_var!(::std::module_path!(), $crate::_metrics::Level::INFO);
368 $crate::_metrics::with_recorder(|recorder| recorder.register_gauge(&key, metadata))
369 .increment($value as f64);
370 }};
371 // gauge decrement
372 (gauge($id:expr) -= $value:expr $(, $($k:ident).* = $v:expr)* $(,)?) => {{
373 let key = $crate::key_var!($crate::GaugeMetric::name(&$id) $(, stringify!($($k).*) => $v)*);
374 let metadata = $crate::_metrics::metadata_var!(::std::module_path!(), $crate::_metrics::Level::INFO);
375 $crate::_metrics::with_recorder(|recorder| recorder.register_gauge(&key, metadata))
376 .decrement($value as f64);
377 }};
378
379 // distribution
380 (distribution($id:expr) = $value:expr $(, $($k:ident).* = $v:expr)* $(,)?) => {{
381 let key = $crate::key_var!($crate::DistributionMetric::name(&$id) $(, stringify!($($k).*) => $v)*);
382 let metadata = $crate::_metrics::metadata_var!(::std::module_path!(), $crate::_metrics::Level::INFO);
383 $crate::_metrics::with_recorder(|recorder| recorder.register_histogram(&key, metadata))
384 .record($value as f64);
385 }};
386
387 // timer value
388 (timer($id:expr) = $value:expr $(, $($k:ident).* = $v:expr)* $(,)?) => {{
389 let key = $crate::key_var!($crate::TimerMetric::name(&$id) $(, stringify!($($k).*) => $v)*);
390 let metadata = $crate::_metrics::metadata_var!(::std::module_path!(), $crate::_metrics::Level::INFO);
391 $crate::_metrics::with_recorder(|recorder| recorder.register_histogram(&key, metadata))
392 .record($value.as_nanos() as f64 / 1e6);
393 }};
394
395 // timed block
396 (timer($id:expr), $($($k:ident).* = $v:expr,)* $block:block) => {{
397 struct TimerOnDrop {
398 start: std::time::Instant,
399 key: $crate::_metrics::Key,
400 }
401 impl Drop for TimerOnDrop {
402 fn drop(&mut self) {
403 let elapsed = self.start.elapsed();
404 let metadata = $crate::_metrics::metadata_var!(::std::module_path!(), $crate::_metrics::Level::INFO);
405 $crate::_metrics::with_recorder(|recorder| recorder.register_histogram(&self.key, metadata))
406 .record(elapsed.as_nanos() as f64 / 1e6);
407 }
408 }
409
410 let _timer = TimerOnDrop {
411 start: std::time::Instant::now(),
412 key: $crate::key_var!($crate::TimerMetric::name(&$id) $(, stringify!($($k).*) => $v)*),
413 };
414 {$block}
415 }};
416}
417
418#[cfg(test)]
419mod tests {
420 use std::time::Duration;
421
422 use super::*;
423
424 enum TestGauges {
425 Foo,
426 Bar,
427 }
428
429 impl GaugeMetric for TestGauges {
430 fn name(&self) -> &'static str {
431 match self {
432 Self::Foo => "foo",
433 Self::Bar => "bar",
434 }
435 }
436 }
437
438 struct TestCounter;
439
440 impl CounterMetric for TestCounter {
441 fn name(&self) -> &'static str {
442 "counter"
443 }
444 }
445
446 struct TestDistribution;
447
448 impl DistributionMetric for TestDistribution {
449 fn name(&self) -> &'static str {
450 "distribution"
451 }
452 }
453
454 struct TestTimer;
455
456 impl TimerMetric for TestTimer {
457 fn name(&self) -> &'static str {
458 "timer"
459 }
460 }
461
462 #[test]
463 fn test_capturing_client() {
464 let captures = with_capturing_test_client(|| {
465 metric!(
466 gauge(TestGauges::Foo) = 123,
467 server = "server1",
468 host = "host1"
469 );
470 metric!(
471 gauge(TestGauges::Bar) = 456,
472 server = "server2",
473 host = "host2"
474 );
475 });
476
477 assert_eq!(
478 captures,
479 [
480 "foo:123|g|#server:server1,host:host1",
481 "bar:456|g|#server:server2,host:host2"
482 ]
483 )
484 }
485
486 #[test]
487 fn test_counter_tags_with_dots() {
488 let captures = with_capturing_test_client(|| {
489 metric!(
490 counter(TestCounter) += 10,
491 hc.project_id = "567",
492 server = "server1",
493 );
494 metric!(
495 counter(TestCounter) += 5,
496 hc.project_id = "567",
497 server = "server1",
498 );
499 });
500 assert_eq!(
501 captures,
502 [
503 "counter:10|c|#hc.project_id:567,server:server1",
504 "counter:5|c|#hc.project_id:567,server:server1"
505 ]
506 );
507 }
508
509 #[test]
510 fn test_gauge_tags_with_dots() {
511 let captures = with_capturing_test_client(|| {
512 metric!(
513 gauge(TestGauges::Foo) = 123,
514 hc.project_id = "567",
515 server = "server1",
516 );
517 });
518 assert_eq!(captures, ["foo:123|g|#hc.project_id:567,server:server1"]);
519 }
520
521 #[test]
522 fn test_distribution_tags_with_dots() {
523 let captures = with_capturing_test_client(|| {
524 metric!(
525 distribution(TestDistribution) = 123,
526 hc.project_id = "567",
527 server = "server1",
528 );
529 });
530 assert_eq!(
531 captures,
532 ["distribution:123|d|#hc.project_id:567,server:server1"]
533 );
534 }
535
536 #[test]
537 fn test_timer_tags_with_dots() {
538 let captures = with_capturing_test_client(|| {
539 let duration = Duration::from_secs(100);
540 metric!(
541 timer(TestTimer) = duration,
542 hc.project_id = "567",
543 server = "server1",
544 );
545 });
546 assert_eq!(
547 captures,
548 ["timer:100000|d|#hc.project_id:567,server:server1"]
549 );
550 }
551
552 #[test]
553 fn test_timed_block_tags_with_dots() {
554 let captures = with_capturing_test_client(|| {
555 metric!(
556 timer(TestTimer),
557 hc.project_id = "567",
558 server = "server1",
559 {
560 // your code could be here
561 }
562 )
563 });
564 // just check the tags to not make this flaky
565 assert!(captures[0].ends_with("|d|#hc.project_id:567,server:server1"));
566 }
567
568 #[test]
569 fn test_timer_nanos_rounding_error() {
570 let one_day = Duration::from_secs(60 * 60 * 24);
571 let captures = with_capturing_test_client(|| {
572 metric!(timer(TestTimer) = one_day + Duration::from_nanos(1),);
573 });
574
575 // for "short" durations, precision is preserved:
576 assert_eq!(captures, ["timer:86400000.000001|d|#"]);
577
578 let one_year = Duration::from_secs(60 * 60 * 24 * 365);
579 let captures = with_capturing_test_client(|| {
580 metric!(timer(TestTimer) = one_year + Duration::from_nanos(1),);
581 });
582
583 // for very long durations, precision is lost:
584 assert_eq!(captures, ["timer:31536000000|d|#"]);
585 }
586
587 #[test]
588 fn test_timer_early_return() {
589 #[expect(unreachable_code)]
590 let captures = with_capturing_test_client(|| {
591 metric!(timer(TestTimer), {
592 // An early return still needs to capture the timer.
593 return;
594 });
595 // This shouldn't be reached due to the `return`.
596 unreachable!()
597 });
598 assert_eq!(captures.len(), 1);
599 }
600}