Skip to main content

laminar_connectors/prom/
mod.rs

1//! Shared Prometheus counter/gauge construction for per-connector
2//! metric structs. Replaces the per-file `let local; let reg = …`
3//! boilerplate and `reg_c!` macros that used to be duplicated in
4//! eleven places.
5
6use prometheus::{IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Opts, Registry};
7
8/// Borrowed registry handle used during metric-struct construction.
9pub struct RegHandle<'a> {
10    registry: &'a Registry,
11    _local: Option<Registry>,
12}
13
14impl RegHandle<'_> {
15    /// Borrow the inner registry for non-helper registrations (e.g. `Histogram`).
16    #[must_use]
17    pub fn registry(&self) -> &Registry {
18        self.registry
19    }
20
21    /// Register an `IntCounter`.
22    ///
23    /// # Panics
24    ///
25    /// Panics if `name`/`help` is not a valid Prometheus identifier.
26    #[must_use]
27    pub fn counter(&self, name: &str, help: &str) -> IntCounter {
28        let c =
29            IntCounter::new(name, help).unwrap_or_else(|e| panic!("invalid counter '{name}': {e}"));
30        self.registry.register(Box::new(c.clone())).ok();
31        c
32    }
33
34    /// Register an `IntGauge`.
35    ///
36    /// # Panics
37    ///
38    /// Panics if `name`/`help` is not a valid Prometheus identifier.
39    #[must_use]
40    pub fn gauge(&self, name: &str, help: &str) -> IntGauge {
41        let g = IntGauge::new(name, help).unwrap_or_else(|e| panic!("invalid gauge '{name}': {e}"));
42        self.registry.register(Box::new(g.clone())).ok();
43        g
44    }
45
46    /// Register an `IntCounterVec`.
47    ///
48    /// # Panics
49    ///
50    /// Panics if `name`/`help`/`labels` are not valid Prometheus identifiers.
51    #[must_use]
52    pub fn counter_vec(&self, name: &str, help: &str, labels: &[&str]) -> IntCounterVec {
53        let v = IntCounterVec::new(Opts::new(name, help), labels)
54            .unwrap_or_else(|e| panic!("invalid counter_vec '{name}': {e}"));
55        self.registry.register(Box::new(v.clone())).ok();
56        v
57    }
58
59    /// Register an `IntGaugeVec`.
60    ///
61    /// # Panics
62    ///
63    /// Panics if `name`/`help`/`labels` are not valid Prometheus identifiers.
64    #[must_use]
65    pub fn gauge_vec(&self, name: &str, help: &str, labels: &[&str]) -> IntGaugeVec {
66        let v = IntGaugeVec::new(Opts::new(name, help), labels)
67            .unwrap_or_else(|e| panic!("invalid gauge_vec '{name}': {e}"));
68        self.registry.register(Box::new(v.clone())).ok();
69        v
70    }
71}
72
73/// `RegHandle` over `registry`, falling back to a fresh registry parked
74/// in `local` (which must outlive construction). Pattern:
75/// `let mut local = None; let reg = reg_or_local(registry, &mut local);`
76#[must_use]
77#[allow(clippy::missing_panics_doc)] // `expect` follows an unconditional `*local = Some(..)`
78pub fn reg_or_local<'a>(
79    registry: Option<&'a Registry>,
80    local: &'a mut Option<Registry>,
81) -> RegHandle<'a> {
82    if let Some(r) = registry {
83        RegHandle {
84            registry: r,
85            _local: None,
86        }
87    } else {
88        *local = Some(Registry::new());
89        RegHandle {
90            registry: local.as_ref().expect("just initialized"),
91            _local: None,
92        }
93    }
94}
95
96/// Metric families of one connector instance, labelled by one bounded constant label and
97/// unregistered on drop so a restarted generation can register again.
98///
99/// A family another live instance already exports keeps this instance's metrics unexported
100/// rather than failing connector startup.
101pub struct LabelledFamilies {
102    label: (&'static str, String),
103    counters: Vec<IntCounter>,
104    gauges: Vec<IntGauge>,
105    registry: Option<Registry>,
106}
107
108impl LabelledFamilies {
109    /// Start a set labelled `key = value`.
110    #[must_use]
111    pub fn new(key: &'static str, value: &str) -> Self {
112        Self {
113            label: (key, value.to_string()),
114            counters: Vec::new(),
115            gauges: Vec::new(),
116            registry: None,
117        }
118    }
119
120    /// A labelled counter added to this set.
121    ///
122    /// # Errors
123    /// Returns an error when `name` or `help` is not a valid descriptor.
124    pub fn counter(&mut self, name: &str, help: &str) -> Result<IntCounter, prometheus::Error> {
125        let counter =
126            IntCounter::with_opts(Opts::new(name, help).const_label(self.label.0, &self.label.1))?;
127        self.counters.push(counter.clone());
128        Ok(counter)
129    }
130
131    /// A labelled gauge added to this set.
132    ///
133    /// # Errors
134    /// Returns an error when `name` or `help` is not a valid descriptor.
135    pub fn gauge(&mut self, name: &str, help: &str) -> Result<IntGauge, prometheus::Error> {
136        let gauge =
137            IntGauge::with_opts(Opts::new(name, help).const_label(self.label.0, &self.label.1))?;
138        self.gauges.push(gauge.clone());
139        Ok(gauge)
140    }
141
142    fn collectors(&self) -> Vec<Box<dyn prometheus::core::Collector>> {
143        let counters = self
144            .counters
145            .iter()
146            .map(|counter| Box::new(counter.clone()) as Box<dyn prometheus::core::Collector>);
147        let gauges = self
148            .gauges
149            .iter()
150            .map(|gauge| Box::new(gauge.clone()) as Box<dyn prometheus::core::Collector>);
151        counters.chain(gauges).collect()
152    }
153
154    /// Register every family on `registry`, rolling back on the first failure.
155    #[must_use]
156    pub fn register(mut self, registry: Option<&Registry>) -> Self {
157        let Some(registry) = registry else {
158            return self;
159        };
160        for (index, collector) in self.collectors().into_iter().enumerate() {
161            if let Err(error) = registry.register(collector) {
162                for previous in self.collectors().into_iter().take(index) {
163                    let _ = registry.unregister(previous);
164                }
165                tracing::warn!(%error, "connector metrics are not exported for this instance");
166                return self;
167            }
168        }
169        self.registry = Some(registry.clone());
170        self
171    }
172}
173
174impl Drop for LabelledFamilies {
175    fn drop(&mut self) {
176        if let Some(registry) = self.registry.take() {
177            for collector in self.collectors() {
178                let _ = registry.unregister(collector);
179            }
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests;