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}