1use std::sync::atomic::{AtomicU64, Ordering};
6
7use rusqlite::Connection;
8
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub enum TelemetryLevel {
16 #[default]
18 Counters,
19 Statements,
21 Profiling,
24}
25
26#[derive(Debug, Default)]
31#[allow(clippy::struct_field_names)]
32pub struct TelemetryCounters {
33 queries_total: AtomicU64,
34 writes_total: AtomicU64,
35 write_rows_total: AtomicU64,
36 errors_total: AtomicU64,
37 admin_ops_total: AtomicU64,
38}
39
40impl TelemetryCounters {
41 pub fn increment_queries(&self) {
43 self.queries_total.fetch_add(1, Ordering::Relaxed);
44 }
45
46 pub fn increment_writes(&self, row_count: u64) {
48 self.writes_total.fetch_add(1, Ordering::Relaxed);
49 self.write_rows_total
50 .fetch_add(row_count, Ordering::Relaxed);
51 }
52
53 pub fn increment_errors(&self) {
55 self.errors_total.fetch_add(1, Ordering::Relaxed);
56 }
57
58 pub fn increment_admin_ops(&self) {
60 self.admin_ops_total.fetch_add(1, Ordering::Relaxed);
61 }
62
63 #[must_use]
69 pub fn snapshot(&self) -> TelemetrySnapshot {
70 TelemetrySnapshot {
71 queries_total: self.queries_total.load(Ordering::Relaxed),
72 writes_total: self.writes_total.load(Ordering::Relaxed),
73 write_rows_total: self.write_rows_total.load(Ordering::Relaxed),
74 errors_total: self.errors_total.load(Ordering::Relaxed),
75 admin_ops_total: self.admin_ops_total.load(Ordering::Relaxed),
76 sqlite_cache: SqliteCacheStatus::default(),
77 }
78 }
79}
80
81#[derive(Clone, Debug, Default, PartialEq, Eq)]
85pub struct SqliteCacheStatus {
86 pub cache_hits: i64,
88 pub cache_misses: i64,
90 pub cache_writes: i64,
92 pub cache_spills: i64,
94}
95
96impl SqliteCacheStatus {
97 pub fn add(&mut self, other: &Self) {
101 self.cache_hits = self.cache_hits.saturating_add(other.cache_hits);
102 self.cache_misses = self.cache_misses.saturating_add(other.cache_misses);
103 self.cache_writes = self.cache_writes.saturating_add(other.cache_writes);
104 self.cache_spills = self.cache_spills.saturating_add(other.cache_spills);
105 }
106}
107
108pub fn read_db_cache_status(conn: &Connection) -> SqliteCacheStatus {
124 let mut status = SqliteCacheStatus::default();
125
126 let read_one = |op: i32| -> i64 {
129 let mut current: i32 = 0;
130 let mut highwater: i32 = 0;
131 let rc = unsafe {
134 rusqlite::ffi::sqlite3_db_status(
135 conn.handle(),
136 op,
137 &raw mut current,
138 &raw mut highwater,
139 0, )
141 };
142 if rc == rusqlite::ffi::SQLITE_OK {
143 i64::from(current)
144 } else {
145 0
146 }
147 };
148
149 status.cache_hits = read_one(rusqlite::ffi::SQLITE_DBSTATUS_CACHE_HIT);
150 status.cache_misses = read_one(rusqlite::ffi::SQLITE_DBSTATUS_CACHE_MISS);
151 status.cache_writes = read_one(rusqlite::ffi::SQLITE_DBSTATUS_CACHE_WRITE);
152 status.cache_spills = read_one(rusqlite::ffi::SQLITE_DBSTATUS_CACHE_SPILL);
153
154 status
155}
156
157#[derive(Clone, Debug, Default, PartialEq, Eq)]
159pub struct TelemetrySnapshot {
160 pub queries_total: u64,
162 pub writes_total: u64,
164 pub write_rows_total: u64,
166 pub errors_total: u64,
168 pub admin_ops_total: u64,
170 pub sqlite_cache: SqliteCacheStatus,
172}
173
174#[cfg(test)]
175#[allow(clippy::expect_used)]
176mod tests {
177 use rusqlite::Connection;
178
179 use super::{SqliteCacheStatus, TelemetryCounters, TelemetryLevel, read_db_cache_status};
180
181 #[test]
182 fn telemetry_level_default_is_counters() {
183 assert_eq!(TelemetryLevel::default(), TelemetryLevel::Counters);
184 }
185
186 #[test]
187 fn counter_defaults_to_zero() {
188 let counters = TelemetryCounters::default();
189 let snap = counters.snapshot();
190 assert_eq!(snap.queries_total, 0);
191 assert_eq!(snap.writes_total, 0);
192 assert_eq!(snap.write_rows_total, 0);
193 assert_eq!(snap.errors_total, 0);
194 assert_eq!(snap.admin_ops_total, 0);
195 }
196
197 #[test]
198 fn counter_increment_and_snapshot() {
199 let counters = TelemetryCounters::default();
200
201 counters.increment_queries();
202 counters.increment_queries();
203 counters.increment_writes(5);
204 counters.increment_writes(3);
205 counters.increment_errors();
206 counters.increment_admin_ops();
207 counters.increment_admin_ops();
208 counters.increment_admin_ops();
209
210 let snap = counters.snapshot();
211 assert_eq!(snap.queries_total, 2);
212 assert_eq!(snap.writes_total, 2);
213 assert_eq!(snap.write_rows_total, 8);
214 assert_eq!(snap.errors_total, 1);
215 assert_eq!(snap.admin_ops_total, 3);
216 }
217
218 #[test]
219 fn read_db_cache_status_on_fresh_connection() {
220 let conn = Connection::open_in_memory().expect("open in-memory db");
221 let status = read_db_cache_status(&conn);
222 assert!(status.cache_hits >= 0);
224 assert!(status.cache_misses >= 0);
225 assert!(status.cache_writes >= 0);
226 assert!(status.cache_spills >= 0);
227 }
228
229 #[test]
230 fn cache_status_reflects_queries() {
231 let conn = Connection::open_in_memory().expect("open in-memory db");
232 conn.execute_batch(
233 "CREATE TABLE t (id INTEGER PRIMARY KEY, value TEXT);
234 INSERT INTO t VALUES (1, 'a');
235 INSERT INTO t VALUES (2, 'b');
236 INSERT INTO t VALUES (3, 'c');",
237 )
238 .expect("setup");
239
240 for _ in 0..10 {
242 let mut stmt = conn.prepare("SELECT * FROM t").expect("prepare");
243 let _rows: Vec<i64> = stmt
244 .query_map([], |row| row.get(0))
245 .expect("query")
246 .map(|r| r.expect("row"))
247 .collect();
248 }
249
250 let status = read_db_cache_status(&conn);
251 assert!(
253 status.cache_hits + status.cache_misses > 0,
254 "expected cache activity after queries, got hits={} misses={}",
255 status.cache_hits,
256 status.cache_misses,
257 );
258 }
259
260 #[test]
261 fn cache_status_add_sums_correctly() {
262 let a = SqliteCacheStatus {
263 cache_hits: 10,
264 cache_misses: 2,
265 cache_writes: 5,
266 cache_spills: 1,
267 };
268 let b = SqliteCacheStatus {
269 cache_hits: 3,
270 cache_misses: 7,
271 cache_writes: 0,
272 cache_spills: 4,
273 };
274 let mut total = SqliteCacheStatus::default();
275 total.add(&a);
276 total.add(&b);
277 assert_eq!(total.cache_hits, 13);
278 assert_eq!(total.cache_misses, 9);
279 assert_eq!(total.cache_writes, 5);
280 assert_eq!(total.cache_spills, 5);
281 }
282}