mcpr_integrations/store/query/
stats.rs1use rusqlite::params;
4use serde::Serialize;
5
6use super::QueryEngine;
7
8type AggRow = (String, i64, f64, i64, i64, f64, i64, i64);
10
11pub struct StatsParams {
13 pub proxy: Option<String>,
15 pub since_ts: i64,
17}
18
19#[derive(Debug, Clone, Serialize)]
21pub struct ToolStats {
22 pub label: String,
24 pub calls: i64,
26 pub avg_us: f64,
28 pub min_us: i64,
30 pub max_us: i64,
32 pub p95_us: i64,
34 pub error_pct: f64,
36 pub total_bytes_in: i64,
38 pub total_bytes_out: i64,
40}
41
42#[derive(Debug, Serialize)]
44pub struct StatsResult {
45 pub tools: Vec<ToolStats>,
47 pub total_calls: i64,
49 pub error_pct: f64,
51}
52
53impl QueryEngine {
54 pub fn stats(&self, params: &StatsParams) -> Result<StatsResult, rusqlite::Error> {
60 let agg_sql = "
62 SELECT
63 COALESCE(tool, '<' || method || '>') AS label,
64 COUNT(*) AS calls,
65 AVG(latency_us) AS avg_us,
66 MIN(latency_us) AS min_us,
67 MAX(latency_us) AS max_us,
68 SUM(CASE WHEN status != 'ok' THEN 1 ELSE 0 END) * 100.0
69 / COUNT(*) AS error_pct,
70 COALESCE(SUM(bytes_in), 0) AS total_bytes_in,
71 COALESCE(SUM(bytes_out), 0) AS total_bytes_out
72 FROM requests
73 WHERE (?1 IS NULL OR proxy = ?1) AND ts >= ?2
74 GROUP BY COALESCE(tool, '<' || method || '>')
75 ORDER BY calls DESC
76 ";
77
78 let mut stmt = self.conn().prepare(agg_sql)?;
79 let groups: Vec<AggRow> = stmt
80 .query_map(params![params.proxy, params.since_ts], |row| {
81 Ok((
82 row.get(0)?,
83 row.get(1)?,
84 row.get(2)?,
85 row.get(3)?,
86 row.get(4)?,
87 row.get(5)?,
88 row.get(6)?,
89 row.get(7)?,
90 ))
91 })?
92 .collect::<Result<Vec<_>, _>>()?;
93
94 let p95_sql = "
96 SELECT latency_us
97 FROM requests
98 WHERE (?1 IS NULL OR proxy = ?1) AND ts >= ?2
99 AND COALESCE(tool, '<' || method || '>') = ?3
100 ORDER BY latency_us
101 ";
102
103 let mut total_calls: i64 = 0;
104 let mut total_errors: f64 = 0.0;
105 let mut tools = Vec::with_capacity(groups.len());
106
107 for (label, calls, avg_us, min_us, max_us, error_pct, bytes_in, bytes_out) in &groups {
108 let mut p95_stmt = self.conn().prepare(p95_sql)?;
110 let latencies: Vec<i64> = p95_stmt
111 .query_map(params![params.proxy, params.since_ts, label], |row| {
112 row.get(0)
113 })?
114 .collect::<Result<Vec<_>, _>>()?;
115
116 let p95 = percentile(&latencies, 95);
117
118 total_calls += calls;
119 total_errors += (*calls as f64) * error_pct / 100.0;
120
121 tools.push(ToolStats {
122 label: label.clone(),
123 calls: *calls,
124 avg_us: *avg_us,
125 min_us: *min_us,
126 max_us: *max_us,
127 p95_us: p95,
128 error_pct: *error_pct,
129 total_bytes_in: *bytes_in,
130 total_bytes_out: *bytes_out,
131 });
132 }
133
134 let overall_error_pct = if total_calls > 0 {
135 total_errors / total_calls as f64 * 100.0
136 } else {
137 0.0
138 };
139
140 Ok(StatsResult {
141 tools,
142 total_calls,
143 error_pct: overall_error_pct,
144 })
145 }
146}
147
148fn percentile(sorted_values: &[i64], pct: u8) -> i64 {
152 if sorted_values.is_empty() {
153 return 0;
154 }
155 let idx = ((pct as f64 / 100.0) * sorted_values.len() as f64).ceil() as usize;
156 let idx = idx.min(sorted_values.len()) - 1;
157 sorted_values[idx]
158}
159
160#[cfg(test)]
161#[allow(non_snake_case)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn percentile__basic() {
167 let values = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
168 assert_eq!(percentile(&values, 50), 5);
169 assert_eq!(percentile(&values, 95), 10);
170 assert_eq!(percentile(&values, 100), 10);
171 }
172
173 #[test]
174 fn percentile__empty() {
175 assert_eq!(percentile(&[], 95), 0);
176 }
177
178 #[test]
179 fn percentile__single() {
180 assert_eq!(percentile(&[42], 95), 42);
181 }
182}