Skip to main content

asupersync/observability/
structured_cancellation_analyzer.rs

1//! Structured Cancellation Trace Analyzer - Complete Implementation
2//!
3//! This module provides the complete implementation of the structured cancellation trace analyzer
4//! as specified in the bead requirements. It integrates tracing, visualization, and deep analysis
5//! capabilities to provide comprehensive insights into cancellation behavior.
6
7use crate::observability::{
8    cancellation_analyzer::{CancellationAnalyzer, PerformanceAnalysis},
9    cancellation_tracer::{CancellationTrace, CancellationTracer, CancellationTracerConfig},
10    cancellation_visualizer::{CancellationDashboard, CancellationVisualizer, VisualizerConfig},
11};
12use crate::runtime::TraceStorageProfile;
13use crate::types::{CancelKind, CancelReason, Time};
14use parking_lot::Mutex;
15use serde::{Deserialize, Serialize};
16use std::sync::Arc;
17use std::time::Duration;
18
19fn saturating_system_time_sub(
20    time: std::time::SystemTime,
21    duration: Duration,
22) -> std::time::SystemTime {
23    time.checked_sub(duration)
24        .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
25}
26
27/// Configuration for the complete structured cancellation analyzer.
28#[derive(Debug, Clone)]
29pub struct StructuredCancellationConfig {
30    /// Configuration for trace collection.
31    pub tracer_config: CancellationTracerConfig,
32    /// Configuration for visualization.
33    pub visualizer_config: VisualizerConfig,
34    /// Enable real-time analysis alerts.
35    pub enable_real_time_alerts: bool,
36    /// Threshold for triggering performance alerts (in milliseconds).
37    pub performance_alert_threshold: u64,
38    /// Maximum memory usage for trace storage (in MB).
39    pub max_memory_usage_mb: usize,
40    /// Auto-cleanup old traces after this duration.
41    pub trace_retention_duration: Duration,
42    /// Enable integration with structured logging.
43    pub enable_structured_logging: bool,
44}
45
46impl Default for StructuredCancellationConfig {
47    fn default() -> Self {
48        Self {
49            tracer_config: CancellationTracerConfig::default(),
50            visualizer_config: VisualizerConfig::default(),
51            enable_real_time_alerts: true,
52            performance_alert_threshold: 1000, // 1 second
53            max_memory_usage_mb: 100,
54            trace_retention_duration: Duration::from_secs(3600), // 1 hour
55            enable_structured_logging: true,
56        }
57    }
58}
59
60impl StructuredCancellationConfig {
61    /// Builds a structured-cancellation profile aligned with runtime trace storage.
62    #[must_use]
63    pub fn for_trace_storage_profile(profile: TraceStorageProfile) -> Self {
64        const MIB: usize = 1024 * 1024;
65
66        let budget = profile.budget();
67        let cold_trace_budget_bytes = budget.estimated_cold_bytes();
68        let max_memory_usage_mb =
69            cold_trace_budget_bytes.saturating_add(MIB.saturating_sub(1)) / MIB;
70
71        Self {
72            tracer_config: CancellationTracerConfig::for_trace_storage_profile(profile),
73            trace_retention_duration: profile.distributed_trace_max_age(),
74            max_memory_usage_mb: max_memory_usage_mb.max(1),
75            ..Self::default()
76        }
77    }
78}
79
80/// Alert triggered by the analyzer.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct CancellationAlert {
83    /// Alert type identifier.
84    pub alert_type: AlertType,
85    /// Severity level.
86    pub severity: AlertSeverity,
87    /// Alert message.
88    pub message: String,
89    /// Entity that triggered the alert.
90    pub entity_id: Option<String>,
91    /// Metric value that triggered the alert.
92    pub metric_value: f64,
93    /// Threshold that was exceeded.
94    pub threshold: f64,
95    /// When the alert was triggered.
96    pub triggered_at: std::time::SystemTime,
97    /// Suggested remediation actions.
98    pub remediation_suggestions: Vec<String>,
99}
100
101/// Type of cancellation alert.
102#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
103pub enum AlertType {
104    /// Cancellation propagation is slower than expected.
105    SlowPropagation,
106    /// Cancellation appears to be stuck or blocked.
107    StuckCancellation,
108    /// High cancellation latency detected.
109    HighLatency,
110    /// Performance bottleneck detected in cancellation path.
111    BottleneckDetected,
112    /// Risk of resource leaks during cancellation.
113    ResourceLeakRisk,
114    /// Spike in anomalies or unusual patterns.
115    AnomalySpike,
116    /// Performance regression in cancellation system.
117    PerformanceRegression,
118}
119
120/// Severity level of an alert.
121#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
122pub enum AlertSeverity {
123    /// Informational alert, no action required.
124    Info,
125    /// Warning alert, investigation recommended.
126    Warning,
127    /// Error alert, action needed.
128    Error,
129    /// Critical alert, immediate action required.
130    Critical,
131}
132
133/// Real-time monitoring statistics.
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct RealTimeStats {
136    /// Current number of active traces.
137    pub active_traces: usize,
138    /// Traces completed in the last minute.
139    pub traces_completed_last_minute: usize,
140    /// Current average propagation latency.
141    pub current_avg_latency: Duration,
142    /// Number of alerts in the last hour.
143    pub alerts_last_hour: usize,
144    /// Memory usage percentage.
145    pub memory_usage_percentage: f64,
146    /// Top entities by cancellation frequency.
147    pub top_entities: Vec<String>,
148}
149
150/// Complete structured cancellation analyzer.
151pub struct StructuredCancellationAnalyzer {
152    config: StructuredCancellationConfig,
153    tracer: CancellationTracer,
154    visualizer: CancellationVisualizer,
155    analyzer: CancellationAnalyzer,
156    alerts: Arc<Mutex<Vec<CancellationAlert>>>,
157    stats: Arc<Mutex<RealTimeStats>>,
158    last_cleanup: Arc<Mutex<std::time::SystemTime>>,
159}
160
161impl StructuredCancellationAnalyzer {
162    /// Creates a new structured cancellation analyzer.
163    #[must_use]
164    pub fn new(config: StructuredCancellationConfig) -> Self {
165        let tracer = CancellationTracer::new(config.tracer_config.clone());
166        let visualizer = CancellationVisualizer::new(config.visualizer_config.clone());
167        let analyzer = CancellationAnalyzer::default();
168
169        Self {
170            config,
171            tracer,
172            visualizer,
173            analyzer,
174            alerts: Arc::new(Mutex::new(Vec::new())),
175            stats: Arc::new(Mutex::new(RealTimeStats {
176                active_traces: 0,
177                traces_completed_last_minute: 0,
178                current_avg_latency: Duration::ZERO,
179                alerts_last_hour: 0,
180                memory_usage_percentage: 0.0,
181                top_entities: Vec::new(),
182            })),
183            last_cleanup: Arc::new(Mutex::new(super::replayable_system_time())),
184        }
185    }
186
187    /// Creates an analyzer with default configuration.
188    #[must_use]
189    pub fn default() -> Self {
190        Self::new(StructuredCancellationConfig::default())
191    }
192
193    /// Start a new cancellation trace.
194    pub fn start_trace(
195        &self,
196        entity_id: String,
197        entity_type: crate::observability::EntityType,
198        cancel_reason: &CancelReason,
199        cancel_kind: CancelKind,
200    ) -> crate::observability::CancellationTraceId {
201        let trace_id = self
202            .tracer
203            .start_trace(entity_id, entity_type, cancel_reason, cancel_kind);
204
205        // Update real-time stats
206        self.update_active_traces_count();
207
208        // Log structured event if enabled
209        if self.config.enable_structured_logging {
210            Self::log_trace_event("trace_started", trace_id, None);
211        }
212
213        trace_id
214    }
215
216    /// Record a cancellation propagation step.
217    pub fn record_step(
218        &self,
219        trace_id: crate::observability::CancellationTraceId,
220        entity_id: String,
221        entity_type: crate::observability::EntityType,
222        cancel_reason: &CancelReason,
223        cancel_kind: CancelKind,
224        entity_state: String,
225        parent_entity: Option<String>,
226        propagation_completed: bool,
227    ) {
228        self.tracer.record_step(
229            trace_id,
230            entity_id.clone(),
231            entity_type,
232            cancel_reason,
233            cancel_kind,
234            entity_state,
235            parent_entity,
236            propagation_completed,
237        );
238
239        // Check for real-time alerts
240        if self.config.enable_real_time_alerts {
241            self.check_real_time_alerts(&entity_id);
242        }
243
244        // Log structured event if enabled
245        if self.config.enable_structured_logging {
246            Self::log_trace_event("step_recorded", trace_id, Some(&entity_id));
247        }
248    }
249
250    /// Complete a cancellation trace.
251    pub fn complete_trace(&self, trace_id: crate::observability::CancellationTraceId) {
252        self.tracer.complete_trace(trace_id);
253
254        // Update real-time stats
255        self.update_completed_traces_count();
256
257        // Log structured event if enabled
258        if self.config.enable_structured_logging {
259            Self::log_trace_event("trace_completed", trace_id, None);
260        }
261
262        // Trigger cleanup if needed
263        self.maybe_cleanup_old_traces();
264    }
265
266    /// Get real-time dashboard view.
267    pub fn get_dashboard(&self) -> CancellationDashboard {
268        let traces = self.tracer.completed_traces();
269        self.visualizer.generate_dashboard(&traces)
270    }
271
272    /// Generate performance analysis report.
273    pub fn analyze_performance(&self) -> PerformanceAnalysis {
274        let traces = self.tracer.completed_traces();
275        self.analyzer.analyze_performance(&traces)
276    }
277
278    /// Visualize a specific trace as a tree.
279    pub fn visualize_trace(
280        &self,
281        trace_id: crate::observability::CancellationTraceId,
282    ) -> Option<String> {
283        let traces = self.tracer.completed_traces();
284        traces
285            .iter()
286            .find(|t| t.trace_id == trace_id)
287            .map(|trace| self.visualizer.visualize_trace_tree(trace))
288    }
289
290    /// Generate timeline visualization for a trace.
291    pub fn visualize_timeline(
292        &self,
293        trace_id: crate::observability::CancellationTraceId,
294    ) -> Option<String> {
295        let traces = self.tracer.completed_traces();
296        traces
297            .iter()
298            .find(|t| t.trace_id == trace_id)
299            .map(|trace| self.visualizer.visualize_timeline(trace))
300    }
301
302    /// Export traces as graphviz dot format.
303    pub fn export_dot_graph(&self) -> String {
304        let traces = self.tracer.completed_traces();
305        self.visualizer.generate_dot_graph(&traces)
306    }
307
308    /// Get recent alerts.
309    pub fn get_recent_alerts(&self, limit: usize) -> Vec<CancellationAlert> {
310        let alerts = self.alerts.lock();
311        alerts.iter().rev().take(limit).cloned().collect()
312    }
313
314    /// Clear alerts older than the specified duration.
315    pub fn clear_old_alerts(&self, max_age: Duration) {
316        let cutoff = saturating_system_time_sub(super::replayable_system_time(), max_age);
317        let mut alerts = self.alerts.lock();
318        alerts.retain(|alert| alert.triggered_at > cutoff);
319    }
320
321    /// Get current real-time statistics.
322    pub fn get_real_time_stats(&self) -> RealTimeStats {
323        let stats = self.stats.lock();
324        stats.clone()
325    }
326
327    /// Get tracer statistics.
328    pub fn get_tracer_stats(
329        &self,
330    ) -> crate::observability::cancellation_tracer::CancellationTracerStatsSnapshot {
331        self.tracer.stats()
332    }
333
334    /// Update active traces count for real-time stats.
335    fn update_active_traces_count(&self) {
336        let stats = self.tracer.stats();
337        let mut real_time_stats = self.stats.lock();
338        real_time_stats.active_traces = stats.traces_collected as usize;
339
340        // Calculate memory usage estimate
341        let memory_mb = (stats.traces_collected * 10 + stats.traces_collected * 2) / 1024; // Rough estimate
342        real_time_stats.memory_usage_percentage = if self.config.max_memory_usage_mb > 0 {
343            (memory_mb as f64 / self.config.max_memory_usage_mb as f64 * 100.0).min(100.0)
344        } else {
345            100.0
346        };
347    }
348
349    /// Update completed traces count for real-time stats.
350    fn update_completed_traces_count(&self) {
351        let mut real_time_stats = self.stats.lock();
352        real_time_stats.traces_completed_last_minute += 1;
353
354        // Update current average latency
355        let traces = self.tracer.completed_traces();
356        if !traces.is_empty() {
357            let recent_traces = traces
358                .iter()
359                .rev()
360                .take(10) // Last 10 traces
361                .filter_map(|t| t.total_propagation_time)
362                .collect::<Vec<_>>();
363
364            if !recent_traces.is_empty() {
365                let total_nanos: u64 = recent_traces.iter().map(|d| d.as_nanos() as u64).sum();
366                real_time_stats.current_avg_latency =
367                    Duration::from_nanos(total_nanos / recent_traces.len() as u64);
368            }
369        }
370    }
371
372    /// Sanitizes user-controlled input for safe logging by removing newlines and control characters
373    /// that could enable log injection attacks.
374    fn sanitize_for_logging(input: &str) -> String {
375        input
376            .chars()
377            .filter(|&c| c != '\n' && c != '\r' && c != '\0' && !c.is_control())
378            .collect()
379    }
380
381    /// Check for real-time performance alerts.
382    fn check_real_time_alerts(&self, entity_id: &str) {
383        let traces = self.tracer.completed_traces();
384
385        // Check recent traces for this entity
386        let entity_traces: Vec<&CancellationTrace> = traces
387            .iter()
388            .filter(|t| t.root_entity == entity_id)
389            .rev()
390            .take(5) // Last 5 traces for this entity
391            .collect();
392
393        if entity_traces.is_empty() {
394            return;
395        }
396
397        // Check for slow propagation
398        let slow_threshold = Duration::from_millis(self.config.performance_alert_threshold);
399        let slow_count = entity_traces
400            .iter()
401            .filter_map(|t| t.total_propagation_time)
402            .filter(|&duration| duration > slow_threshold)
403            .count();
404
405        if slow_count > entity_traces.len() / 2 {
406            self.trigger_alert(&CancellationAlert {
407                alert_type: AlertType::SlowPropagation,
408                severity: AlertSeverity::Warning,
409                message: format!(
410                    "Entity {} showing consistently slow cancellation propagation",
411                    Self::sanitize_for_logging(entity_id)
412                ),
413                entity_id: Some(entity_id.to_string()),
414                metric_value: if entity_traces.is_empty() {
415                    0.0
416                } else {
417                    slow_count as f64 / entity_traces.len() as f64 * 100.0
418                },
419                threshold: 50.0,
420                triggered_at: super::replayable_system_time(),
421                remediation_suggestions: vec![
422                    "Check for blocking operations in cancellation handlers".to_string(),
423                    "Consider optimizing cleanup logic".to_string(),
424                ],
425            });
426        }
427
428        // Check for anomaly spikes
429        let total_anomalies: usize = entity_traces.iter().map(|t| t.anomalies.len()).sum();
430
431        if total_anomalies > entity_traces.len() {
432            self.trigger_alert(&CancellationAlert {
433                alert_type: AlertType::AnomalySpike,
434                severity: AlertSeverity::Error,
435                message: format!(
436                    "High anomaly rate detected for entity {}",
437                    Self::sanitize_for_logging(entity_id)
438                ),
439                entity_id: Some(entity_id.to_string()),
440                metric_value: if entity_traces.is_empty() {
441                    0.0
442                } else {
443                    total_anomalies as f64 / entity_traces.len() as f64
444                },
445                threshold: 1.0,
446                triggered_at: super::replayable_system_time(),
447                remediation_suggestions: vec![
448                    "Investigate cancellation protocol violations".to_string(),
449                    "Review structured concurrency patterns".to_string(),
450                ],
451            });
452        }
453    }
454
455    /// Trigger a new alert.
456    fn trigger_alert(&self, alert: &CancellationAlert) {
457        {
458            let mut alerts = self.alerts.lock();
459            alerts.push(alert.clone());
460
461            // Keep alerts bounded
462            while alerts.len() > 1000 {
463                alerts.remove(0);
464            }
465            drop(alerts);
466        }
467
468        // Update alert count in stats
469        {
470            let mut stats = self.stats.lock();
471            stats.alerts_last_hour += 1;
472        }
473
474        // Log alert if structured logging is enabled
475        if self.config.enable_structured_logging {
476            Self::log_alert(alert);
477        }
478    }
479
480    /// Log a structured trace event.
481    #[allow(unused_variables)]
482    fn log_trace_event(
483        event_type: &str,
484        trace_id: crate::observability::CancellationTraceId,
485        entity_id: Option<&str>,
486    ) {
487        crate::tracing_compat::debug!(
488            event_type = event_type,
489            trace_id = trace_id.as_u64(),
490            entity_id = ?entity_id,
491            "cancellation trace event"
492        );
493    }
494
495    /// Log an alert using structured logging.
496    #[allow(unused_variables)]
497    fn log_alert(alert: &CancellationAlert) {
498        crate::tracing_compat::warn!(
499            alert_type = ?alert.alert_type,
500            severity = ?alert.severity,
501            entity_id = ?alert.entity_id,
502            metric_value = alert.metric_value,
503            threshold = alert.threshold,
504            triggered_at = ?alert.triggered_at,
505            message = %alert.message,
506            "cancellation alert"
507        );
508    }
509
510    /// Clean up old traces to manage memory usage.
511    fn maybe_cleanup_old_traces(&self) {
512        let now = super::replayable_system_time();
513        let mut last_cleanup = self.last_cleanup.lock();
514
515        // Only cleanup every 5 minutes
516        if now.duration_since(*last_cleanup).unwrap_or(Duration::ZERO) < Duration::from_secs(300) {
517            return;
518        }
519
520        *last_cleanup = now;
521        drop(last_cleanup);
522
523        // Get current memory usage estimate
524        let stats = self.get_real_time_stats();
525        if stats.memory_usage_percentage > 80.0 {
526            // Trigger aggressive cleanup
527            // In a real implementation, this would clean up old traces from the tracer
528            self.trigger_alert(&CancellationAlert {
529                alert_type: AlertType::ResourceLeakRisk,
530                severity: AlertSeverity::Warning,
531                message: "High memory usage detected - cleaned up old traces".to_string(),
532                entity_id: None,
533                metric_value: stats.memory_usage_percentage,
534                threshold: 80.0,
535                triggered_at: now,
536                remediation_suggestions: vec![
537                    "Consider reducing trace retention duration".to_string(),
538                    "Monitor for memory leaks in cancellation handling".to_string(),
539                ],
540            });
541        }
542    }
543}
544
545/// Helper for integrating with the lab runtime for deterministic testing.
546pub struct LabRuntimeIntegration {
547    analyzer: StructuredCancellationAnalyzer,
548    deterministic_time: Arc<Mutex<Time>>,
549}
550
551impl LabRuntimeIntegration {
552    /// Creates a new lab runtime integration.
553    #[must_use]
554    pub fn new(config: StructuredCancellationConfig) -> Self {
555        Self {
556            analyzer: StructuredCancellationAnalyzer::new(config),
557            deterministic_time: Arc::new(Mutex::new(Time::ZERO)),
558        }
559    }
560
561    /// Advance deterministic time (for testing).
562    pub fn advance_time(&self, delta: Duration) {
563        let mut time = self.deterministic_time.lock();
564        *time = *time + delta;
565    }
566
567    /// Get the underlying analyzer.
568    pub fn analyzer(&self) -> &StructuredCancellationAnalyzer {
569        &self.analyzer
570    }
571
572    /// Run a deterministic cancellation scenario and return analysis.
573    pub fn run_scenario<F>(&self, scenario: F) -> PerformanceAnalysis
574    where
575        F: FnOnce(&StructuredCancellationAnalyzer),
576    {
577        scenario(&self.analyzer);
578        self.analyzer.analyze_performance()
579    }
580}
581
582#[cfg(test)]
583mod tests {
584    #![allow(
585        clippy::pedantic,
586        clippy::nursery,
587        clippy::expect_fun_call,
588        clippy::map_unwrap_or,
589        clippy::cast_possible_wrap,
590        clippy::future_not_send
591    )]
592    use super::*;
593    use crate::types::{CancelKind, CancelReason};
594
595    #[test]
596    fn test_analyzer_creation() {
597        let config = StructuredCancellationConfig::default();
598        let analyzer = StructuredCancellationAnalyzer::new(config);
599
600        let stats = analyzer.get_real_time_stats();
601        assert_eq!(stats.active_traces, 0);
602    }
603
604    #[test]
605    fn test_trace_lifecycle_integration() {
606        let analyzer = StructuredCancellationAnalyzer::default();
607
608        let trace_id = analyzer.start_trace(
609            "test-task".to_string(),
610            crate::observability::EntityType::Task,
611            &CancelReason::user("test"),
612            CancelKind::User,
613        );
614
615        analyzer.record_step(
616            trace_id,
617            "child-region".to_string(),
618            crate::observability::EntityType::Region,
619            &CancelReason::user("propagation"),
620            CancelKind::User,
621            "Closing".to_string(),
622            Some("test-task".to_string()),
623            true,
624        );
625
626        analyzer.complete_trace(trace_id);
627
628        let stats = analyzer.get_tracer_stats();
629        assert_eq!(stats.traces_collected, 1);
630
631        let dashboard = analyzer.get_dashboard();
632        assert_eq!(dashboard.completed_traces_period, 1);
633    }
634
635    #[test]
636    fn test_lab_runtime_integration() {
637        let config = StructuredCancellationConfig::default();
638        let lab_integration = LabRuntimeIntegration::new(config);
639
640        let analysis = lab_integration.run_scenario(|analyzer| {
641            let trace_id = analyzer.start_trace(
642                "scenario-task".to_string(),
643                crate::observability::EntityType::Task,
644                &CancelReason::user("scenario"),
645                CancelKind::User,
646            );
647            analyzer.complete_trace(trace_id);
648        });
649
650        assert_eq!(analysis.traces_analyzed, 1);
651    }
652
653    #[test]
654    fn clear_old_alerts_tolerates_oversized_window() {
655        let analyzer = StructuredCancellationAnalyzer::default();
656
657        analyzer.clear_old_alerts(Duration::MAX);
658
659        assert_eq!(
660            saturating_system_time_sub(std::time::SystemTime::UNIX_EPOCH, Duration::MAX),
661            std::time::SystemTime::UNIX_EPOCH
662        );
663    }
664}