1use 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#[derive(Debug, Clone)]
29pub struct StructuredCancellationConfig {
30 pub tracer_config: CancellationTracerConfig,
32 pub visualizer_config: VisualizerConfig,
34 pub enable_real_time_alerts: bool,
36 pub performance_alert_threshold: u64,
38 pub max_memory_usage_mb: usize,
40 pub trace_retention_duration: Duration,
42 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, max_memory_usage_mb: 100,
54 trace_retention_duration: Duration::from_secs(3600), enable_structured_logging: true,
56 }
57 }
58}
59
60impl StructuredCancellationConfig {
61 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct CancellationAlert {
83 pub alert_type: AlertType,
85 pub severity: AlertSeverity,
87 pub message: String,
89 pub entity_id: Option<String>,
91 pub metric_value: f64,
93 pub threshold: f64,
95 pub triggered_at: std::time::SystemTime,
97 pub remediation_suggestions: Vec<String>,
99}
100
101#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
103pub enum AlertType {
104 SlowPropagation,
106 StuckCancellation,
108 HighLatency,
110 BottleneckDetected,
112 ResourceLeakRisk,
114 AnomalySpike,
116 PerformanceRegression,
118}
119
120#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
122pub enum AlertSeverity {
123 Info,
125 Warning,
127 Error,
129 Critical,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct RealTimeStats {
136 pub active_traces: usize,
138 pub traces_completed_last_minute: usize,
140 pub current_avg_latency: Duration,
142 pub alerts_last_hour: usize,
144 pub memory_usage_percentage: f64,
146 pub top_entities: Vec<String>,
148}
149
150pub 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 #[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 #[must_use]
189 pub fn default() -> Self {
190 Self::new(StructuredCancellationConfig::default())
191 }
192
193 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 self.update_active_traces_count();
207
208 if self.config.enable_structured_logging {
210 Self::log_trace_event("trace_started", trace_id, None);
211 }
212
213 trace_id
214 }
215
216 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 if self.config.enable_real_time_alerts {
241 self.check_real_time_alerts(&entity_id);
242 }
243
244 if self.config.enable_structured_logging {
246 Self::log_trace_event("step_recorded", trace_id, Some(&entity_id));
247 }
248 }
249
250 pub fn complete_trace(&self, trace_id: crate::observability::CancellationTraceId) {
252 self.tracer.complete_trace(trace_id);
253
254 self.update_completed_traces_count();
256
257 if self.config.enable_structured_logging {
259 Self::log_trace_event("trace_completed", trace_id, None);
260 }
261
262 self.maybe_cleanup_old_traces();
264 }
265
266 pub fn get_dashboard(&self) -> CancellationDashboard {
268 let traces = self.tracer.completed_traces();
269 self.visualizer.generate_dashboard(&traces)
270 }
271
272 pub fn analyze_performance(&self) -> PerformanceAnalysis {
274 let traces = self.tracer.completed_traces();
275 self.analyzer.analyze_performance(&traces)
276 }
277
278 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 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 pub fn export_dot_graph(&self) -> String {
304 let traces = self.tracer.completed_traces();
305 self.visualizer.generate_dot_graph(&traces)
306 }
307
308 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 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 pub fn get_real_time_stats(&self) -> RealTimeStats {
323 let stats = self.stats.lock();
324 stats.clone()
325 }
326
327 pub fn get_tracer_stats(
329 &self,
330 ) -> crate::observability::cancellation_tracer::CancellationTracerStatsSnapshot {
331 self.tracer.stats()
332 }
333
334 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 let memory_mb = (stats.traces_collected * 10 + stats.traces_collected * 2) / 1024; 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 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 let traces = self.tracer.completed_traces();
356 if !traces.is_empty() {
357 let recent_traces = traces
358 .iter()
359 .rev()
360 .take(10) .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 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 fn check_real_time_alerts(&self, entity_id: &str) {
383 let traces = self.tracer.completed_traces();
384
385 let entity_traces: Vec<&CancellationTrace> = traces
387 .iter()
388 .filter(|t| t.root_entity == entity_id)
389 .rev()
390 .take(5) .collect();
392
393 if entity_traces.is_empty() {
394 return;
395 }
396
397 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 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 fn trigger_alert(&self, alert: &CancellationAlert) {
457 {
458 let mut alerts = self.alerts.lock();
459 alerts.push(alert.clone());
460
461 while alerts.len() > 1000 {
463 alerts.remove(0);
464 }
465 drop(alerts);
466 }
467
468 {
470 let mut stats = self.stats.lock();
471 stats.alerts_last_hour += 1;
472 }
473
474 if self.config.enable_structured_logging {
476 Self::log_alert(alert);
477 }
478 }
479
480 #[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 #[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 fn maybe_cleanup_old_traces(&self) {
512 let now = super::replayable_system_time();
513 let mut last_cleanup = self.last_cleanup.lock();
514
515 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 let stats = self.get_real_time_stats();
525 if stats.memory_usage_percentage > 80.0 {
526 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
545pub struct LabRuntimeIntegration {
547 analyzer: StructuredCancellationAnalyzer,
548 deterministic_time: Arc<Mutex<Time>>,
549}
550
551impl LabRuntimeIntegration {
552 #[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 pub fn advance_time(&self, delta: Duration) {
563 let mut time = self.deterministic_time.lock();
564 *time = *time + delta;
565 }
566
567 pub fn analyzer(&self) -> &StructuredCancellationAnalyzer {
569 &self.analyzer
570 }
571
572 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}