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 let now = std::time::Instant::now();
398 let rv = {$block};
399 $crate::metric!(timer($id) = now.elapsed() $(, $($k).* = $v)*);
400 rv
401 }};
402}
403
404#[cfg(test)]
405mod tests {
406 use std::time::Duration;
407
408 use super::*;
409
410 enum TestGauges {
411 Foo,
412 Bar,
413 }
414
415 impl GaugeMetric for TestGauges {
416 fn name(&self) -> &'static str {
417 match self {
418 Self::Foo => "foo",
419 Self::Bar => "bar",
420 }
421 }
422 }
423
424 struct TestCounter;
425
426 impl CounterMetric for TestCounter {
427 fn name(&self) -> &'static str {
428 "counter"
429 }
430 }
431
432 struct TestDistribution;
433
434 impl DistributionMetric for TestDistribution {
435 fn name(&self) -> &'static str {
436 "distribution"
437 }
438 }
439
440 struct TestTimer;
441
442 impl TimerMetric for TestTimer {
443 fn name(&self) -> &'static str {
444 "timer"
445 }
446 }
447
448 #[test]
449 fn test_capturing_client() {
450 let captures = with_capturing_test_client(|| {
451 metric!(
452 gauge(TestGauges::Foo) = 123,
453 server = "server1",
454 host = "host1"
455 );
456 metric!(
457 gauge(TestGauges::Bar) = 456,
458 server = "server2",
459 host = "host2"
460 );
461 });
462
463 assert_eq!(
464 captures,
465 [
466 "foo:123|g|#server:server1,host:host1",
467 "bar:456|g|#server:server2,host:host2"
468 ]
469 )
470 }
471
472 #[test]
473 fn test_counter_tags_with_dots() {
474 let captures = with_capturing_test_client(|| {
475 metric!(
476 counter(TestCounter) += 10,
477 hc.project_id = "567",
478 server = "server1",
479 );
480 metric!(
481 counter(TestCounter) += 5,
482 hc.project_id = "567",
483 server = "server1",
484 );
485 });
486 assert_eq!(
487 captures,
488 [
489 "counter:10|c|#hc.project_id:567,server:server1",
490 "counter:5|c|#hc.project_id:567,server:server1"
491 ]
492 );
493 }
494
495 #[test]
496 fn test_gauge_tags_with_dots() {
497 let captures = with_capturing_test_client(|| {
498 metric!(
499 gauge(TestGauges::Foo) = 123,
500 hc.project_id = "567",
501 server = "server1",
502 );
503 });
504 assert_eq!(captures, ["foo:123|g|#hc.project_id:567,server:server1"]);
505 }
506
507 #[test]
508 fn test_distribution_tags_with_dots() {
509 let captures = with_capturing_test_client(|| {
510 metric!(
511 distribution(TestDistribution) = 123,
512 hc.project_id = "567",
513 server = "server1",
514 );
515 });
516 assert_eq!(
517 captures,
518 ["distribution:123|d|#hc.project_id:567,server:server1"]
519 );
520 }
521
522 #[test]
523 fn test_timer_tags_with_dots() {
524 let captures = with_capturing_test_client(|| {
525 let duration = Duration::from_secs(100);
526 metric!(
527 timer(TestTimer) = duration,
528 hc.project_id = "567",
529 server = "server1",
530 );
531 });
532 assert_eq!(
533 captures,
534 ["timer:100000|d|#hc.project_id:567,server:server1"]
535 );
536 }
537
538 #[test]
539 fn test_timed_block_tags_with_dots() {
540 let captures = with_capturing_test_client(|| {
541 metric!(
542 timer(TestTimer),
543 hc.project_id = "567",
544 server = "server1",
545 {
546 // your code could be here
547 }
548 )
549 });
550 // just check the tags to not make this flaky
551 assert!(captures[0].ends_with("|d|#hc.project_id:567,server:server1"));
552 }
553
554 #[test]
555 fn nanos_rounding_error() {
556 let one_day = Duration::from_secs(60 * 60 * 24);
557 let captures = with_capturing_test_client(|| {
558 metric!(timer(TestTimer) = one_day + Duration::from_nanos(1),);
559 });
560
561 // for "short" durations, precision is preserved:
562 assert_eq!(captures, ["timer:86400000.000001|d|#"]);
563
564 let one_year = Duration::from_secs(60 * 60 * 24 * 365);
565 let captures = with_capturing_test_client(|| {
566 metric!(timer(TestTimer) = one_year + Duration::from_nanos(1),);
567 });
568
569 // for very long durations, precision is lost:
570 assert_eq!(captures, ["timer:31536000000|d|#"]);
571 }
572}