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}