rtrtr/
metrics.rs

1//! Maintaining and outputting metrics.
2//!
3//! Metrics are operational data maintained by components that allow users to
4//! understand what their instance of RTRTR is doing. Because they are updated
5//! by components and printed by other components in different threads,
6//! management is a bit tricky.
7//!
8//! Typically, all metrics of a component are kept in a single object that is
9//! shared between that component and everything that could possibly output
10//! metrics. We use atomic data types (such as `std::sync::atomic::AtomicU32`)
11//! the keep and allow updating the actual values and keep the value behind an
12//! arc for easy sharing.
13//!
14//! When a component is started, it registers its metrics object with a
15//! metrics [`Collection`] it receives via its
16//! [`Component`][crate::manager::Component].
17//!
18//! The object needs to implement the [`Source`] trait by appending all its
19//! data to a [`Target`]. To make that task easier, the [`Metric`] type is
20//! used to define all the properties of an individual metric. Values of this
21//! type can be created as constants.
22
23use std::fmt;
24use std::sync::{Arc, Mutex, Weak};
25use std::fmt::Write;
26use arc_swap::ArcSwap;
27use clap::{crate_name, crate_version};
28
29
30//------------ Module Configuration ------------------------------------------
31
32/// The application prefix to use in the names of Prometheus metrics.
33const PROMETHEUS_PREFIX: &str = "rtrtr";
34
35
36//------------ Collection ----------------------------------------------------
37
38/// A collection of metrics sources.
39///
40/// This type provides a shared collection. I.e., if a value is cloned, both
41/// clones will reference the same collection. Both will see newly
42/// added sources.
43///
44/// Such new sources can be registered with the [`register`][Self::register]
45/// method. A string with all the current values of all known sources can be
46/// obtained via the [`assemble`][Self::assemble] method.
47#[derive(Clone, Default)]
48pub struct Collection {
49    /// The currently registered sources.
50    sources: Arc<ArcSwap<Vec<RegisteredSource>>>,
51
52    /// A mutex to be held during registration of a new source.
53    ///
54    /// Updating `sources` is done by taking the existing sources,
55    /// construct a new vec, and then swapping that vec into the arc. Because
56    /// of this, updates cannot be done concurrently. The mutex guarantees
57    /// that.
58    register: Arc<Mutex<()>>,
59}
60
61impl Collection {
62    /// Registers a new source with the collection.
63    ///
64    /// The name of the component registering the source is passed via `name`.
65    /// The source itself is given as a weak pointer so that it gets dropped
66    /// when the owning component terminates.
67    pub fn register(&self, name: Arc<str>, source: Weak<dyn Source>) {
68        let lock = self.register.lock().unwrap();
69        let old_sources = self.sources.load();
70        let mut new_sources = Vec::new();
71        for item in old_sources.iter() {
72            if item.source.strong_count() > 0 {
73                new_sources.push(item.clone())
74            }
75        }
76        new_sources.push(
77            RegisteredSource { name, source }
78        );
79        new_sources.sort_by(|l, r| l.name.as_ref().cmp(r.name.as_ref()));
80        self.sources.store(new_sources.into());
81        drop(lock);
82    }
83
84    /// Assembles metrics output.
85    ///
86    /// Produces an output of all the sources in the collection in the given
87    /// format and returns it as a string.
88    pub fn assemble(&self, format: OutputFormat) -> String {
89        let sources = self.sources.load();
90        let mut target = Target::new(format);
91        for item in sources.iter() {
92            if let Some(source) = item.source.upgrade() {
93                source.append(&item.name, &mut target)
94            }
95        }
96        target.into_string()
97    }
98}
99
100
101impl fmt::Debug for Collection {
102    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
103        let len = self.sources.load().len();
104        write!(f, "Collection({len} sources)")
105    }
106}
107
108
109//------------ RegisteredSource ----------------------------------------------
110
111/// All information on a source registered with a collection.
112#[derive(Clone)]
113struct RegisteredSource {
114    /// The name of the component owning the source.
115    name: Arc<str>,
116
117    /// A weak pointer to the source.
118    source: Weak<dyn Source>,
119}
120
121
122//------------ Source --------------------------------------------------------
123
124/// A type producing some metrics.
125///
126/// All this type needs to be able to do is output its metrics.
127pub trait Source: Send + Sync {
128    /// Appends the metrics to the target.
129    ///
130    /// The unit name is provided so a source doesn’t need to keep it around.
131    fn append(&self, unit_name: &str, target: &mut Target);
132}
133
134impl<T: Source> Source for Arc<T> {
135    fn append(&self, unit_name: &str, target: &mut Target) {
136        AsRef::<T>::as_ref(self).append(unit_name, target)
137    }
138}
139
140
141//------------ Target --------------------------------------------------------
142
143/// A target for outputting metrics.
144///
145/// A new target can be created via [`new`](Self::new), passing in the
146/// requested output format. Individual metrics are appended to the target
147/// via [`append`](Self::append) or the shortcut
148/// [`append_simple`](Self::append_simple). Finally, when all metrics are
149/// assembled, you can turn the target into a string of the output via
150/// [`into_string`](Self::into_string).
151#[derive(Clone, Debug)]
152pub struct Target {
153    /// The format of the assembled output.
154    format: OutputFormat,
155
156    /// The output assembled so far.
157    target: String,
158}
159
160impl Target {
161    /// Creates a new target.
162    ///
163    /// The target will produce output in the given format.
164    pub fn new(format: OutputFormat) -> Self {
165        let mut target = String::new();
166        if matches!(format, OutputFormat::Plain) {
167            target.push_str(
168                concat!(
169                    "version: ", crate_name!(), "/", crate_version!(), "\n"
170                )
171            );
172        }
173        Target { format, target }
174    }
175
176    /// Converts the target into a string with the assembled output.
177    pub fn into_string(self) -> String {
178        self.target
179    }
180
181    /// Appends metrics to the target.
182    ///
183    /// The method can append multiple metrics values at once via the closure.
184    /// All values are, however, for the same metrics described by `metric`.
185    /// If the values are for a specific component, it’s name is given via
186    /// `unit_name`. If they are global, this can be left at `None`.
187    pub fn append<F: FnOnce(&mut Records)>(
188        &mut self,
189        metric: &Metric,
190        unit_name: Option<&str>,
191        values: F,
192    ) {
193        if !self.format.supports_type(metric.metric_type) {
194            return
195        }
196
197        if matches!(self.format, OutputFormat::Prometheus) {
198            self.target.push_str("# HELP ");
199            self.append_metric_name(metric, unit_name);
200            self.target.push(' ');
201            self.target.push_str(metric.help);
202            self.target.push('\n');
203
204            self.target.push_str("# TYPE ");
205            self.append_metric_name(metric, unit_name);
206            writeln!(&mut self.target, " {}", metric.metric_type).unwrap();
207        }
208        values(&mut Records { target: self, metric, unit_name })
209    }
210
211    /// Append a single metric value to the target.
212    ///
213    /// This is a shortcut version of [`append`](Self::append) when there is
214    /// only a single value to be append for a metric. The metric is described
215    /// by `metric`.  If the value is for a specific component, it’s name is
216    /// given via `unit_name`. If they are global, this can be left at `None`.
217    pub fn append_simple(
218        &mut self,
219        metric: &Metric,
220        unit_name: Option<&str>,
221        value: impl fmt::Display,
222    ) {
223        self.append(metric, unit_name, |records| {
224            records.value(value)
225        })
226    }
227
228    /// Constructs and appends the name of the given metric.
229    fn append_metric_name(
230        &mut self, metric: &Metric, unit_name: Option<&str>
231    ) {
232        match self.format {
233            OutputFormat::Prometheus => {
234                write!(&mut self.target,
235                    "{}_{}_{}",
236                    PROMETHEUS_PREFIX, metric.name, metric.unit
237                ).unwrap();
238            }
239            OutputFormat::Plain => {
240                match unit_name {
241                    Some(unit) => {
242                        write!(&mut self.target,
243                            "{} {}", unit, metric.name
244                        ).unwrap();
245                    }
246                    None => {
247                        write!(&mut self.target,
248                            "{}", metric.name
249                        ).unwrap();
250                    }
251                }
252            }
253        }
254    }
255}
256
257
258//------------ Records -------------------------------------------------------
259
260/// Allows adding all values for an individual metric.
261///
262/// Values can either be simple, in which case they only consist of a value
263/// and are appended via [`value`](Self::value), or they can be labelled, in
264/// which case there are multiple values for a metric that are distinguished
265/// via a set of labels. Such values are appended via
266/// [`label_value`](Self::label_value).
267pub struct Records<'a> {
268    /// A reference to the target.
269    target: &'a mut Target,
270
271    /// A reference to the properties of the metric in question.
272    metric: &'a Metric,
273
274    /// An reference to the name of the component if any.
275    unit_name: Option<&'a str>,
276}
277
278impl Records<'_> {
279    /// Appends a simple value to the metrics target.
280    ///
281    /// The value is simply output via the `Display` trait.
282    pub fn value(&mut self, value: impl fmt::Display) {
283        match self.target.format {
284            OutputFormat::Prometheus => {
285                self.target.append_metric_name(
286                    self.metric, self.unit_name
287                );
288                if let Some(unit_name) = self.unit_name {
289                    write!(&mut self.target.target,
290                        "{{component=\"{unit_name}\"}}"
291                    ).unwrap();
292                }
293                writeln!(&mut self.target.target, " {value}").unwrap()
294            }
295            OutputFormat::Plain => {
296                self.target.append_metric_name(self.metric, self.unit_name);
297                writeln!(&mut self.target.target, ": {value}").unwrap()
298            }
299        }
300    }
301
302    /// Appends a single labelled value to the metrics target.
303    ///
304    /// The labels are a slice of pairs of strings with the first element the
305    /// name of the label and the second the label value. The metrics value
306    /// is simply printed via the `Display` trait.
307    pub fn label_value(
308        &mut self,
309        labels: &[(&str, &str)],
310        value: impl fmt::Display
311    ) {
312        match self.target.format {
313            OutputFormat::Prometheus => {
314                self.target.append_metric_name(self.metric, self.unit_name);
315                self.target.target.push('{');
316                let mut comma = false;
317                if let Some(unit_name) = self.unit_name {
318                    write!(&mut self.target.target,
319                        "component=\"{unit_name}\""
320                    ).unwrap();
321                    comma = true;
322                }
323                for (name, value) in labels {
324                    if comma {
325                        write!(&mut self.target.target,
326                            ", {name}=\"{value}\""
327                        ).unwrap();
328                    }
329                    else {
330                        write!(&mut self.target.target,
331                            "{name}=\"{value}\""
332                        ).unwrap();
333                        comma = true;
334                    }
335                }
336                writeln!(&mut self.target.target, "}} {value}").unwrap()
337            }
338            OutputFormat::Plain => {
339                self.target.append_metric_name(self.metric, self.unit_name);
340                for (name, value) in labels {
341                    write!(&mut self.target.target,
342                        " {name}={value}"
343                    ).unwrap();
344                }
345                writeln!(&mut self.target.target, ": {value}").unwrap()
346            }
347        }
348    }
349}
350
351
352//------------ OutputFormat --------------------------------------------------
353
354/// The output format for metrics.
355///
356/// This is a non-exhaustive enum so that we can add additional metrics
357/// without having to do breaking releases. Output for unknown formats should
358/// be empty.
359#[non_exhaustive]
360#[derive(Clone, Copy, Debug)]
361pub enum OutputFormat {
362    /// Prometheus’ text-base exposition format.
363    ///
364    /// See <https://prometheus.io/docs/instrumenting/exposition_formats/>
365    /// for details.
366    Prometheus,
367
368    /// Simple, human-readable plain-text output.
369    Plain
370}
371
372impl OutputFormat {
373    /// Returns whether the format supports non-numerical metrics.
374    #[allow(clippy::match_like_matches_macro)]
375    pub fn allows_text(self) -> bool {
376        match self {
377            OutputFormat::Prometheus => false,
378            OutputFormat::Plain => true,
379        }
380    }
381
382    /// Returns whether this output format supports this metric type.
383    #[allow(clippy::match_like_matches_macro)]
384    pub fn supports_type(self, metric: MetricType) -> bool {
385        match (self, metric) {
386            (OutputFormat::Prometheus, MetricType::Text) => false,
387            _ => true
388        }
389    }
390}
391
392
393//------------ Metric --------------------------------------------------------
394
395/// The properties of a metric.
396pub struct Metric {
397    /// The name of the metric.
398    ///
399    /// The final name written to the target will be composed of more than
400    /// just this name according to the rules stipulated by the output format.
401    pub name: &'static str,
402
403    /// The help text for the metric.
404    pub help: &'static str,
405
406    /// The type of the metric.
407    pub metric_type: MetricType,
408
409    /// The unit of the metric.
410    pub unit: MetricUnit,
411}
412
413impl Metric {
414    /// Constructs a new metric from all values.
415    ///
416    /// This is a const function and can be used to construct associated
417    /// constants.
418    pub const fn new(
419        name: &'static str, help: &'static str,
420        metric_type: MetricType, unit: MetricUnit
421    ) -> Self {
422        Metric { name, help, metric_type, unit
423        }
424    }
425}
426
427
428//------------ MetricType ----------------------------------------------------
429
430/// The type of a metric.
431#[derive(Clone, Copy, Debug)]
432pub enum MetricType {
433    /// A monotonically increasing counter.
434    ///
435    /// Values can only increase or be reset to zero.
436    Counter,
437
438    /// A value that can go up and down.
439    Gauge,
440
441    /// A Prometheus-style histogram.
442    Histogram,
443
444    /// A Prometheus-style summary.
445    Summary,
446
447    /// A text metric.
448    ///
449    /// Metrics of this type are only output to output formats that allow
450    /// text metrics.
451    Text,
452}
453
454impl fmt::Display for MetricType {
455    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
456        match *self {
457            MetricType::Counter => f.write_str("counter"),
458            MetricType::Gauge => f.write_str("gauge"),
459            MetricType::Histogram => f.write_str("histogram"),
460            MetricType::Summary => f.write_str("summary"),
461            MetricType::Text => f.write_str("text"),
462        }
463    }
464}
465
466
467//------------ MetricUnit ----------------------------------------------------
468
469/// A unit of measure for a metric.
470///
471/// This determines what a value of 1 means.
472#[derive(Clone, Copy, Debug)]
473pub enum MetricUnit {
474    Second,
475    Celsius,
476    Meter,
477    Byte,
478    Ratio,
479    Volt,
480    Ampere,
481    Joule,
482    Gram,
483
484    /// Use this for counting things.
485    Total,
486
487    /// Use this for non-numerical metrics.
488    Info,
489}
490
491impl fmt::Display for MetricUnit {
492    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
493        match *self {
494            MetricUnit::Second => f.write_str("seconds"),
495            MetricUnit::Celsius => f.write_str("celsius"),
496            MetricUnit::Meter => f.write_str("meters"),
497            MetricUnit::Byte => f.write_str("bytes"),
498            MetricUnit::Ratio => f.write_str("ratio"),
499            MetricUnit::Volt => f.write_str("volts"),
500            MetricUnit::Ampere => f.write_str("amperes"),
501            MetricUnit::Joule => f.write_str("joules"),
502            MetricUnit::Gram => f.write_str("grams"),
503            MetricUnit::Total => f.write_str("total"),
504            MetricUnit::Info => f.write_str("info"),
505        }
506    }
507}
508