1use std::fmt;
52
53#[derive(Debug, Clone)]
55pub struct ParseError {
56 pub message: String,
58}
59
60impl fmt::Display for ParseError {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 write!(f, "metrics parse error: {}", self.message)
63 }
64}
65
66impl std::error::Error for ParseError {}
67
68#[derive(Debug, Default, Clone, PartialEq, Eq)]
75pub struct VfsMetrics {
76 pub disk_reads: i64,
79 pub disk_writes: i64,
81 pub disk_bytes_read: i64,
83 pub disk_bytes_written: i64,
85
86 pub blob_reads: i64,
89 pub blob_writes: i64,
91 pub blob_bytes_read: i64,
93 pub blob_bytes_written: i64,
95
96 pub cache_hits: i64,
99 pub cache_misses: i64,
101 pub cache_miss_pages: i64,
103 pub prefetch_pages: i64,
105
106 pub lease_acquires: i64,
109 pub lease_renewals: i64,
111 pub lease_releases: i64,
113
114 pub syncs: i64,
117 pub dirty_pages_synced: i64,
119 pub blob_resizes: i64,
121
122 pub revalidations: i64,
125 pub revalidation_downloads: i64,
127 pub revalidation_diffs: i64,
129 pub pages_invalidated: i64,
131
132 pub journal_uploads: i64,
135 pub journal_bytes_uploaded: i64,
137 pub wal_uploads: i64,
139 pub wal_bytes_uploaded: i64,
141
142 pub azure_errors: i64,
145}
146
147impl VfsMetrics {
148 pub const FIELD_COUNT: usize = 27;
150
151 pub fn parse(text: &str) -> Result<Self, ParseError> {
161 let mut m = VfsMetrics::default();
162
163 for line in text.lines() {
164 let line = line.trim();
165 if line.is_empty() {
166 continue;
167 }
168
169 let (key, value) = match line.split_once('=') {
170 Some(pair) => pair,
171 None => {
172 return Err(ParseError {
173 message: format!("expected key=value, got: {line}"),
174 });
175 }
176 };
177
178 let v: i64 = value.parse().map_err(|_| ParseError {
179 message: format!("invalid integer for key '{key}': {value}"),
180 })?;
181
182 match key {
183 "disk_reads" => m.disk_reads = v,
184 "disk_writes" => m.disk_writes = v,
185 "disk_bytes_read" => m.disk_bytes_read = v,
186 "disk_bytes_written" => m.disk_bytes_written = v,
187 "blob_reads" => m.blob_reads = v,
188 "blob_writes" => m.blob_writes = v,
189 "blob_bytes_read" => m.blob_bytes_read = v,
190 "blob_bytes_written" => m.blob_bytes_written = v,
191 "cache_hits" => m.cache_hits = v,
192 "cache_misses" => m.cache_misses = v,
193 "cache_miss_pages" => m.cache_miss_pages = v,
194 "prefetch_pages" => m.prefetch_pages = v,
195 "lease_acquires" => m.lease_acquires = v,
196 "lease_renewals" => m.lease_renewals = v,
197 "lease_releases" => m.lease_releases = v,
198 "syncs" => m.syncs = v,
199 "dirty_pages_synced" => m.dirty_pages_synced = v,
200 "blob_resizes" => m.blob_resizes = v,
201 "revalidations" => m.revalidations = v,
202 "revalidation_downloads" => m.revalidation_downloads = v,
203 "revalidation_diffs" => m.revalidation_diffs = v,
204 "pages_invalidated" => m.pages_invalidated = v,
205 "journal_uploads" => m.journal_uploads = v,
206 "journal_bytes_uploaded" => m.journal_bytes_uploaded = v,
207 "wal_uploads" => m.wal_uploads = v,
208 "wal_bytes_uploaded" => m.wal_bytes_uploaded = v,
209 "azure_errors" => m.azure_errors = v,
210 _ => { }
211 }
212 }
213
214 Ok(m)
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 const FULL_SAMPLE: &str = "\
224disk_reads=10\n\
225disk_writes=5\n\
226disk_bytes_read=40960\n\
227disk_bytes_written=20480\n\
228blob_reads=2\n\
229blob_writes=1\n\
230blob_bytes_read=1048576\n\
231blob_bytes_written=4096\n\
232cache_hits=100\n\
233cache_misses=3\n\
234cache_miss_pages=12\n\
235prefetch_pages=256\n\
236lease_acquires=1\n\
237lease_renewals=0\n\
238lease_releases=1\n\
239syncs=2\n\
240dirty_pages_synced=5\n\
241blob_resizes=0\n\
242revalidations=1\n\
243revalidation_downloads=0\n\
244revalidation_diffs=1\n\
245pages_invalidated=0\n\
246journal_uploads=1\n\
247journal_bytes_uploaded=4096\n\
248wal_uploads=0\n\
249wal_bytes_uploaded=0\n\
250azure_errors=0";
251
252 #[test]
253 fn parse_full_sample() {
254 let m = VfsMetrics::parse(FULL_SAMPLE).unwrap();
255 assert_eq!(m.disk_reads, 10);
256 assert_eq!(m.disk_writes, 5);
257 assert_eq!(m.disk_bytes_read, 40960);
258 assert_eq!(m.disk_bytes_written, 20480);
259 assert_eq!(m.blob_reads, 2);
260 assert_eq!(m.blob_writes, 1);
261 assert_eq!(m.blob_bytes_read, 1_048_576);
262 assert_eq!(m.blob_bytes_written, 4096);
263 assert_eq!(m.cache_hits, 100);
264 assert_eq!(m.cache_misses, 3);
265 assert_eq!(m.cache_miss_pages, 12);
266 assert_eq!(m.prefetch_pages, 256);
267 assert_eq!(m.lease_acquires, 1);
268 assert_eq!(m.lease_renewals, 0);
269 assert_eq!(m.lease_releases, 1);
270 assert_eq!(m.syncs, 2);
271 assert_eq!(m.dirty_pages_synced, 5);
272 assert_eq!(m.blob_resizes, 0);
273 assert_eq!(m.revalidations, 1);
274 assert_eq!(m.revalidation_downloads, 0);
275 assert_eq!(m.revalidation_diffs, 1);
276 assert_eq!(m.pages_invalidated, 0);
277 assert_eq!(m.journal_uploads, 1);
278 assert_eq!(m.journal_bytes_uploaded, 4096);
279 assert_eq!(m.wal_uploads, 0);
280 assert_eq!(m.wal_bytes_uploaded, 0);
281 assert_eq!(m.azure_errors, 0);
282 }
283
284 #[test]
285 fn parse_empty_string() {
286 let m = VfsMetrics::parse("").unwrap();
287 assert_eq!(m, VfsMetrics::default());
288 }
289
290 #[test]
291 fn parse_missing_keys_default_to_zero() {
292 let text = "disk_reads=42\ncache_hits=7";
293 let m = VfsMetrics::parse(text).unwrap();
294 assert_eq!(m.disk_reads, 42);
295 assert_eq!(m.cache_hits, 7);
296 assert_eq!(m.blob_reads, 0); }
298
299 #[test]
300 fn parse_unknown_keys_ignored() {
301 let text = "disk_reads=1\nfuture_counter=999";
302 let m = VfsMetrics::parse(text).unwrap();
303 assert_eq!(m.disk_reads, 1);
304 }
305
306 #[test]
307 fn parse_bad_integer_is_error() {
308 let text = "disk_reads=not_a_number";
309 let err = VfsMetrics::parse(text).unwrap_err();
310 assert!(err.message.contains("disk_reads"));
311 assert!(err.message.contains("not_a_number"));
312 }
313
314 #[test]
315 fn parse_missing_equals_is_error() {
316 let text = "disk_reads 10";
317 let err = VfsMetrics::parse(text).unwrap_err();
318 assert!(err.message.contains("key=value"));
319 }
320
321 #[test]
322 fn parse_blank_lines_ignored() {
323 let text = "\n\ndisk_reads=5\n\n\ncache_hits=3\n\n";
324 let m = VfsMetrics::parse(text).unwrap();
325 assert_eq!(m.disk_reads, 5);
326 assert_eq!(m.cache_hits, 3);
327 }
328
329 #[test]
330 fn parse_whitespace_trimmed() {
331 let text = " disk_reads=5 \n cache_hits=3 ";
332 let m = VfsMetrics::parse(text).unwrap();
333 assert_eq!(m.disk_reads, 5);
334 assert_eq!(m.cache_hits, 3);
335 }
336
337 #[test]
338 fn parse_negative_values() {
339 let text = "disk_reads=-1";
342 let m = VfsMetrics::parse(text).unwrap();
343 assert_eq!(m.disk_reads, -1);
344 }
345
346 #[test]
347 fn default_is_all_zeros() {
348 let m = VfsMetrics::default();
349 assert_eq!(m.disk_reads, 0);
350 assert_eq!(m.azure_errors, 0);
351 assert_eq!(m.wal_bytes_uploaded, 0);
352 }
353}