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