microclaw_engine/internal/storage/db/
meta.rs1use 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 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 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 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 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 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 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}