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`, `histogram`, 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, either use [`set_client`] to pass a custom client, or use
21//! [`init`] to create a default client with known arguments:
22//!
23//! ```no_run
24//! # use std::collections::BTreeMap;
25//! # use relay_statsd::MetricsClientConfig;
26//!
27//! relay_statsd::init(MetricsClientConfig {
28//!     prefix: "myprefix",
29//!     host: "localhost:8125",
30//!     default_tags: BTreeMap::new(),
31//!     sample_rate: 1.0,
32//!     aggregate: true,
33//!     allow_high_cardinality_tags: false
34//! });
35//! ```
36//!
37//! ## Macro Usage
38//!
39//! The recommended way to record metrics is by using the [`metric!`] macro. See the trait docs
40//! for more information on how to record each type of metric.
41//!
42//! ```
43//! use relay_statsd::{metric, CounterMetric};
44//!
45//! struct MyCounter;
46//!
47//! impl CounterMetric for MyCounter {
48//!     fn name(&self) -> &'static str {
49//!         "counter"
50//!     }
51//! }
52//!
53//! metric!(counter(MyCounter) += 1);
54//! ```
55//!
56//! ## Manual Usage
57//!
58//! ```
59//! use relay_statsd::prelude::*;
60//!
61//! relay_statsd::with_client(|client| {
62//!     client.count("mymetric", 1).ok();
63//! });
64//! ```
65//!
66//! [Metric Types]: https://github.com/statsd/statsd/blob/master/docs/metric_types.md
67pub use statsdproxy::config::DenyTagConfig;
68
69use cadence::{Metric, MetricBuilder, StatsdClient};
70use parking_lot::RwLock;
71use rand::distributions::{Distribution, Uniform};
72use statsdproxy::cadence::StatsdProxyMetricSink;
73use statsdproxy::config::AggregateMetricsConfig;
74use statsdproxy::middleware::deny_tag::DenyTag;
75use std::collections::BTreeMap;
76use std::net::ToSocketAddrs;
77use std::ops::{Deref, DerefMut};
78use std::sync::Arc;
79use std::time::Duration;
80
81/// Maximum number of metric events that can be queued before we start dropping them
82const METRICS_MAX_QUEUE_SIZE: usize = 100_000;
83
84/// Client configuration object to store globally.
85#[derive(Debug)]
86pub struct MetricsClient {
87    /// The raw statsd client.
88    pub statsd_client: StatsdClient,
89    /// Default tags to apply to every metric.
90    pub default_tags: BTreeMap<String, String>,
91    /// Global sample rate.
92    pub sample_rate: f32,
93    /// Receiver for external listeners.
94    ///
95    /// Only available when the client was initialized with `init_basic`.
96    pub rx: Option<crossbeam_channel::Receiver<Vec<u8>>>,
97}
98
99/// Client configuration used for initialization of [`MetricsClient`].
100#[derive(Debug)]
101pub struct MetricsClientConfig<'a, A> {
102    /// Prefix which is appended to all metric names.
103    pub prefix: &'a str,
104    /// Host of the metrics upstream.
105    pub host: A,
106    /// Tags that are added to all metrics.
107    pub default_tags: BTreeMap<String, String>,
108    /// Sample rate for metrics, between 0.0 (= 0%) and 1.0 (= 100%)
109    pub sample_rate: f32,
110    /// If metrics should be batched or send immediately upstream.
111    pub aggregate: bool,
112    /// If high cardinality tags should be removed from metrics.
113    pub allow_high_cardinality_tags: bool,
114}
115
116impl Deref for MetricsClient {
117    type Target = StatsdClient;
118
119    fn deref(&self) -> &StatsdClient {
120        &self.statsd_client
121    }
122}
123
124impl DerefMut for MetricsClient {
125    fn deref_mut(&mut self) -> &mut StatsdClient {
126        &mut self.statsd_client
127    }
128}
129
130impl MetricsClient {
131    /// Send a metric with the default tags defined on this `MetricsClient`.
132    #[inline(always)]
133    pub fn send_metric<'a, T>(&'a self, mut metric: MetricBuilder<'a, '_, T>)
134    where
135        T: Metric + From<String>,
136    {
137        if !self._should_send() {
138            return;
139        }
140
141        for (k, v) in &self.default_tags {
142            metric = metric.with_tag(k, v);
143        }
144
145        if let Err(error) = metric.try_send() {
146            relay_log::error!(
147                error = &error as &dyn std::error::Error,
148                maximum_capacity = METRICS_MAX_QUEUE_SIZE,
149                "Error sending a metric",
150            );
151        }
152    }
153
154    fn _should_send(&self) -> bool {
155        if self.sample_rate <= 0.0 {
156            false
157        } else if self.sample_rate >= 1.0 {
158            true
159        } else {
160            // Using thread local RNG and uniform distribution here because Rng::gen_range is
161            // "optimized for the case that only a single sample is made from the given range".
162            // See https://docs.rs/rand/0.7.3/rand/distributions/uniform/struct.Uniform.html for more
163            // details.
164            let mut rng = rand::thread_rng();
165            RNG_UNIFORM_DISTRIBUTION
166                .with(|uniform_dist| uniform_dist.sample(&mut rng) <= self.sample_rate)
167        }
168    }
169}
170
171static METRICS_CLIENT: RwLock<Option<Arc<MetricsClient>>> = RwLock::new(None);
172
173thread_local! {
174    static CURRENT_CLIENT: std::cell::RefCell<Option<Arc<MetricsClient>>>  = METRICS_CLIENT.read().clone().into();
175    static RNG_UNIFORM_DISTRIBUTION: Uniform<f32> = Uniform::new(0.0, 1.0);
176}
177
178/// Internal prelude for the macro
179#[doc(hidden)]
180pub mod _pred {
181    pub use cadence::prelude::*;
182}
183
184/// The metrics prelude that is necessary to use the client.
185pub mod prelude {
186    pub use cadence::prelude::*;
187}
188
189/// Set a new statsd client.
190pub fn set_client(client: MetricsClient) {
191    *METRICS_CLIENT.write() = Some(Arc::new(client));
192    CURRENT_CLIENT.with(|cell| cell.replace(METRICS_CLIENT.read().clone()));
193}
194
195/// Set a test client for the period of the called function (only affects the current thread).
196// TODO: replace usages with `init_basic`
197pub fn with_capturing_test_client(f: impl FnOnce()) -> Vec<String> {
198    let (rx, sink) = cadence::SpyMetricSink::new();
199    let test_client = MetricsClient {
200        statsd_client: StatsdClient::from_sink("", sink),
201        default_tags: Default::default(),
202        sample_rate: 1.0,
203        rx: None,
204    };
205
206    CURRENT_CLIENT.with(|cell| {
207        let old_client = cell.replace(Some(Arc::new(test_client)));
208        f();
209        cell.replace(old_client);
210    });
211
212    rx.iter().map(|x| String::from_utf8(x).unwrap()).collect()
213}
214
215// Setup a simple metrics listener.
216//
217// Returns `None` if the global metrics client has already been configured.
218pub fn init_basic() -> Option<crossbeam_channel::Receiver<Vec<u8>>> {
219    CURRENT_CLIENT.with(|cell| {
220        if cell.borrow().is_none() {
221            // Setup basic observable metrics sink.
222            let (receiver, sink) = cadence::SpyMetricSink::new();
223            let test_client = MetricsClient {
224                statsd_client: StatsdClient::from_sink("", sink),
225                default_tags: Default::default(),
226                sample_rate: 1.0,
227                rx: Some(receiver.clone()),
228            };
229            cell.replace(Some(Arc::new(test_client)));
230        }
231    });
232
233    CURRENT_CLIENT.with(|cell| {
234        cell.borrow()
235            .as_deref()
236            .and_then(|client| match &client.rx {
237                Some(rx) => Some(rx.clone()),
238                None => {
239                    relay_log::error!("Metrics client was already set up.");
240                    None
241                }
242            })
243    })
244}
245
246/// Disable the client again.
247pub fn disable() {
248    *METRICS_CLIENT.write() = None;
249}
250
251/// Tell the metrics system to report to statsd.
252pub fn init<A: ToSocketAddrs>(config: MetricsClientConfig<A>) {
253    let addrs: Vec<_> = config.host.to_socket_addrs().unwrap().collect();
254    if !addrs.is_empty() {
255        relay_log::info!("reporting metrics to statsd at {}", addrs[0]);
256    }
257
258    // Normalize sample_rate
259    let sample_rate = config.sample_rate.clamp(0., 1.);
260    relay_log::debug!(
261        "metrics sample rate is set to {sample_rate}{}",
262        if sample_rate == 0.0 {
263            ", no metrics will be reported"
264        } else {
265            ""
266        }
267    );
268
269    let deny_config = DenyTagConfig {
270        starts_with: match config.allow_high_cardinality_tags {
271            true => vec![],
272            false => vec!["hc.".to_owned()],
273        },
274        tags: vec![],
275        ends_with: vec![],
276    };
277
278    let statsd_client = if config.aggregate {
279        let statsdproxy_sink = StatsdProxyMetricSink::new(move || {
280            let upstream = statsdproxy::middleware::upstream::Upstream::new(addrs[0])
281                .expect("failed to create statsdproxy metric sink");
282
283            let aggregate = statsdproxy::middleware::aggregate::AggregateMetrics::new(
284                AggregateMetricsConfig {
285                    aggregate_gauges: true,
286                    aggregate_counters: true,
287                    flush_interval: Duration::from_millis(50),
288                    flush_offset: 0,
289                    max_map_size: None,
290                },
291                upstream,
292            );
293
294            DenyTag::new(deny_config.clone(), aggregate)
295        });
296
297        StatsdClient::from_sink(config.prefix, statsdproxy_sink)
298    } else {
299        let statsdproxy_sink = StatsdProxyMetricSink::new(move || {
300            let upstream = statsdproxy::middleware::upstream::Upstream::new(addrs[0])
301                .expect("failed to create statsdproxy metric sind");
302
303            DenyTag::new(deny_config.clone(), upstream)
304        });
305        StatsdClient::from_sink(config.prefix, statsdproxy_sink)
306    };
307
308    set_client(MetricsClient {
309        statsd_client,
310        default_tags: config.default_tags,
311        sample_rate,
312        rx: None,
313    });
314}
315
316/// Invoke a callback with the current statsd client.
317///
318/// If statsd is not configured the callback is not invoked.  For the most part
319/// the [`metric!`] macro should be used instead.
320#[inline(always)]
321pub fn with_client<F, R>(f: F) -> R
322where
323    F: FnOnce(&MetricsClient) -> R,
324    R: Default,
325{
326    CURRENT_CLIENT.with(|client| {
327        if let Some(client) = client.borrow().as_deref() {
328            f(client)
329        } else {
330            R::default()
331        }
332    })
333}
334
335/// A metric for capturing timings.
336///
337/// Timings are a positive number of milliseconds between a start and end time. Examples include
338/// time taken to render a web page or time taken for a database call to return.
339///
340/// ## Example
341///
342/// ```
343/// use relay_statsd::{metric, TimerMetric};
344///
345/// enum MyTimer {
346///     ProcessA,
347///     ProcessB,
348/// }
349///
350/// impl TimerMetric for MyTimer {
351///     fn name(&self) -> &'static str {
352///         match self {
353///             Self::ProcessA => "process_a",
354///             Self::ProcessB => "process_b",
355///         }
356///     }
357/// }
358///
359/// # fn process_a() {}
360///
361/// // measure time by explicitly setting a std::timer::Duration
362/// # use std::time::Instant;
363/// let start_time = Instant::now();
364/// process_a();
365/// metric!(timer(MyTimer::ProcessA) = start_time.elapsed());
366///
367/// // provide tags to a timer
368/// metric!(
369///     timer(MyTimer::ProcessA) = start_time.elapsed(),
370///     server = "server1",
371///     host = "host1",
372/// );
373///
374/// // measure time implicitly by enclosing a code block in a metric
375/// metric!(timer(MyTimer::ProcessA), {
376///     process_a();
377/// });
378///
379/// // measure block and also provide tags
380/// metric!(
381///     timer(MyTimer::ProcessB),
382///     server = "server1",
383///     host = "host1",
384///     {
385///         process_a();
386///     }
387/// );
388///
389/// ```
390pub trait TimerMetric {
391    /// Returns the timer metric name that will be sent to statsd.
392    fn name(&self) -> &'static str;
393}
394
395/// A metric for capturing counters.
396///
397/// Counters are simple values incremented or decremented by a client. The rates at which these
398/// events occur or average values will be determined by the server receiving them. Examples of
399/// counter uses include number of logins to a system or requests received.
400///
401/// ## Example
402///
403/// ```
404/// use relay_statsd::{metric, CounterMetric};
405///
406/// enum MyCounter {
407///     TotalRequests,
408///     TotalBytes,
409/// }
410///
411/// impl CounterMetric for MyCounter {
412///     fn name(&self) -> &'static str {
413///         match self {
414///             Self::TotalRequests => "total_requests",
415///             Self::TotalBytes => "total_bytes",
416///         }
417///     }
418/// }
419///
420/// # let buffer = &[(), ()];
421///
422/// // add to the counter
423/// metric!(counter(MyCounter::TotalRequests) += 1);
424/// metric!(counter(MyCounter::TotalBytes) += buffer.len() as i64);
425///
426/// // add to the counter and provide tags
427/// metric!(
428///     counter(MyCounter::TotalRequests) += 1,
429///     server = "s1",
430///     host = "h1"
431/// );
432///
433/// // subtract from the counter
434/// metric!(counter(MyCounter::TotalRequests) -= 1);
435///
436/// // subtract from the counter and provide tags
437/// metric!(
438///     counter(MyCounter::TotalRequests) -= 1,
439///     server = "s1",
440///     host = "h1"
441/// );
442/// ```
443pub trait CounterMetric {
444    /// Returns the counter metric name that will be sent to statsd.
445    fn name(&self) -> &'static str;
446}
447
448/// A metric for capturing histograms.
449///
450/// Histograms are values whose distribution is calculated by the server. The distribution
451/// calculated for histograms is often similar to that of timers. Histograms can be thought of as a
452/// more general (not limited to timing things) form of timers.
453///
454/// ## Example
455///
456/// ```
457/// use relay_statsd::{metric, HistogramMetric};
458///
459/// struct QueueSize;
460///
461/// impl HistogramMetric for QueueSize {
462///     fn name(&self) -> &'static str {
463///         "queue_size"
464///     }
465/// }
466///
467/// # use std::collections::VecDeque;
468/// let queue = VecDeque::new();
469/// # let _hint: &VecDeque<()> = &queue;
470///
471/// // record a histogram value
472/// metric!(histogram(QueueSize) = queue.len() as u64);
473///
474/// // record with tags
475/// metric!(
476///     histogram(QueueSize) = queue.len() as u64,
477///     server = "server1",
478///     host = "host1",
479/// );
480/// ```
481pub trait HistogramMetric {
482    /// Returns the histogram metric name that will be sent to statsd.
483    fn name(&self) -> &'static str;
484}
485
486/// A metric for capturing sets.
487///
488/// Sets count the number of unique elements in a group. You can use them to, for example, count the
489/// unique visitors to your site.
490///
491/// ## Example
492///
493/// ```
494/// use relay_statsd::{metric, SetMetric};
495///
496/// enum MySet {
497///     UniqueProjects,
498///     UniqueUsers,
499/// }
500///
501/// impl SetMetric for MySet {
502///     fn name(&self) -> &'static str {
503///         match self {
504///             MySet::UniqueProjects => "unique_projects",
505///             MySet::UniqueUsers => "unique_users",
506///         }
507///     }
508/// }
509///
510/// # use std::collections::HashSet;
511/// let users = HashSet::new();
512/// # let _hint: &HashSet<()> = &users;
513///
514/// // use a set metric
515/// metric!(set(MySet::UniqueUsers) = users.len() as i64);
516///
517/// // use a set metric with tags
518/// metric!(
519///     set(MySet::UniqueUsers) = users.len() as i64,
520///     server = "server1",
521///     host = "host1",
522/// );
523/// ```
524pub trait SetMetric {
525    /// Returns the set metric name that will be sent to statsd.
526    fn name(&self) -> &'static str;
527}
528
529/// A metric for capturing gauges.
530///
531/// Gauge values are an instantaneous measurement of a value determined by the client. They do not
532/// change unless changed by the client. Examples include things like load average or how many
533/// connections are active.
534///
535/// ## Example
536///
537/// ```
538/// use relay_statsd::{metric, GaugeMetric};
539///
540/// struct QueueSize;
541///
542/// impl GaugeMetric for QueueSize {
543///     fn name(&self) -> &'static str {
544///         "queue_size"
545///     }
546/// }
547///
548/// # use std::collections::VecDeque;
549/// let queue = VecDeque::new();
550/// # let _hint: &VecDeque<()> = &queue;
551///
552/// // a simple gauge value
553/// metric!(gauge(QueueSize) = queue.len() as u64);
554///
555/// // a gauge with tags
556/// metric!(
557///     gauge(QueueSize) = queue.len() as u64,
558///     server = "server1",
559///     host = "host1"
560/// );
561/// ```
562pub trait GaugeMetric {
563    /// Returns the gauge metric name that will be sent to statsd.
564    fn name(&self) -> &'static str;
565}
566
567/// Emits a metric.
568///
569/// See [crate-level documentation](self) for examples.
570#[macro_export]
571macro_rules! metric {
572    // counter increment
573    (counter($id:expr) += $value:expr $(, $($k:ident).* = $v:expr)* $(,)?) => {
574        match $value {
575            value if value != 0 => {
576                $crate::with_client(|client| {
577                    use $crate::_pred::*;
578                    client.send_metric(
579                        client.count_with_tags(&$crate::CounterMetric::name(&$id), value)
580                        $(.with_tag(stringify!($($k).*), $v))*
581                    )
582                })
583            },
584            _ => {},
585        };
586    };
587
588    // counter decrement
589    (counter($id:expr) -= $value:expr $(, $($k:ident).* = $v:expr)* $(,)?) => {
590        match $value {
591            value if value != 0 => {
592                $crate::with_client(|client| {
593                    use $crate::_pred::*;
594                    client.send_metric(
595                        client.count_with_tags(&$crate::CounterMetric::name(&$id), -value)
596                            $(.with_tag(stringify!($($k).*), $v))*
597                    )
598                })
599            },
600            _ => {},
601        };
602    };
603
604    // gauge set
605    (gauge($id:expr) = $value:expr $(, $($k:ident).* = $v:expr)* $(,)?) => {
606        $crate::with_client(|client| {
607            use $crate::_pred::*;
608            client.send_metric(
609                client.gauge_with_tags(&$crate::GaugeMetric::name(&$id), $value)
610                    $(.with_tag(stringify!($($k).*), $v))*
611            )
612        })
613    };
614
615    // histogram
616    (histogram($id:expr) = $value:expr $(, $($k:ident).* = $v:expr)* $(,)?) => {
617        $crate::with_client(|client| {
618            use $crate::_pred::*;
619            client.send_metric(
620                client.histogram_with_tags(&$crate::HistogramMetric::name(&$id), $value)
621                    $(.with_tag(stringify!($($k).*), $v))*
622            )
623        })
624    };
625
626    // sets (count unique occurrences of a value per time interval)
627    (set($id:expr) = $value:expr $(, $($k:ident).* = $v:expr)* $(,)?) => {
628        $crate::with_client(|client| {
629            use $crate::_pred::*;
630            client.send_metric(
631                client.set_with_tags(&$crate::SetMetric::name(&$id), $value)
632                    $(.with_tag(stringify!($($k).*), $v))*
633            )
634        })
635    };
636
637    // timer value (duration)
638    (timer($id:expr) = $value:expr $(, $($k:ident).* = $v:expr)* $(,)?) => {
639        $crate::with_client(|client| {
640            use $crate::_pred::*;
641            client.send_metric(
642                // NOTE: cadence histograms support Duration out of the box and converts it to nanos,
643                // but we want milliseconds for historical reasons.
644                client.histogram_with_tags(&$crate::TimerMetric::name(&$id), $value.as_nanos() as f64 / 1e6)
645                    $(.with_tag(stringify!($($k).*), $v))*
646            )
647        })
648    };
649
650    // timed block
651    (timer($id:expr), $($($k:ident).* = $v:expr,)* $block:block) => {{
652        let now = std::time::Instant::now();
653        let rv = {$block};
654        $crate::metric!(timer($id) = now.elapsed() $(, $($k).* = $v)*);
655        rv
656    }};
657}
658
659#[cfg(test)]
660mod tests {
661    use std::time::Duration;
662
663    use cadence::{NopMetricSink, StatsdClient};
664
665    use crate::{
666        CounterMetric, GaugeMetric, HistogramMetric, MetricsClient, SetMetric, TimerMetric,
667        set_client, with_capturing_test_client, with_client,
668    };
669
670    enum TestGauges {
671        Foo,
672        Bar,
673    }
674
675    impl GaugeMetric for TestGauges {
676        fn name(&self) -> &'static str {
677            match self {
678                Self::Foo => "foo",
679                Self::Bar => "bar",
680            }
681        }
682    }
683
684    struct TestCounter;
685
686    impl CounterMetric for TestCounter {
687        fn name(&self) -> &'static str {
688            "counter"
689        }
690    }
691
692    struct TestHistogram;
693
694    impl HistogramMetric for TestHistogram {
695        fn name(&self) -> &'static str {
696            "histogram"
697        }
698    }
699
700    struct TestSet;
701
702    impl SetMetric for TestSet {
703        fn name(&self) -> &'static str {
704            "set"
705        }
706    }
707
708    struct TestTimer;
709
710    impl TimerMetric for TestTimer {
711        fn name(&self) -> &'static str {
712            "timer"
713        }
714    }
715
716    #[test]
717    fn test_capturing_client() {
718        let captures = with_capturing_test_client(|| {
719            metric!(
720                gauge(TestGauges::Foo) = 123,
721                server = "server1",
722                host = "host1"
723            );
724            metric!(
725                gauge(TestGauges::Bar) = 456,
726                server = "server2",
727                host = "host2"
728            );
729        });
730
731        assert_eq!(
732            captures,
733            [
734                "foo:123|g|#server:server1,host:host1",
735                "bar:456|g|#server:server2,host:host2"
736            ]
737        )
738    }
739
740    #[test]
741    fn current_client_is_global_client() {
742        let client1 = with_client(|c| format!("{c:?}"));
743        set_client(MetricsClient {
744            statsd_client: StatsdClient::from_sink("", NopMetricSink),
745            default_tags: Default::default(),
746            sample_rate: 1.0,
747            rx: None,
748        });
749        let client2 = with_client(|c| format!("{c:?}"));
750
751        // After setting the global client,the current client must change:
752        assert_ne!(client1, client2);
753    }
754
755    #[test]
756    fn test_counter_tags_with_dots() {
757        let captures = with_capturing_test_client(|| {
758            metric!(
759                counter(TestCounter) += 10,
760                hc.project_id = "567",
761                server = "server1",
762            );
763            metric!(
764                counter(TestCounter) -= 5,
765                hc.project_id = "567",
766                server = "server1",
767            );
768        });
769        assert_eq!(
770            captures,
771            [
772                "counter:10|c|#hc.project_id:567,server:server1",
773                "counter:-5|c|#hc.project_id:567,server:server1"
774            ]
775        );
776    }
777
778    #[test]
779    fn test_gauge_tags_with_dots() {
780        let captures = with_capturing_test_client(|| {
781            metric!(
782                gauge(TestGauges::Foo) = 123,
783                hc.project_id = "567",
784                server = "server1",
785            );
786        });
787        assert_eq!(captures, ["foo:123|g|#hc.project_id:567,server:server1"]);
788    }
789
790    #[test]
791    fn test_histogram_tags_with_dots() {
792        let captures = with_capturing_test_client(|| {
793            metric!(
794                histogram(TestHistogram) = 123,
795                hc.project_id = "567",
796                server = "server1",
797            );
798        });
799        assert_eq!(
800            captures,
801            ["histogram:123|h|#hc.project_id:567,server:server1"]
802        );
803    }
804
805    #[test]
806    fn test_set_tags_with_dots() {
807        let captures = with_capturing_test_client(|| {
808            metric!(
809                set(TestSet) = 123,
810                hc.project_id = "567",
811                server = "server1",
812            );
813        });
814        assert_eq!(captures, ["set:123|s|#hc.project_id:567,server:server1"]);
815    }
816
817    #[test]
818    fn test_timer_tags_with_dots() {
819        let captures = with_capturing_test_client(|| {
820            let duration = Duration::from_secs(100);
821            metric!(
822                timer(TestTimer) = duration,
823                hc.project_id = "567",
824                server = "server1",
825            );
826        });
827        assert_eq!(
828            captures,
829            ["timer:100000|h|#hc.project_id:567,server:server1"]
830        );
831    }
832
833    #[test]
834    fn test_timed_block_tags_with_dots() {
835        let captures = with_capturing_test_client(|| {
836            metric!(
837                timer(TestTimer),
838                hc.project_id = "567",
839                server = "server1",
840                {
841                    // your code could be here
842                }
843            )
844        });
845        // just check the tags to not make this flaky
846        assert!(captures[0].ends_with("|h|#hc.project_id:567,server:server1"));
847    }
848
849    #[test]
850    fn nanos_rounding_error() {
851        let one_day = Duration::from_secs(60 * 60 * 24);
852        let captures = with_capturing_test_client(|| {
853            metric!(timer(TestTimer) = one_day + Duration::from_nanos(1),);
854        });
855
856        // for "short" durations, precision is preserved:
857        assert_eq!(captures, ["timer:86400000.000001|h"]); // h is for histogram, not hours
858
859        let one_year = Duration::from_secs(60 * 60 * 24 * 365);
860        let captures = with_capturing_test_client(|| {
861            metric!(timer(TestTimer) = one_year + Duration::from_nanos(1),);
862        });
863
864        // for very long durations, precision is lost:
865        assert_eq!(captures, ["timer:31536000000|h"]);
866    }
867}