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