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}