Skip to main content

fraiseql_core/observability/
validation_metrics.rs

1//! Validation metrics collection for observability.
2//!
3//! Tracks validation performance, errors, and patterns to enable
4//! introspection, learning, and performance analysis.
5
6use std::sync::{
7    Arc,
8    atomic::{AtomicU64, Ordering},
9};
10
11use serde::{Deserialize, Serialize};
12
13/// Validation metrics collected during request processing.
14#[derive(Debug, Clone)]
15pub struct ValidationMetricsCollector {
16    /// Total validation checks performed
17    pub validation_checks_total: Arc<AtomicU64>,
18
19    /// Total validation failures
20    pub validation_errors_total: Arc<AtomicU64>,
21
22    /// Async validator executions
23    pub async_validation_total: Arc<AtomicU64>,
24
25    /// Async validator failures
26    pub async_validation_errors: Arc<AtomicU64>,
27
28    /// Total async validator duration (microseconds)
29    pub async_validation_duration_us: Arc<AtomicU64>,
30
31    /// Total validation duration (microseconds)
32    pub validation_duration_us: Arc<AtomicU64>,
33
34    /// Per-field validation error counts
35    pub field_validation_errors: Arc<parking_lot::RwLock<std::collections::HashMap<String, u64>>>,
36
37    /// Per-rule-type validation error counts
38    pub rule_type_errors: Arc<parking_lot::RwLock<std::collections::HashMap<String, u64>>>,
39}
40
41/// A single validation metric entry for structured logging.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ValidationMetricEntry {
44    /// Field name being validated
45    pub field: String,
46
47    /// Validation rule type (required, pattern, range, etc.)
48    pub rule_type: String,
49
50    /// Whether validation passed
51    pub valid: bool,
52
53    /// Duration in microseconds
54    pub duration_us: u64,
55
56    /// Validator type (async, checksum, pattern, etc.)
57    pub validator_type: String,
58
59    /// Failure reason if invalid
60    pub failure_reason: Option<String>,
61}
62
63impl ValidationMetricsCollector {
64    /// Create a new validation metrics collector.
65    #[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    /// Record a validation check.
84    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            // Track per-field errors
92            {
93                let mut errors = self.field_validation_errors.write();
94                *errors.entry(field.to_string()).or_insert(0) += 1;
95            }
96
97            // Track per-rule-type errors
98            {
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    /// Record an async validation execution.
106    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            // Track per-field errors
120            {
121                let mut errors = self.field_validation_errors.write();
122                *errors.entry(field.to_string()).or_insert(0) += 1;
123            }
124
125            // Track per-rule-type errors
126            {
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    /// Get current per-field error counts.
134    pub fn get_field_errors(&self) -> std::collections::HashMap<String, u64> {
135        self.field_validation_errors.read().clone()
136    }
137
138    /// Get current per-rule-type error counts.
139    pub fn get_rule_type_errors(&self) -> std::collections::HashMap<String, u64> {
140        self.rule_type_errors.read().clone()
141    }
142
143    /// Clear all metrics.
144    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    /// Get a snapshot of the current metrics as Prometheus format.
156    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/// Prometheus metrics format for validation metrics.
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct PrometheusValidationMetrics {
170    /// Total validation checks performed
171    pub validation_checks_total: u64,
172
173    /// Total validation failures
174    pub validation_errors_total: u64,
175
176    /// Async validator executions
177    pub async_validation_total: u64,
178
179    /// Async validator failures
180    pub async_validation_errors: u64,
181
182    /// Average validation duration in microseconds
183    pub validation_avg_duration_us: f64,
184
185    /// Average async validation duration in microseconds
186    pub async_validation_avg_duration_us: f64,
187}
188
189impl PrometheusValidationMetrics {
190    /// Generate Prometheus text format output.
191    #[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        // Multiple fields
350        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        // Multiple rule types
355        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); // 100 + 150 + 50
376    }
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); // 500 + 600
386    }
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        // Each field should have 2 errors (10 total / 5 fields)
428        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); // 1000 us total
520
521        let metrics = PrometheusValidationMetrics::from(&collector);
522        assert!((metrics.validation_avg_duration_us - 100.0).abs() < 0.01); // 100 us average
523    }
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); // 2500 us total
530
531        let metrics = PrometheusValidationMetrics::from(&collector);
532        assert!((metrics.async_validation_avg_duration_us - 500.0).abs() < 0.01); // 500 us average
533    }
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}