Skip to main content

brainos_httpadapter/
metrics.rs

1//! Atomic counters and gauge caching exposed at `GET /metrics`.
2
3use std::sync::atomic::{AtomicU64, Ordering};
4
5/// Cached gauge snapshot so `/metrics` doesn't hit SQLite on every scrape.
6#[derive(Default)]
7pub struct GaugeCache {
8    pub fact_count: i64,
9    pub episode_count: i64,
10    pub last_updated_ms: u64,
11}
12
13const GAUGE_TTL_MS: u64 = 10_000;
14
15/// Atomic counters exposed at `GET /metrics` in Prometheus text format.
16#[derive(Default)]
17pub struct Metrics {
18    /// Total POST /v1/signals requests processed.
19    pub signals_total: AtomicU64,
20    /// Signals that returned a non-5xx response.
21    pub signals_ok: AtomicU64,
22    /// Signals that returned a 5xx error.
23    pub signals_error: AtomicU64,
24    /// Total POST /v1/memory/search requests.
25    pub search_total: AtomicU64,
26    /// Total GET /v1/memory/facts requests.
27    pub facts_total: AtomicU64,
28    /// Cumulative POST /v1/signals processing time in milliseconds.
29    pub signals_latency_ms_total: AtomicU64,
30    /// Cached memory gauges (refreshed on scrape, 10s TTL).
31    pub gauge_cache: std::sync::Mutex<GaugeCache>,
32}
33
34impl Metrics {
35    /// Refresh cached fact/episode gauges from SQLite (throttled to every 10s).
36    pub fn refresh_gauges(&self, processor: &signal::SignalProcessor) {
37        let now_ms = std::time::SystemTime::now()
38            .duration_since(std::time::UNIX_EPOCH)
39            .unwrap_or_default()
40            .as_millis() as u64;
41        let mut guard = self.gauge_cache.lock().unwrap_or_else(|e| e.into_inner());
42        if now_ms.saturating_sub(guard.last_updated_ms) < GAUGE_TTL_MS {
43            return;
44        }
45        let pool = processor.episodic().pool();
46        let fact_count = pool
47            .with_conn(|conn| {
48                let mut stmt = conn.prepare("SELECT COUNT(*) FROM semantic_facts")?;
49                let row: i64 = stmt.query_row([], |r| r.get(0))?;
50                Ok(row)
51            })
52            .unwrap_or(0);
53        let episode_count = pool
54            .with_conn(|conn| {
55                let mut stmt = conn.prepare("SELECT COUNT(*) FROM episodes")?;
56                let row: i64 = stmt.query_row([], |r| r.get(0))?;
57                Ok(row)
58            })
59            .unwrap_or(0);
60        guard.fact_count = fact_count;
61        guard.episode_count = episode_count;
62        guard.last_updated_ms = now_ms;
63    }
64
65    /// Render counters as Prometheus plain-text format (text/plain; version=0.0.4).
66    pub fn render(&self, subsystems: &metrics::SubsystemMetrics) -> String {
67        let signals_total = self.signals_total.load(Ordering::Relaxed);
68        let signals_ok = self.signals_ok.load(Ordering::Relaxed);
69        let signals_error = self.signals_error.load(Ordering::Relaxed);
70        let search_total = self.search_total.load(Ordering::Relaxed);
71        let facts_total = self.facts_total.load(Ordering::Relaxed);
72        let latency_ms = self.signals_latency_ms_total.load(Ordering::Relaxed);
73        let sub = subsystems.snapshot();
74        let (fact_count, episode_count) = {
75            let g = self.gauge_cache.lock().unwrap_or_else(|e| e.into_inner());
76            (g.fact_count, g.episode_count)
77        };
78
79        format!(
80            "# HELP brain_signals_total Total signal requests received.\n\
81             # TYPE brain_signals_total counter\n\
82             brain_signals_total {signals_total}\n\
83             # HELP brain_signals_ok_total Successful signal requests.\n\
84             # TYPE brain_signals_ok_total counter\n\
85             brain_signals_ok_total {signals_ok}\n\
86             # HELP brain_signals_error_total Failed signal requests (5xx).\n\
87             # TYPE brain_signals_error_total counter\n\
88             brain_signals_error_total {signals_error}\n\
89             # HELP brain_search_total Total memory search requests.\n\
90             # TYPE brain_search_total counter\n\
91             brain_search_total {search_total}\n\
92             # HELP brain_facts_total Total memory facts requests.\n\
93             # TYPE brain_facts_total counter\n\
94             brain_facts_total {facts_total}\n\
95             # HELP brain_signals_latency_ms_total Cumulative signal processing latency in ms.\n\
96             # TYPE brain_signals_latency_ms_total counter\n\
97             brain_signals_latency_ms_total {latency_ms}\n\
98             # HELP brain_facts_count Total semantic facts currently stored.\n\
99             # TYPE brain_facts_count gauge\n\
100             brain_facts_count {fact_count}\n\
101             # HELP brain_episodes_count Total episodes currently stored.\n\
102             # TYPE brain_episodes_count gauge\n\
103             brain_episodes_count {episode_count}\n\
104             # HELP brain_embedding_requests_total Total embedding requests.\n\
105             # TYPE brain_embedding_requests_total counter\n\
106             brain_embedding_requests_total {embed_req}\n\
107             # HELP brain_embedding_fallbacks_total Times deterministic fallback vectors were used.\n\
108             # TYPE brain_embedding_fallbacks_total counter\n\
109             brain_embedding_fallbacks_total {embed_fb}\n\
110             # HELP brain_consolidation_runs_total Number of consolidation cycles completed.\n\
111             # TYPE brain_consolidation_runs_total counter\n\
112             brain_consolidation_runs_total {cons_runs}\n\
113             # HELP brain_consolidation_pruned_total Episodes pruned during consolidation.\n\
114             # TYPE brain_consolidation_pruned_total counter\n\
115             brain_consolidation_pruned_total {cons_pruned}\n\
116             # HELP brain_consolidation_promoted_total Episodes promoted to semantic facts.\n\
117             # TYPE brain_consolidation_promoted_total counter\n\
118             brain_consolidation_promoted_total {cons_promoted}\n\
119             # HELP brain_circuit_open_total Times a circuit breaker opened.\n\
120             # TYPE brain_circuit_open_total counter\n\
121             brain_circuit_open_total {cb_open}\n\
122             # HELP brain_circuit_resets_total Times a circuit breaker reset on success.\n\
123             # TYPE brain_circuit_resets_total counter\n\
124             brain_circuit_resets_total {cb_reset}\n\
125             # HELP brain_intent_classifications_total Total intent classifications.\n\
126             # TYPE brain_intent_classifications_total counter\n\
127             brain_intent_classifications_total {intents}\n\
128             # HELP brain_intent_llm_fallbacks_total Intents resolved via LLM fallback.\n\
129             # TYPE brain_intent_llm_fallbacks_total counter\n\
130             brain_intent_llm_fallbacks_total {intent_llm}\n",
131            embed_req = sub.embedding_requests_total,
132            embed_fb = sub.embedding_fallbacks_total,
133            cons_runs = sub.consolidation_runs_total,
134            cons_pruned = sub.consolidation_pruned_total,
135            cons_promoted = sub.consolidation_promoted_total,
136            cb_open = sub.circuit_open_total,
137            cb_reset = sub.circuit_resets_total,
138            intents = sub.intent_classifications_total,
139            intent_llm = sub.intent_llm_fallbacks_total,
140        )
141    }
142}