Skip to main content

memlink_runtime/
metrics.rs

1//! Runtime metrics collection with Prometheus-compatible output.
2
3use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
4use std::time::Duration;
5
6#[derive(Debug, Default)]
7pub struct Counter {
8    value: AtomicU64,
9}
10
11impl Counter {
12    pub fn new() -> Self {
13        Counter {
14            value: AtomicU64::new(0),
15        }
16    }
17
18    pub fn inc(&self) {
19        self.value.fetch_add(1, Ordering::Relaxed);
20    }
21
22    pub fn inc_by(&self, amount: u64) {
23        self.value.fetch_add(amount, Ordering::Relaxed);
24    }
25
26    pub fn get(&self) -> u64 {
27        self.value.load(Ordering::Relaxed)
28    }
29
30    pub fn reset(&self) {
31        self.value.store(0, Ordering::Relaxed);
32    }
33}
34
35#[derive(Debug)]
36pub struct Histogram {
37    count: AtomicU64,
38    sum: AtomicU64,
39    min: AtomicU64,
40    max: AtomicU64,
41    buckets: &'static [u64],
42    bucket_counts: Box<[AtomicU64]>,
43}
44
45impl Histogram {
46    pub fn new() -> Self {
47        const DEFAULT_BUCKETS: &[u64] = &[
48            10, 50, 100, 500, 1_000, 5_000, 10_000, 50_000, 100_000, 500_000, 1_000_000,
49        ];
50        Self::with_buckets(DEFAULT_BUCKETS)
51    }
52
53    pub fn with_buckets(buckets: &'static [u64]) -> Self {
54        let bucket_counts: Box<[AtomicU64]> = buckets
55            .iter()
56            .map(|_| AtomicU64::new(0))
57            .collect();
58
59        Histogram {
60            count: AtomicU64::new(0),
61            sum: AtomicU64::new(0),
62            min: AtomicU64::new(u64::MAX),
63            max: AtomicU64::new(0),
64            buckets,
65            bucket_counts,
66        }
67    }
68
69    pub fn observe(&self, duration: Duration) {
70        let micros = duration.as_micros() as u64;
71
72        self.count.fetch_add(1, Ordering::Relaxed);
73        self.sum.fetch_add(micros, Ordering::Relaxed);
74
75        let mut current_min = self.min.load(Ordering::Relaxed);
76        while micros < current_min {
77            match self.min.compare_exchange_weak(
78                current_min,
79                micros,
80                Ordering::Relaxed,
81                Ordering::Relaxed,
82            ) {
83                Ok(_) => break,
84                Err(x) => current_min = x,
85            }
86        }
87
88        let mut current_max = self.max.load(Ordering::Relaxed);
89        while micros > current_max {
90            match self.max.compare_exchange_weak(
91                current_max,
92                micros,
93                Ordering::Relaxed,
94                Ordering::Relaxed,
95            ) {
96                Ok(_) => break,
97                Err(x) => current_max = x,
98            }
99        }
100
101        for (i, &bucket) in self.buckets.iter().enumerate() {
102            if micros <= bucket {
103                self.bucket_counts[i].fetch_add(1, Ordering::Relaxed);
104            }
105        }
106    }
107
108    pub fn count(&self) -> u64 {
109        self.count.load(Ordering::Relaxed)
110    }
111
112    pub fn sum(&self) -> u64 {
113        self.sum.load(Ordering::Relaxed)
114    }
115
116    pub fn avg(&self) -> f64 {
117        let count = self.count.load(Ordering::Relaxed);
118        if count == 0 {
119            0.0
120        } else {
121            self.sum.load(Ordering::Relaxed) as f64 / count as f64
122        }
123    }
124
125    pub fn min(&self) -> u64 {
126        let min = self.min.load(Ordering::Relaxed);
127        if min == u64::MAX { 0 } else { min }
128    }
129
130    pub fn max(&self) -> u64 {
131        self.max.load(Ordering::Relaxed)
132    }
133
134    pub fn bucket_counts(&self) -> Vec<u64> {
135        self.bucket_counts
136            .iter()
137            .map(|b| b.load(Ordering::Relaxed))
138            .collect()
139    }
140
141    pub fn reset(&self) {
142        self.count.store(0, Ordering::Relaxed);
143        self.sum.store(0, Ordering::Relaxed);
144        self.min.store(u64::MAX, Ordering::Relaxed);
145        self.max.store(0, Ordering::Relaxed);
146        for bucket in self.bucket_counts.iter() {
147            bucket.store(0, Ordering::Relaxed);
148        }
149    }
150}
151
152impl Default for Histogram {
153    fn default() -> Self {
154        Self::new()
155    }
156}
157
158#[derive(Debug)]
159pub struct RuntimeMetrics {
160    pub loads: Counter,
161    pub unloads: Counter,
162    pub calls: Counter,
163    pub panics: Counter,
164    pub reloads: Counter,
165    pub timeouts: Counter,
166    pub load_time: Histogram,
167    pub call_latency: Histogram,
168    pub modules_loaded: AtomicUsize,
169    pub calls_in_flight: AtomicUsize,
170}
171
172impl RuntimeMetrics {
173    pub fn new() -> Self {
174        RuntimeMetrics {
175            loads: Counter::new(),
176            unloads: Counter::new(),
177            calls: Counter::new(),
178            panics: Counter::new(),
179            reloads: Counter::new(),
180            timeouts: Counter::new(),
181            load_time: Histogram::new(),
182            call_latency: Histogram::new(),
183            modules_loaded: AtomicUsize::new(0),
184            calls_in_flight: AtomicUsize::new(0),
185        }
186    }
187
188    pub fn record_load(&self, duration: Duration) {
189        self.loads.inc();
190        self.modules_loaded.fetch_add(1, Ordering::Relaxed);
191        self.load_time.observe(duration);
192    }
193
194    pub fn record_unload(&self) {
195        self.unloads.inc();
196        self.modules_loaded.fetch_sub(1, Ordering::Relaxed);
197    }
198
199    pub fn record_call(&self, duration: Duration) {
200        self.calls.inc();
201        self.calls_in_flight.fetch_sub(1, Ordering::Relaxed);
202        self.call_latency.observe(duration);
203    }
204
205    pub fn record_call_start(&self) {
206        self.calls_in_flight.fetch_add(1, Ordering::Relaxed);
207    }
208
209    pub fn record_panic(&self) {
210        self.panics.inc();
211    }
212
213    pub fn record_reload(&self) {
214        self.reloads.inc();
215    }
216
217    pub fn record_timeout(&self) {
218        self.timeouts.inc();
219    }
220
221    pub fn prometheus_export(&self) -> String {
222        let mut output = String::new();
223
224        output.push_str(&format!(
225            "# HELP memlink_loads_total Total number of module loads\n\
226             # TYPE memlink_loads_total counter\n\
227             memlink_loads_total {}\n",
228            self.loads.get()
229        ));
230
231        output.push_str(&format!(
232            "# HELP memlink_unloads_total Total number of module unloads\n\
233             # TYPE memlink_unloads_total counter\n\
234             memlink_unloads_total {}\n",
235            self.unloads.get()
236        ));
237
238        output.push_str(&format!(
239            "# HELP memlink_calls_total Total number of module calls\n\
240             # TYPE memlink_calls_total counter\n\
241             memlink_calls_total {}\n",
242            self.calls.get()
243        ));
244
245        output.push_str(&format!(
246            "# HELP memlink_panics_total Total number of panics caught\n\
247             # TYPE memlink_panics_total counter\n\
248             memlink_panics_total {}\n",
249            self.panics.get()
250        ));
251
252        output.push_str(&format!(
253            "# HELP memlink_reloads_total Total number of reload operations\n\
254             # TYPE memlink_reloads_total counter\n\
255             memlink_reloads_total {}\n",
256            self.reloads.get()
257        ));
258
259        output.push_str(&format!(
260            "# HELP memlink_timeouts_total Total number of call timeouts\n\
261             # TYPE memlink_timeouts_total counter\n\
262             memlink_timeouts_total {}\n",
263            self.timeouts.get()
264        ));
265
266        output.push_str(&format!(
267            "# HELP memlink_modules_loaded Current number of loaded modules\n\
268             # TYPE memlink_modules_loaded gauge\n\
269             memlink_modules_loaded {}\n",
270            self.modules_loaded.load(Ordering::Relaxed)
271        ));
272
273        output.push_str(&format!(
274            "# HELP memlink_calls_in_flight Current number of in-flight calls\n\
275             # TYPE memlink_calls_in_flight gauge\n\
276             memlink_calls_in_flight {}\n",
277            self.calls_in_flight.load(Ordering::Relaxed)
278        ));
279
280        output.push_str(
281            "# HELP memlink_load_time_us Module load time in microseconds\n\
282             # TYPE memlink_load_time_us histogram\n"
283        );
284        let mut cumulative = 0u64;
285        for (i, &bucket) in self.load_time.buckets.iter().enumerate() {
286            cumulative += self.load_time.bucket_counts()[i];
287            output.push_str(&format!(
288                "memlink_load_time_us_bucket{{le=\"{}\"}} {}\n",
289                bucket, cumulative
290            ));
291        }
292        output.push_str(&format!(
293            "memlink_load_time_us_bucket{{le=\"+Inf\"}} {}\n\
294             memlink_load_time_us_sum {}\n\
295             memlink_load_time_us_count {}\n",
296            self.load_time.count(),
297            self.load_time.sum(),
298            self.load_time.count()
299        ));
300
301        output.push_str(
302            "# HELP memlink_call_latency_us Module call latency in microseconds\n\
303             # TYPE memlink_call_latency_us histogram\n"
304        );
305        cumulative = 0;
306        for (i, &bucket) in self.call_latency.buckets.iter().enumerate() {
307            cumulative += self.call_latency.bucket_counts()[i];
308            output.push_str(&format!(
309                "memlink_call_latency_us_bucket{{le=\"{}\"}} {}\n",
310                bucket, cumulative
311            ));
312        }
313        output.push_str(&format!(
314            "memlink_call_latency_us_bucket{{le=\"+Inf\"}} {}\n\
315             memlink_call_latency_us_sum {}\n\
316             memlink_call_latency_us_count {}\n",
317            self.call_latency.count(),
318            self.call_latency.sum(),
319            self.call_latency.count()
320        ));
321
322        output
323    }
324
325    pub fn reset(&self) {
326        self.loads.reset();
327        self.unloads.reset();
328        self.calls.reset();
329        self.panics.reset();
330        self.reloads.reset();
331        self.timeouts.reset();
332        self.load_time.reset();
333        self.call_latency.reset();
334        self.modules_loaded.store(0, Ordering::Relaxed);
335        self.calls_in_flight.store(0, Ordering::Relaxed);
336    }
337}
338
339impl Default for RuntimeMetrics {
340    fn default() -> Self {
341        Self::new()
342    }
343}