1use std::sync::{
7 Arc,
8 atomic::{AtomicU64, Ordering},
9};
10
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone)]
15pub struct ValidationMetricsCollector {
16 pub validation_checks_total: Arc<AtomicU64>,
18
19 pub validation_errors_total: Arc<AtomicU64>,
21
22 pub async_validation_total: Arc<AtomicU64>,
24
25 pub async_validation_errors: Arc<AtomicU64>,
27
28 pub async_validation_duration_us: Arc<AtomicU64>,
30
31 pub validation_duration_us: Arc<AtomicU64>,
33
34 pub field_validation_errors: Arc<parking_lot::RwLock<std::collections::HashMap<String, u64>>>,
36
37 pub rule_type_errors: Arc<parking_lot::RwLock<std::collections::HashMap<String, u64>>>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ValidationMetricEntry {
44 pub field: String,
46
47 pub rule_type: String,
49
50 pub valid: bool,
52
53 pub duration_us: u64,
55
56 pub validator_type: String,
58
59 pub failure_reason: Option<String>,
61}
62
63impl ValidationMetricsCollector {
64 #[must_use]
66 pub fn new() -> Self {
67 Self {
68 validation_checks_total: Arc::new(AtomicU64::new(0)),
69 validation_errors_total: Arc::new(AtomicU64::new(0)),
70 async_validation_total: Arc::new(AtomicU64::new(0)),
71 async_validation_errors: Arc::new(AtomicU64::new(0)),
72 async_validation_duration_us: Arc::new(AtomicU64::new(0)),
73 validation_duration_us: Arc::new(AtomicU64::new(0)),
74 field_validation_errors: Arc::new(parking_lot::RwLock::new(
75 std::collections::HashMap::new(),
76 )),
77 rule_type_errors: Arc::new(parking_lot::RwLock::new(
78 std::collections::HashMap::new(),
79 )),
80 }
81 }
82
83 pub fn record_validation(&self, field: &str, rule_type: &str, valid: bool, duration_us: u64) {
85 self.validation_checks_total.fetch_add(1, Ordering::Relaxed);
86 self.validation_duration_us.fetch_add(duration_us, Ordering::Relaxed);
87
88 if !valid {
89 self.validation_errors_total.fetch_add(1, Ordering::Relaxed);
90
91 {
93 let mut errors = self.field_validation_errors.write();
94 *errors.entry(field.to_string()).or_insert(0) += 1;
95 }
96
97 {
99 let mut errors = self.rule_type_errors.write();
100 *errors.entry(rule_type.to_string()).or_insert(0) += 1;
101 }
102 }
103 }
104
105 pub fn record_async_validation(
107 &self,
108 field: &str,
109 rule_type: &str,
110 valid: bool,
111 duration_us: u64,
112 ) {
113 self.async_validation_total.fetch_add(1, Ordering::Relaxed);
114 self.async_validation_duration_us.fetch_add(duration_us, Ordering::Relaxed);
115
116 if !valid {
117 self.async_validation_errors.fetch_add(1, Ordering::Relaxed);
118
119 {
121 let mut errors = self.field_validation_errors.write();
122 *errors.entry(field.to_string()).or_insert(0) += 1;
123 }
124
125 {
127 let mut errors = self.rule_type_errors.write();
128 *errors.entry(rule_type.to_string()).or_insert(0) += 1;
129 }
130 }
131 }
132
133 pub fn get_field_errors(&self) -> std::collections::HashMap<String, u64> {
135 self.field_validation_errors.read().clone()
136 }
137
138 pub fn get_rule_type_errors(&self) -> std::collections::HashMap<String, u64> {
140 self.rule_type_errors.read().clone()
141 }
142
143 pub fn reset(&self) {
145 self.validation_checks_total.store(0, Ordering::Relaxed);
146 self.validation_errors_total.store(0, Ordering::Relaxed);
147 self.async_validation_total.store(0, Ordering::Relaxed);
148 self.async_validation_errors.store(0, Ordering::Relaxed);
149 self.async_validation_duration_us.store(0, Ordering::Relaxed);
150 self.validation_duration_us.store(0, Ordering::Relaxed);
151 self.field_validation_errors.write().clear();
152 self.rule_type_errors.write().clear();
153 }
154
155 pub fn snapshot_prometheus(&self) -> PrometheusValidationMetrics {
157 PrometheusValidationMetrics::from(self)
158 }
159}
160
161impl Default for ValidationMetricsCollector {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct PrometheusValidationMetrics {
170 pub validation_checks_total: u64,
172
173 pub validation_errors_total: u64,
175
176 pub async_validation_total: u64,
178
179 pub async_validation_errors: u64,
181
182 pub validation_avg_duration_us: f64,
184
185 pub async_validation_avg_duration_us: f64,
187}
188
189impl PrometheusValidationMetrics {
190 #[must_use]
192 pub fn to_prometheus_format(&self) -> String {
193 format!(
194 r"# HELP fraiseql_validation_checks_total Total validation checks performed
195# TYPE fraiseql_validation_checks_total counter
196fraiseql_validation_checks_total {}
197
198# HELP fraiseql_validation_errors_total Total validation errors
199# TYPE fraiseql_validation_errors_total counter
200fraiseql_validation_errors_total {}
201
202# HELP fraiseql_async_validation_total Total async validation checks
203# TYPE fraiseql_async_validation_total counter
204fraiseql_async_validation_total {}
205
206# HELP fraiseql_async_validation_errors_total Total async validation errors
207# TYPE fraiseql_async_validation_errors_total counter
208fraiseql_async_validation_errors_total {}
209
210# HELP fraiseql_validation_avg_duration_us Average validation duration in microseconds
211# TYPE fraiseql_validation_avg_duration_us gauge
212fraiseql_validation_avg_duration_us {:.2}
213
214# HELP fraiseql_async_validation_avg_duration_us Average async validation duration in microseconds
215# TYPE fraiseql_async_validation_avg_duration_us gauge
216fraiseql_async_validation_avg_duration_us {:.2}
217",
218 self.validation_checks_total,
219 self.validation_errors_total,
220 self.async_validation_total,
221 self.async_validation_errors,
222 self.validation_avg_duration_us,
223 self.async_validation_avg_duration_us,
224 )
225 }
226}
227
228impl From<&ValidationMetricsCollector> for PrometheusValidationMetrics {
229 fn from(collector: &ValidationMetricsCollector) -> Self {
230 let validation_checks = collector.validation_checks_total.load(Ordering::Relaxed);
231 let validation_duration = collector.validation_duration_us.load(Ordering::Relaxed);
232 let async_checks = collector.async_validation_total.load(Ordering::Relaxed);
233 let async_duration = collector.async_validation_duration_us.load(Ordering::Relaxed);
234
235 Self {
236 validation_checks_total: validation_checks,
237 validation_errors_total: collector
238 .validation_errors_total
239 .load(Ordering::Relaxed),
240 async_validation_total: async_checks,
241 async_validation_errors: collector
242 .async_validation_errors
243 .load(Ordering::Relaxed),
244 validation_avg_duration_us: if validation_checks > 0 {
245 validation_duration as f64 / validation_checks as f64
246 } else {
247 0.0
248 },
249 async_validation_avg_duration_us: if async_checks > 0 {
250 async_duration as f64 / async_checks as f64
251 } else {
252 0.0
253 },
254 }
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn test_validation_metrics_creation() {
264 let collector = ValidationMetricsCollector::new();
265 assert_eq!(collector.validation_checks_total.load(Ordering::Relaxed), 0);
266 assert_eq!(collector.validation_errors_total.load(Ordering::Relaxed), 0);
267 assert_eq!(collector.async_validation_total.load(Ordering::Relaxed), 0);
268 assert_eq!(collector.async_validation_errors.load(Ordering::Relaxed), 0);
269 }
270
271 #[test]
272 fn test_record_validation_success() {
273 let collector = ValidationMetricsCollector::new();
274 collector.record_validation("email", "pattern", true, 100);
275
276 assert_eq!(collector.validation_checks_total.load(Ordering::Relaxed), 1);
277 assert_eq!(collector.validation_errors_total.load(Ordering::Relaxed), 0);
278 assert_eq!(collector.validation_duration_us.load(Ordering::Relaxed), 100);
279 }
280
281 #[test]
282 fn test_record_validation_failure() {
283 let collector = ValidationMetricsCollector::new();
284 collector.record_validation("email", "pattern", false, 150);
285
286 assert_eq!(collector.validation_checks_total.load(Ordering::Relaxed), 1);
287 assert_eq!(collector.validation_errors_total.load(Ordering::Relaxed), 1);
288 assert_eq!(collector.validation_duration_us.load(Ordering::Relaxed), 150);
289 }
290
291 #[test]
292 fn test_per_field_error_tracking() {
293 let collector = ValidationMetricsCollector::new();
294 collector.record_validation("email", "pattern", false, 100);
295 collector.record_validation("email", "length", false, 100);
296 collector.record_validation("name", "required", false, 50);
297
298 let field_errors = collector.get_field_errors();
299 assert_eq!(field_errors.get("email"), Some(&2));
300 assert_eq!(field_errors.get("name"), Some(&1));
301 }
302
303 #[test]
304 fn test_per_rule_type_error_tracking() {
305 let collector = ValidationMetricsCollector::new();
306 collector.record_validation("email", "pattern", false, 100);
307 collector.record_validation("age", "pattern", false, 100);
308 collector.record_validation("name", "required", false, 50);
309
310 let rule_errors = collector.get_rule_type_errors();
311 assert_eq!(rule_errors.get("pattern"), Some(&2));
312 assert_eq!(rule_errors.get("required"), Some(&1));
313 }
314
315 #[test]
316 fn test_record_async_validation_success() {
317 let collector = ValidationMetricsCollector::new();
318 collector.record_async_validation("email", "async", true, 500);
319
320 assert_eq!(collector.async_validation_total.load(Ordering::Relaxed), 1);
321 assert_eq!(collector.async_validation_errors.load(Ordering::Relaxed), 0);
322 assert_eq!(collector.async_validation_duration_us.load(Ordering::Relaxed), 500);
323 }
324
325 #[test]
326 fn test_record_async_validation_failure() {
327 let collector = ValidationMetricsCollector::new();
328 collector.record_async_validation("email", "async", false, 600);
329
330 assert_eq!(collector.async_validation_total.load(Ordering::Relaxed), 1);
331 assert_eq!(collector.async_validation_errors.load(Ordering::Relaxed), 1);
332 assert_eq!(collector.async_validation_duration_us.load(Ordering::Relaxed), 600);
333 }
334
335 #[test]
336 fn test_async_validation_in_field_errors() {
337 let collector = ValidationMetricsCollector::new();
338 collector.record_async_validation("email", "async_email", false, 500);
339 collector.record_async_validation("email", "async_domain", false, 500);
340
341 let field_errors = collector.get_field_errors();
342 assert_eq!(field_errors.get("email"), Some(&2));
343 }
344
345 #[test]
346 fn test_multiple_fields_and_rules() {
347 let collector = ValidationMetricsCollector::new();
348
349 collector.record_validation("email", "pattern", false, 100);
351 collector.record_validation("phone", "pattern", false, 150);
352 collector.record_validation("age", "range", false, 50);
353
354 collector.record_validation("password", "length", false, 75);
356 collector.record_validation("password", "pattern", false, 75);
357 collector.record_validation("country", "enum", false, 25);
358
359 let field_errors = collector.get_field_errors();
360 assert_eq!(field_errors.len(), 5);
361
362 let rule_errors = collector.get_rule_type_errors();
363 assert_eq!(rule_errors.len(), 4);
364 assert_eq!(rule_errors.get("pattern"), Some(&3));
365 }
366
367 #[test]
368 fn test_validation_duration_accumulation() {
369 let collector = ValidationMetricsCollector::new();
370 collector.record_validation("email", "pattern", true, 100);
371 collector.record_validation("email", "pattern", false, 150);
372 collector.record_validation("name", "required", true, 50);
373
374 let total_duration = collector.validation_duration_us.load(Ordering::Relaxed);
375 assert_eq!(total_duration, 300); }
377
378 #[test]
379 fn test_async_validation_duration_accumulation() {
380 let collector = ValidationMetricsCollector::new();
381 collector.record_async_validation("email", "async", true, 500);
382 collector.record_async_validation("email", "async", false, 600);
383
384 let total_duration = collector.async_validation_duration_us.load(Ordering::Relaxed);
385 assert_eq!(total_duration, 1100); }
387
388 #[test]
389 fn test_reset_clears_all_metrics() {
390 let collector = ValidationMetricsCollector::new();
391 collector.record_validation("email", "pattern", false, 100);
392 collector.record_async_validation("email", "async", false, 500);
393
394 assert!(collector.validation_errors_total.load(Ordering::Relaxed) > 0);
395 assert!(collector.async_validation_errors.load(Ordering::Relaxed) > 0);
396
397 collector.reset();
398
399 assert_eq!(collector.validation_checks_total.load(Ordering::Relaxed), 0);
400 assert_eq!(collector.validation_errors_total.load(Ordering::Relaxed), 0);
401 assert_eq!(collector.async_validation_total.load(Ordering::Relaxed), 0);
402 assert_eq!(collector.async_validation_errors.load(Ordering::Relaxed), 0);
403 assert_eq!(collector.get_field_errors().len(), 0);
404 assert_eq!(collector.get_rule_type_errors().len(), 0);
405 }
406
407 #[test]
408 fn test_thread_safety_field_errors() {
409 let collector = Arc::new(ValidationMetricsCollector::new());
410 let mut handles = vec![];
411
412 for i in 0..10 {
413 let collector_clone = collector.clone();
414 let handle = std::thread::spawn(move || {
415 let field = format!("field_{}", i % 5);
416 collector_clone.record_validation(&field, "pattern", false, 100);
417 });
418 handles.push(handle);
419 }
420
421 for handle in handles {
422 handle.join().unwrap();
423 }
424
425 let field_errors = collector.get_field_errors();
426 assert_eq!(field_errors.len(), 5);
427 for i in 0..5 {
429 let field = format!("field_{}", i);
430 assert_eq!(field_errors.get(&field), Some(&2));
431 }
432 }
433
434 #[test]
435 fn test_concurrent_validation_counting() {
436 let collector = Arc::new(ValidationMetricsCollector::new());
437 let mut handles = vec![];
438
439 for _ in 0..100 {
440 let collector_clone = collector.clone();
441 let handle = std::thread::spawn(move || {
442 collector_clone.record_validation("email", "pattern", false, 10);
443 });
444 handles.push(handle);
445 }
446
447 for handle in handles {
448 handle.join().unwrap();
449 }
450
451 assert_eq!(collector.validation_checks_total.load(Ordering::Relaxed), 100);
452 assert_eq!(collector.validation_errors_total.load(Ordering::Relaxed), 100);
453 }
454
455 #[test]
456 fn test_validation_metric_entry_serialization() {
457 let entry = ValidationMetricEntry {
458 field: "email".to_string(),
459 rule_type: "pattern".to_string(),
460 valid: false,
461 duration_us: 150,
462 validator_type: "regex".to_string(),
463 failure_reason: Some("Invalid email format".to_string()),
464 };
465
466 let json = serde_json::to_string(&entry).unwrap();
467 assert!(json.contains("email"));
468 assert!(json.contains("pattern"));
469 assert!(!json.contains("\"valid\":true"));
470
471 let deserialized: ValidationMetricEntry = serde_json::from_str(&json).unwrap();
472 assert_eq!(deserialized.field, entry.field);
473 assert_eq!(deserialized.rule_type, entry.rule_type);
474 assert!(!deserialized.valid);
475 }
476
477 #[test]
478 fn test_default_constructor() {
479 let collector = ValidationMetricsCollector::default();
480 assert_eq!(collector.validation_checks_total.load(Ordering::Relaxed), 0);
481 assert_eq!(collector.validation_errors_total.load(Ordering::Relaxed), 0);
482 }
483
484 #[test]
485 fn test_prometheus_validation_metrics_conversion() {
486 let collector = ValidationMetricsCollector::new();
487 collector.validation_checks_total.store(100, Ordering::Relaxed);
488 collector.validation_errors_total.store(10, Ordering::Relaxed);
489 collector.async_validation_total.store(50, Ordering::Relaxed);
490 collector.async_validation_errors.store(5, Ordering::Relaxed);
491
492 let metrics = PrometheusValidationMetrics::from(&collector);
493
494 assert_eq!(metrics.validation_checks_total, 100);
495 assert_eq!(metrics.validation_errors_total, 10);
496 assert_eq!(metrics.async_validation_total, 50);
497 assert_eq!(metrics.async_validation_errors, 5);
498 }
499
500 #[test]
501 fn test_prometheus_validation_metrics_output_format() {
502 let collector = ValidationMetricsCollector::new();
503 collector.validation_checks_total.store(100, Ordering::Relaxed);
504 collector.validation_errors_total.store(10, Ordering::Relaxed);
505
506 let metrics = PrometheusValidationMetrics::from(&collector);
507 let output = metrics.to_prometheus_format();
508
509 assert!(output.contains("fraiseql_validation_checks_total 100"));
510 assert!(output.contains("fraiseql_validation_errors_total 10"));
511 assert!(output.contains("# HELP"));
512 assert!(output.contains("# TYPE"));
513 }
514
515 #[test]
516 fn test_prometheus_validation_metrics_average_calculation() {
517 let collector = ValidationMetricsCollector::new();
518 collector.validation_checks_total.store(10, Ordering::Relaxed);
519 collector.validation_duration_us.store(1000, Ordering::Relaxed); let metrics = PrometheusValidationMetrics::from(&collector);
522 assert!((metrics.validation_avg_duration_us - 100.0).abs() < 0.01); }
524
525 #[test]
526 fn test_prometheus_validation_metrics_async_average() {
527 let collector = ValidationMetricsCollector::new();
528 collector.async_validation_total.store(5, Ordering::Relaxed);
529 collector.async_validation_duration_us.store(2500, Ordering::Relaxed); let metrics = PrometheusValidationMetrics::from(&collector);
532 assert!((metrics.async_validation_avg_duration_us - 500.0).abs() < 0.01); }
534
535 #[test]
536 fn test_prometheus_validation_metrics_zero_checks() {
537 let collector = ValidationMetricsCollector::new();
538 let metrics = PrometheusValidationMetrics::from(&collector);
539
540 assert_eq!(metrics.validation_avg_duration_us, 0.0);
541 assert_eq!(metrics.async_validation_avg_duration_us, 0.0);
542 }
543
544 #[test]
545 fn test_prometheus_validation_metrics_serialization() {
546 let metrics = PrometheusValidationMetrics {
547 validation_checks_total: 100,
548 validation_errors_total: 10,
549 async_validation_total: 50,
550 async_validation_errors: 5,
551 validation_avg_duration_us: 100.5,
552 async_validation_avg_duration_us: 250.75,
553 };
554
555 let json = serde_json::to_string(&metrics).unwrap();
556 assert!(json.contains("100"));
557 assert!(json.contains("10"));
558
559 let deserialized: PrometheusValidationMetrics = serde_json::from_str(&json).unwrap();
560 assert_eq!(deserialized.validation_checks_total, 100);
561 assert_eq!(deserialized.validation_errors_total, 10);
562 }
563
564 #[test]
565 fn test_snapshot_prometheus() {
566 let collector = ValidationMetricsCollector::new();
567 collector.validation_checks_total.store(100, Ordering::Relaxed);
568 collector.validation_errors_total.store(10, Ordering::Relaxed);
569 collector.async_validation_total.store(50, Ordering::Relaxed);
570
571 let snapshot = collector.snapshot_prometheus();
572
573 assert_eq!(snapshot.validation_checks_total, 100);
574 assert_eq!(snapshot.validation_errors_total, 10);
575 assert_eq!(snapshot.async_validation_total, 50);
576 }
577}