saluki_core/data_model/event/metric/
mod.rs

1//! Metric types.
2
3pub mod context;
4use self::context::Context;
5
6mod metadata;
7use std::time::Duration;
8
9pub use self::metadata::*;
10
11mod value;
12pub use self::value::{
13    Histogram, HistogramPoints, HistogramSummary, MetricValues, ScalarPoints, SetPoints, SketchPoints,
14};
15
16/// A metric.
17///
18/// Metrics represent the measurement of a particular quantity at a particular point in time. Several different metric
19/// types exist that provide different views into the underlying quantity: counters for representing the quantities that
20/// are aggregated/totaled over time, gauges for tracking the latest value of a quantity, and histograms for tracking
21/// the distribution of a quantity.
22///
23/// ## Structure
24///
25/// A metric is composed of three parts: the context, the value, and the metadata.
26///
27/// The context represents the "full" name of the metric, which includes not only the name (for example, `http_requests_total`),
28/// but the tags as well. Effectively, a context is meant to be a unique name for a metric.
29///
30/// The value is precisely what it sounds like: the value of the metric. The value holds both the metric type and the
31/// measurement (or measurements) tied to that metric type. This ensures that the measurements are always represented
32/// correctly for the given metric type.
33///
34/// The metadata contains ancillary data related to the metric, such as the timestamp, sample rate, and origination
35/// information like hostname and sender.
36#[derive(Clone, Debug, PartialEq)]
37pub struct Metric {
38    context: Context,
39    values: MetricValues,
40    metadata: MetricMetadata,
41}
42
43impl Metric {
44    /// Creates a counter metric from the given context and values.
45    ///
46    /// Default metadata will be used.
47    pub fn counter<C, V>(context: C, values: V) -> Self
48    where
49        C: Into<Context>,
50        V: Into<ScalarPoints>,
51    {
52        Self {
53            context: context.into(),
54            values: MetricValues::counter(values),
55            metadata: MetricMetadata::default(),
56        }
57    }
58
59    /// Creates a gauge metric from the given context and values.
60    ///
61    /// Default metadata will be used.
62    pub fn gauge<C, V>(context: C, values: V) -> Self
63    where
64        C: Into<Context>,
65        V: Into<ScalarPoints>,
66    {
67        Self {
68            context: context.into(),
69            values: MetricValues::gauge(values),
70            metadata: MetricMetadata::default(),
71        }
72    }
73
74    /// Creates a rate metric from the given context and values.
75    ///
76    /// Default metadata will be used.
77    pub fn rate<C, V>(context: C, values: V, interval: Duration) -> Self
78    where
79        C: Into<Context>,
80        V: Into<ScalarPoints>,
81    {
82        Self {
83            context: context.into(),
84            values: MetricValues::rate(values, interval),
85            metadata: MetricMetadata::default(),
86        }
87    }
88
89    /// Creates a set metric from the given context and values.
90    ///
91    /// Default metadata will be used.
92    pub fn set<C, V>(context: C, values: V) -> Self
93    where
94        C: Into<Context>,
95        V: Into<SetPoints>,
96    {
97        Self {
98            context: context.into(),
99            values: MetricValues::set(values),
100            metadata: MetricMetadata::default(),
101        }
102    }
103
104    /// Creates a histogram metric from the given context and values.
105    ///
106    /// Default metadata will be used.
107    pub fn histogram<C, V>(context: C, values: V) -> Self
108    where
109        C: Into<Context>,
110        V: Into<HistogramPoints>,
111    {
112        Self {
113            context: context.into(),
114            values: MetricValues::histogram(values),
115            metadata: MetricMetadata::default(),
116        }
117    }
118
119    /// Creates a distribution metric from the given context and values.
120    ///
121    /// Default metadata will be used.
122    pub fn distribution<C, V>(context: C, values: V) -> Self
123    where
124        C: Into<Context>,
125        V: Into<SketchPoints>,
126    {
127        Self {
128            context: context.into(),
129            values: MetricValues::distribution(values),
130            metadata: MetricMetadata::default(),
131        }
132    }
133
134    /// Gets a reference to the context.
135    pub fn context(&self) -> &Context {
136        &self.context
137    }
138
139    /// Gets a mutable reference to the context.
140    pub fn context_mut(&mut self) -> &mut Context {
141        &mut self.context
142    }
143
144    /// Gets a reference to the values.
145    pub fn values(&self) -> &MetricValues {
146        &self.values
147    }
148
149    /// Gets a mutable reference to the values.
150    pub fn values_mut(&mut self) -> &mut MetricValues {
151        &mut self.values
152    }
153
154    /// Gets a reference to the metadata.
155    pub fn metadata(&self) -> &MetricMetadata {
156        &self.metadata
157    }
158
159    /// Gets a mutable reference to the metadata.
160    pub fn metadata_mut(&mut self) -> &mut MetricMetadata {
161        &mut self.metadata
162    }
163
164    /// Consumes the metric and returns the individual parts.
165    pub fn into_parts(self) -> (Context, MetricValues, MetricMetadata) {
166        (self.context, self.values, self.metadata)
167    }
168
169    /// Creates a `Metric` from the given parts.
170    pub fn from_parts(context: Context, values: MetricValues, metadata: MetricMetadata) -> Self {
171        Self {
172            context,
173            values,
174            metadata,
175        }
176    }
177}
178
179/// A sample rate.
180///
181/// Sample rates are used to indicate the rate at which a metric was sampled, and are represented by a value between 0.0
182/// and 1.0 (inclusive). For example, when handling a value with a sample rate of 0.25, this indicates the value is only
183/// being sent 25% of the time. This means it has a "weight" of 4: this single value should be considered to represent
184/// 4 actual samples with the same value.
185#[derive(Clone, Copy)]
186pub struct SampleRate(f64);
187
188impl SampleRate {
189    /// Creates a new sample rate indicating the metric was unsampled.
190    pub const fn unsampled() -> Self {
191        Self(1.0)
192    }
193
194    /// Returns the sample rate.
195    pub const fn rate(&self) -> f64 {
196        self.0
197    }
198
199    /// Returns the weight of the sample rate.
200    pub fn weight(&self) -> u64 {
201        (1.0 / self.0) as u64
202    }
203
204    /// Returns the weight of the sample rate as a raw floating-point value.
205    pub fn raw_weight(&self) -> f64 {
206        1.0 / self.0
207    }
208}
209
210impl TryFrom<f64> for SampleRate {
211    type Error = &'static str;
212
213    fn try_from(value: f64) -> Result<Self, Self::Error> {
214        if !(0.0..=1.0).contains(&value) {
215            Err("sample rate must be between 0.0 and 1.0")
216        } else {
217            Ok(Self(value))
218        }
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::SampleRate;
225
226    #[test]
227    fn try_from_accepts_the_inclusive_unit_interval() {
228        // The documented valid range is 0.0..=1.0 inclusive, so both endpoints must parse.
229        assert!(SampleRate::try_from(0.0).is_ok());
230        assert!(SampleRate::try_from(0.5).is_ok());
231        assert!(SampleRate::try_from(1.0).is_ok());
232    }
233
234    #[test]
235    fn try_from_rejects_values_outside_the_unit_interval() {
236        assert!(SampleRate::try_from(-0.1).is_err());
237        assert!(SampleRate::try_from(1.1).is_err());
238    }
239
240    #[test]
241    fn weight_is_the_reciprocal_of_the_rate() {
242        // The doc's worked example: a rate of 0.25 means the value stands in for 4 samples (weight 4).
243        let rate = SampleRate::try_from(0.25).unwrap();
244        assert_eq!(rate.rate(), 0.25);
245        assert_eq!(rate.weight(), 4);
246        assert_eq!(rate.raw_weight(), 4.0);
247    }
248
249    #[test]
250    fn integer_weight_truncates_non_integer_reciprocals() {
251        // `weight()` truncates to an integer: 1.0 / 0.3 == 3.333..., which becomes 3. `raw_weight()` keeps the float.
252        let rate = SampleRate::try_from(0.3).unwrap();
253        assert_eq!(rate.weight(), 3);
254        assert!((rate.raw_weight() - (1.0 / 0.3)).abs() < f64::EPSILON);
255    }
256
257    #[test]
258    fn unsampled_has_unit_rate_and_weight() {
259        let rate = SampleRate::unsampled();
260        assert_eq!(rate.rate(), 1.0);
261        assert_eq!(rate.weight(), 1);
262        assert_eq!(rate.raw_weight(), 1.0);
263    }
264}