1use chrono::{DateTime, Utc};
9use std::collections::HashMap;
10use std::time::{Duration, Instant};
11use sysinfo::{Pid, System};
12use tracing::{Level, debug, error, info, span, warn};
13use tracing_subscriber::{
14 EnvFilter,
15 fmt::{self, format::FmtSpan},
16 layer::SubscriberExt,
17 util::SubscriberInitExt,
18};
19use xchecker_redaction::SecretRedactor;
20
21#[allow(dead_code)] pub fn init_tracing(verbose: bool) -> Result<(), Box<dyn std::error::Error>> {
33 let env_filter = EnvFilter::try_from_default_env()
34 .or_else(|_| {
35 if verbose {
36 EnvFilter::try_new("xchecker=debug,info")
37 } else {
38 EnvFilter::try_new("xchecker=info,warn")
39 }
40 })
41 .unwrap_or_else(|_| EnvFilter::new("info"));
42
43 if verbose {
44 tracing_subscriber::registry()
46 .with(env_filter)
47 .with(
48 fmt::layer()
49 .with_target(true)
50 .with_thread_ids(false)
51 .with_thread_names(false)
52 .with_line_number(false)
53 .with_file(false)
54 .with_span_events(FmtSpan::CLOSE)
55 .compact(),
56 )
57 .try_init()?;
58 } else {
59 tracing_subscriber::registry()
61 .with(env_filter)
62 .with(
63 fmt::layer()
64 .with_target(false)
65 .with_thread_ids(false)
66 .with_thread_names(false)
67 .with_line_number(false)
68 .with_file(false)
69 .compact(),
70 )
71 .try_init()?;
72 }
73
74 Ok(())
75}
76
77#[allow(dead_code)] pub fn phase_span(spec_id: &str, phase: &str, runner_mode: &str) -> tracing::Span {
88 span!(
89 Level::INFO,
90 "phase_execution",
91 spec_id = %spec_id,
92 phase = %phase,
93 runner_mode = %runner_mode,
94 )
95}
96
97#[allow(dead_code)] pub fn log_phase_start(spec_id: &str, phase: &str, runner_mode: &str) {
100 info!(
101 spec_id = %spec_id,
102 phase = %phase,
103 runner_mode = %runner_mode,
104 "Starting phase execution"
105 );
106}
107
108#[allow(dead_code)] pub fn log_phase_complete(spec_id: &str, phase: &str, duration_ms: u128) {
111 info!(
112 spec_id = %spec_id,
113 phase = %phase,
114 duration_ms = %duration_ms,
115 "Phase execution completed"
116 );
117}
118
119#[allow(dead_code)] pub fn log_phase_error(spec_id: &str, phase: &str, error: &str, duration_ms: u128) {
124 let redactor = SecretRedactor::new().expect("Failed to create SecretRedactor");
126 let sanitized_error = match redactor.redact_content(error, "<log>") {
127 Ok(result) => result.content,
128 Err(_) => "[REDACTION_ERROR]".to_string(),
129 };
130
131 error!(
132 spec_id = %spec_id,
133 phase = %phase,
134 duration_ms = %duration_ms,
135 error = %sanitized_error,
136 "Phase execution failed"
137 );
138}
139
140pub struct Logger {
142 verbose: bool,
143 start_time: Instant,
144 operation_timings: HashMap<String, Duration>,
145 file_operations: Vec<FileOperation>,
146 operation_samples: HashMap<String, Vec<Duration>>,
148 system: System,
150 initial_memory: u64,
152 peak_memory: u64,
154 performance_targets: PerformanceTargets,
156 spec_id: Option<String>,
158 phase: Option<String>,
160 runner_mode: Option<String>,
162 redactor: SecretRedactor,
164}
165
166#[allow(dead_code)] #[derive(Debug, Clone)]
169pub struct FileOperation {
170 pub path: String,
171 pub operation: String,
172 pub size_bytes: Option<usize>,
173 pub hash: Option<String>,
174 pub timestamp: DateTime<Utc>,
175}
176
177#[allow(dead_code)] #[derive(Debug, Clone)]
180pub struct TimingInfo {
181 pub operation: String,
182 pub duration: Duration,
183 pub timestamp: DateTime<Utc>,
184}
185
186#[allow(dead_code)] #[derive(Debug, Clone)]
189pub struct PerformanceMetrics {
190 pub total_duration: Duration,
192 pub empty_run_duration: Option<Duration>,
194 pub packetization_duration: Option<Duration>,
196 pub files_processed: usize,
198 pub memory_stats: MemoryStats,
200 pub timing_percentiles: HashMap<String, TimingPercentiles>,
202}
203
204#[allow(dead_code)] #[derive(Debug, Clone, Default)]
207pub struct MemoryStats {
208 pub peak_memory_bytes: u64,
210 pub initial_memory_bytes: u64,
212 pub final_memory_bytes: u64,
214}
215
216#[allow(dead_code)] #[derive(Debug, Clone)]
219pub struct TimingPercentiles {
220 pub p50: Duration,
221 pub p95: Duration,
222 pub min: Duration,
223 pub max: Duration,
224 pub count: usize,
225}
226
227#[derive(Debug, Clone)]
229pub struct PerformanceTargets {
230 pub empty_run_target: Duration,
232 pub packetization_target_per_100_files: Duration,
234}
235
236impl Logger {
237 #[must_use]
239 pub fn new(verbose: bool) -> Self {
240 let mut system = System::new_all();
241 system.refresh_all();
242
243 let initial_memory = Self::get_current_memory_usage(&system);
244
245 Self {
246 verbose,
247 start_time: Instant::now(),
248 operation_timings: HashMap::new(),
249 file_operations: Vec::new(),
250 operation_samples: HashMap::new(),
251 system,
252 initial_memory,
253 peak_memory: initial_memory,
254 performance_targets: PerformanceTargets {
255 empty_run_target: Duration::from_secs(5), packetization_target_per_100_files: Duration::from_millis(200), },
258 spec_id: None,
259 phase: None,
260 runner_mode: None,
261 redactor: SecretRedactor::new().expect("Failed to create SecretRedactor"),
262 }
263 }
264
265 fn redact(&self, content: &str) -> String {
270 match self.redactor.redact_content(content, "<log>") {
271 Ok(result) => result.content,
272 Err(_) => {
273 "[REDACTION_ERROR]".to_string()
275 }
276 }
277 }
278
279 fn sanitize(&self, content: &str) -> String {
284 let sanitized = if content.contains('=')
287 && (content.contains("KEY")
288 || content.contains("TOKEN")
289 || content.contains("SECRET")
290 || content.contains("PASSWORD"))
291 {
292 "[ENV_VAR_REDACTED]".to_string()
293 } else {
294 content.to_string()
295 };
296
297 self.redact(&sanitized)
299 }
300
301 #[allow(dead_code)] pub fn set_spec_id(&mut self, spec_id: String) {
304 self.spec_id = Some(spec_id);
305 }
306
307 #[allow(dead_code)] pub fn set_phase(&mut self, phase: String) {
310 self.phase = Some(phase);
311 }
312
313 #[allow(dead_code)] pub fn set_runner_mode(&mut self, runner_mode: String) {
316 self.runner_mode = Some(runner_mode);
317 }
318
319 #[must_use]
321 #[allow(dead_code)] pub fn spec_id(&self) -> Option<&str> {
323 self.spec_id.as_deref()
324 }
325
326 #[must_use]
328 #[allow(dead_code)] pub fn phase(&self) -> Option<&str> {
330 self.phase.as_deref()
331 }
332
333 #[must_use]
335 #[allow(dead_code)] pub fn runner_mode(&self) -> Option<&str> {
337 self.runner_mode.as_deref()
338 }
339
340 #[must_use]
342 #[allow(dead_code)] pub const fn is_verbose(&self) -> bool {
344 self.verbose
345 }
346
347 pub fn verbose(&self, message: &str) {
351 if self.verbose {
352 let elapsed = self.start_time.elapsed();
353 let sanitized_message = self.sanitize(message);
354
355 if let (Some(spec_id), Some(phase), Some(runner_mode)) = (
357 self.spec_id.as_ref(),
358 self.phase.as_ref(),
359 self.runner_mode.as_ref(),
360 ) {
361 debug!(
362 spec_id = %spec_id,
363 phase = %phase,
364 runner_mode = %runner_mode,
365 duration_ms = %elapsed.as_millis(),
366 message = %sanitized_message,
367 "Verbose log"
368 );
369 } else {
370 debug!(
371 duration_ms = %elapsed.as_millis(),
372 message = %sanitized_message,
373 "Verbose log"
374 );
375 }
376
377 println!("[{:>8.3}s] {}", elapsed.as_secs_f64(), sanitized_message);
379 }
380 }
381
382 #[allow(dead_code)] pub fn verbose_fmt(&self, args: std::fmt::Arguments) {
387 if self.verbose {
388 let elapsed = self.start_time.elapsed();
389 let message = format!("{args}");
390 let sanitized_message = self.sanitize(&message);
391
392 if let (Some(spec_id), Some(phase), Some(runner_mode)) = (
394 self.spec_id.as_ref(),
395 self.phase.as_ref(),
396 self.runner_mode.as_ref(),
397 ) {
398 debug!(
399 spec_id = %spec_id,
400 phase = %phase,
401 runner_mode = %runner_mode,
402 duration_ms = %elapsed.as_millis(),
403 message = %sanitized_message,
404 "Verbose log"
405 );
406 } else {
407 debug!(
408 duration_ms = %elapsed.as_millis(),
409 message = %sanitized_message,
410 "Verbose log"
411 );
412 }
413
414 println!("[{:>8.3}s] {}", elapsed.as_secs_f64(), sanitized_message);
416 }
417 }
418
419 #[allow(dead_code)] pub fn info(&self, message: &str) {
424 let sanitized_message = self.sanitize(message);
425 if let (Some(spec_id), Some(phase), Some(runner_mode)) = (
426 self.spec_id.as_ref(),
427 self.phase.as_ref(),
428 self.runner_mode.as_ref(),
429 ) {
430 info!(
431 spec_id = %spec_id,
432 phase = %phase,
433 runner_mode = %runner_mode,
434 duration_ms = %self.start_time.elapsed().as_millis(),
435 message = %sanitized_message,
436 );
437 } else {
438 info!(message = %sanitized_message);
439 }
440 }
441
442 #[allow(dead_code)] pub fn warn(&self, message: &str) {
447 let sanitized_message = self.sanitize(message);
448 if let (Some(spec_id), Some(phase), Some(runner_mode)) = (
449 self.spec_id.as_ref(),
450 self.phase.as_ref(),
451 self.runner_mode.as_ref(),
452 ) {
453 warn!(
454 spec_id = %spec_id,
455 phase = %phase,
456 runner_mode = %runner_mode,
457 duration_ms = %self.start_time.elapsed().as_millis(),
458 message = %sanitized_message,
459 );
460 } else {
461 warn!(message = %sanitized_message);
462 }
463 }
464
465 #[allow(dead_code)] pub fn error(&self, message: &str) {
471 let sanitized_message = self.sanitize(message);
472 if let (Some(spec_id), Some(phase), Some(runner_mode)) = (
473 self.spec_id.as_ref(),
474 self.phase.as_ref(),
475 self.runner_mode.as_ref(),
476 ) {
477 error!(
478 spec_id = %spec_id,
479 phase = %phase,
480 runner_mode = %runner_mode,
481 duration_ms = %self.start_time.elapsed().as_millis(),
482 message = %sanitized_message,
483 );
484 } else {
485 error!(message = %sanitized_message);
486 }
487 }
488
489 pub fn start_timing(&mut self, operation: &str) {
491 if self.verbose {
492 self.verbose(&format!("Starting: {operation}"));
493 }
494 }
495
496 pub fn end_timing(&mut self, operation: &str) -> Duration {
498 let duration = self.start_time.elapsed();
499 self.operation_timings
500 .insert(operation.to_string(), duration);
501
502 self.operation_samples
504 .entry(operation.to_string())
505 .or_default()
506 .push(duration);
507
508 self.update_memory_usage();
510
511 if self.verbose {
512 self.verbose(&format!(
513 "Completed: {} ({:.3}s)",
514 operation,
515 duration.as_secs_f64()
516 ));
517 }
518
519 duration
520 }
521
522 #[allow(dead_code)] pub fn log_file_operation(
525 &mut self,
526 path: &str,
527 operation: &str,
528 size_bytes: Option<usize>,
529 hash: Option<String>,
530 ) {
531 let file_op = FileOperation {
532 path: path.to_string(),
533 operation: operation.to_string(),
534 size_bytes,
535 hash: hash.clone(),
536 timestamp: Utc::now(),
537 };
538
539 self.file_operations.push(file_op);
540
541 if self.verbose {
542 let size_info = size_bytes
543 .map(|s| format!(" ({s} bytes)"))
544 .unwrap_or_default();
545
546 let hash_info = hash
547 .map(|h| format!(" [{}]", &h[..8.min(h.len())]))
548 .unwrap_or_default();
549
550 self.verbose(&format!("File {operation}: {path}{size_info}{hash_info}"));
551 }
552 }
553
554 #[allow(dead_code)] pub fn log_selected_files(&self, file_count: usize, total_bytes: usize) {
557 if !self.verbose {
558 return;
559 }
560
561 self.verbose(&format!("Selected {file_count} files for packet"));
562 self.verbose(&format!("Total packet size: {total_bytes} bytes"));
563 }
564
565 #[allow(dead_code)] pub fn log_file_selected(&self, path: &str, bytes: usize, priority: &str) {
568 if self.verbose {
569 self.verbose(&format!(" - {path} ({bytes} bytes) [{priority}]"));
570 }
571 }
572
573 #[allow(dead_code)] pub fn log_packet_construction(
576 &self,
577 used_bytes: usize,
578 used_lines: usize,
579 max_bytes: usize,
580 max_lines: usize,
581 ) {
582 if self.verbose {
583 let bytes_pct = (used_bytes as f64 / max_bytes as f64) * 100.0;
584 let lines_pct = (used_lines as f64 / max_lines as f64) * 100.0;
585
586 self.verbose(&format!(
587 "Packet budget: {used_bytes} / {max_bytes} bytes ({bytes_pct:.1}%), {used_lines} / {max_lines} lines ({lines_pct:.1}%)"
588 ));
589 }
590 }
591
592 #[allow(dead_code)] pub fn log_claude_execution(&self, model: &str, runner: &str, args: &[String]) {
595 if self.verbose {
596 self.verbose("Executing Claude CLI:");
597 self.verbose(&format!(" Model: {model}"));
598 self.verbose(&format!(" Runner: {runner}"));
599 self.verbose(&format!(" Args: {}", args.join(" ")));
600 }
601 }
602
603 #[allow(dead_code)] pub fn log_canonicalization(
606 &self,
607 file_type: &str,
608 original_size: usize,
609 canonical_size: usize,
610 hash: &str,
611 ) {
612 if self.verbose {
613 let short_hash = &hash[..8.min(hash.len())];
614 self.verbose(&format!(
615 "Canonicalized {file_type}: {original_size} → {canonical_size} bytes [{short_hash}]"
616 ));
617 }
618 }
619
620 #[allow(dead_code)] pub fn log_redaction_results(
623 &self,
624 files_scanned: usize,
625 patterns_matched: usize,
626 files_with_secrets: usize,
627 ) {
628 if self.verbose {
629 self.verbose(&format!(
630 "Secret redaction: {files_scanned} files scanned, {patterns_matched} patterns matched in {files_with_secrets} files"
631 ));
632
633 if patterns_matched > 0 {
634 self.verbose(" ⚠ Secrets detected - run aborted for security");
635 }
636 }
637 }
638
639 #[must_use]
641 #[allow(dead_code)] pub fn get_timing_summary(&self) -> Vec<TimingInfo> {
643 self.operation_timings
644 .iter()
645 .map(|(op, duration)| TimingInfo {
646 operation: op.clone(),
647 duration: *duration,
648 timestamp: Utc::now(), })
650 .collect()
651 }
652
653 #[must_use]
655 #[allow(dead_code)] pub fn get_file_operations(&self) -> &[FileOperation] {
657 &self.file_operations
658 }
659
660 #[must_use]
662 pub fn total_elapsed(&self) -> Duration {
663 self.start_time.elapsed()
664 }
665
666 fn update_memory_usage(&mut self) {
668 self.system.refresh_memory();
669 let current_memory = Self::get_current_memory_usage(&self.system);
670 if current_memory > self.peak_memory {
671 self.peak_memory = current_memory;
672 }
673 }
674
675 fn get_current_memory_usage(system: &System) -> u64 {
677 let pid = Pid::from(std::process::id() as usize);
678 system.process(pid).map_or(0, sysinfo::Process::memory) * 1024 }
680
681 pub fn record_packetization_timing(&mut self, duration: Duration, file_count: usize) {
683 self.operation_samples
684 .entry("packetization".to_string())
685 .or_default()
686 .push(duration);
687
688 if self.verbose {
689 let per_file_ms = if file_count > 0 {
690 duration.as_millis() as f64 / file_count as f64
691 } else {
692 0.0
693 };
694
695 self.verbose(&format!(
696 "Packetization: {:.1}ms for {} files ({:.2}ms/file)",
697 duration.as_millis(),
698 file_count,
699 per_file_ms
700 ));
701
702 let target_for_files = if file_count > 0 {
704 let target_millis = (self
705 .performance_targets
706 .packetization_target_per_100_files
707 .as_millis() as u64
708 * file_count as u64)
709 / 100;
710 Duration::from_millis(target_millis)
711 } else {
712 Duration::from_millis(0)
713 };
714
715 if duration > target_for_files {
716 self.verbose(&format!(
717 " ⚠ Packetization slower than target ({:.1}ms vs {:.1}ms)",
718 duration.as_millis(),
719 target_for_files.as_millis()
720 ));
721 } else {
722 self.verbose(" ✓ Packetization within target");
723 }
724 }
725 }
726
727 pub fn record_empty_run_timing(&mut self, duration: Duration) {
729 self.operation_samples
730 .entry("empty_run".to_string())
731 .or_default()
732 .push(duration);
733
734 if self.verbose {
735 self.verbose(&format!("Empty run: {:.3}s", duration.as_secs_f64()));
736
737 if duration > self.performance_targets.empty_run_target {
739 self.verbose(&format!(
740 " ⚠ Empty run slower than target ({:.1}s vs {:.1}s)",
741 duration.as_secs_f64(),
742 self.performance_targets.empty_run_target.as_secs_f64()
743 ));
744 } else {
745 self.verbose(" ✓ Empty run within target");
746 }
747 }
748 }
749
750 #[must_use]
752 pub fn calculate_percentiles(&self, operation: &str) -> Option<TimingPercentiles> {
753 let samples = self.operation_samples.get(operation)?;
754 if samples.is_empty() {
755 return None;
756 }
757
758 let mut sorted_samples = samples.clone();
759 sorted_samples.sort();
760
761 let count = sorted_samples.len();
762 let p50_idx = count / 2;
763 let p95_idx = (count * 95) / 100;
764
765 Some(TimingPercentiles {
766 p50: sorted_samples[p50_idx],
767 p95: sorted_samples[p95_idx.min(count - 1)],
768 min: sorted_samples[0],
769 max: sorted_samples[count - 1],
770 count,
771 })
772 }
773
774 pub fn generate_performance_metrics(&mut self) -> PerformanceMetrics {
776 self.update_memory_usage();
777
778 let total_duration = self.total_elapsed();
779 let empty_run_duration = self
780 .operation_samples
781 .get("empty_run")
782 .and_then(|samples| samples.last())
783 .copied();
784 let packetization_duration = self
785 .operation_samples
786 .get("packetization")
787 .and_then(|samples| samples.last())
788 .copied();
789
790 let files_processed = self.file_operations.len();
791
792 let memory_stats = MemoryStats {
793 peak_memory_bytes: self.peak_memory,
794 initial_memory_bytes: self.initial_memory,
795 final_memory_bytes: Self::get_current_memory_usage(&self.system),
796 };
797
798 let mut timing_percentiles = HashMap::new();
799 for operation in self.operation_samples.keys() {
800 if let Some(percentiles) = self.calculate_percentiles(operation) {
801 timing_percentiles.insert(operation.clone(), percentiles);
802 }
803 }
804
805 PerformanceMetrics {
806 total_duration,
807 empty_run_duration,
808 packetization_duration,
809 files_processed,
810 memory_stats,
811 timing_percentiles,
812 }
813 }
814
815 #[must_use]
817 pub fn validate_performance_targets(&self) -> Vec<String> {
818 let mut violations = Vec::new();
819
820 if let Some(samples) = self.operation_samples.get("empty_run")
822 && let Some(&last_empty_run) = samples.last()
823 && last_empty_run > self.performance_targets.empty_run_target
824 {
825 violations.push(format!(
826 "Empty run exceeded target: {:.3}s > {:.3}s",
827 last_empty_run.as_secs_f64(),
828 self.performance_targets.empty_run_target.as_secs_f64()
829 ));
830 }
831
832 if let Some(samples) = self.operation_samples.get("packetization")
834 && let Some(&last_packetization) = samples.last()
835 {
836 let file_count = self.file_operations.len();
837 if file_count > 0 {
838 let target_millis = (self
839 .performance_targets
840 .packetization_target_per_100_files
841 .as_millis() as u64
842 * file_count as u64)
843 / 100;
844 let target_for_files = Duration::from_millis(target_millis);
845
846 if last_packetization > target_for_files {
847 violations.push(format!(
848 "Packetization exceeded target: {:.1}ms > {:.1}ms for {} files",
849 last_packetization.as_millis(),
850 target_for_files.as_millis(),
851 file_count
852 ));
853 }
854 }
855 }
856
857 violations
858 }
859
860 #[allow(dead_code)] pub fn log_cache_stats(&self, stats: &crate::cache::CacheStats) {
863 if !self.verbose {
864 return;
865 }
866
867 self.verbose(&format!(
868 "Insight cache: {} hits, {} misses, {} entries, hit_ratio={:.2}",
869 stats.hits,
870 stats.misses,
871 stats.hits + stats.misses,
872 stats.hit_ratio()
873 ));
874 }
875
876 pub fn print_performance_summary(&self) {
878 if !self.verbose {
879 return;
880 }
881
882 let total_time = self.total_elapsed();
883 self.verbose("=== Performance Summary ===");
884 self.verbose(&format!(
885 "Total execution time: {:.3}s",
886 total_time.as_secs_f64()
887 ));
888
889 let memory_mb = self.peak_memory as f64 / (1024.0 * 1024.0);
891 let initial_mb = self.initial_memory as f64 / (1024.0 * 1024.0);
892 self.verbose(&format!(
893 "Memory usage: {memory_mb:.1}MB peak ({initial_mb:.1}MB initial)"
894 ));
895
896 let violations = self.validate_performance_targets();
898 if violations.is_empty() {
899 self.verbose("✓ All performance targets met");
900 } else {
901 self.verbose("⚠ Performance target violations:");
902 for violation in violations {
903 self.verbose(&format!(" - {violation}"));
904 }
905 }
906
907 if !self.operation_timings.is_empty() {
908 self.verbose("Operation timings:");
909 let mut timings: Vec<_> = self.operation_timings.iter().collect();
910 timings.sort_by(|a, b| b.1.cmp(a.1)); for (operation, duration) in timings {
913 let percentage = (duration.as_secs_f64() / total_time.as_secs_f64()) * 100.0;
914
915 if let Some(percentiles) = self.calculate_percentiles(operation) {
917 self.verbose(&format!(
918 " {}: {:.3}s ({:.1}%) [P50: {:.3}s, P95: {:.3}s, n={}]",
919 operation,
920 duration.as_secs_f64(),
921 percentage,
922 percentiles.p50.as_secs_f64(),
923 percentiles.p95.as_secs_f64(),
924 percentiles.count
925 ));
926 } else {
927 self.verbose(&format!(
928 " {}: {:.3}s ({:.1}%)",
929 operation,
930 duration.as_secs_f64(),
931 percentage
932 ));
933 }
934 }
935 }
936
937 if !self.file_operations.is_empty() {
938 let total_files = self.file_operations.len();
939 let total_bytes: usize = self
940 .file_operations
941 .iter()
942 .filter_map(|op| op.size_bytes)
943 .sum();
944
945 self.verbose(&format!(
946 "File operations: {total_files} total, {total_bytes} bytes processed"
947 ));
948 }
949 }
950}
951
952pub fn log_doctor_report(report: &crate::types::DoctorOutput) {
954 use crate::types::CheckStatus;
955 use crossterm::style::{Attribute, Color, Stylize};
956
957 println!(
958 "{}",
959 "🩺 xchecker Environment Health Check"
960 .with(Color::Cyan)
961 .attribute(Attribute::Bold)
962 );
963 println!(
964 "{}",
965 "─────────────────────────────────────"
966 .with(Color::Cyan)
967 .attribute(Attribute::Bold)
968 );
969 println!();
970
971 for check in &report.checks {
972 let (status_symbol, color) = match check.status {
973 CheckStatus::Pass => ("✓", Color::Green),
974 CheckStatus::Warn => ("⚠", Color::Yellow),
975 CheckStatus::Fail => ("✗", Color::Red),
976 };
977
978 let formatted_name = to_title_case(&check.name);
980
981 match check.status {
986 CheckStatus::Pass => {
987 println!(
988 "{} {}",
989 status_symbol.with(color).attribute(Attribute::Bold),
990 formatted_name.attribute(Attribute::Bold)
991 );
992 }
993 CheckStatus::Warn => {
994 println!(
995 "{} {} {}",
996 status_symbol.with(color).attribute(Attribute::Bold),
997 formatted_name.attribute(Attribute::Bold),
998 "[WARN]".with(color).attribute(Attribute::Bold)
999 );
1000 }
1001 CheckStatus::Fail => {
1002 println!(
1003 "{} {} {}",
1004 status_symbol.with(color).attribute(Attribute::Bold),
1005 formatted_name.attribute(Attribute::Bold),
1006 "[FAIL]".with(color).attribute(Attribute::Bold)
1007 );
1008 }
1009 }
1010
1011 println!(" {}", check.details);
1012 println!();
1013 }
1014
1015 println!(
1017 "{}",
1018 "─────────────────────────────────────".with(Color::DarkGrey)
1019 );
1020
1021 let (overall_text, overall_color) = if report.ok {
1022 ("✓ HEALTHY: All systems operational", Color::Green)
1023 } else {
1024 ("✗ ISSUES DETECTED", Color::Red)
1025 };
1026
1027 println!(
1028 "{}",
1029 overall_text.with(overall_color).attribute(Attribute::Bold)
1030 );
1031
1032 if !report.ok {
1033 println!();
1034 println!(
1035 "{}",
1036 "Tip: Run 'xchecker doctor --verbose' for detailed diagnostics.".with(Color::Yellow)
1037 );
1038 println!(
1039 "{}",
1040 " See docs/DOCTOR.md for troubleshooting steps.".with(Color::Yellow)
1041 );
1042 }
1043}
1044
1045fn to_title_case(s: &str) -> String {
1047 const ACRONYMS: &[&str] = &[
1049 "CLI", "LLM", "WSL", "API", "URL", "JSON", "YAML", "GH", "FS", "HTTP",
1050 ];
1051
1052 s.split('_')
1053 .map(|word| {
1054 let upper_word = word.to_uppercase();
1056 if ACRONYMS.contains(&upper_word.as_str()) {
1057 return upper_word;
1058 }
1059
1060 let mut c = word.chars();
1062 match c.next() {
1063 None => String::new(),
1064 Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
1065 }
1066 })
1067 .collect::<Vec<String>>()
1068 .join(" ")
1069}
1070
1071pub fn log_cache_stats(stats: &crate::cache::CacheStats) {
1073 tracing::info!(
1074 target: "xchecker::cache",
1075 hits = stats.hits,
1076 misses = stats.misses,
1077 entries = stats.hits + stats.misses,
1078 hit_ratio = stats.hit_ratio(),
1079 "Insight cache stats: hits={}, misses={}, entries={}, hit_ratio={:.2}",
1080 stats.hits,
1081 stats.misses,
1082 stats.hits + stats.misses,
1083 stats.hit_ratio()
1084 );
1085}
1086
1087#[macro_export]
1089macro_rules! verbose {
1090 ($logger:expr, $($arg:tt)*) => {
1091 $logger.verbose_fmt(format_args!($($arg)*))
1092 };
1093}
1094
1095#[macro_export]
1097macro_rules! time_operation {
1098 ($logger:expr, $operation:expr, $block:block) => {{
1099 $logger.start_timing($operation);
1100 let result = $block;
1101 $logger.end_timing($operation);
1102 result
1103 }};
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108 use super::*;
1109 use crate::test_support;
1110 use std::thread;
1111
1112 #[test]
1113 fn test_logger_creation() {
1114 let logger = Logger::new(true);
1115 assert!(logger.is_verbose());
1116 assert!(logger.operation_timings.is_empty());
1117 assert!(logger.file_operations.is_empty());
1118 assert!(logger.spec_id.is_none());
1119 assert!(logger.phase.is_none());
1120 assert!(logger.runner_mode.is_none());
1121 }
1122
1123 #[test]
1124 fn test_logger_context_setters() {
1125 let mut logger = Logger::new(false);
1126
1127 logger.set_spec_id("test-spec".to_string());
1128 logger.set_phase("requirements".to_string());
1129 logger.set_runner_mode("native".to_string());
1130
1131 assert_eq!(logger.spec_id(), Some("test-spec"));
1132 assert_eq!(logger.phase(), Some("requirements"));
1133 assert_eq!(logger.runner_mode(), Some("native"));
1134 }
1135
1136 #[test]
1137 fn test_timing_operations() {
1138 let mut logger = Logger::new(false); logger.start_timing("test_operation");
1141 thread::sleep(Duration::from_millis(10));
1142 let duration = logger.end_timing("test_operation");
1143
1144 assert!(duration >= Duration::from_millis(10));
1145 assert!(logger.operation_timings.contains_key("test_operation"));
1146 }
1147
1148 #[test]
1149 fn test_file_operation_logging() {
1150 let mut logger = Logger::new(false);
1151
1152 logger.log_file_operation(
1153 "test.txt",
1154 "read",
1155 Some(1024),
1156 Some("abc123def456".to_string()),
1157 );
1158
1159 assert_eq!(logger.file_operations.len(), 1);
1160 let op = &logger.file_operations[0];
1161 assert_eq!(op.path, "test.txt");
1162 assert_eq!(op.operation, "read");
1163 assert_eq!(op.size_bytes, Some(1024));
1164 assert_eq!(op.hash, Some("abc123def456".to_string()));
1165 }
1166
1167 #[test]
1168 fn test_timing_summary() {
1169 let mut logger = Logger::new(false);
1170
1171 logger.start_timing("op1");
1172 thread::sleep(Duration::from_millis(5));
1173 logger.end_timing("op1");
1174
1175 logger.start_timing("op2");
1176 thread::sleep(Duration::from_millis(10));
1177 logger.end_timing("op2");
1178
1179 let summary = logger.get_timing_summary();
1180 assert_eq!(summary.len(), 2);
1181
1182 let op2_timing = summary.iter().find(|t| t.operation == "op2").unwrap();
1184 assert!(op2_timing.duration >= Duration::from_millis(10));
1185 }
1186
1187 #[test]
1188 fn test_verbose_mode_disabled() {
1189 let logger = Logger::new(false);
1190
1191 logger.verbose("test message");
1193 logger.verbose_fmt(format_args!("formatted {}", "message"));
1194 }
1195
1196 #[test]
1197 fn test_performance_summary() {
1198 let mut logger = Logger::new(false);
1199
1200 logger.log_file_operation("file1.txt", "read", Some(100), None);
1201 logger.log_file_operation("file2.txt", "write", Some(200), Some("hash123".to_string()));
1202
1203 logger.start_timing("test_op");
1204 thread::sleep(Duration::from_millis(5));
1205 logger.end_timing("test_op");
1206
1207 logger.print_performance_summary();
1209
1210 assert_eq!(logger.get_file_operations().len(), 2);
1211 assert!(logger.total_elapsed() >= Duration::from_millis(5));
1212 }
1213
1214 #[test]
1215 fn test_tracing_initialization_compact() {
1216 let result = init_tracing(false);
1220 assert!(result.is_ok() || result.is_err());
1222 }
1223
1224 #[test]
1225 fn test_tracing_initialization_verbose() {
1226 let result = init_tracing(true);
1229 assert!(result.is_ok() || result.is_err());
1231 }
1232
1233 #[test]
1234 fn test_phase_span_creation() {
1235 let span = phase_span("test-spec", "requirements", "native");
1237 if let Some(metadata) = span.metadata() {
1239 assert_eq!(metadata.name(), "phase_execution");
1240 }
1241 }
1243
1244 #[test]
1245 fn test_structured_logging_functions() {
1246 log_phase_start("test-spec", "requirements", "native");
1248 log_phase_complete("test-spec", "requirements", 1000);
1249 log_phase_error("test-spec", "requirements", "test error", 1000);
1250 }
1251
1252 #[test]
1253 fn test_logger_structured_methods() {
1254 let mut logger = Logger::new(false);
1255
1256 logger.set_spec_id("test-spec".to_string());
1258 logger.set_phase("requirements".to_string());
1259 logger.set_runner_mode("native".to_string());
1260
1261 logger.info("test info message");
1263 logger.warn("test warning message");
1264 logger.error("test error message");
1265 }
1266
1267 #[test]
1268 fn test_logger_structured_methods_without_context() {
1269 let logger = Logger::new(false);
1270
1271 logger.info("test info message");
1273 logger.warn("test warning message");
1274 logger.error("test error message");
1275 }
1276
1277 #[test]
1278 fn test_verbose_format_includes_required_fields() {
1279 let mut logger = Logger::new(true);
1281
1282 logger.set_spec_id("test-spec-123".to_string());
1284 logger.set_phase("design".to_string());
1285 logger.set_runner_mode("wsl".to_string());
1286
1287 assert_eq!(logger.spec_id(), Some("test-spec-123"));
1289 assert_eq!(logger.phase(), Some("design"));
1290 assert_eq!(logger.runner_mode(), Some("wsl"));
1291
1292 logger.info("test message with all fields");
1294 logger.verbose("verbose message with all fields");
1295
1296 }
1299
1300 #[test]
1301 fn test_default_format_is_compact() {
1302 let logger = Logger::new(false);
1304 assert!(!logger.is_verbose());
1305
1306 logger.info("compact message");
1308 }
1309
1310 #[test]
1311 fn test_phase_logging_functions() {
1312 log_phase_start("spec-1", "requirements", "native");
1314 log_phase_complete("spec-1", "requirements", 5000);
1315 log_phase_error("spec-1", "requirements", "timeout occurred", 10000);
1316
1317 }
1319
1320 #[test]
1323 fn test_redact_github_token_in_log() {
1324 let logger = Logger::new(false);
1325 let token = test_support::github_pat();
1326 let message = format!("Using token {} for auth", token);
1327 let redacted = logger.redact(&message);
1328
1329 assert!(!redacted.contains(&token));
1331 assert!(redacted.contains("[REDACTED:github_pat]"));
1333 }
1334
1335 #[test]
1336 fn test_redact_aws_key_in_log() {
1337 let logger = Logger::new(false);
1338 let aws_key = test_support::aws_access_key_id();
1339 let message = format!("AWS key: {}", aws_key);
1340 let redacted = logger.redact(&message);
1341
1342 assert!(!redacted.contains(&aws_key));
1343 assert!(redacted.contains("[REDACTED:aws_access_key]"));
1344 }
1345
1346 #[test]
1347 fn test_redact_bearer_token_in_log() {
1348 let logger = Logger::new(false);
1349 let token = test_support::bearer_token();
1350 let message = format!("Authorization: {}", token);
1351 let redacted = logger.redact(&message);
1352
1353 assert!(!redacted.contains(&token));
1354 assert!(redacted.contains("[REDACTED:bearer_token]"));
1355 }
1356
1357 #[test]
1358 fn test_sanitize_environment_variables() {
1359 let logger = Logger::new(false);
1360 let aws_secret = test_support::aws_secret_access_key();
1361
1362 let test_cases = vec![
1364 "API_KEY=secret123".to_string(),
1365 "SECRET_TOKEN=abc".to_string(),
1366 "PASSWORD=mypass".to_string(),
1367 aws_secret,
1368 ];
1369
1370 for test_case in test_cases {
1371 let sanitized = logger.sanitize(&test_case);
1372 assert_eq!(sanitized, "[ENV_VAR_REDACTED]");
1374 }
1375 }
1376
1377 #[test]
1378 fn test_sanitize_normal_content() {
1379 let logger = Logger::new(false);
1380 let message = "Processing file test.txt with 1024 bytes";
1381 let sanitized = logger.sanitize(message);
1382
1383 assert_eq!(sanitized, message);
1385 }
1386
1387 #[test]
1388 fn test_verbose_logging_redacts_secrets() {
1389 let logger = Logger::new(true);
1390 let token = test_support::github_pat();
1391
1392 logger.verbose(&format!("Token: {}", token));
1395 }
1396
1397 #[test]
1398 fn test_info_logging_redacts_secrets() {
1399 let logger = Logger::new(false);
1400 let aws_key = test_support::aws_access_key_id();
1401
1402 logger.info(&format!("Using AWS key {}", aws_key));
1404 }
1405
1406 #[test]
1407 fn test_warn_logging_redacts_secrets() {
1408 let logger = Logger::new(false);
1409 let token = test_support::github_pat();
1410
1411 logger.warn(&format!("Warning: exposed token {}", token));
1413 }
1414
1415 #[test]
1416 fn test_error_logging_redacts_secrets() {
1417 let logger = Logger::new(false);
1418 let token = test_support::bearer_token();
1419
1420 logger.error(&format!("Error: failed with {}", token));
1422 }
1423
1424 #[test]
1425 fn test_multiple_secrets_in_message() {
1426 let logger = Logger::new(false);
1427 let github_token = test_support::github_pat();
1428 let aws_key = test_support::aws_access_key_id();
1429 let message = format!("GitHub token {}\nAWS key {}", github_token, aws_key);
1431 let redacted = logger.redact(&message);
1432
1433 assert!(!redacted.contains(&github_token));
1435 assert!(!redacted.contains(&aws_key));
1436 assert!(redacted.contains("[REDACTED:github_pat]"));
1437 assert!(redacted.contains("[REDACTED:aws_access_key]"));
1438 }
1439
1440 #[test]
1441 fn test_log_phase_error_redacts_secrets() {
1442 let token = test_support::github_pat();
1443 log_phase_error(
1445 "test-spec",
1446 "requirements",
1447 &format!("Failed with token {}", token),
1448 1000,
1449 );
1450
1451 }
1454
1455 #[test]
1456 fn test_redaction_with_context() {
1457 let mut logger = Logger::new(false);
1458 logger.set_spec_id("test-spec".to_string());
1459 logger.set_phase("requirements".to_string());
1460 logger.set_runner_mode("native".to_string());
1461 let token = test_support::github_pat();
1462
1463 logger.error(&format!("Error with secret: {}", token));
1465 }
1466
1467 #[test]
1468 fn test_no_environment_variables_in_logs() {
1469 let logger = Logger::new(false);
1470
1471 let message = "Config: DATABASE_PASSWORD=secret123";
1473 let sanitized = logger.sanitize(message);
1474
1475 assert!(!sanitized.contains("secret123"));
1477 assert_eq!(sanitized, "[ENV_VAR_REDACTED]");
1478 }
1479
1480 #[test]
1481 fn test_error_context_without_sensitive_data() {
1482 let logger = Logger::new(false);
1483
1484 let error_msg = "Authentication failed with API_KEY=secret123";
1486 let sanitized = logger.sanitize(error_msg);
1487
1488 assert!(!sanitized.contains("secret123"));
1490 assert_eq!(sanitized, "[ENV_VAR_REDACTED]");
1491 }
1492
1493 #[test]
1494 fn test_redaction_preserves_safe_content() {
1495 let logger = Logger::new(false);
1496 let message = "Processing completed successfully in 1.5 seconds";
1497 let redacted = logger.redact(message);
1498
1499 assert_eq!(redacted, message);
1501 }
1502
1503 #[test]
1504 fn test_sanitize_slack_token() {
1505 let logger = Logger::new(false);
1506 let token = test_support::slack_bot_token();
1507 let message = format!("Slack webhook: {}", token);
1508 let sanitized = logger.sanitize(&message);
1509
1510 assert!(!sanitized.contains(&token));
1512 assert!(sanitized.contains("[REDACTED:slack_token]"));
1513 }
1514
1515 #[test]
1516 fn test_verbose_fmt_redacts_secrets() {
1517 let logger = Logger::new(true);
1518 let token = test_support::github_pat();
1519
1520 logger.verbose_fmt(format_args!("Token: {}", token));
1522
1523 }
1525
1526 #[test]
1527 fn test_to_title_case() {
1528 assert_eq!(to_title_case("claude_path"), "Claude Path");
1529 assert_eq!(to_title_case("claude_cli_version"), "Claude CLI Version");
1530 assert_eq!(to_title_case("wsl_availability"), "WSL Availability");
1531 assert_eq!(to_title_case("llm_provider"), "LLM Provider");
1532 assert_eq!(
1533 to_title_case("http_provider_config"),
1534 "HTTP Provider Config"
1535 );
1536 assert_eq!(to_title_case("json_output"), "JSON Output");
1537 assert_eq!(to_title_case("simple_test"), "Simple Test");
1538 assert_eq!(to_title_case("api_key"), "API Key");
1539 assert_eq!(to_title_case("gh_repo"), "GH Repo");
1540 assert_eq!(to_title_case("fs_path"), "FS Path");
1541 }
1542}