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::runtime::get_runtime_config;
12use crate::sampling::{should_sample, Signal};
13
14static METRICS_INITIALIZED: AtomicBool = AtomicBool::new(false);
15
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct Meter {
18    name: String,
19}
20
21impl Meter {
22    pub fn name(&self) -> &str {
23        &self.name
24    }
25}
26
27#[derive(Clone, Debug, Default)]
28struct CounterState {
29    value: f64,
30}
31
32#[derive(Clone, Debug)]
33pub struct Counter {
34    name: String,
35    #[allow(dead_code)]
36    description: Option<String>,
37    #[allow(dead_code)]
38    unit: Option<String>,
39    state: Arc<Mutex<CounterState>>,
40}
41
42impl Counter {
43    pub fn add(&self, value: f64, _attributes: Option<BTreeMap<String, String>>) {
44        if !metrics_enabled() {
45            return;
46        }
47        if !should_sample(Signal::Metrics, Some(&self.name)).unwrap_or(true) {
48            return;
49        }
50        let Some(ticket) = try_acquire(Signal::Metrics) else {
51            return;
52        };
53        self.state
54            .lock()
55            .expect("counter state lock poisoned")
56            .value += value;
57        release(ticket);
58    }
59
60    pub fn value(&self) -> f64 {
61        self.state
62            .lock()
63            .expect("counter state lock poisoned")
64            .value
65    }
66}
67
68#[derive(Clone, Debug, Default)]
69struct GaugeState {
70    last_value: f64,
71}
72
73#[derive(Clone, Debug)]
74pub struct Gauge {
75    name: String,
76    #[allow(dead_code)]
77    description: Option<String>,
78    #[allow(dead_code)]
79    unit: Option<String>,
80    state: Arc<Mutex<GaugeState>>,
81}
82
83impl Gauge {
84    pub fn add(&self, value: f64, _attributes: Option<BTreeMap<String, String>>) {
85        if !metrics_enabled() {
86            return;
87        }
88        if !should_sample(Signal::Metrics, Some(&self.name)).unwrap_or(true) {
89            return;
90        }
91        let Some(ticket) = try_acquire(Signal::Metrics) else {
92            return;
93        };
94        self.state
95            .lock()
96            .expect("gauge state lock poisoned")
97            .last_value += value;
98        release(ticket);
99    }
100
101    pub fn set(&self, value: f64, _attributes: Option<BTreeMap<String, String>>) {
102        if !metrics_enabled() {
103            return;
104        }
105        if !should_sample(Signal::Metrics, Some(&self.name)).unwrap_or(true) {
106            return;
107        }
108        let Some(ticket) = try_acquire(Signal::Metrics) else {
109            return;
110        };
111        self.state
112            .lock()
113            .expect("gauge state lock poisoned")
114            .last_value = value;
115        release(ticket);
116    }
117
118    pub fn value(&self) -> f64 {
119        self.state
120            .lock()
121            .expect("gauge state lock poisoned")
122            .last_value
123    }
124}
125
126#[derive(Clone, Debug, Default)]
127struct HistogramState {
128    count: usize,
129    total: f64,
130}
131
132#[derive(Clone, Debug)]
133pub struct Histogram {
134    name: String,
135    #[allow(dead_code)]
136    description: Option<String>,
137    #[allow(dead_code)]
138    unit: Option<String>,
139    state: Arc<Mutex<HistogramState>>,
140}
141
142impl Histogram {
143    pub fn record(&self, value: f64, _attributes: Option<BTreeMap<String, String>>) {
144        if !metrics_enabled() {
145            return;
146        }
147        if !should_sample(Signal::Metrics, Some(&self.name)).unwrap_or(true) {
148            return;
149        }
150        let Some(ticket) = try_acquire(Signal::Metrics) else {
151            return;
152        };
153        let mut state = self.state.lock().expect("histogram state lock poisoned");
154        state.count += 1;
155        state.total += value;
156        release(ticket);
157    }
158
159    pub fn count(&self) -> usize {
160        self.state
161            .lock()
162            .expect("histogram state lock poisoned")
163            .count
164    }
165
166    pub fn total(&self) -> f64 {
167        self.state
168            .lock()
169            .expect("histogram state lock poisoned")
170            .total
171    }
172}
173
174fn metrics_enabled() -> bool {
175    get_runtime_config()
176        .map(|config| config.metrics.enabled)
177        .unwrap_or(true)
178}
179
180pub fn get_meter(name: Option<&str>) -> Meter {
181    Meter {
182        name: name.unwrap_or("provide.telemetry").to_string(),
183    }
184}
185
186pub fn counter(name: &str, description: Option<&str>, unit: Option<&str>) -> Counter {
187    METRICS_INITIALIZED.store(true, Ordering::SeqCst);
188    Counter {
189        name: name.to_string(),
190        description: description.map(str::to_string),
191        unit: unit.map(str::to_string),
192        state: Arc::new(Mutex::new(CounterState::default())),
193    }
194}
195
196pub fn gauge(name: &str, description: Option<&str>, unit: Option<&str>) -> Gauge {
197    METRICS_INITIALIZED.store(true, Ordering::SeqCst);
198    Gauge {
199        name: name.to_string(),
200        description: description.map(str::to_string),
201        unit: unit.map(str::to_string),
202        state: Arc::new(Mutex::new(GaugeState::default())),
203    }
204}
205
206pub fn histogram(name: &str, description: Option<&str>, unit: Option<&str>) -> Histogram {
207    METRICS_INITIALIZED.store(true, Ordering::SeqCst);
208    Histogram {
209        name: name.to_string(),
210        description: description.map(str::to_string),
211        unit: unit.map(str::to_string),
212        state: Arc::new(Mutex::new(HistogramState::default())),
213    }
214}
215
216pub fn metrics_initialized_for_tests() -> bool {
217    METRICS_INITIALIZED.load(Ordering::SeqCst)
218}
219
220pub fn reset_metrics_for_tests() {
221    METRICS_INITIALIZED.store(false, Ordering::SeqCst);
222}