1use 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}