1use 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#[cfg(not(feature = "governance"))]
19#[cfg_attr(test, mutants::skip)] #[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;