organizational_intelligence_plugin/
observability.rs

1//! Observability: Logging, Tracing, and Metrics
2//!
3//! PROD-003: Production-ready observability using tracing
4//! Provides structured logging, span tracing, and performance metrics
5
6use tracing::{debug, error, info, instrument, trace, warn};
7use tracing_subscriber::{fmt, prelude::*, EnvFilter};
8
9/// Initialize the tracing subscriber
10///
11/// # Arguments
12/// * `verbose` - Enable debug-level logging
13/// * `_json` - Reserved for JSON output (requires tracing-subscriber json feature)
14pub fn init_tracing(verbose: bool, _json: bool) {
15    let filter = if verbose {
16        EnvFilter::new("debug")
17    } else {
18        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))
19    };
20
21    // Note: JSON output requires tracing-subscriber with "json" feature
22    // For now, always use compact format
23    tracing_subscriber::registry()
24        .with(filter)
25        .with(fmt::layer().compact())
26        .init();
27}
28
29/// Initialize tracing with custom filter
30pub fn init_with_filter(filter: &str) {
31    let filter = EnvFilter::new(filter);
32    tracing_subscriber::registry()
33        .with(filter)
34        .with(fmt::layer().compact())
35        .init();
36}
37
38/// Span for tracking analysis operations
39#[derive(Debug)]
40pub struct AnalysisSpan {
41    pub repo: String,
42    pub operation: String,
43}
44
45impl AnalysisSpan {
46    pub fn new(repo: impl Into<String>, operation: impl Into<String>) -> Self {
47        Self {
48            repo: repo.into(),
49            operation: operation.into(),
50        }
51    }
52}
53
54/// Log levels for different operations
55pub struct LogOps;
56
57impl LogOps {
58    /// Log start of analysis
59    #[instrument(skip_all, fields(repo = %repo, commits = commits))]
60    pub fn analysis_start(repo: &str, commits: usize) {
61        info!("Starting repository analysis");
62    }
63
64    /// Log analysis completion
65    #[instrument(skip_all, fields(repo = %repo, patterns = patterns, duration_ms = duration_ms))]
66    pub fn analysis_complete(repo: &str, patterns: usize, duration_ms: u64) {
67        info!("Analysis complete");
68    }
69
70    /// Log feature extraction
71    #[instrument(skip_all, fields(count = count))]
72    pub fn features_extracted(count: usize) {
73        debug!("Features extracted");
74    }
75
76    /// Log correlation computation
77    #[instrument(skip_all, fields(size = size, backend = %backend))]
78    pub fn correlation_start(size: usize, backend: &str) {
79        debug!("Computing correlation");
80    }
81
82    /// Log correlation result
83    #[instrument(skip_all, fields(result = %format!("{:.4}", result), duration_us = duration_us))]
84    pub fn correlation_complete(result: f32, duration_us: u64) {
85        trace!("Correlation computed");
86    }
87
88    /// Log ML training
89    #[instrument(skip_all, fields(model = %model, samples = samples))]
90    pub fn training_start(model: &str, samples: usize) {
91        info!("Training model");
92    }
93
94    /// Log ML training complete
95    #[instrument(skip_all, fields(model = %model, accuracy = %format!("{:.2}%", accuracy * 100.0)))]
96    pub fn training_complete(model: &str, accuracy: f32) {
97        info!("Model training complete");
98    }
99
100    /// Log prediction
101    #[instrument(skip_all, fields(model = %model))]
102    pub fn prediction(model: &str, category: u8) {
103        debug!(category = category, "Prediction made");
104    }
105
106    /// Log clustering
107    #[instrument(skip_all, fields(k = k, samples = samples))]
108    pub fn clustering_start(k: usize, samples: usize) {
109        debug!("Starting clustering");
110    }
111
112    /// Log clustering complete
113    #[instrument(skip_all, fields(k = k, inertia = %format!("{:.2}", inertia)))]
114    pub fn clustering_complete(k: usize, inertia: f32) {
115        debug!("Clustering complete");
116    }
117
118    /// Log storage operation
119    #[instrument(skip_all, fields(operation = %operation, count = count))]
120    pub fn storage_op(operation: &str, count: usize) {
121        trace!("Storage operation");
122    }
123
124    /// Log GPU operation
125    #[instrument(skip_all, fields(operation = %operation, backend = %backend))]
126    pub fn gpu_op(operation: &str, backend: &str) {
127        debug!("GPU operation");
128    }
129
130    /// Log error with context
131    pub fn error_with_context(error: &impl std::fmt::Display, context: &str) {
132        error!(context = context, error = %error, "Operation failed");
133    }
134
135    /// Log warning
136    pub fn warning(message: &str, context: &str) {
137        warn!(context = context, "{}", message);
138    }
139
140    /// Log performance metric
141    #[instrument(skip_all, fields(operation = %operation, duration_ms = duration_ms, throughput = throughput))]
142    pub fn performance(operation: &str, duration_ms: u64, throughput: Option<f64>) {
143        if let Some(tp) = throughput {
144            info!(throughput_per_sec = %format!("{:.2}", tp), "Performance metric");
145        } else {
146            info!("Performance metric");
147        }
148    }
149}
150
151/// Timer for measuring operation duration
152pub struct Timer {
153    start: std::time::Instant,
154    operation: String,
155}
156
157impl Timer {
158    pub fn new(operation: impl Into<String>) -> Self {
159        Self {
160            start: std::time::Instant::now(),
161            operation: operation.into(),
162        }
163    }
164
165    pub fn elapsed_ms(&self) -> u64 {
166        self.start.elapsed().as_millis() as u64
167    }
168
169    pub fn elapsed_us(&self) -> u64 {
170        self.start.elapsed().as_micros() as u64
171    }
172
173    pub fn log_completion(&self) {
174        let duration = self.elapsed_ms();
175        debug!(
176            operation = %self.operation,
177            duration_ms = duration,
178            "Operation completed"
179        );
180    }
181}
182
183impl Drop for Timer {
184    fn drop(&mut self) {
185        // Optionally log on drop
186    }
187}
188
189/// Metrics collector for aggregating statistics
190#[derive(Debug, Default)]
191pub struct Metrics {
192    pub analyses_count: u64,
193    pub features_extracted: u64,
194    pub correlations_computed: u64,
195    pub predictions_made: u64,
196    pub errors_count: u64,
197    pub total_duration_ms: u64,
198}
199
200impl Metrics {
201    pub fn new() -> Self {
202        Self::default()
203    }
204
205    pub fn record_analysis(&mut self, duration_ms: u64) {
206        self.analyses_count += 1;
207        self.total_duration_ms += duration_ms;
208    }
209
210    pub fn record_features(&mut self, count: u64) {
211        self.features_extracted += count;
212    }
213
214    pub fn record_correlation(&mut self) {
215        self.correlations_computed += 1;
216    }
217
218    pub fn record_prediction(&mut self) {
219        self.predictions_made += 1;
220    }
221
222    pub fn record_error(&mut self) {
223        self.errors_count += 1;
224    }
225
226    pub fn summary(&self) -> String {
227        format!(
228            "Metrics: analyses={}, features={}, correlations={}, predictions={}, errors={}, total_time={}ms",
229            self.analyses_count,
230            self.features_extracted,
231            self.correlations_computed,
232            self.predictions_made,
233            self.errors_count,
234            self.total_duration_ms
235        )
236    }
237
238    pub fn log_summary(&self) {
239        info!(
240            analyses = self.analyses_count,
241            features = self.features_extracted,
242            correlations = self.correlations_computed,
243            predictions = self.predictions_made,
244            errors = self.errors_count,
245            total_duration_ms = self.total_duration_ms,
246            "Session metrics"
247        );
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn test_timer_creation() {
257        let timer = Timer::new("test_operation");
258        assert_eq!(timer.operation, "test_operation");
259    }
260
261    #[test]
262    fn test_timer_elapsed() {
263        let timer = Timer::new("test");
264        std::thread::sleep(std::time::Duration::from_millis(10));
265        assert!(timer.elapsed_ms() >= 10);
266    }
267
268    #[test]
269    fn test_metrics_default() {
270        let metrics = Metrics::new();
271        assert_eq!(metrics.analyses_count, 0);
272        assert_eq!(metrics.errors_count, 0);
273    }
274
275    #[test]
276    fn test_metrics_recording() {
277        let mut metrics = Metrics::new();
278        metrics.record_analysis(100);
279        metrics.record_features(50);
280        metrics.record_correlation();
281        metrics.record_prediction();
282        metrics.record_error();
283
284        assert_eq!(metrics.analyses_count, 1);
285        assert_eq!(metrics.features_extracted, 50);
286        assert_eq!(metrics.correlations_computed, 1);
287        assert_eq!(metrics.predictions_made, 1);
288        assert_eq!(metrics.errors_count, 1);
289        assert_eq!(metrics.total_duration_ms, 100);
290    }
291
292    #[test]
293    fn test_metrics_summary() {
294        let mut metrics = Metrics::new();
295        metrics.record_analysis(50);
296        metrics.record_features(100);
297
298        let summary = metrics.summary();
299        assert!(summary.contains("analyses=1"));
300        assert!(summary.contains("features=100"));
301    }
302
303    #[test]
304    fn test_analysis_span() {
305        let span = AnalysisSpan::new("owner/repo", "analyze");
306        assert_eq!(span.repo, "owner/repo");
307        assert_eq!(span.operation, "analyze");
308    }
309
310    #[test]
311    fn test_timer_elapsed_us() {
312        let timer = Timer::new("test");
313        std::thread::sleep(std::time::Duration::from_micros(100));
314        assert!(timer.elapsed_us() >= 100);
315    }
316
317    #[test]
318    fn test_timer_log_completion() {
319        let timer = Timer::new("test_op");
320        timer.log_completion(); // Should not panic
321    }
322
323    #[test]
324    fn test_timer_drop() {
325        let _timer = Timer::new("test");
326        // Drop should not panic
327    }
328
329    #[test]
330    fn test_metrics_log_summary() {
331        let mut metrics = Metrics::new();
332        metrics.record_analysis(100);
333        metrics.log_summary(); // Should not panic
334    }
335
336    #[test]
337    fn test_logops_analysis_lifecycle() {
338        LogOps::analysis_start("test/repo", 100);
339        LogOps::analysis_complete("test/repo", 42, 1000);
340    }
341
342    #[test]
343    fn test_logops_features() {
344        LogOps::features_extracted(50);
345    }
346
347    #[test]
348    fn test_logops_correlation() {
349        LogOps::correlation_start(100, "simd");
350        LogOps::correlation_complete(0.95, 5000);
351    }
352
353    #[test]
354    fn test_logops_ml_training() {
355        LogOps::training_start("k-means", 100);
356        LogOps::training_complete("k-means", 0.85);
357    }
358
359    #[test]
360    fn test_logops_prediction() {
361        LogOps::prediction("DefectPredictor", 3);
362    }
363
364    #[test]
365    fn test_logops_clustering() {
366        LogOps::clustering_start(5, 100);
367        LogOps::clustering_complete(5, 123.45);
368    }
369
370    #[test]
371    fn test_logops_storage() {
372        LogOps::storage_op("save", 100);
373    }
374
375    #[test]
376    fn test_logops_gpu() {
377        LogOps::gpu_op("correlate", "gpu");
378    }
379
380    #[test]
381    fn test_logops_error_with_context() {
382        let error = std::io::Error::other("test error");
383        LogOps::error_with_context(&error, "during analysis");
384    }
385
386    #[test]
387    fn test_logops_warning() {
388        LogOps::warning("low memory", "performance");
389    }
390
391    #[test]
392    fn test_logops_performance_with_throughput() {
393        LogOps::performance("analysis", 1000, Some(100.5));
394    }
395
396    #[test]
397    fn test_logops_performance_without_throughput() {
398        LogOps::performance("analysis", 1000, None);
399    }
400
401    #[test]
402    fn test_multiple_metrics_recordings() {
403        let mut metrics = Metrics::new();
404        for i in 0..10 {
405            metrics.record_analysis(i * 10);
406            metrics.record_features(i * 5);
407        }
408        assert_eq!(metrics.analyses_count, 10);
409        assert_eq!(metrics.features_extracted, 225); // 0+5+10+...+45
410        assert_eq!(metrics.total_duration_ms, 450); // 0+10+20+...+90
411    }
412
413    #[test]
414    fn test_metrics_default_trait() {
415        let metrics = Metrics::default();
416        assert_eq!(metrics.analyses_count, 0);
417        assert_eq!(metrics.features_extracted, 0);
418        assert_eq!(metrics.correlations_computed, 0);
419        assert_eq!(metrics.predictions_made, 0);
420        assert_eq!(metrics.errors_count, 0);
421        assert_eq!(metrics.total_duration_ms, 0);
422    }
423
424    #[test]
425    fn test_timer_with_string_operation() {
426        let timer = Timer::new(String::from("string_op"));
427        assert_eq!(timer.operation, "string_op");
428    }
429
430    #[test]
431    fn test_timer_elapsed_immediately() {
432        let timer = Timer::new("instant");
433        let elapsed = timer.elapsed_ms();
434        // Just verify it returns without panic (elapsed is always >= 0 for u64)
435        assert!(elapsed < 1000); // Should be < 1 second
436    }
437
438    #[test]
439    fn test_analysis_span_with_string() {
440        let span = AnalysisSpan::new(String::from("owner/repo"), String::from("op"));
441        assert_eq!(span.repo, "owner/repo");
442        assert_eq!(span.operation, "op");
443    }
444
445    #[test]
446    fn test_metrics_multiple_errors() {
447        let mut metrics = Metrics::new();
448        for _ in 0..5 {
449            metrics.record_error();
450        }
451        assert_eq!(metrics.errors_count, 5);
452    }
453
454    #[test]
455    fn test_metrics_multiple_predictions() {
456        let mut metrics = Metrics::new();
457        for _ in 0..10 {
458            metrics.record_prediction();
459        }
460        assert_eq!(metrics.predictions_made, 10);
461    }
462
463    #[test]
464    fn test_metrics_multiple_correlations() {
465        let mut metrics = Metrics::new();
466        for _ in 0..3 {
467            metrics.record_correlation();
468        }
469        assert_eq!(metrics.correlations_computed, 3);
470    }
471
472    #[test]
473    fn test_metrics_zero_duration() {
474        let mut metrics = Metrics::new();
475        metrics.record_analysis(0);
476        assert_eq!(metrics.analyses_count, 1);
477        assert_eq!(metrics.total_duration_ms, 0);
478    }
479
480    #[test]
481    fn test_metrics_large_duration() {
482        let mut metrics = Metrics::new();
483        metrics.record_analysis(1_000_000);
484        assert_eq!(metrics.total_duration_ms, 1_000_000);
485    }
486
487    #[test]
488    fn test_metrics_summary_contains_all_fields() {
489        let mut metrics = Metrics::new();
490        metrics.record_analysis(100);
491        metrics.record_features(50);
492        metrics.record_correlation();
493        metrics.record_prediction();
494        metrics.record_error();
495
496        let summary = metrics.summary();
497        assert!(summary.contains("analyses="));
498        assert!(summary.contains("features="));
499        assert!(summary.contains("correlations="));
500        assert!(summary.contains("predictions="));
501        assert!(summary.contains("errors="));
502        assert!(summary.contains("total_time="));
503    }
504
505    #[test]
506    fn test_timer_microsecond_precision() {
507        let timer = Timer::new("precision_test");
508        std::thread::sleep(std::time::Duration::from_micros(500));
509        let us = timer.elapsed_us();
510        let ms = timer.elapsed_ms();
511
512        // Microseconds should be at least 500
513        assert!(us >= 500);
514        // Microseconds should be at least milliseconds * 1000 (rough conversion)
515        assert!(us >= ms * 1000);
516    }
517
518    #[test]
519    fn test_analysis_span_debug() {
520        let span = AnalysisSpan::new("test/repo", "operation");
521        let debug_str = format!("{:?}", span);
522        assert!(debug_str.contains("AnalysisSpan"));
523    }
524
525    #[test]
526    fn test_metrics_debug() {
527        let metrics = Metrics::new();
528        let debug_str = format!("{:?}", metrics);
529        assert!(debug_str.contains("Metrics"));
530    }
531
532    #[test]
533    fn test_logops_all_operations_no_panic() {
534        // Ensure all LogOps methods work without panicking
535        LogOps::analysis_start("repo", 100);
536        LogOps::analysis_complete("repo", 10, 1000);
537        LogOps::features_extracted(50);
538        LogOps::correlation_start(100, "cpu");
539        LogOps::correlation_complete(0.5, 1000);
540        LogOps::training_start("model", 100);
541        LogOps::training_complete("model", 0.9);
542        LogOps::prediction("model", 1);
543        LogOps::clustering_start(3, 100);
544        LogOps::clustering_complete(3, 50.0);
545        LogOps::storage_op("load", 50);
546        LogOps::gpu_op("compute", "gpu");
547
548        let err = std::io::Error::other("test");
549        LogOps::error_with_context(&err, "test context");
550        LogOps::warning("test warning", "test");
551        LogOps::performance("op", 100, Some(10.0));
552        LogOps::performance("op", 100, None);
553    }
554
555    #[test]
556    fn test_metrics_accumulation() {
557        let mut metrics = Metrics::new();
558
559        // Record multiple analyses with different durations
560        metrics.record_analysis(100);
561        metrics.record_analysis(200);
562        metrics.record_analysis(300);
563
564        assert_eq!(metrics.analyses_count, 3);
565        assert_eq!(metrics.total_duration_ms, 600);
566
567        // Record multiple features
568        metrics.record_features(10);
569        metrics.record_features(20);
570
571        assert_eq!(metrics.features_extracted, 30);
572    }
573}