1use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
8
9use dashmap::DashMap;
10use serde::{Deserialize, Serialize};
11
12pub const DEFAULT_BUCKETS: &[f64] = &[1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0];
14
15pub const METRIC_MESSAGES_SENT: &str = "kernel.messages_sent";
19pub const METRIC_MESSAGES_DELIVERED: &str = "kernel.messages_delivered";
21pub const METRIC_MESSAGES_DROPPED: &str = "kernel.messages_dropped";
23pub const METRIC_AGENT_SPAWNS: &str = "kernel.agent_spawns";
25pub const METRIC_AGENT_CRASHES: &str = "kernel.agent_crashes";
27pub const METRIC_TOOL_EXECUTIONS: &str = "kernel.tool_executions";
29pub const METRIC_ACTIVE_AGENTS: &str = "kernel.active_agents";
31pub const METRIC_ACTIVE_SERVICES: &str = "kernel.active_services";
33pub const METRIC_CHAIN_LENGTH: &str = "kernel.chain_length";
35pub const METRIC_IPC_LATENCY_MS: &str = "kernel.ipc_latency_ms";
37pub const METRIC_TOOL_EXECUTION_MS: &str = "kernel.tool_execution_ms";
39pub const METRIC_GOVERNANCE_EVAL_MS: &str = "kernel.governance_evaluation_ms";
41
42pub struct Histogram {
47 buckets: Vec<(f64, AtomicU64)>,
50 sum_bits: AtomicU64,
52 count: AtomicU64,
54}
55
56impl Histogram {
57 pub fn new(boundaries: &[f64]) -> Self {
62 let mut sorted: Vec<f64> = boundaries.to_vec();
63 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
64 sorted.dedup();
65
66 let buckets = sorted.into_iter().map(|b| (b, AtomicU64::new(0))).collect();
67
68 Self {
69 buckets,
70 sum_bits: AtomicU64::new(0),
71 count: AtomicU64::new(0),
72 }
73 }
74
75 pub fn with_defaults() -> Self {
77 Self::new(DEFAULT_BUCKETS)
78 }
79
80 pub fn record(&self, value: f64) {
82 for (bound, count) in &self.buckets {
85 if value <= *bound {
86 count.fetch_add(1, Ordering::Relaxed);
87 }
88 }
89
90 loop {
92 let old_bits = self.sum_bits.load(Ordering::Relaxed);
93 let old_val = f64::from_bits(old_bits);
94 let new_val = old_val + value;
95 let new_bits = new_val.to_bits();
96 if self
97 .sum_bits
98 .compare_exchange_weak(old_bits, new_bits, Ordering::Relaxed, Ordering::Relaxed)
99 .is_ok()
100 {
101 break;
102 }
103 }
104
105 self.count.fetch_add(1, Ordering::Relaxed);
106 }
107
108 pub fn total_count(&self) -> u64 {
110 self.count.load(Ordering::Relaxed)
111 }
112
113 pub fn sum(&self) -> f64 {
115 f64::from_bits(self.sum_bits.load(Ordering::Relaxed))
116 }
117
118 pub fn bucket_snapshot(&self) -> Vec<(f64, u64)> {
120 self.buckets
121 .iter()
122 .map(|(bound, count)| (*bound, count.load(Ordering::Relaxed)))
123 .collect()
124 }
125
126 pub fn percentile(&self, p: f64) -> Option<f64> {
131 let total = self.total_count();
132 if total == 0 {
133 return None;
134 }
135
136 let target = (p * total as f64).ceil() as u64;
137 let buckets = self.bucket_snapshot();
138
139 for (i, (bound, cumulative)) in buckets.iter().enumerate() {
141 if *cumulative >= target {
142 if i == 0 {
143 return Some(*bound);
144 }
145 let prev_bound = buckets[i - 1].0;
147 let prev_count = buckets[i - 1].1;
148 let remaining = target.saturating_sub(prev_count) as f64;
149 let bucket_count = cumulative.saturating_sub(prev_count) as f64;
150 if bucket_count <= 0.0 {
151 return Some(*bound);
152 }
153 let fraction = remaining / bucket_count;
154 return Some(prev_bound + fraction * (bound - prev_bound));
155 }
156 }
157
158 buckets.last().map(|(b, _)| *b)
160 }
161}
162
163#[non_exhaustive]
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub enum MetricSnapshot {
167 Counter {
169 name: String,
171 value: u64,
173 },
174 Gauge {
176 name: String,
178 value: i64,
180 },
181 Histogram {
183 name: String,
185 buckets: Vec<(f64, u64)>,
187 sum: f64,
189 count: u64,
191 },
192}
193
194pub struct MetricsRegistry {
200 counters: DashMap<String, AtomicU64>,
201 gauges: DashMap<String, AtomicI64>,
202 histograms: DashMap<String, Histogram>,
203}
204
205impl MetricsRegistry {
206 pub fn new() -> Self {
208 Self {
209 counters: DashMap::new(),
210 gauges: DashMap::new(),
211 histograms: DashMap::new(),
212 }
213 }
214
215 pub fn with_builtins() -> Self {
217 let registry = Self::new();
218
219 for name in [
221 METRIC_MESSAGES_SENT,
222 METRIC_MESSAGES_DELIVERED,
223 METRIC_MESSAGES_DROPPED,
224 METRIC_AGENT_SPAWNS,
225 METRIC_AGENT_CRASHES,
226 METRIC_TOOL_EXECUTIONS,
227 ] {
228 registry
229 .counters
230 .insert(name.to_string(), AtomicU64::new(0));
231 }
232
233 for name in [
235 METRIC_ACTIVE_AGENTS,
236 METRIC_ACTIVE_SERVICES,
237 METRIC_CHAIN_LENGTH,
238 ] {
239 registry.gauges.insert(name.to_string(), AtomicI64::new(0));
240 }
241
242 for name in [
244 METRIC_IPC_LATENCY_MS,
245 METRIC_TOOL_EXECUTION_MS,
246 METRIC_GOVERNANCE_EVAL_MS,
247 ] {
248 registry
249 .histograms
250 .insert(name.to_string(), Histogram::with_defaults());
251 }
252
253 registry
254 }
255
256 pub fn counter_inc(&self, name: &str) {
260 self.counter_add(name, 1);
261 }
262
263 pub fn counter_add(&self, name: &str, value: u64) {
265 match self.counters.get(name) {
266 Some(counter) => {
267 counter.fetch_add(value, Ordering::Relaxed);
268 }
269 None => {
270 self.counters
271 .insert(name.to_string(), AtomicU64::new(value));
272 }
273 }
274 }
275
276 pub fn counter_get(&self, name: &str) -> u64 {
278 self.counters
279 .get(name)
280 .map(|c| c.load(Ordering::Relaxed))
281 .unwrap_or(0)
282 }
283
284 pub fn gauge_set(&self, name: &str, value: i64) {
288 match self.gauges.get(name) {
289 Some(gauge) => {
290 gauge.store(value, Ordering::Relaxed);
291 }
292 None => {
293 self.gauges.insert(name.to_string(), AtomicI64::new(value));
294 }
295 }
296 }
297
298 pub fn gauge_inc(&self, name: &str, delta: i64) {
300 match self.gauges.get(name) {
301 Some(gauge) => {
302 gauge.fetch_add(delta, Ordering::Relaxed);
303 }
304 None => {
305 self.gauges.insert(name.to_string(), AtomicI64::new(delta));
306 }
307 }
308 }
309
310 pub fn gauge_dec(&self, name: &str, delta: i64) {
312 self.gauge_inc(name, -delta);
313 }
314
315 pub fn gauge_get(&self, name: &str) -> i64 {
317 self.gauges
318 .get(name)
319 .map(|g| g.load(Ordering::Relaxed))
320 .unwrap_or(0)
321 }
322
323 pub fn histogram_record(&self, name: &str, value: f64) {
329 match self.histograms.get(name) {
330 Some(hist) => {
331 hist.record(value);
332 }
333 None => {
334 let hist = Histogram::with_defaults();
335 hist.record(value);
336 self.histograms.insert(name.to_string(), hist);
337 }
338 }
339 }
340
341 pub fn histogram_register(&self, name: &str, boundaries: &[f64]) {
343 if !self.histograms.contains_key(name) {
344 self.histograms
345 .insert(name.to_string(), Histogram::new(boundaries));
346 }
347 }
348
349 pub fn histogram_percentile(&self, name: &str, p: f64) -> Option<f64> {
351 self.histograms.get(name).and_then(|h| h.percentile(p))
352 }
353
354 pub fn histogram_buckets(&self, name: &str) -> Option<Vec<(f64, u64)>> {
356 self.histograms.get(name).map(|h| h.bucket_snapshot())
357 }
358
359 pub fn snapshot_all(&self) -> Vec<MetricSnapshot> {
363 let mut snapshots = Vec::new();
364
365 for entry in self.counters.iter() {
366 snapshots.push(MetricSnapshot::Counter {
367 name: entry.key().clone(),
368 value: entry.value().load(Ordering::Relaxed),
369 });
370 }
371
372 for entry in self.gauges.iter() {
373 snapshots.push(MetricSnapshot::Gauge {
374 name: entry.key().clone(),
375 value: entry.value().load(Ordering::Relaxed),
376 });
377 }
378
379 for entry in self.histograms.iter() {
380 let hist = entry.value();
381 snapshots.push(MetricSnapshot::Histogram {
382 name: entry.key().clone(),
383 buckets: hist.bucket_snapshot(),
384 sum: hist.sum(),
385 count: hist.total_count(),
386 });
387 }
388
389 snapshots
390 }
391
392 pub fn list_names(&self) -> Vec<String> {
394 let mut names: Vec<String> = Vec::new();
395 for e in self.counters.iter() {
396 names.push(e.key().clone());
397 }
398 for e in self.gauges.iter() {
399 names.push(e.key().clone());
400 }
401 for e in self.histograms.iter() {
402 names.push(e.key().clone());
403 }
404 names
405 }
406}
407
408impl Default for MetricsRegistry {
409 fn default() -> Self {
410 Self::new()
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417
418 #[test]
419 fn counter_increment_and_get() {
420 let registry = MetricsRegistry::new();
421 registry.counter_inc("test.counter");
422 registry.counter_inc("test.counter");
423 assert_eq!(registry.counter_get("test.counter"), 2);
424 }
425
426 #[test]
427 fn counter_add() {
428 let registry = MetricsRegistry::new();
429 registry.counter_add("test.counter", 10);
430 registry.counter_add("test.counter", 5);
431 assert_eq!(registry.counter_get("test.counter"), 15);
432 }
433
434 #[test]
435 fn counter_get_nonexistent_returns_zero() {
436 let registry = MetricsRegistry::new();
437 assert_eq!(registry.counter_get("nope"), 0);
438 }
439
440 #[test]
441 fn gauge_set_and_get() {
442 let registry = MetricsRegistry::new();
443 registry.gauge_set("test.gauge", 42);
444 assert_eq!(registry.gauge_get("test.gauge"), 42);
445 }
446
447 #[test]
448 fn gauge_increment_decrement() {
449 let registry = MetricsRegistry::new();
450 registry.gauge_set("test.gauge", 10);
451 registry.gauge_inc("test.gauge", 5);
452 assert_eq!(registry.gauge_get("test.gauge"), 15);
453 registry.gauge_dec("test.gauge", 3);
454 assert_eq!(registry.gauge_get("test.gauge"), 12);
455 }
456
457 #[test]
458 fn gauge_negative() {
459 let registry = MetricsRegistry::new();
460 registry.gauge_set("test.gauge", -5);
461 assert_eq!(registry.gauge_get("test.gauge"), -5);
462 }
463
464 #[test]
465 fn histogram_record_and_count() {
466 let registry = MetricsRegistry::new();
467 registry.histogram_record("test.hist", 5.0);
468 registry.histogram_record("test.hist", 15.0);
469 registry.histogram_record("test.hist", 150.0);
470
471 let buckets = registry.histogram_buckets("test.hist").unwrap();
472 let b5 = buckets
478 .iter()
479 .find(|(b, _)| (*b - 5.0).abs() < f64::EPSILON);
480 assert_eq!(b5.unwrap().1, 1);
481
482 let b25 = buckets
484 .iter()
485 .find(|(b, _)| (*b - 25.0).abs() < f64::EPSILON);
486 assert_eq!(b25.unwrap().1, 2);
487
488 let b250 = buckets
490 .iter()
491 .find(|(b, _)| (*b - 250.0).abs() < f64::EPSILON);
492 assert_eq!(b250.unwrap().1, 3);
493 }
494
495 #[test]
496 fn histogram_sum_and_count() {
497 let hist = Histogram::with_defaults();
498 hist.record(10.0);
499 hist.record(20.0);
500 hist.record(30.0);
501
502 assert_eq!(hist.total_count(), 3);
503 assert!((hist.sum() - 60.0).abs() < f64::EPSILON);
504 }
505
506 #[test]
507 fn histogram_percentile_p50() {
508 let hist = Histogram::new(&[10.0, 20.0, 30.0, 40.0, 50.0]);
509 for v in [5.0, 15.0, 25.0, 35.0, 45.0] {
510 hist.record(v);
511 }
512
513 let p50 = hist.percentile(0.50).unwrap();
514 assert!(p50 >= 20.0 && p50 <= 30.0, "p50 = {p50}");
516 }
517
518 #[test]
519 fn histogram_percentile_p95() {
520 let hist = Histogram::new(&[10.0, 50.0, 100.0, 500.0, 1000.0]);
521 for _ in 0..95 {
522 hist.record(5.0);
523 }
524 for _ in 0..5 {
525 hist.record(800.0);
526 }
527
528 let p95 = hist.percentile(0.95).unwrap();
529 assert!(p95 <= 10.0, "p95 = {p95}");
531 }
532
533 #[test]
534 fn histogram_percentile_p99() {
535 let hist = Histogram::new(&[10.0, 50.0, 100.0, 500.0, 1000.0]);
536 for _ in 0..99 {
537 hist.record(5.0);
538 }
539 hist.record(800.0);
540
541 let p99 = hist.percentile(0.99).unwrap();
542 assert!(p99 <= 10.0, "p99 = {p99}");
543 }
544
545 #[test]
546 fn histogram_percentile_empty() {
547 let hist = Histogram::with_defaults();
548 assert!(hist.percentile(0.50).is_none());
549 }
550
551 #[test]
552 fn registry_with_builtins() {
553 let registry = MetricsRegistry::with_builtins();
554 assert_eq!(registry.counter_get(METRIC_MESSAGES_SENT), 0);
555 assert_eq!(registry.gauge_get(METRIC_ACTIVE_AGENTS), 0);
556 assert!(registry.histogram_buckets(METRIC_IPC_LATENCY_MS).is_some());
557 }
558
559 #[test]
560 fn snapshot_all() {
561 let registry = MetricsRegistry::new();
562 registry.counter_inc("c1");
563 registry.gauge_set("g1", 42);
564 registry.histogram_record("h1", 10.0);
565
566 let snap = registry.snapshot_all();
567 assert_eq!(snap.len(), 3);
568
569 let has_counter = snap
570 .iter()
571 .any(|s| matches!(s, MetricSnapshot::Counter { name, .. } if name == "c1"));
572 let has_gauge = snap.iter().any(
573 |s| matches!(s, MetricSnapshot::Gauge { name, value } if name == "g1" && *value == 42),
574 );
575 let has_hist = snap
576 .iter()
577 .any(|s| matches!(s, MetricSnapshot::Histogram { name, .. } if name == "h1"));
578
579 assert!(has_counter);
580 assert!(has_gauge);
581 assert!(has_hist);
582 }
583
584 #[test]
585 fn list_names() {
586 let registry = MetricsRegistry::new();
587 registry.counter_inc("alpha");
588 registry.gauge_set("beta", 1);
589 registry.histogram_record("gamma", 1.0);
590
591 let names = registry.list_names();
592 assert_eq!(names.len(), 3);
593 assert!(names.contains(&"alpha".to_string()));
594 assert!(names.contains(&"beta".to_string()));
595 assert!(names.contains(&"gamma".to_string()));
596 }
597
598 #[test]
599 fn counter_concurrent_atomicity() {
600 let registry = MetricsRegistry::new();
601 for _ in 0..1000 {
603 registry.counter_inc("concurrent");
604 }
605 assert_eq!(registry.counter_get("concurrent"), 1000);
606 }
607
608 #[test]
609 fn histogram_custom_buckets() {
610 let registry = MetricsRegistry::new();
611 registry.histogram_register("custom", &[1.0, 2.0, 5.0]);
612 registry.histogram_record("custom", 1.5);
613
614 let buckets = registry.histogram_buckets("custom").unwrap();
615 assert_eq!(buckets.len(), 3);
616 assert_eq!(buckets[0].1, 0); assert_eq!(buckets[1].1, 1); assert_eq!(buckets[2].1, 1); }
621
622 #[test]
623 fn metric_snapshot_serde() {
624 let snap = MetricSnapshot::Counter {
625 name: "test".into(),
626 value: 42,
627 };
628 let json = serde_json::to_string(&snap).unwrap();
629 let restored: MetricSnapshot = serde_json::from_str(&json).unwrap();
630 assert!(matches!(
631 restored,
632 MetricSnapshot::Counter { value: 42, .. }
633 ));
634 }
635
636 #[test]
637 fn default_registry() {
638 let registry = MetricsRegistry::default();
639 assert!(registry.list_names().is_empty());
640 }
641}