metricus/
histogram.rs

1//! A `Histogram` proxy struct for managing a metrics histogram.
2
3use crate::access::get_metrics_mut;
4use crate::{Id, Tags};
5#[cfg(feature = "rdtsc")]
6use quanta::Clock;
7use std::cell::{LazyCell, UnsafeCell};
8#[cfg(not(feature = "rdtsc"))]
9use std::time::Instant;
10
11/// Facilitates the creation of a new histogram, recording of values, and
12/// generation of spans for timing operations.
13/// The `Histogram` does not have an inherent notion of measurement units (e.g., milliseconds, bytes)
14/// and some convention should be in place.
15///
16/// ## Examples
17///
18/// Create a histogram and record values without specifying units explicitly:
19///
20/// ```no_run
21/// use metricus::{Histogram, HistogramOps};
22///
23/// let tags = [("operation", "db_query"), ("status", "success")];
24/// let histogram = Histogram::new("query_duration", &tags);
25///
26/// // Here, 1500 might represent microseconds, but it's implied and must be consistent in usage.
27/// histogram.record(1500);
28/// ```
29///
30/// Another option is to use `#[span]` macro to instrument your code to automatically measure duration
31/// of a given function.
32///
33/// ```no_run
34/// use metricus_macros::span;
35///
36/// #[span(measurement = "latencies", tags(key1 = "value1", key2 = "value2"))]
37/// fn my_function_with_tags() {
38///     // function body
39/// }
40///
41/// my_function_with_tags();
42/// ````
43
44#[derive(Debug)]
45pub struct Histogram {
46    id: Id,
47    #[cfg(feature = "rdtsc")]
48    clock: Clock,
49}
50
51impl Histogram {
52    /// Creates a new histogram with the specified name and tags.
53    /// Units of measurement are not defined by the histogram itself but should be implied
54    /// and consistently used based on the metric being tracked.
55    ///
56    /// ## Examples
57    ///
58    /// Create a histogram with tags.
59    /// ```no_run
60    /// use metricus::Histogram;
61    ///
62    /// let tags = [("feature", "login"), ("result", "success")];
63    /// let histogram = Histogram::new("login_duration", &tags);
64    /// ```
65    ///
66    /// Create a histogram without tags.
67    /// ```no_run
68    /// use metricus::{empty_tags, Histogram};
69    ///
70    /// let histogram = Histogram::new("login_duration", empty_tags());
71    /// ```
72    pub fn new(name: &str, tags: Tags) -> Self {
73        let histogram_id = get_metrics_mut().new_histogram(name, tags);
74        Self {
75            id: histogram_id,
76            #[cfg(feature = "rdtsc")]
77            clock: Clock::new(),
78        }
79    }
80}
81
82/// Defines a series of operations that can be performed on a `Histogram`.
83pub trait HistogramOps {
84    /// Records a value in the histogram.
85    /// The unit of the value is implied and should be consistent with the histogram's intended use.
86    ///
87    /// ```no_run
88    /// use metricus::{Histogram, HistogramOps};
89    ///
90    /// let histogram = Histogram::new("response_time", &[]);
91    /// // Assuming milliseconds as the unit for response time.
92    /// histogram.record(200);
93    /// ```
94    fn record(&self, value: u64);
95
96    /// Starts a span for timing an operation, automatically recording the duration upon completion.
97    /// The duration recorded is in nanoseconds.
98    ///
99    /// ```no_run
100    /// use metricus::{Histogram, HistogramOps};
101    /// let histogram = Histogram::new("task_duration", &[]);
102    /// {
103    ///     let _span = histogram.span(); // Timing starts here, in nanoseconds.
104    ///     // Execute operation...
105    /// } // Timing ends and duration is recorded here.
106    /// ```
107    ///
108    /// It is important to use a named binding when assigning the `Span` instead of `let _ = histogram.span()`.
109    /// The latter form will result in `Span` being dropped immediately. Instead, prefer to use the [Histogram::with_span]
110    /// method to prevent any miss-use.
111    fn span(&self) -> Span<'_>;
112
113    /// Accepts a closure whose duration will be measured. The duration recorded is in nanoseconds.
114    ///
115    /// ```no_run
116    /// use metricus::{Histogram, HistogramOps};
117    ///
118    /// let histogram = Histogram::new("task_duration", &[]);
119    /// histogram.with_span(|| {
120    ///   // Execute operation...
121    /// });
122    /// ```
123    fn with_span<F: FnOnce() -> R, R>(&self, f: F) -> R;
124}
125
126impl HistogramOps for Histogram {
127    fn record(&self, value: u64) {
128        get_metrics_mut().record(self.id, value);
129    }
130
131    fn span(&self) -> Span<'_> {
132        Span {
133            histogram: self,
134            #[cfg(feature = "rdtsc")]
135            start_raw: self.clock.raw(),
136            #[cfg(not(feature = "rdtsc"))]
137            start_instant: Instant::now(),
138        }
139    }
140
141    fn with_span<F: FnOnce() -> R, R>(&self, f: F) -> R {
142        let _span = self.span();
143        f()
144    }
145}
146
147impl HistogramOps for LazyCell<UnsafeCell<Histogram>> {
148    fn record(&self, value: u64) {
149        unsafe { &mut *self.get() }.record(value)
150    }
151
152    fn span(&self) -> Span<'_> {
153        unsafe { &mut *self.get() }.span()
154    }
155
156    fn with_span<F: FnOnce() -> R, R>(&self, f: F) -> R {
157        unsafe { &mut *self.get() }.with_span(f)
158    }
159}
160
161impl Drop for Histogram {
162    fn drop(&mut self) {
163        get_metrics_mut().delete_histogram(self.id);
164    }
165}
166
167/// Used for measuring how long given operation takes. The duration is recorded in nanoseconds.
168pub struct Span<'a> {
169    histogram: &'a Histogram,
170    #[cfg(feature = "rdtsc")]
171    start_raw: u64,
172    #[cfg(not(feature = "rdtsc"))]
173    start_instant: Instant,
174}
175
176impl Drop for Span<'_> {
177    fn drop(&mut self) {
178        #[cfg(feature = "rdtsc")]
179        {
180            let end_raw = self.histogram.clock.raw();
181            let elapsed = self.histogram.clock.delta_as_nanos(self.start_raw, end_raw);
182            self.histogram.record(elapsed);
183        }
184        #[cfg(not(feature = "rdtsc"))]
185        self.histogram.record(self.start_instant.elapsed().as_nanos() as u64);
186    }
187}