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}