1use std::sync::atomic::{AtomicU64, Ordering};
6
7use rusqlite::Connection;
8use serde::Serialize;
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16pub enum TelemetryLevel {
17 #[default]
19 Counters,
20 Statements,
22 Profiling,
25}
26
27#[derive(Debug, Default)]
32#[allow(clippy::struct_field_names)]
33pub struct TelemetryCounters {
34 queries_total: AtomicU64,
35 writes_total: AtomicU64,
36 write_rows_total: AtomicU64,
37 errors_total: AtomicU64,
38 admin_ops_total: AtomicU64,
39}
40
41impl TelemetryCounters {
42 pub fn increment_queries(&self) {
44 self.queries_total.fetch_add(1, Ordering::Relaxed);
45 }
46
47 pub fn increment_writes(&self, row_count: u64) {
49 self.writes_total.fetch_add(1, Ordering::Relaxed);
50 self.write_rows_total
51 .fetch_add(row_count, Ordering::Relaxed);
52 }
53
54 pub fn increment_errors(&self) {
56 self.errors_total.fetch_add(1, Ordering::Relaxed);
57 }
58
59 pub fn increment_admin_ops(&self) {
61 self.admin_ops_total.fetch_add(1, Ordering::Relaxed);
62 }
63
64 #[must_use]
70 pub fn snapshot(&self) -> TelemetrySnapshot {
71 TelemetrySnapshot {
72 queries_total: self.queries_total.load(Ordering::Relaxed),
73 writes_total: self.writes_total.load(Ordering::Relaxed),
74 write_rows_total: self.write_rows_total.load(Ordering::Relaxed),
75 errors_total: self.errors_total.load(Ordering::Relaxed),
76 admin_ops_total: self.admin_ops_total.load(Ordering::Relaxed),
77 sqlite_cache: SqliteCacheStatus::default(),
78 }
79 }
80}
81
82#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
86pub struct SqliteCacheStatus {
87 pub cache_hits: i64,
89 pub cache_misses: i64,
91 pub cache_writes: i64,
93 pub cache_spills: i64,
95}
96
97impl SqliteCacheStatus {
98 pub fn add(&mut self, other: &Self) {
102 self.cache_hits = self.cache_hits.saturating_add(other.cache_hits);
103 self.cache_misses = self.cache_misses.saturating_add(other.cache_misses);
104 self.cache_writes = self.cache_writes.saturating_add(other.cache_writes);
105 self.cache_spills = self.cache_spills.saturating_add(other.cache_spills);
106 }
107}
108
109pub fn read_db_cache_status(conn: &Connection) -> SqliteCacheStatus {
125 let mut status = SqliteCacheStatus::default();
126
127 let read_one = |op: i32| -> i64 {
130 let mut current: i32 = 0;
131 let mut highwater: i32 = 0;
132 let rc = unsafe {
135 rusqlite::ffi::sqlite3_db_status(
136 conn.handle(),
137 op,
138 &raw mut current,
139 &raw mut highwater,
140 0, )
142 };
143 if rc == rusqlite::ffi::SQLITE_OK {
144 i64::from(current)
145 } else {
146 0
147 }
148 };
149
150 status.cache_hits = read_one(rusqlite::ffi::SQLITE_DBSTATUS_CACHE_HIT);
151 status.cache_misses = read_one(rusqlite::ffi::SQLITE_DBSTATUS_CACHE_MISS);
152 status.cache_writes = read_one(rusqlite::ffi::SQLITE_DBSTATUS_CACHE_WRITE);
153 status.cache_spills = read_one(rusqlite::ffi::SQLITE_DBSTATUS_CACHE_SPILL);
154
155 status
156}
157
158#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
160pub struct TelemetrySnapshot {
161 pub queries_total: u64,
163 pub writes_total: u64,
165 pub write_rows_total: u64,
167 pub errors_total: u64,
169 pub admin_ops_total: u64,
171 #[serde(flatten)]
173 pub sqlite_cache: SqliteCacheStatus,
174}
175
176#[cfg(test)]
177#[allow(clippy::expect_used)]
178mod tests {
179 use rusqlite::Connection;
180
181 use super::{
182 SqliteCacheStatus, TelemetryCounters, TelemetryLevel, TelemetrySnapshot,
183 read_db_cache_status,
184 };
185
186 #[test]
187 fn telemetry_snapshot_serializes_to_json() {
188 let snap = TelemetrySnapshot {
189 queries_total: 5,
190 ..Default::default()
191 };
192 let json = serde_json::to_value(&snap).expect("serializes");
193 assert_eq!(json["queries_total"], 5);
194 assert_eq!(json["cache_hits"], 0);
196 assert!(
197 json.get("sqlite_cache").is_none(),
198 "sqlite_cache must be flattened"
199 );
200 }
201
202 #[test]
203 fn telemetry_level_default_is_counters() {
204 assert_eq!(TelemetryLevel::default(), TelemetryLevel::Counters);
205 }
206
207 #[test]
208 fn counter_defaults_to_zero() {
209 let counters = TelemetryCounters::default();
210 let snap = counters.snapshot();
211 assert_eq!(snap.queries_total, 0);
212 assert_eq!(snap.writes_total, 0);
213 assert_eq!(snap.write_rows_total, 0);
214 assert_eq!(snap.errors_total, 0);
215 assert_eq!(snap.admin_ops_total, 0);
216 }
217
218 #[test]
219 fn counter_increment_and_snapshot() {
220 let counters = TelemetryCounters::default();
221
222 counters.increment_queries();
223 counters.increment_queries();
224 counters.increment_writes(5);
225 counters.increment_writes(3);
226 counters.increment_errors();
227 counters.increment_admin_ops();
228 counters.increment_admin_ops();
229 counters.increment_admin_ops();
230
231 let snap = counters.snapshot();
232 assert_eq!(snap.queries_total, 2);
233 assert_eq!(snap.writes_total, 2);
234 assert_eq!(snap.write_rows_total, 8);
235 assert_eq!(snap.errors_total, 1);
236 assert_eq!(snap.admin_ops_total, 3);
237 }
238
239 #[test]
240 fn read_db_cache_status_on_fresh_connection() {
241 let conn = Connection::open_in_memory().expect("open in-memory db");
242 let status = read_db_cache_status(&conn);
243 assert!(status.cache_hits >= 0);
245 assert!(status.cache_misses >= 0);
246 assert!(status.cache_writes >= 0);
247 assert!(status.cache_spills >= 0);
248 }
249
250 #[test]
251 fn cache_status_reflects_queries() {
252 let conn = Connection::open_in_memory().expect("open in-memory db");
253 conn.execute_batch(
254 "CREATE TABLE t (id INTEGER PRIMARY KEY, value TEXT);
255 INSERT INTO t VALUES (1, 'a');
256 INSERT INTO t VALUES (2, 'b');
257 INSERT INTO t VALUES (3, 'c');",
258 )
259 .expect("setup");
260
261 for _ in 0..10 {
263 let mut stmt = conn.prepare("SELECT * FROM t").expect("prepare");
264 let _rows: Vec<i64> = stmt
265 .query_map([], |row| row.get(0))
266 .expect("query")
267 .map(|r| r.expect("row"))
268 .collect();
269 }
270
271 let status = read_db_cache_status(&conn);
272 assert!(
274 status.cache_hits + status.cache_misses > 0,
275 "expected cache activity after queries, got hits={} misses={}",
276 status.cache_hits,
277 status.cache_misses,
278 );
279 }
280
281 #[test]
282 fn cache_status_add_sums_correctly() {
283 let a = SqliteCacheStatus {
284 cache_hits: 10,
285 cache_misses: 2,
286 cache_writes: 5,
287 cache_spills: 1,
288 };
289 let b = SqliteCacheStatus {
290 cache_hits: 3,
291 cache_misses: 7,
292 cache_writes: 0,
293 cache_spills: 4,
294 };
295 let mut total = SqliteCacheStatus::default();
296 total.add(&a);
297 total.add(&b);
298 assert_eq!(total.cache_hits, 13);
299 assert_eq!(total.cache_misses, 9);
300 assert_eq!(total.cache_writes, 5);
301 assert_eq!(total.cache_spills, 5);
302 }
303}