Skip to main content

microclaw_engine/internal/storage/db/
meta.rs

1use super::*;
2
3#[derive(Debug, Clone)]
4pub struct MetricsHistoryPoint {
5    pub timestamp_ms: i64,
6    pub llm_completions: i64,
7    pub llm_input_tokens: i64,
8    pub llm_output_tokens: i64,
9    pub http_requests: i64,
10    pub tool_executions: i64,
11    pub mcp_calls: i64,
12    pub mcp_rate_limited_rejections: i64,
13    pub mcp_bulkhead_rejections: i64,
14    pub mcp_circuit_open_rejections: i64,
15    pub active_sessions: i64,
16}
17
18impl Database {
19    pub fn upsert_metrics_history(
20        &self,
21        point: &MetricsHistoryPoint,
22    ) -> Result<(), MicroClawError> {
23        let conn = self.lock_conn();
24        conn.execute(
25            "INSERT INTO metrics_history(
26                timestamp_ms, llm_completions, llm_input_tokens, llm_output_tokens,
27                http_requests, tool_executions, mcp_calls,
28                mcp_rate_limited_rejections, mcp_bulkhead_rejections, mcp_circuit_open_rejections,
29                active_sessions
30             ) VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
31             ON CONFLICT(timestamp_ms) DO UPDATE SET
32                llm_completions = excluded.llm_completions,
33                llm_input_tokens = excluded.llm_input_tokens,
34                llm_output_tokens = excluded.llm_output_tokens,
35                http_requests = excluded.http_requests,
36                tool_executions = excluded.tool_executions,
37                mcp_calls = excluded.mcp_calls,
38                mcp_rate_limited_rejections = excluded.mcp_rate_limited_rejections,
39                mcp_bulkhead_rejections = excluded.mcp_bulkhead_rejections,
40                mcp_circuit_open_rejections = excluded.mcp_circuit_open_rejections,
41                active_sessions = excluded.active_sessions",
42            params![
43                point.timestamp_ms,
44                point.llm_completions,
45                point.llm_input_tokens,
46                point.llm_output_tokens,
47                point.http_requests,
48                point.tool_executions,
49                point.mcp_calls,
50                point.mcp_rate_limited_rejections,
51                point.mcp_bulkhead_rejections,
52                point.mcp_circuit_open_rejections,
53                point.active_sessions
54            ],
55        )?;
56        Ok(())
57    }
58
59    pub fn get_metrics_history(
60        &self,
61        since_ts_ms: i64,
62        limit: usize,
63    ) -> Result<Vec<MetricsHistoryPoint>, MicroClawError> {
64        let conn = self.lock_conn();
65        let mut stmt = conn.prepare(
66            "SELECT
67                timestamp_ms, llm_completions, llm_input_tokens, llm_output_tokens,
68                http_requests, tool_executions, mcp_calls,
69                mcp_rate_limited_rejections, mcp_bulkhead_rejections, mcp_circuit_open_rejections,
70                active_sessions
71             FROM metrics_history
72             WHERE timestamp_ms >= ?1
73             ORDER BY timestamp_ms ASC
74             LIMIT ?2",
75        )?;
76        let rows = stmt
77            .query_map(params![since_ts_ms, limit as i64], |row| {
78                Ok(MetricsHistoryPoint {
79                    timestamp_ms: row.get(0)?,
80                    llm_completions: row.get(1)?,
81                    llm_input_tokens: row.get(2)?,
82                    llm_output_tokens: row.get(3)?,
83                    http_requests: row.get(4)?,
84                    tool_executions: row.get(5)?,
85                    mcp_calls: row.get(6)?,
86                    mcp_rate_limited_rejections: row.get(7)?,
87                    mcp_bulkhead_rejections: row.get(8)?,
88                    mcp_circuit_open_rejections: row.get(9)?,
89                    active_sessions: row.get(10)?,
90                })
91            })?
92            .collect::<Result<Vec<_>, _>>()?;
93        Ok(rows)
94    }
95
96    pub fn cleanup_metrics_history_before(
97        &self,
98        before_ts_ms: i64,
99    ) -> Result<usize, MicroClawError> {
100        let conn = self.lock_conn();
101        let n = conn.execute(
102            "DELETE FROM metrics_history WHERE timestamp_ms < ?1",
103            params![before_ts_ms],
104        )?;
105        Ok(n)
106    }
107
108    /// Read a runtime key from the `db_meta` KV table (also used for
109    /// schema_version/embedding_dim). Namespaced keys recommended.
110    pub fn get_runtime_meta(&self, key: &str) -> Result<Option<String>, MicroClawError> {
111        let conn = self.lock_conn();
112        let value = conn
113            .query_row("SELECT value FROM db_meta WHERE key = ?1", [key], |row| {
114                row.get::<_, String>(0)
115            })
116            .optional()?;
117        Ok(value)
118    }
119
120    /// Upsert a runtime key in the `db_meta` KV table.
121    pub fn set_runtime_meta(&self, key: &str, value: &str) -> Result<(), MicroClawError> {
122        let conn = self.lock_conn();
123        conn.execute(
124            "INSERT INTO db_meta(key, value) VALUES(?1, ?2)
125             ON CONFLICT(key) DO UPDATE SET value = excluded.value",
126            [key, value],
127        )?;
128        Ok(())
129    }
130
131    /// List runtime keys sharing a prefix (sorted by key).
132    pub fn list_runtime_meta_prefix(
133        &self,
134        prefix: &str,
135    ) -> Result<Vec<(String, String)>, MicroClawError> {
136        let conn = self.lock_conn();
137        let pattern = format!("{}%", prefix.replace('%', "\\%").replace('_', "\\_"));
138        let mut stmt = conn
139            .prepare("SELECT key, value FROM db_meta WHERE key LIKE ?1 ESCAPE '\\' ORDER BY key")?;
140        let rows = stmt
141            .query_map([&pattern], |row| Ok((row.get(0)?, row.get(1)?)))?
142            .collect::<Result<Vec<_>, _>>()?;
143        Ok(rows)
144    }
145
146    /// Delete a single runtime key; returns whether a row was removed.
147    pub fn delete_runtime_meta(&self, key: &str) -> Result<bool, MicroClawError> {
148        let conn = self.lock_conn();
149        let n = conn.execute("DELETE FROM db_meta WHERE key = ?1", [key])?;
150        Ok(n > 0)
151    }
152
153    /// Delete every runtime key sharing a prefix; returns removed count.
154    pub fn delete_runtime_meta_prefix(&self, prefix: &str) -> Result<usize, MicroClawError> {
155        let conn = self.lock_conn();
156        let pattern = format!("{}%", prefix.replace('%', "\\%").replace('_', "\\_"));
157        let n = conn.execute(
158            "DELETE FROM db_meta WHERE key LIKE ?1 ESCAPE '\\'",
159            [&pattern],
160        )?;
161        Ok(n)
162    }
163
164    /// Completion-contract verdict tallies (verified, failed) recorded at
165    /// or after `since` (RFC 3339). Verdicts are the `contract` events
166    /// written when a sub-agent run's exit criteria are checked.
167    pub fn contract_verdict_counts_since(&self, since: &str) -> Result<(i64, i64), MicroClawError> {
168        let conn = self.lock_conn();
169        let counts = conn.query_row(
170            "SELECT
171                COALESCE(SUM(CASE WHEN detail LIKE 'verified%' THEN 1 ELSE 0 END), 0),
172                COALESCE(SUM(CASE WHEN detail LIKE 'failed%' THEN 1 ELSE 0 END), 0)
173             FROM subagent_events
174             WHERE event_type = 'contract' AND created_at >= ?1",
175            [since],
176            |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
177        )?;
178        Ok(counts)
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::internal::storage::db::test_support::*;
186
187    #[test]
188    fn test_metrics_history_roundtrip_with_mcp_rejection_fields() {
189        let (db, dir) = test_db();
190        let point = MetricsHistoryPoint {
191            timestamp_ms: 1_700_000_000_000,
192            llm_completions: 10,
193            llm_input_tokens: 1000,
194            llm_output_tokens: 500,
195            http_requests: 20,
196            tool_executions: 8,
197            mcp_calls: 3,
198            mcp_rate_limited_rejections: 2,
199            mcp_bulkhead_rejections: 1,
200            mcp_circuit_open_rejections: 4,
201            active_sessions: 6,
202        };
203        db.upsert_metrics_history(&point).unwrap();
204        let rows = db.get_metrics_history(point.timestamp_ms, 10).unwrap();
205        assert_eq!(rows.len(), 1);
206        let got = &rows[0];
207        assert_eq!(got.mcp_rate_limited_rejections, 2);
208        assert_eq!(got.mcp_bulkhead_rejections, 1);
209        assert_eq!(got.mcp_circuit_open_rejections, 4);
210        cleanup(&dir);
211    }
212}