1#![allow(missing_docs)]
7
8use parking_lot::{Mutex, RwLock};
9use std::sync::atomic::{AtomicU64, Ordering};
10
11#[derive(Default)]
13pub struct MetricsRegistry {
14 counters: RwLock<Vec<Counter>>,
15 gauges: RwLock<Vec<Gauge>>,
16 histograms: RwLock<Vec<Histogram>>,
17 labeled_counters: RwLock<Vec<LabeledCounter>>,
24}
25
26struct LabeledCounter {
27 name: String,
28 help: String,
29 label_key: &'static str,
30 label_value: String,
31 value: AtomicU64,
32}
33
34impl MetricsRegistry {
35 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn counter(
42 &self,
43 name: &'static str,
44 help: &'static str,
45 labels: &[(&'static str, &'static str)],
46 ) -> CounterHandle {
47 let mut counters = self.counters.write();
48 let id = counters.len();
49 counters.push(Counter {
50 name: name.into(),
51 help: help.into(),
52 labels: labels.into(),
53 value: AtomicU64::new(0),
54 });
55 CounterHandle { id }
56 }
57
58 pub fn gauge(&self, name: &'static str, help: &'static str, initial: f64) -> GaugeHandle {
60 let mut gauges = self.gauges.write();
61 let id = gauges.len();
62 gauges.push(Gauge {
63 name: name.into(),
64 help: help.into(),
65 value: Mutex::new(initial),
66 });
67 GaugeHandle { id }
68 }
69
70 pub fn histogram(
72 &self,
73 name: &'static str,
74 help: &'static str,
75 buckets: Vec<f64>,
76 ) -> HistogramHandle {
77 let mut histograms = self.histograms.write();
78 let id = histograms.len();
79 let counts: Vec<usize> = vec![0; buckets.len() + 1];
80 histograms.push(Histogram {
81 name: name.into(),
82 help: help.into(),
83 buckets: buckets.clone(),
84 counts: RwLock::new(counts),
85 sum: Mutex::new(0.0),
86 count: Mutex::new(0u64),
87 });
88 HistogramHandle { id, buckets }
89 }
90 pub fn labeled_counter(
95 &self,
96 name: &'static str,
97 help: &'static str,
98 label_key: &'static str,
99 label_value: &str,
100 ) -> LabeledCounterHandle {
101 let mut labeled = self.labeled_counters.write();
102 for (i, lc) in labeled.iter().enumerate() {
104 if lc.name == name && lc.label_key == label_key && lc.label_value == label_value {
105 return LabeledCounterHandle { id: i };
106 }
107 }
108 let id = labeled.len();
109 labeled.push(LabeledCounter {
110 name: name.into(),
111 help: help.into(),
112 label_key,
113 label_value: label_value.into(),
114 value: AtomicU64::new(0),
115 });
116 LabeledCounterHandle { id }
117 }
118
119 pub fn export(&self) -> String {
121 let mut out = String::new();
122
123 {
125 let counters = self.counters.read();
126 for c in counters.iter() {
127 out.push_str(&format!("# HELP {} {}\n", c.name, c.help));
128 out.push_str(&format!("# TYPE {} counter\n", c.name));
129 let value = c.value.load(Ordering::Relaxed);
130 let labels_str = if c.labels.is_empty() {
131 String::new()
132 } else {
133 format!(
134 "{{{}}}",
135 c.labels
136 .iter()
137 .map(|(k, v)| format!("{k}=\"{v}\""))
138 .collect::<Vec<_>>()
139 .join(",")
140 )
141 };
142 out.push_str(&format!("{}{} {}\n", c.name, labels_str, value));
143 }
144 }
145
146 {
148 let gauges = self.gauges.read();
149 for g in gauges.iter() {
150 out.push_str(&format!("# HELP {} {}\n", g.name, g.help));
151 out.push_str(&format!("# TYPE {} gauge\n", g.name));
152 let value = *g.value.lock();
153 out.push_str(&format!("{} {}\n", g.name, value));
154 }
155 }
156
157 {
159 let histograms = self.histograms.read();
160 for h in histograms.iter() {
161 out.push_str(&format!("# HELP {} {}\n", h.name, h.help));
162 out.push_str(&format!("# TYPE {} histogram\n", h.name));
163 let sum = *h.sum.lock();
164 let count = *h.count.lock();
165 let bucket_values = h.buckets.clone();
166 let counts = h.counts.read();
167 let mut cumulative = 0usize;
168 for (i, _) in bucket_values.iter().enumerate() {
169 cumulative += counts[i];
170 let boundary = bucket_values[i];
171 out.push_str(&format!(
172 "{}{{le=\"{}\"}} {}\n",
173 h.name, boundary, cumulative
174 ));
175 }
176 out.push_str(&format!("{}{{le=\"+Inf\"}} {}\n", h.name, cumulative));
178 out.push_str(&format!("{}_sum {} \n", h.name, sum));
179 out.push_str(&format!("{}_count {} \n", h.name, count));
180 }
181 }
182
183 {
187 let labeled = self.labeled_counters.read();
188 let mut seen_help: std::collections::HashSet<String> = std::collections::HashSet::new();
189 for lc in labeled.iter() {
190 if seen_help.insert(lc.name.clone()) {
191 out.push_str(&format!("# HELP {} {}\n", lc.name, lc.help));
192 out.push_str(&format!("# TYPE {} counter\n", lc.name));
193 }
194 out.push_str(&format!(
195 "{}{{{}=\"{}\"}} {}\n",
196 lc.name,
197 lc.label_key,
198 lc.label_value,
199 lc.value.load(Ordering::Relaxed)
200 ));
201 }
202 }
203
204 out
205 }
206}
207
208static REGISTRY: std::sync::OnceLock<MetricsRegistry> = std::sync::OnceLock::new();
210
211pub fn registry() -> &'static MetricsRegistry {
213 REGISTRY.get_or_init(MetricsRegistry::new)
214}
215
216#[derive(Clone)]
217pub struct CounterHandle {
218 id: usize,
219}
220
221impl CounterHandle {
222 pub fn inc(&self) {
224 let r = registry();
225 let counters = r.counters.read();
226 if let Some(c) = counters.get(self.id) {
227 c.value.fetch_add(1, Ordering::Relaxed);
228 }
229 }
230
231 pub fn inc_by(&self, n: u64) {
233 if n == 0 {
234 return;
235 }
236 let r = registry();
237 let counters = r.counters.read();
238 if let Some(c) = counters.get(self.id) {
239 c.value.fetch_add(n, Ordering::Relaxed);
240 }
241 }
242}
243
244#[derive(Clone)]
247pub struct LabeledCounterHandle {
248 id: usize,
249}
250
251impl LabeledCounterHandle {
252 pub fn inc(&self) {
254 let r = registry();
255 let labeled = r.labeled_counters.read();
256 if let Some(c) = labeled.get(self.id) {
257 c.value.fetch_add(1, Ordering::Relaxed);
258 }
259 }
260
261 pub fn inc_by(&self, n: u64) {
263 if n == 0 {
264 return;
265 }
266 let r = registry();
267 let labeled = r.labeled_counters.read();
268 if let Some(c) = labeled.get(self.id) {
269 c.value.fetch_add(n, Ordering::Relaxed);
270 }
271 }
272}
273
274#[derive(Clone)]
275pub struct GaugeHandle {
276 id: usize,
277}
278
279impl GaugeHandle {
280 pub fn set(&self, v: f64) {
282 let r = registry();
283 let gauges = r.gauges.read();
284 if let Some(g) = gauges.get(self.id) {
285 *g.value.lock() = v;
286 }
287 }
288
289 pub fn inc(&self) {
291 let r = registry();
292 let gauges = r.gauges.read();
293 if let Some(g) = gauges.get(self.id) {
294 let mut val = g.value.lock();
295 *val += 1.0;
296 }
297 }
298
299 pub fn dec(&self) {
301 let r = registry();
302 let gauges = r.gauges.read();
303 if let Some(g) = gauges.get(self.id) {
304 let mut val = g.value.lock();
305 *val -= 1.0;
306 }
307 }
308}
309
310#[derive(Clone)]
311pub struct HistogramHandle {
312 id: usize,
313 buckets: Vec<f64>,
314}
315
316impl HistogramHandle {
317 pub fn observe(&self, value: f64) {
319 let r = registry();
320 let histograms = r.histograms.read();
321 if let Some(h) = histograms.get(self.id) {
322 {
323 let mut sum = h.sum.lock();
324 *sum += value;
325 }
326 {
327 let mut count = h.count.lock();
328 *count += 1;
329 }
330 {
331 let mut counts = h.counts.write();
332 for (i, boundary) in self.buckets.iter().enumerate() {
333 if value <= *boundary {
334 counts[i] += 1;
335 }
336 }
337 counts[self.buckets.len()] += 1;
339 }
340 }
341 }
342}
343
344struct Counter {
345 name: String,
346 help: String,
347 labels: Vec<(&'static str, &'static str)>,
348 value: AtomicU64,
349}
350
351struct Gauge {
352 name: String,
353 help: String,
354 value: Mutex<f64>,
355}
356
357struct Histogram {
358 name: String,
359 help: String,
360 buckets: Vec<f64>,
361 counts: RwLock<Vec<usize>>,
362 sum: Mutex<f64>,
363 count: Mutex<u64>,
364}
365
366#[derive(Clone)]
368pub struct MetricsHandles {
369 pub agents_forked: CounterHandle,
370 pub agents_completed: CounterHandle,
371 pub agents_failed: CounterHandle,
372 pub retry_attempted: CounterHandle,
374 pub retry_improved: CounterHandle,
375 pub retry_unchanged: CounterHandle,
376 pub retry_degraded: CounterHandle,
377 pub orch_duration: HistogramHandle,
378 pub messages: CounterHandle,
379 pub llm_circuit_breaker_state: GaugeHandle,
381 pub tool_calls: CounterHandle,
383 pub tool_errors: CounterHandle,
384 pub tool_duration: HistogramHandle,
385 pub llm_calls: CounterHandle,
387 pub llm_errors: CounterHandle,
388 pub audit_lagged_events: CounterHandle,
389
390 pub gateway_messages_delivered: LabeledCounterHandle,
396 pub gateway_messages_dropped: LabeledCounterHandle,
397 pub gateway_messages_resynced: LabeledCounterHandle,
398 pub gateway_messages_timed_out: LabeledCounterHandle,
399
400 pub gateway_replay_replay: LabeledCounterHandle,
402 pub gateway_replay_resync: LabeledCounterHandle,
403
404 pub gateway_response_duration: HistogramHandle,
406
407 pub sse_connections_open: LabeledCounterHandle,
412 pub sse_connections_close: LabeledCounterHandle,
413
414 pub ws_connections_open: LabeledCounterHandle,
416 pub ws_connections_close: LabeledCounterHandle,
417 pub ws_connections_keepalive_timeout: LabeledCounterHandle,
418
419 pub web_dist_swaps: CounterHandle,
421 pub supervisor_restarts: CounterHandle,
423
424 pub readiness_state: GaugeHandle,
427}
428
429impl MetricsHandles {
430 pub fn inc_agents_forked(&self) {
432 self.agents_forked.inc();
433 }
434
435 pub fn inc_agents_completed(&self) {
437 self.agents_completed.inc();
438 }
439
440 pub fn inc_agents_failed(&self) {
442 self.agents_failed.inc();
443 }
444
445 pub fn inc_messages(&self) {
447 self.messages.inc();
448 }
449 pub fn inc_supervisor_restart(&self) {
451 self.supervisor_restarts.inc();
452 }
453
454 pub fn observe_orch_duration(&self, value: f64) {
456 self.orch_duration.observe(value);
457 }
458}
459static METRICS: std::sync::OnceLock<MetricsHandles> = std::sync::OnceLock::new();
461
462pub fn get_metrics() -> &'static MetricsHandles {
464 METRICS.get_or_init(|| {
465 let r = registry();
466 MetricsHandles {
467 agents_forked: r.counter("oxios_agents_forked_total", "Total agents forked", &[]),
468 agents_completed: r.counter(
469 "oxios_agents_completed_total",
470 "Total agents completed",
471 &[],
472 ),
473 agents_failed: r.counter("oxios_agents_failed_total", "Total agents failed", &[]),
474 retry_attempted: r.counter(
475 "oxios_retry_attempted_total",
476 "Review retries attempted",
477 &[],
478 ),
479 retry_improved: r.counter(
480 "oxios_retry_improved_total",
481 "Retries that improved score",
482 &[],
483 ),
484 retry_unchanged: r.counter(
485 "oxios_retry_unchanged_total",
486 "Retries with same score",
487 &[],
488 ),
489 retry_degraded: r.counter(
490 "oxios_retry_degraded_total",
491 "Retries that degraded score",
492 &[],
493 ),
494 orch_duration: r.histogram(
495 "oxios_orchestration_duration_seconds",
496 "Orchestration duration",
497 vec![0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0],
498 ),
499 messages: r.counter("oxios_messages_processed_total", "Messages processed", &[]),
500 llm_circuit_breaker_state: r.gauge(
501 "oxios_llm_circuit_breaker_state",
502 "LLM circuit breaker state: 0=closed, 1=open, 2=half_open",
503 0.0,
504 ),
505 tool_calls: r.counter("oxios_tool_calls_total", "Tool calls", &[]),
506 tool_errors: r.counter("oxios_tool_errors_total", "Tool errors", &[]),
507 tool_duration: r.histogram(
508 "oxios_tool_duration_seconds",
509 "Tool call duration",
510 vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0],
511 ),
512 llm_calls: r.counter("oxios_llm_calls_total", "LLM API calls", &[]),
513 llm_errors: r.counter("oxios_llm_errors_total", "LLM API errors", &[]),
514 audit_lagged_events: r.counter(
515 "oxios_audit_lagged_events_total",
516 "Audit events dropped due to broadcast subscriber lag",
517 &[],
518 ),
519
520 gateway_messages_delivered: r.labeled_counter(
526 "oxios_gateway_messages_total",
527 "Outgoing messages (result=delivered|dropped|resynced|timed_out)",
528 "result",
529 "delivered",
530 ),
531 gateway_messages_dropped: r.labeled_counter(
532 "oxios_gateway_messages_total",
533 "Outgoing messages (result=delivered|dropped|resynced|timed_out)",
534 "result",
535 "dropped",
536 ),
537 gateway_messages_resynced: r.labeled_counter(
538 "oxios_gateway_messages_total",
539 "Outgoing messages (result=delivered|dropped|resynced|timed_out)",
540 "result",
541 "resynced",
542 ),
543 gateway_messages_timed_out: r.labeled_counter(
544 "oxios_gateway_messages_total",
545 "Outgoing messages (result=delivered|dropped|resynced|timed_out)",
546 "result",
547 "timed_out",
548 ),
549 gateway_replay_replay: r.labeled_counter(
550 "oxios_gateway_replay_requests_total",
551 "Replay requests (outcome=replay|resync)",
552 "outcome",
553 "replay",
554 ),
555 gateway_replay_resync: r.labeled_counter(
556 "oxios_gateway_replay_requests_total",
557 "Replay requests (outcome=replay|resync)",
558 "outcome",
559 "resync",
560 ),
561 gateway_response_duration: r.histogram(
562 "oxios_gateway_response_duration_seconds",
563 "send_and_wait duration in seconds",
564 vec![0.05, 0.25, 1.0, 5.0, 30.0, 60.0, 120.0],
565 ),
566 sse_connections_open: r.labeled_counter(
567 "oxios_sse_connections_total",
568 "SSE client connections (action=open|close)",
569 "action",
570 "open",
571 ),
572 sse_connections_close: r.labeled_counter(
573 "oxios_sse_connections_total",
574 "SSE client connections (action=open|close)",
575 "action",
576 "close",
577 ),
578 ws_connections_open: r.labeled_counter(
579 "oxios_ws_connections_total",
580 "WS client connections (action=open|close|keepalive_timeout)",
581 "action",
582 "open",
583 ),
584 ws_connections_close: r.labeled_counter(
585 "oxios_ws_connections_total",
586 "WS client connections (action=open|close|keepalive_timeout)",
587 "action",
588 "close",
589 ),
590 ws_connections_keepalive_timeout: r.labeled_counter(
591 "oxios_ws_connections_total",
592 "WS client connections (action=open|close|keepalive_timeout)",
593 "action",
594 "keepalive_timeout",
595 ),
596 web_dist_swaps: r.counter(
597 "oxios_web_dist_swaps_total",
598 "Atomic web-dist swaps (RFC-024 SP3)",
599 &[],
600 ),
601 supervisor_restarts: r.counter(
602 "oxios_supervisor_restarts_total",
603 "Web surface scoped restarts (RFC-030)",
604 &[],
605 ),
606 readiness_state: r.gauge(
607 "oxios_readiness_state",
608 "0 = warming up, 1 = ready (RFC-024 SP4)",
609 0.0,
610 ),
611 }
612 })
613}
614
615pub fn register_builtin_metrics() {
617 let r = registry();
618
619 r.counter("oxios_agents_forked_total", "Total agents forked", &[]);
621 r.gauge("oxios_agents_running", "Currently running agents", 0.0);
622 r.counter(
623 "oxios_agents_completed_total",
624 "Total agents completed",
625 &[],
626 );
627 r.counter("oxios_agents_failed_total", "Total agents failed", &[]);
628
629 r.counter(
631 "oxios_messages_processed_total",
632 "User messages processed",
633 &[],
634 );
635 r.histogram(
636 "oxios_orchestration_duration_seconds",
637 "Full orchestration duration",
638 vec![0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0],
639 );
640
641 r.histogram(
642 "oxios_phase_duration_seconds",
643 "Phase duration",
644 vec![0.01, 0.05, 0.1, 0.5, 1.0, 2.5, 5.0, 10.0],
645 );
646
647 r.counter("oxios_llm_calls_total", "LLM API calls", &[]);
649 r.histogram(
650 "oxios_llm_duration_seconds",
651 "LLM call duration",
652 vec![0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0],
653 );
654 r.counter("oxios_llm_errors_total", "LLM API errors", &[]);
655 r.counter(
658 "oxios_audit_lagged_events_total",
659 "Audit events dropped due to broadcast subscriber lag",
660 &[],
661 );
662
663 r.counter("oxios_tool_calls_total", "Tool calls", &[]);
665 r.histogram(
666 "oxios_tool_duration_seconds",
667 "Tool call duration",
668 vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0],
669 );
670 r.counter("oxios_tool_errors_total", "Tool errors", &[]);
671
672 r.gauge("oxios_memory_entries_total", "Total memory entries", 0.0);
674 r.counter("oxios_memory_recall_total", "Memory recall operations", &[]);
675
676 r.counter("oxios_exec_total", "Exec calls", &[]);
678 r.histogram(
679 "oxios_exec_duration_seconds",
680 "Exec duration",
681 vec![0.1, 0.5, 1.0, 5.0, 10.0, 30.0],
682 );
683
684 r.gauge("oxios_active_sessions", "Active sessions", 0.0);
686
687 let _ = get_metrics();
689}