1use std::sync::atomic::{AtomicU64, Ordering};
2use std::time::Duration;
3
4#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
5pub struct MetricsSnapshot {
6 pub poison_recoveries_total: u64,
7
8 pub poison_path_inodes: u64,
9 pub poison_inodes: u64,
10 pub poison_inode_paths: u64,
11 pub poison_directories: u64,
12 pub poison_file_cache: u64,
13
14 pub sub_cache_hits: u64,
15 pub sub_cache_misses: u64,
16 pub sub_cache_evictions: u64,
17
18 pub index_cache_hits: u64,
19 pub index_cache_misses: u64,
20 pub index_cache_evictions: u64,
21
22 pub retrieval_query_calls: u64,
23 pub retrieval_query_ns_total: u64,
24 pub retrieval_query_ns_max: u64,
25
26 pub rerank_calls: u64,
27 pub rerank_ns_total: u64,
28 pub rerank_ns_max: u64,
29
30 pub hier_query_calls: u64,
31 pub hier_query_ns_total: u64,
32 pub hier_query_ns_max: u64,
33}
34
35pub struct Metrics {
36 poison_recoveries_total: AtomicU64,
37
38 poison_path_inodes: AtomicU64,
39 poison_inodes: AtomicU64,
40 poison_inode_paths: AtomicU64,
41 poison_directories: AtomicU64,
42 poison_file_cache: AtomicU64,
43
44 sub_cache_hits: AtomicU64,
45 sub_cache_misses: AtomicU64,
46 sub_cache_evictions: AtomicU64,
47
48 index_cache_hits: AtomicU64,
49 index_cache_misses: AtomicU64,
50 index_cache_evictions: AtomicU64,
51
52 retrieval_query_calls: AtomicU64,
53 retrieval_query_ns_total: AtomicU64,
54 retrieval_query_ns_max: AtomicU64,
55
56 rerank_calls: AtomicU64,
57 rerank_ns_total: AtomicU64,
58 rerank_ns_max: AtomicU64,
59
60 hier_query_calls: AtomicU64,
61 hier_query_ns_total: AtomicU64,
62 hier_query_ns_max: AtomicU64,
63}
64
65impl Default for Metrics {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl Metrics {
72 pub const fn new() -> Self {
73 Self {
74 poison_recoveries_total: AtomicU64::new(0),
75
76 poison_path_inodes: AtomicU64::new(0),
77 poison_inodes: AtomicU64::new(0),
78 poison_inode_paths: AtomicU64::new(0),
79 poison_directories: AtomicU64::new(0),
80 poison_file_cache: AtomicU64::new(0),
81
82 sub_cache_hits: AtomicU64::new(0),
83 sub_cache_misses: AtomicU64::new(0),
84 sub_cache_evictions: AtomicU64::new(0),
85
86 index_cache_hits: AtomicU64::new(0),
87 index_cache_misses: AtomicU64::new(0),
88 index_cache_evictions: AtomicU64::new(0),
89
90 retrieval_query_calls: AtomicU64::new(0),
91 retrieval_query_ns_total: AtomicU64::new(0),
92 retrieval_query_ns_max: AtomicU64::new(0),
93
94 rerank_calls: AtomicU64::new(0),
95 rerank_ns_total: AtomicU64::new(0),
96 rerank_ns_max: AtomicU64::new(0),
97
98 hier_query_calls: AtomicU64::new(0),
99 hier_query_ns_total: AtomicU64::new(0),
100 hier_query_ns_max: AtomicU64::new(0),
101 }
102 }
103
104 pub fn snapshot(&self) -> MetricsSnapshot {
105 MetricsSnapshot {
106 poison_recoveries_total: self.poison_recoveries_total.load(Ordering::Relaxed),
107
108 poison_path_inodes: self.poison_path_inodes.load(Ordering::Relaxed),
109 poison_inodes: self.poison_inodes.load(Ordering::Relaxed),
110 poison_inode_paths: self.poison_inode_paths.load(Ordering::Relaxed),
111 poison_directories: self.poison_directories.load(Ordering::Relaxed),
112 poison_file_cache: self.poison_file_cache.load(Ordering::Relaxed),
113
114 sub_cache_hits: self.sub_cache_hits.load(Ordering::Relaxed),
115 sub_cache_misses: self.sub_cache_misses.load(Ordering::Relaxed),
116 sub_cache_evictions: self.sub_cache_evictions.load(Ordering::Relaxed),
117
118 index_cache_hits: self.index_cache_hits.load(Ordering::Relaxed),
119 index_cache_misses: self.index_cache_misses.load(Ordering::Relaxed),
120 index_cache_evictions: self.index_cache_evictions.load(Ordering::Relaxed),
121
122 retrieval_query_calls: self.retrieval_query_calls.load(Ordering::Relaxed),
123 retrieval_query_ns_total: self.retrieval_query_ns_total.load(Ordering::Relaxed),
124 retrieval_query_ns_max: self.retrieval_query_ns_max.load(Ordering::Relaxed),
125
126 rerank_calls: self.rerank_calls.load(Ordering::Relaxed),
127 rerank_ns_total: self.rerank_ns_total.load(Ordering::Relaxed),
128 rerank_ns_max: self.rerank_ns_max.load(Ordering::Relaxed),
129
130 hier_query_calls: self.hier_query_calls.load(Ordering::Relaxed),
131 hier_query_ns_total: self.hier_query_ns_total.load(Ordering::Relaxed),
132 hier_query_ns_max: self.hier_query_ns_max.load(Ordering::Relaxed),
133 }
134 }
135
136 pub fn inc_poison_path_inodes(&self) {
137 #[cfg(feature = "metrics")]
138 {
139 self.poison_recoveries_total.fetch_add(1, Ordering::Relaxed);
140 self.poison_path_inodes.fetch_add(1, Ordering::Relaxed);
141 }
142 }
143
144 pub fn inc_poison_inodes(&self) {
145 #[cfg(feature = "metrics")]
146 {
147 self.poison_recoveries_total.fetch_add(1, Ordering::Relaxed);
148 self.poison_inodes.fetch_add(1, Ordering::Relaxed);
149 }
150 }
151
152 pub fn inc_poison_inode_paths(&self) {
153 #[cfg(feature = "metrics")]
154 {
155 self.poison_recoveries_total.fetch_add(1, Ordering::Relaxed);
156 self.poison_inode_paths.fetch_add(1, Ordering::Relaxed);
157 }
158 }
159
160 pub fn inc_poison_directories(&self) {
161 #[cfg(feature = "metrics")]
162 {
163 self.poison_recoveries_total.fetch_add(1, Ordering::Relaxed);
164 self.poison_directories.fetch_add(1, Ordering::Relaxed);
165 }
166 }
167
168 pub fn inc_poison_file_cache(&self) {
169 #[cfg(feature = "metrics")]
170 {
171 self.poison_recoveries_total.fetch_add(1, Ordering::Relaxed);
172 self.poison_file_cache.fetch_add(1, Ordering::Relaxed);
173 }
174 }
175
176 pub fn inc_sub_cache_hit(&self) {
177 #[cfg(feature = "metrics")]
178 {
179 self.sub_cache_hits.fetch_add(1, Ordering::Relaxed);
180 }
181 }
182
183 pub fn inc_sub_cache_miss(&self) {
184 #[cfg(feature = "metrics")]
185 {
186 self.sub_cache_misses.fetch_add(1, Ordering::Relaxed);
187 }
188 }
189
190 pub fn inc_sub_cache_eviction(&self) {
191 #[cfg(feature = "metrics")]
192 {
193 self.sub_cache_evictions.fetch_add(1, Ordering::Relaxed);
194 }
195 }
196
197 pub fn inc_index_cache_hit(&self) {
198 #[cfg(feature = "metrics")]
199 {
200 self.index_cache_hits.fetch_add(1, Ordering::Relaxed);
201 }
202 }
203
204 pub fn inc_index_cache_miss(&self) {
205 #[cfg(feature = "metrics")]
206 {
207 self.index_cache_misses.fetch_add(1, Ordering::Relaxed);
208 }
209 }
210
211 pub fn inc_index_cache_eviction(&self) {
212 #[cfg(feature = "metrics")]
213 {
214 self.index_cache_evictions.fetch_add(1, Ordering::Relaxed);
215 }
216 }
217
218 pub fn record_retrieval_query(&self, _dur: Duration) {
219 #[cfg(feature = "metrics")]
220 {
221 record_duration(
222 &self.retrieval_query_calls,
223 &self.retrieval_query_ns_total,
224 &self.retrieval_query_ns_max,
225 _dur,
226 );
227 }
228 }
229
230 pub fn record_rerank(&self, _dur: Duration) {
231 #[cfg(feature = "metrics")]
232 {
233 record_duration(
234 &self.rerank_calls,
235 &self.rerank_ns_total,
236 &self.rerank_ns_max,
237 _dur,
238 );
239 }
240 }
241
242 pub fn record_hier_query(&self, _dur: Duration) {
243 #[cfg(feature = "metrics")]
244 {
245 record_duration(
246 &self.hier_query_calls,
247 &self.hier_query_ns_total,
248 &self.hier_query_ns_max,
249 _dur,
250 );
251 }
252 }
253}
254
255#[cfg(feature = "metrics")]
256fn record_duration(calls: &AtomicU64, total_ns: &AtomicU64, max_ns: &AtomicU64, dur: Duration) {
257 let ns = dur.as_nanos().min(u128::from(u64::MAX)) as u64;
258 calls.fetch_add(1, Ordering::Relaxed);
259 total_ns.fetch_add(ns, Ordering::Relaxed);
260
261 let mut cur = max_ns.load(Ordering::Relaxed);
262 while ns > cur {
263 match max_ns.compare_exchange_weak(cur, ns, Ordering::Relaxed, Ordering::Relaxed) {
264 Ok(_) => break,
265 Err(next) => cur = next,
266 }
267 }
268}
269
270static METRICS: Metrics = Metrics::new();
271
272pub fn metrics() -> &'static Metrics {
273 &METRICS
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn metrics_snapshot_delta_behaves_under_feature_gate() {
282 let before = metrics().snapshot();
283
284 metrics().inc_poison_inodes();
285 metrics().inc_sub_cache_hit();
286 metrics().record_retrieval_query(Duration::from_millis(2));
287
288 let after = metrics().snapshot();
289
290 #[cfg(feature = "metrics")]
291 {
292 assert!(after.poison_inodes >= before.poison_inodes + 1);
293 assert!(after.poison_recoveries_total >= before.poison_recoveries_total + 1);
294 assert!(after.sub_cache_hits >= before.sub_cache_hits + 1);
295 assert!(after.retrieval_query_calls >= before.retrieval_query_calls + 1);
296 assert!(after.retrieval_query_ns_total >= before.retrieval_query_ns_total);
297 assert!(after.retrieval_query_ns_max >= before.retrieval_query_ns_max);
298 }
299
300 #[cfg(not(feature = "metrics"))]
301 {
302 assert_eq!(after, before);
303 }
304 }
305}