oxideshield_guard/telemetry/
recorder.rs1use std::sync::Arc;
6use std::time::Instant;
7
8use super::metrics::{global_metrics, GuardMetricsCollector};
9use crate::guard::{Guard, GuardAction, GuardCheckResult};
10
11pub struct InstrumentedGuard<G: Guard> {
13 inner: G,
14 collector: Arc<GuardMetricsCollector>,
15 guard_type: String,
17}
18
19impl<G: Guard> InstrumentedGuard<G> {
20 pub fn new(guard: G) -> Self {
22 Self::with_collector(guard, global_metrics())
23 }
24
25 pub fn with_collector(guard: G, collector: Arc<GuardMetricsCollector>) -> Self {
27 Self {
28 guard_type: std::any::type_name::<G>().to_string(),
29 inner: guard,
30 collector,
31 }
32 }
33
34 pub fn inner(&self) -> &G {
36 &self.inner
37 }
38
39 pub fn collector(&self) -> Arc<GuardMetricsCollector> {
41 self.collector.clone()
42 }
43}
44
45impl<G: Guard> Guard for InstrumentedGuard<G> {
46 fn name(&self) -> &str {
47 self.inner.name()
48 }
49
50 fn action(&self) -> crate::guard::GuardAction {
51 self.inner.action()
52 }
53
54 fn check(&self, content: &str) -> GuardCheckResult {
55 self.collector.record_check_started();
56 let start = Instant::now();
57
58 let result = self.inner.check(content);
59
60 let elapsed = start.elapsed();
61 let sanitized = matches!(result.action, GuardAction::Sanitize);
62
63 self.collector.record_check_completed_with_type(
65 self.inner.name(),
66 &self.guard_type,
67 result.passed,
68 sanitized,
69 elapsed,
70 );
71
72 result
73 }
74}
75
76impl<G: Guard> InstrumentedGuard<G> {
77 pub fn guard_type(&self) -> &str {
79 &self.guard_type
80 }
81}
82
83pub trait InstrumentGuard: Guard + Sized {
85 fn instrumented(self) -> InstrumentedGuard<Self> {
87 InstrumentedGuard::new(self)
88 }
89
90 fn instrumented_with(self, collector: Arc<GuardMetricsCollector>) -> InstrumentedGuard<Self> {
92 InstrumentedGuard::with_collector(self, collector)
93 }
94}
95
96impl<G: Guard + Sized> InstrumentGuard for G {}
98
99#[derive(Debug, Clone)]
101pub struct MetricsSnapshot {
102 pub total_checks: u64,
104 pub total_blocks: u64,
106 pub total_passed: u64,
108 pub block_rate: f64,
110 pub avg_latency_ms: f64,
112 pub p50_latency_ms: f64,
114 pub p99_latency_ms: f64,
116 pub timestamp: chrono::DateTime<chrono::Utc>,
118}
119
120impl MetricsSnapshot {
121 pub fn from_collector(collector: &GuardMetricsCollector) -> Self {
123 Self {
124 total_checks: collector.total_checks(),
125 total_blocks: collector.total_blocks(),
126 total_passed: collector.total_passed(),
127 block_rate: collector.block_rate(),
128 avg_latency_ms: collector.avg_latency_ms(),
129 p50_latency_ms: collector.p50_latency_ms(),
130 p99_latency_ms: collector.p99_latency_ms(),
131 timestamp: chrono::Utc::now(),
132 }
133 }
134
135 pub fn global() -> Self {
137 Self::from_collector(&global_metrics())
138 }
139}
140
141pub struct MetricsReporter {
143 collector: Arc<GuardMetricsCollector>,
144 last_snapshot: parking_lot::RwLock<Option<MetricsSnapshot>>,
145}
146
147impl MetricsReporter {
148 pub fn new(collector: Arc<GuardMetricsCollector>) -> Self {
150 Self {
151 collector,
152 last_snapshot: parking_lot::RwLock::new(None),
153 }
154 }
155
156 pub fn global() -> Self {
158 Self::new(global_metrics())
159 }
160
161 pub fn current(&self) -> MetricsSnapshot {
163 MetricsSnapshot::from_collector(&self.collector)
164 }
165
166 pub fn delta(&self) -> MetricsDelta {
168 let current = self.current();
169 let last = self.last_snapshot.read().clone();
170
171 let delta = if let Some(ref last) = last {
172 MetricsDelta {
173 checks: current.total_checks.saturating_sub(last.total_checks),
174 blocks: current.total_blocks.saturating_sub(last.total_blocks),
175 passed: current.total_passed.saturating_sub(last.total_passed),
176 duration: current.timestamp - last.timestamp,
177 }
178 } else {
179 MetricsDelta {
180 checks: current.total_checks,
181 blocks: current.total_blocks,
182 passed: current.total_passed,
183 duration: chrono::Duration::zero(),
184 }
185 };
186
187 *self.last_snapshot.write() = Some(current);
188 delta
189 }
190
191 pub fn throughput(&self) -> f64 {
193 let delta = self.delta();
194 let seconds = delta.duration.num_milliseconds() as f64 / 1000.0;
195 if seconds > 0.0 {
196 delta.checks as f64 / seconds
197 } else {
198 0.0
199 }
200 }
201}
202
203#[derive(Debug, Clone)]
205pub struct MetricsDelta {
206 pub checks: u64,
208 pub blocks: u64,
210 pub passed: u64,
212 pub duration: chrono::Duration,
214}
215
216impl MetricsDelta {
217 pub fn block_rate(&self) -> f64 {
219 if self.checks == 0 {
220 0.0
221 } else {
222 self.blocks as f64 / self.checks as f64
223 }
224 }
225
226 pub fn checks_per_second(&self) -> f64 {
228 let seconds = self.duration.num_milliseconds() as f64 / 1000.0;
229 if seconds > 0.0 {
230 self.checks as f64 / seconds
231 } else {
232 0.0
233 }
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::guard::LengthGuard;
241
242 #[test]
243 fn test_instrumented_guard() {
244 let collector = GuardMetricsCollector::shared();
245 let guard = LengthGuard::new("length")
246 .with_max_chars(100)
247 .instrumented_with(collector.clone());
248
249 let result = guard.check("short text");
250 assert!(result.passed);
251
252 assert_eq!(collector.total_checks(), 1);
253 assert_eq!(collector.total_passed(), 1);
254 }
255
256 #[test]
257 fn test_instrumented_guard_blocked() {
258 let collector = GuardMetricsCollector::shared();
259 let guard = LengthGuard::new("length")
260 .with_max_chars(5)
261 .instrumented_with(collector.clone());
262
263 let result = guard.check("this is too long");
264 assert!(!result.passed);
265
266 assert_eq!(collector.total_blocks(), 1);
267 }
268
269 #[test]
270 fn test_metrics_snapshot() {
271 let collector = GuardMetricsCollector::new();
272 collector.record_check_completed("test", true, false, std::time::Duration::from_millis(10));
273
274 let snapshot = MetricsSnapshot::from_collector(&collector);
275 assert_eq!(snapshot.total_checks, 1);
276 assert_eq!(snapshot.total_passed, 1);
277 }
278
279 #[test]
280 fn test_metrics_reporter() {
281 let collector = GuardMetricsCollector::shared();
282 let reporter = MetricsReporter::new(collector.clone());
283
284 let delta = reporter.delta();
286 assert_eq!(delta.checks, 0);
287
288 for _ in 0..5 {
290 collector.record_check_completed(
291 "test",
292 true,
293 false,
294 std::time::Duration::from_millis(10),
295 );
296 }
297
298 let delta = reporter.delta();
300 assert_eq!(delta.checks, 5);
301 }
302
303 #[test]
304 fn test_instrument_trait() {
305 let guard = LengthGuard::new("length")
306 .with_max_chars(100)
307 .instrumented();
308
309 let result = guard.check("test");
311 assert!(result.passed);
312 }
313
314 #[test]
315 fn test_per_type_metrics() {
316 let collector = GuardMetricsCollector::shared();
317
318 collector.record_check_completed_with_type(
320 "guard_a",
321 "oxideshield_guard::guards::length::LengthGuard",
322 true,
323 false,
324 std::time::Duration::from_millis(5),
325 );
326 collector.record_check_completed_with_type(
327 "guard_b",
328 "oxideshield_guard::guards::length::LengthGuard",
329 false,
330 false,
331 std::time::Duration::from_millis(10),
332 );
333 collector.record_check_completed_with_type(
334 "guard_c",
335 "oxideshield_guard::guards::pii::PIIGuard",
336 true,
337 false,
338 std::time::Duration::from_millis(15),
339 );
340
341 let per_guard = collector.per_guard_metrics();
343 assert!(per_guard.contains_key("guard_a"));
344 assert!(per_guard.contains_key("guard_b"));
345 assert!(per_guard.contains_key("guard_c"));
346
347 let per_type = collector.per_type_metrics();
349 assert!(per_type.contains_key("oxideshield_guard::guards::length::LengthGuard"));
350 assert!(per_type.contains_key("oxideshield_guard::guards::pii::PIIGuard"));
351
352 let length_type = &per_type["oxideshield_guard::guards::length::LengthGuard"];
354 assert_eq!(length_type.checks, 2);
355 assert_eq!(length_type.passed, 1);
356 assert_eq!(length_type.blocks, 1);
357
358 let pii_type = &per_type["oxideshield_guard::guards::pii::PIIGuard"];
360 assert_eq!(pii_type.checks, 1);
361 assert_eq!(pii_type.passed, 1);
362 }
363
364 #[test]
365 fn test_instrumented_guard_records_type() {
366 let collector = GuardMetricsCollector::shared();
367 collector.reset();
368
369 let guard = LengthGuard::new("typed_length")
370 .with_max_chars(100)
371 .instrumented_with(collector.clone());
372
373 assert!(guard.guard_type().contains("LengthGuard"));
375
376 guard.check("test text");
378
379 let per_type = collector.per_type_metrics();
381 assert!(!per_type.is_empty(), "Should have per-type metrics");
382
383 let has_length_type = per_type.keys().any(|k| k.contains("LengthGuard"));
385 assert!(has_length_type, "Should have LengthGuard type metrics");
386 }
387}