Skip to main content

provide_telemetry/
metrics.rs

1// SPDX-FileCopyrightText: Copyright (C) 2026 provide.io llc
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-Comment: Part of provide-telemetry.
4//
5
6use std::collections::BTreeMap;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::{Arc, Mutex};
9
10use crate::backpressure::{release, try_acquire};
11#[cfg(feature = "governance")]
12use crate::consent::should_allow;
13use crate::health::increment_emitted;
14use crate::runtime::get_runtime_config;
15use crate::sampling::{should_sample, Signal};
16
17// When the governance feature is disabled, consent is unconditionally granted.
18#[cfg(not(feature = "governance"))]
19#[cfg_attr(test, mutants::skip)] // Dead under the default/governance build used in mutation CI.
20#[inline(always)]
21fn should_allow(_signal: &str, _level: Option<&str>) -> bool {
22    true
23}
24
25static METRICS_INITIALIZED: AtomicBool = AtomicBool::new(false);
26
27#[cfg(feature = "otel")]
28fn maybe_record_counter_add(name: &str, value: f64, attributes: Option<&BTreeMap<String, String>>) {
29    if !crate::otel::metrics::meter_provider_installed() {
30        return;
31    }
32    crate::otel::metrics::record_counter_add(name, value, attributes);
33}
34
35#[cfg(feature = "otel")]
36fn maybe_record_gauge_set(name: &str, value: f64, attributes: Option<&BTreeMap<String, String>>) {
37    if !crate::otel::metrics::meter_provider_installed() {
38        return;
39    }
40    crate::otel::metrics::record_gauge_set(name, value, attributes);
41}
42
43#[cfg(feature = "otel")]
44fn maybe_record_histogram(name: &str, value: f64, attributes: Option<&BTreeMap<String, String>>) {
45    if !crate::otel::metrics::meter_provider_installed() {
46        return;
47    }
48    crate::otel::metrics::record_histogram(name, value, attributes);
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct Meter {
53    name: String,
54}
55
56impl Meter {
57    pub fn name(&self) -> &str {
58        &self.name
59    }
60}
61
62#[derive(Clone, Debug, Default)]
63struct CounterState {
64    value: f64,
65}
66
67#[derive(Clone, Debug)]
68pub struct Counter {
69    name: String,
70    #[allow(dead_code)]
71    description: Option<String>,
72    #[allow(dead_code)]
73    unit: Option<String>,
74    state: Arc<Mutex<CounterState>>,
75}
76
77impl Counter {
78    pub fn add(&self, value: f64, attributes: Option<BTreeMap<String, String>>) {
79        if !metrics_enabled() {
80            return;
81        }
82        if !should_allow("metrics", None) {
83            return;
84        }
85        if !should_sample(Signal::Metrics, Some(&self.name)).unwrap_or(true) {
86            return;
87        }
88        let acquired = try_acquire(Signal::Metrics);
89        if acquired.is_none() {
90            return;
91        }
92        let ticket = acquired.expect("metrics ticket must exist after none guard");
93        crate::_lock::lock(&self.state).value += value;
94        #[cfg(feature = "otel")]
95        {
96            maybe_record_counter_add(&self.name, value, attributes.as_ref());
97        }
98        #[cfg(not(feature = "otel"))]
99        let _ = &attributes;
100        increment_emitted(Signal::Metrics, 1);
101        release(ticket);
102    }
103
104    pub fn value(&self) -> f64 {
105        crate::_lock::lock(&self.state).value
106    }
107}
108
109#[derive(Clone, Debug, Default)]
110struct GaugeState {
111    last_value: f64,
112}
113
114#[derive(Clone, Debug)]
115pub struct Gauge {
116    name: String,
117    #[allow(dead_code)]
118    description: Option<String>,
119    #[allow(dead_code)]
120    unit: Option<String>,
121    state: Arc<Mutex<GaugeState>>,
122}
123
124impl Gauge {
125    pub fn add(&self, value: f64, attributes: Option<BTreeMap<String, String>>) {
126        if !metrics_enabled() {
127            return;
128        }
129        if !should_allow("metrics", None) {
130            return;
131        }
132        if !should_sample(Signal::Metrics, Some(&self.name)).unwrap_or(true) {
133            return;
134        }
135        let acquired = try_acquire(Signal::Metrics);
136        if acquired.is_none() {
137            return;
138        }
139        let ticket = acquired.expect("metrics ticket must exist after none guard");
140        #[cfg_attr(not(feature = "otel"), allow(unused_variables))]
141        let new_absolute = {
142            let mut state = crate::_lock::lock(&self.state);
143            state.last_value += value;
144            state.last_value
145        };
146        #[cfg(feature = "otel")]
147        {
148            maybe_record_gauge_set(&self.name, new_absolute, attributes.as_ref());
149        }
150        #[cfg(not(feature = "otel"))]
151        let _ = &attributes;
152        increment_emitted(Signal::Metrics, 1);
153        release(ticket);
154    }
155
156    pub fn set(&self, value: f64, attributes: Option<BTreeMap<String, String>>) {
157        if !metrics_enabled() {
158            return;
159        }
160        if !should_allow("metrics", None) {
161            return;
162        }
163        if !should_sample(Signal::Metrics, Some(&self.name)).unwrap_or(true) {
164            return;
165        }
166        let acquired = try_acquire(Signal::Metrics);
167        if acquired.is_none() {
168            return;
169        }
170        let ticket = acquired.expect("metrics ticket must exist after none guard");
171        crate::_lock::lock(&self.state).last_value = value;
172        #[cfg(feature = "otel")]
173        {
174            maybe_record_gauge_set(&self.name, value, attributes.as_ref());
175        }
176        #[cfg(not(feature = "otel"))]
177        let _ = &attributes;
178        increment_emitted(Signal::Metrics, 1);
179        release(ticket);
180    }
181
182    pub fn value(&self) -> f64 {
183        crate::_lock::lock(&self.state).last_value
184    }
185}
186
187#[derive(Clone, Debug, Default)]
188struct HistogramState {
189    count: usize,
190    total: f64,
191}
192
193#[derive(Clone, Debug)]
194pub struct Histogram {
195    name: String,
196    #[allow(dead_code)]
197    description: Option<String>,
198    #[allow(dead_code)]
199    unit: Option<String>,
200    state: Arc<Mutex<HistogramState>>,
201}
202
203impl Histogram {
204    pub fn record(&self, value: f64, attributes: Option<BTreeMap<String, String>>) {
205        if !metrics_enabled() {
206            return;
207        }
208        if !should_allow("metrics", None) {
209            return;
210        }
211        if !should_sample(Signal::Metrics, Some(&self.name)).unwrap_or(true) {
212            return;
213        }
214        let acquired = try_acquire(Signal::Metrics);
215        if acquired.is_none() {
216            return;
217        }
218        let ticket = acquired.expect("metrics ticket must exist after none guard");
219        let mut state = crate::_lock::lock(&self.state);
220        state.count += 1;
221        state.total += value;
222        drop(state);
223        #[cfg(feature = "otel")]
224        {
225            maybe_record_histogram(&self.name, value, attributes.as_ref());
226        }
227        #[cfg(not(feature = "otel"))]
228        let _ = &attributes;
229        increment_emitted(Signal::Metrics, 1);
230        release(ticket);
231    }
232
233    pub fn count(&self) -> usize {
234        crate::_lock::lock(&self.state).count
235    }
236
237    pub fn total(&self) -> f64 {
238        crate::_lock::lock(&self.state).total
239    }
240}
241
242fn metrics_enabled() -> bool {
243    get_runtime_config()
244        .map(|config| config.metrics.enabled)
245        .unwrap_or(true)
246}
247
248pub fn get_meter(name: Option<&str>) -> Meter {
249    Meter {
250        name: name.unwrap_or("provide.telemetry").to_string(),
251    }
252}
253
254pub fn counter(name: &str, description: Option<&str>, unit: Option<&str>) -> Counter {
255    METRICS_INITIALIZED.store(true, Ordering::SeqCst);
256    Counter {
257        name: name.to_string(),
258        description: description.map(str::to_string),
259        unit: unit.map(str::to_string),
260        state: Arc::new(Mutex::new(CounterState::default())),
261    }
262}
263
264pub fn gauge(name: &str, description: Option<&str>, unit: Option<&str>) -> Gauge {
265    METRICS_INITIALIZED.store(true, Ordering::SeqCst);
266    Gauge {
267        name: name.to_string(),
268        description: description.map(str::to_string),
269        unit: unit.map(str::to_string),
270        state: Arc::new(Mutex::new(GaugeState::default())),
271    }
272}
273
274pub fn histogram(name: &str, description: Option<&str>, unit: Option<&str>) -> Histogram {
275    METRICS_INITIALIZED.store(true, Ordering::SeqCst);
276    Histogram {
277        name: name.to_string(),
278        description: description.map(str::to_string),
279        unit: unit.map(str::to_string),
280        state: Arc::new(Mutex::new(HistogramState::default())),
281    }
282}
283
284pub fn metrics_initialized_for_tests() -> bool {
285    METRICS_INITIALIZED.load(Ordering::SeqCst)
286}
287
288pub fn reset_metrics_for_tests() {
289    METRICS_INITIALIZED.store(false, Ordering::SeqCst);
290}
291
292#[cfg(test)]
293#[path = "metrics_tests.rs"]
294mod tests;