use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::time::{Duration, Instant};
use sysinfo::{Pid, System};
use tracing::{Level, debug, error, info, span, warn};
use tracing_subscriber::{
EnvFilter,
fmt::{self, format::FmtSpan},
layer::SubscriberExt,
util::SubscriberInitExt,
};
use xchecker_redaction::SecretRedactor;
#[allow(dead_code)] pub fn init_tracing(verbose: bool) -> Result<(), Box<dyn std::error::Error>> {
let env_filter = EnvFilter::try_from_default_env()
.or_else(|_| {
if verbose {
EnvFilter::try_new("xchecker=debug,info")
} else {
EnvFilter::try_new("xchecker=info,warn")
}
})
.unwrap_or_else(|_| EnvFilter::new("info"));
if verbose {
tracing_subscriber::registry()
.with(env_filter)
.with(
fmt::layer()
.with_target(true)
.with_thread_ids(false)
.with_thread_names(false)
.with_line_number(false)
.with_file(false)
.with_span_events(FmtSpan::CLOSE)
.compact(),
)
.try_init()?;
} else {
tracing_subscriber::registry()
.with(env_filter)
.with(
fmt::layer()
.with_target(false)
.with_thread_ids(false)
.with_thread_names(false)
.with_line_number(false)
.with_file(false)
.compact(),
)
.try_init()?;
}
Ok(())
}
#[allow(dead_code)] pub fn phase_span(spec_id: &str, phase: &str, runner_mode: &str) -> tracing::Span {
span!(
Level::INFO,
"phase_execution",
spec_id = %spec_id,
phase = %phase,
runner_mode = %runner_mode,
)
}
#[allow(dead_code)] pub fn log_phase_start(spec_id: &str, phase: &str, runner_mode: &str) {
info!(
spec_id = %spec_id,
phase = %phase,
runner_mode = %runner_mode,
"Starting phase execution"
);
}
#[allow(dead_code)] pub fn log_phase_complete(spec_id: &str, phase: &str, duration_ms: u128) {
info!(
spec_id = %spec_id,
phase = %phase,
duration_ms = %duration_ms,
"Phase execution completed"
);
}
#[allow(dead_code)] pub fn log_phase_error(spec_id: &str, phase: &str, error: &str, duration_ms: u128) {
let redactor = SecretRedactor::new().expect("Failed to create SecretRedactor");
let sanitized_error = match redactor.redact_content(error, "<log>") {
Ok(result) => result.content,
Err(_) => "[REDACTION_ERROR]".to_string(),
};
error!(
spec_id = %spec_id,
phase = %phase,
duration_ms = %duration_ms,
error = %sanitized_error,
"Phase execution failed"
);
}
pub struct Logger {
verbose: bool,
start_time: Instant,
operation_timings: HashMap<String, Duration>,
file_operations: Vec<FileOperation>,
operation_samples: HashMap<String, Vec<Duration>>,
system: System,
initial_memory: u64,
peak_memory: u64,
performance_targets: PerformanceTargets,
spec_id: Option<String>,
phase: Option<String>,
runner_mode: Option<String>,
redactor: SecretRedactor,
}
#[allow(dead_code)] #[derive(Debug, Clone)]
pub struct FileOperation {
pub path: String,
pub operation: String,
pub size_bytes: Option<usize>,
pub hash: Option<String>,
pub timestamp: DateTime<Utc>,
}
#[allow(dead_code)] #[derive(Debug, Clone)]
pub struct TimingInfo {
pub operation: String,
pub duration: Duration,
pub timestamp: DateTime<Utc>,
}
#[allow(dead_code)] #[derive(Debug, Clone)]
pub struct PerformanceMetrics {
pub total_duration: Duration,
pub empty_run_duration: Option<Duration>,
pub packetization_duration: Option<Duration>,
pub files_processed: usize,
pub memory_stats: MemoryStats,
pub timing_percentiles: HashMap<String, TimingPercentiles>,
}
#[allow(dead_code)] #[derive(Debug, Clone, Default)]
pub struct MemoryStats {
pub peak_memory_bytes: u64,
pub initial_memory_bytes: u64,
pub final_memory_bytes: u64,
}
#[allow(dead_code)] #[derive(Debug, Clone)]
pub struct TimingPercentiles {
pub p50: Duration,
pub p95: Duration,
pub min: Duration,
pub max: Duration,
pub count: usize,
}
#[derive(Debug, Clone)]
pub struct PerformanceTargets {
pub empty_run_target: Duration,
pub packetization_target_per_100_files: Duration,
}
impl Logger {
#[must_use]
pub fn new(verbose: bool) -> Self {
let mut system = System::new_all();
system.refresh_all();
let initial_memory = Self::get_current_memory_usage(&system);
Self {
verbose,
start_time: Instant::now(),
operation_timings: HashMap::new(),
file_operations: Vec::new(),
operation_samples: HashMap::new(),
system,
initial_memory,
peak_memory: initial_memory,
performance_targets: PerformanceTargets {
empty_run_target: Duration::from_secs(5), packetization_target_per_100_files: Duration::from_millis(200), },
spec_id: None,
phase: None,
runner_mode: None,
redactor: SecretRedactor::new().expect("Failed to create SecretRedactor"),
}
}
fn redact(&self, content: &str) -> String {
match self.redactor.redact_content(content, "<log>") {
Ok(result) => result.content,
Err(_) => {
"[REDACTION_ERROR]".to_string()
}
}
}
fn sanitize(&self, content: &str) -> String {
let sanitized = if content.contains('=')
&& (content.contains("KEY")
|| content.contains("TOKEN")
|| content.contains("SECRET")
|| content.contains("PASSWORD"))
{
"[ENV_VAR_REDACTED]".to_string()
} else {
content.to_string()
};
self.redact(&sanitized)
}
#[allow(dead_code)] pub fn set_spec_id(&mut self, spec_id: String) {
self.spec_id = Some(spec_id);
}
#[allow(dead_code)] pub fn set_phase(&mut self, phase: String) {
self.phase = Some(phase);
}
#[allow(dead_code)] pub fn set_runner_mode(&mut self, runner_mode: String) {
self.runner_mode = Some(runner_mode);
}
#[must_use]
#[allow(dead_code)] pub fn spec_id(&self) -> Option<&str> {
self.spec_id.as_deref()
}
#[must_use]
#[allow(dead_code)] pub fn phase(&self) -> Option<&str> {
self.phase.as_deref()
}
#[must_use]
#[allow(dead_code)] pub fn runner_mode(&self) -> Option<&str> {
self.runner_mode.as_deref()
}
#[must_use]
#[allow(dead_code)] pub const fn is_verbose(&self) -> bool {
self.verbose
}
pub fn verbose(&self, message: &str) {
if self.verbose {
let elapsed = self.start_time.elapsed();
let sanitized_message = self.sanitize(message);
if let (Some(spec_id), Some(phase), Some(runner_mode)) = (
self.spec_id.as_ref(),
self.phase.as_ref(),
self.runner_mode.as_ref(),
) {
debug!(
spec_id = %spec_id,
phase = %phase,
runner_mode = %runner_mode,
duration_ms = %elapsed.as_millis(),
message = %sanitized_message,
"Verbose log"
);
} else {
debug!(
duration_ms = %elapsed.as_millis(),
message = %sanitized_message,
"Verbose log"
);
}
println!("[{:>8.3}s] {}", elapsed.as_secs_f64(), sanitized_message);
}
}
#[allow(dead_code)] pub fn verbose_fmt(&self, args: std::fmt::Arguments) {
if self.verbose {
let elapsed = self.start_time.elapsed();
let message = format!("{args}");
let sanitized_message = self.sanitize(&message);
if let (Some(spec_id), Some(phase), Some(runner_mode)) = (
self.spec_id.as_ref(),
self.phase.as_ref(),
self.runner_mode.as_ref(),
) {
debug!(
spec_id = %spec_id,
phase = %phase,
runner_mode = %runner_mode,
duration_ms = %elapsed.as_millis(),
message = %sanitized_message,
"Verbose log"
);
} else {
debug!(
duration_ms = %elapsed.as_millis(),
message = %sanitized_message,
"Verbose log"
);
}
println!("[{:>8.3}s] {}", elapsed.as_secs_f64(), sanitized_message);
}
}
#[allow(dead_code)] pub fn info(&self, message: &str) {
let sanitized_message = self.sanitize(message);
if let (Some(spec_id), Some(phase), Some(runner_mode)) = (
self.spec_id.as_ref(),
self.phase.as_ref(),
self.runner_mode.as_ref(),
) {
info!(
spec_id = %spec_id,
phase = %phase,
runner_mode = %runner_mode,
duration_ms = %self.start_time.elapsed().as_millis(),
message = %sanitized_message,
);
} else {
info!(message = %sanitized_message);
}
}
#[allow(dead_code)] pub fn warn(&self, message: &str) {
let sanitized_message = self.sanitize(message);
if let (Some(spec_id), Some(phase), Some(runner_mode)) = (
self.spec_id.as_ref(),
self.phase.as_ref(),
self.runner_mode.as_ref(),
) {
warn!(
spec_id = %spec_id,
phase = %phase,
runner_mode = %runner_mode,
duration_ms = %self.start_time.elapsed().as_millis(),
message = %sanitized_message,
);
} else {
warn!(message = %sanitized_message);
}
}
#[allow(dead_code)] pub fn error(&self, message: &str) {
let sanitized_message = self.sanitize(message);
if let (Some(spec_id), Some(phase), Some(runner_mode)) = (
self.spec_id.as_ref(),
self.phase.as_ref(),
self.runner_mode.as_ref(),
) {
error!(
spec_id = %spec_id,
phase = %phase,
runner_mode = %runner_mode,
duration_ms = %self.start_time.elapsed().as_millis(),
message = %sanitized_message,
);
} else {
error!(message = %sanitized_message);
}
}
pub fn start_timing(&mut self, operation: &str) {
if self.verbose {
self.verbose(&format!("Starting: {operation}"));
}
}
pub fn end_timing(&mut self, operation: &str) -> Duration {
let duration = self.start_time.elapsed();
self.operation_timings
.insert(operation.to_string(), duration);
self.operation_samples
.entry(operation.to_string())
.or_default()
.push(duration);
self.update_memory_usage();
if self.verbose {
self.verbose(&format!(
"Completed: {} ({:.3}s)",
operation,
duration.as_secs_f64()
));
}
duration
}
#[allow(dead_code)] pub fn log_file_operation(
&mut self,
path: &str,
operation: &str,
size_bytes: Option<usize>,
hash: Option<String>,
) {
let file_op = FileOperation {
path: path.to_string(),
operation: operation.to_string(),
size_bytes,
hash: hash.clone(),
timestamp: Utc::now(),
};
self.file_operations.push(file_op);
if self.verbose {
let size_info = size_bytes
.map(|s| format!(" ({s} bytes)"))
.unwrap_or_default();
let hash_info = hash
.map(|h| format!(" [{}]", &h[..8.min(h.len())]))
.unwrap_or_default();
self.verbose(&format!("File {operation}: {path}{size_info}{hash_info}"));
}
}
#[allow(dead_code)] pub fn log_selected_files(&self, file_count: usize, total_bytes: usize) {
if !self.verbose {
return;
}
self.verbose(&format!("Selected {file_count} files for packet"));
self.verbose(&format!("Total packet size: {total_bytes} bytes"));
}
#[allow(dead_code)] pub fn log_file_selected(&self, path: &str, bytes: usize, priority: &str) {
if self.verbose {
self.verbose(&format!(" - {path} ({bytes} bytes) [{priority}]"));
}
}
#[allow(dead_code)] pub fn log_packet_construction(
&self,
used_bytes: usize,
used_lines: usize,
max_bytes: usize,
max_lines: usize,
) {
if self.verbose {
let bytes_pct = (used_bytes as f64 / max_bytes as f64) * 100.0;
let lines_pct = (used_lines as f64 / max_lines as f64) * 100.0;
self.verbose(&format!(
"Packet budget: {used_bytes} / {max_bytes} bytes ({bytes_pct:.1}%), {used_lines} / {max_lines} lines ({lines_pct:.1}%)"
));
}
}
#[allow(dead_code)] pub fn log_claude_execution(&self, model: &str, runner: &str, args: &[String]) {
if self.verbose {
self.verbose("Executing Claude CLI:");
self.verbose(&format!(" Model: {model}"));
self.verbose(&format!(" Runner: {runner}"));
self.verbose(&format!(" Args: {}", args.join(" ")));
}
}
#[allow(dead_code)] pub fn log_canonicalization(
&self,
file_type: &str,
original_size: usize,
canonical_size: usize,
hash: &str,
) {
if self.verbose {
let short_hash = &hash[..8.min(hash.len())];
self.verbose(&format!(
"Canonicalized {file_type}: {original_size} → {canonical_size} bytes [{short_hash}]"
));
}
}
#[allow(dead_code)] pub fn log_redaction_results(
&self,
files_scanned: usize,
patterns_matched: usize,
files_with_secrets: usize,
) {
if self.verbose {
self.verbose(&format!(
"Secret redaction: {files_scanned} files scanned, {patterns_matched} patterns matched in {files_with_secrets} files"
));
if patterns_matched > 0 {
self.verbose(" âš Secrets detected - run aborted for security");
}
}
}
#[must_use]
#[allow(dead_code)] pub fn get_timing_summary(&self) -> Vec<TimingInfo> {
self.operation_timings
.iter()
.map(|(op, duration)| TimingInfo {
operation: op.clone(),
duration: *duration,
timestamp: Utc::now(), })
.collect()
}
#[must_use]
#[allow(dead_code)] pub fn get_file_operations(&self) -> &[FileOperation] {
&self.file_operations
}
#[must_use]
pub fn total_elapsed(&self) -> Duration {
self.start_time.elapsed()
}
fn update_memory_usage(&mut self) {
self.system.refresh_memory();
let current_memory = Self::get_current_memory_usage(&self.system);
if current_memory > self.peak_memory {
self.peak_memory = current_memory;
}
}
fn get_current_memory_usage(system: &System) -> u64 {
let pid = Pid::from(std::process::id() as usize);
system.process(pid).map_or(0, sysinfo::Process::memory) * 1024 }
pub fn record_packetization_timing(&mut self, duration: Duration, file_count: usize) {
self.operation_samples
.entry("packetization".to_string())
.or_default()
.push(duration);
if self.verbose {
let per_file_ms = if file_count > 0 {
duration.as_millis() as f64 / file_count as f64
} else {
0.0
};
self.verbose(&format!(
"Packetization: {:.1}ms for {} files ({:.2}ms/file)",
duration.as_millis(),
file_count,
per_file_ms
));
let target_for_files = if file_count > 0 {
let target_millis = (self
.performance_targets
.packetization_target_per_100_files
.as_millis() as u64
* file_count as u64)
/ 100;
Duration::from_millis(target_millis)
} else {
Duration::from_millis(0)
};
if duration > target_for_files {
self.verbose(&format!(
" âš Packetization slower than target ({:.1}ms vs {:.1}ms)",
duration.as_millis(),
target_for_files.as_millis()
));
} else {
self.verbose(" ✓ Packetization within target");
}
}
}
pub fn record_empty_run_timing(&mut self, duration: Duration) {
self.operation_samples
.entry("empty_run".to_string())
.or_default()
.push(duration);
if self.verbose {
self.verbose(&format!("Empty run: {:.3}s", duration.as_secs_f64()));
if duration > self.performance_targets.empty_run_target {
self.verbose(&format!(
" âš Empty run slower than target ({:.1}s vs {:.1}s)",
duration.as_secs_f64(),
self.performance_targets.empty_run_target.as_secs_f64()
));
} else {
self.verbose(" ✓ Empty run within target");
}
}
}
#[must_use]
pub fn calculate_percentiles(&self, operation: &str) -> Option<TimingPercentiles> {
let samples = self.operation_samples.get(operation)?;
if samples.is_empty() {
return None;
}
let mut sorted_samples = samples.clone();
sorted_samples.sort();
let count = sorted_samples.len();
let p50_idx = count / 2;
let p95_idx = (count * 95) / 100;
Some(TimingPercentiles {
p50: sorted_samples[p50_idx],
p95: sorted_samples[p95_idx.min(count - 1)],
min: sorted_samples[0],
max: sorted_samples[count - 1],
count,
})
}
pub fn generate_performance_metrics(&mut self) -> PerformanceMetrics {
self.update_memory_usage();
let total_duration = self.total_elapsed();
let empty_run_duration = self
.operation_samples
.get("empty_run")
.and_then(|samples| samples.last())
.copied();
let packetization_duration = self
.operation_samples
.get("packetization")
.and_then(|samples| samples.last())
.copied();
let files_processed = self.file_operations.len();
let memory_stats = MemoryStats {
peak_memory_bytes: self.peak_memory,
initial_memory_bytes: self.initial_memory,
final_memory_bytes: Self::get_current_memory_usage(&self.system),
};
let mut timing_percentiles = HashMap::new();
for operation in self.operation_samples.keys() {
if let Some(percentiles) = self.calculate_percentiles(operation) {
timing_percentiles.insert(operation.clone(), percentiles);
}
}
PerformanceMetrics {
total_duration,
empty_run_duration,
packetization_duration,
files_processed,
memory_stats,
timing_percentiles,
}
}
#[must_use]
pub fn validate_performance_targets(&self) -> Vec<String> {
let mut violations = Vec::new();
if let Some(samples) = self.operation_samples.get("empty_run")
&& let Some(&last_empty_run) = samples.last()
&& last_empty_run > self.performance_targets.empty_run_target
{
violations.push(format!(
"Empty run exceeded target: {:.3}s > {:.3}s",
last_empty_run.as_secs_f64(),
self.performance_targets.empty_run_target.as_secs_f64()
));
}
if let Some(samples) = self.operation_samples.get("packetization")
&& let Some(&last_packetization) = samples.last()
{
let file_count = self.file_operations.len();
if file_count > 0 {
let target_millis = (self
.performance_targets
.packetization_target_per_100_files
.as_millis() as u64
* file_count as u64)
/ 100;
let target_for_files = Duration::from_millis(target_millis);
if last_packetization > target_for_files {
violations.push(format!(
"Packetization exceeded target: {:.1}ms > {:.1}ms for {} files",
last_packetization.as_millis(),
target_for_files.as_millis(),
file_count
));
}
}
}
violations
}
#[allow(dead_code)] pub fn log_cache_stats(&self, stats: &crate::cache::CacheStats) {
if !self.verbose {
return;
}
self.verbose(&format!(
"Insight cache: {} hits, {} misses, {} entries, hit_ratio={:.2}",
stats.hits,
stats.misses,
stats.hits + stats.misses,
stats.hit_ratio()
));
}
pub fn print_performance_summary(&self) {
if !self.verbose {
return;
}
let total_time = self.total_elapsed();
self.verbose("=== Performance Summary ===");
self.verbose(&format!(
"Total execution time: {:.3}s",
total_time.as_secs_f64()
));
let memory_mb = self.peak_memory as f64 / (1024.0 * 1024.0);
let initial_mb = self.initial_memory as f64 / (1024.0 * 1024.0);
self.verbose(&format!(
"Memory usage: {memory_mb:.1}MB peak ({initial_mb:.1}MB initial)"
));
let violations = self.validate_performance_targets();
if violations.is_empty() {
self.verbose("✓ All performance targets met");
} else {
self.verbose("âš Performance target violations:");
for violation in violations {
self.verbose(&format!(" - {violation}"));
}
}
if !self.operation_timings.is_empty() {
self.verbose("Operation timings:");
let mut timings: Vec<_> = self.operation_timings.iter().collect();
timings.sort_by(|a, b| b.1.cmp(a.1));
for (operation, duration) in timings {
let percentage = (duration.as_secs_f64() / total_time.as_secs_f64()) * 100.0;
if let Some(percentiles) = self.calculate_percentiles(operation) {
self.verbose(&format!(
" {}: {:.3}s ({:.1}%) [P50: {:.3}s, P95: {:.3}s, n={}]",
operation,
duration.as_secs_f64(),
percentage,
percentiles.p50.as_secs_f64(),
percentiles.p95.as_secs_f64(),
percentiles.count
));
} else {
self.verbose(&format!(
" {}: {:.3}s ({:.1}%)",
operation,
duration.as_secs_f64(),
percentage
));
}
}
}
if !self.file_operations.is_empty() {
let total_files = self.file_operations.len();
let total_bytes: usize = self
.file_operations
.iter()
.filter_map(|op| op.size_bytes)
.sum();
self.verbose(&format!(
"File operations: {total_files} total, {total_bytes} bytes processed"
));
}
}
}
pub fn log_doctor_report(report: &crate::types::DoctorOutput) {
use crate::types::CheckStatus;
use crossterm::style::{Attribute, Color, Stylize};
println!(
"{}",
"🩺 xchecker Environment Health Check"
.with(Color::Cyan)
.attribute(Attribute::Bold)
);
println!(
"{}",
"─────────────────────────────────────"
.with(Color::Cyan)
.attribute(Attribute::Bold)
);
println!();
for check in &report.checks {
let (status_symbol, color) = match check.status {
CheckStatus::Pass => ("✓", Color::Green),
CheckStatus::Warn => ("âš ", Color::Yellow),
CheckStatus::Fail => ("✗", Color::Red),
};
let formatted_name = to_title_case(&check.name);
match check.status {
CheckStatus::Pass => {
println!(
"{} {}",
status_symbol.with(color).attribute(Attribute::Bold),
formatted_name.attribute(Attribute::Bold)
);
}
CheckStatus::Warn => {
println!(
"{} {} {}",
status_symbol.with(color).attribute(Attribute::Bold),
formatted_name.attribute(Attribute::Bold),
"[WARN]".with(color).attribute(Attribute::Bold)
);
}
CheckStatus::Fail => {
println!(
"{} {} {}",
status_symbol.with(color).attribute(Attribute::Bold),
formatted_name.attribute(Attribute::Bold),
"[FAIL]".with(color).attribute(Attribute::Bold)
);
}
}
println!(" {}", check.details);
println!();
}
println!(
"{}",
"─────────────────────────────────────".with(Color::DarkGrey)
);
let (overall_text, overall_color) = if report.ok {
("✓ HEALTHY: All systems operational", Color::Green)
} else {
("✗ ISSUES DETECTED", Color::Red)
};
println!(
"{}",
overall_text.with(overall_color).attribute(Attribute::Bold)
);
if !report.ok {
println!();
println!(
"{}",
"Tip: Run 'xchecker doctor --verbose' for detailed diagnostics.".with(Color::Yellow)
);
println!(
"{}",
" See docs/DOCTOR.md for troubleshooting steps.".with(Color::Yellow)
);
}
}
fn to_title_case(s: &str) -> String {
const ACRONYMS: &[&str] = &[
"CLI", "LLM", "WSL", "API", "URL", "JSON", "YAML", "GH", "FS", "HTTP",
];
s.split('_')
.map(|word| {
let upper_word = word.to_uppercase();
if ACRONYMS.contains(&upper_word.as_str()) {
return upper_word;
}
let mut c = word.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
})
.collect::<Vec<String>>()
.join(" ")
}
pub fn log_cache_stats(stats: &crate::cache::CacheStats) {
tracing::info!(
target: "xchecker::cache",
hits = stats.hits,
misses = stats.misses,
entries = stats.hits + stats.misses,
hit_ratio = stats.hit_ratio(),
"Insight cache stats: hits={}, misses={}, entries={}, hit_ratio={:.2}",
stats.hits,
stats.misses,
stats.hits + stats.misses,
stats.hit_ratio()
);
}
#[macro_export]
macro_rules! verbose {
($logger:expr, $($arg:tt)*) => {
$logger.verbose_fmt(format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! time_operation {
($logger:expr, $operation:expr, $block:block) => {{
$logger.start_timing($operation);
let result = $block;
$logger.end_timing($operation);
result
}};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support;
use std::thread;
#[test]
fn test_logger_creation() {
let logger = Logger::new(true);
assert!(logger.is_verbose());
assert!(logger.operation_timings.is_empty());
assert!(logger.file_operations.is_empty());
assert!(logger.spec_id.is_none());
assert!(logger.phase.is_none());
assert!(logger.runner_mode.is_none());
}
#[test]
fn test_logger_context_setters() {
let mut logger = Logger::new(false);
logger.set_spec_id("test-spec".to_string());
logger.set_phase("requirements".to_string());
logger.set_runner_mode("native".to_string());
assert_eq!(logger.spec_id(), Some("test-spec"));
assert_eq!(logger.phase(), Some("requirements"));
assert_eq!(logger.runner_mode(), Some("native"));
}
#[test]
fn test_timing_operations() {
let mut logger = Logger::new(false);
logger.start_timing("test_operation");
thread::sleep(Duration::from_millis(10));
let duration = logger.end_timing("test_operation");
assert!(duration >= Duration::from_millis(10));
assert!(logger.operation_timings.contains_key("test_operation"));
}
#[test]
fn test_file_operation_logging() {
let mut logger = Logger::new(false);
logger.log_file_operation(
"test.txt",
"read",
Some(1024),
Some("abc123def456".to_string()),
);
assert_eq!(logger.file_operations.len(), 1);
let op = &logger.file_operations[0];
assert_eq!(op.path, "test.txt");
assert_eq!(op.operation, "read");
assert_eq!(op.size_bytes, Some(1024));
assert_eq!(op.hash, Some("abc123def456".to_string()));
}
#[test]
fn test_timing_summary() {
let mut logger = Logger::new(false);
logger.start_timing("op1");
thread::sleep(Duration::from_millis(5));
logger.end_timing("op1");
logger.start_timing("op2");
thread::sleep(Duration::from_millis(10));
logger.end_timing("op2");
let summary = logger.get_timing_summary();
assert_eq!(summary.len(), 2);
let op2_timing = summary.iter().find(|t| t.operation == "op2").unwrap();
assert!(op2_timing.duration >= Duration::from_millis(10));
}
#[test]
fn test_verbose_mode_disabled() {
let logger = Logger::new(false);
logger.verbose("test message");
logger.verbose_fmt(format_args!("formatted {}", "message"));
}
#[test]
fn test_performance_summary() {
let mut logger = Logger::new(false);
logger.log_file_operation("file1.txt", "read", Some(100), None);
logger.log_file_operation("file2.txt", "write", Some(200), Some("hash123".to_string()));
logger.start_timing("test_op");
thread::sleep(Duration::from_millis(5));
logger.end_timing("test_op");
logger.print_performance_summary();
assert_eq!(logger.get_file_operations().len(), 2);
assert!(logger.total_elapsed() >= Duration::from_millis(5));
}
#[test]
fn test_tracing_initialization_compact() {
let result = init_tracing(false);
assert!(result.is_ok() || result.is_err());
}
#[test]
fn test_tracing_initialization_verbose() {
let result = init_tracing(true);
assert!(result.is_ok() || result.is_err());
}
#[test]
fn test_phase_span_creation() {
let span = phase_span("test-spec", "requirements", "native");
if let Some(metadata) = span.metadata() {
assert_eq!(metadata.name(), "phase_execution");
}
}
#[test]
fn test_structured_logging_functions() {
log_phase_start("test-spec", "requirements", "native");
log_phase_complete("test-spec", "requirements", 1000);
log_phase_error("test-spec", "requirements", "test error", 1000);
}
#[test]
fn test_logger_structured_methods() {
let mut logger = Logger::new(false);
logger.set_spec_id("test-spec".to_string());
logger.set_phase("requirements".to_string());
logger.set_runner_mode("native".to_string());
logger.info("test info message");
logger.warn("test warning message");
logger.error("test error message");
}
#[test]
fn test_logger_structured_methods_without_context() {
let logger = Logger::new(false);
logger.info("test info message");
logger.warn("test warning message");
logger.error("test error message");
}
#[test]
fn test_verbose_format_includes_required_fields() {
let mut logger = Logger::new(true);
logger.set_spec_id("test-spec-123".to_string());
logger.set_phase("design".to_string());
logger.set_runner_mode("wsl".to_string());
assert_eq!(logger.spec_id(), Some("test-spec-123"));
assert_eq!(logger.phase(), Some("design"));
assert_eq!(logger.runner_mode(), Some("wsl"));
logger.info("test message with all fields");
logger.verbose("verbose message with all fields");
}
#[test]
fn test_default_format_is_compact() {
let logger = Logger::new(false);
assert!(!logger.is_verbose());
logger.info("compact message");
}
#[test]
fn test_phase_logging_functions() {
log_phase_start("spec-1", "requirements", "native");
log_phase_complete("spec-1", "requirements", 5000);
log_phase_error("spec-1", "requirements", "timeout occurred", 10000);
}
#[test]
fn test_redact_github_token_in_log() {
let logger = Logger::new(false);
let token = test_support::github_pat();
let message = format!("Using token {} for auth", token);
let redacted = logger.redact(&message);
assert!(!redacted.contains(&token));
assert!(redacted.contains("[REDACTED:github_pat]"));
}
#[test]
fn test_redact_aws_key_in_log() {
let logger = Logger::new(false);
let aws_key = test_support::aws_access_key_id();
let message = format!("AWS key: {}", aws_key);
let redacted = logger.redact(&message);
assert!(!redacted.contains(&aws_key));
assert!(redacted.contains("[REDACTED:aws_access_key]"));
}
#[test]
fn test_redact_bearer_token_in_log() {
let logger = Logger::new(false);
let token = test_support::bearer_token();
let message = format!("Authorization: {}", token);
let redacted = logger.redact(&message);
assert!(!redacted.contains(&token));
assert!(redacted.contains("[REDACTED:bearer_token]"));
}
#[test]
fn test_sanitize_environment_variables() {
let logger = Logger::new(false);
let aws_secret = test_support::aws_secret_access_key();
let test_cases = vec![
"API_KEY=secret123".to_string(),
"SECRET_TOKEN=abc".to_string(),
"PASSWORD=mypass".to_string(),
aws_secret,
];
for test_case in test_cases {
let sanitized = logger.sanitize(&test_case);
assert_eq!(sanitized, "[ENV_VAR_REDACTED]");
}
}
#[test]
fn test_sanitize_normal_content() {
let logger = Logger::new(false);
let message = "Processing file test.txt with 1024 bytes";
let sanitized = logger.sanitize(message);
assert_eq!(sanitized, message);
}
#[test]
fn test_verbose_logging_redacts_secrets() {
let logger = Logger::new(true);
let token = test_support::github_pat();
logger.verbose(&format!("Token: {}", token));
}
#[test]
fn test_info_logging_redacts_secrets() {
let logger = Logger::new(false);
let aws_key = test_support::aws_access_key_id();
logger.info(&format!("Using AWS key {}", aws_key));
}
#[test]
fn test_warn_logging_redacts_secrets() {
let logger = Logger::new(false);
let token = test_support::github_pat();
logger.warn(&format!("Warning: exposed token {}", token));
}
#[test]
fn test_error_logging_redacts_secrets() {
let logger = Logger::new(false);
let token = test_support::bearer_token();
logger.error(&format!("Error: failed with {}", token));
}
#[test]
fn test_multiple_secrets_in_message() {
let logger = Logger::new(false);
let github_token = test_support::github_pat();
let aws_key = test_support::aws_access_key_id();
let message = format!("GitHub token {}\nAWS key {}", github_token, aws_key);
let redacted = logger.redact(&message);
assert!(!redacted.contains(&github_token));
assert!(!redacted.contains(&aws_key));
assert!(redacted.contains("[REDACTED:github_pat]"));
assert!(redacted.contains("[REDACTED:aws_access_key]"));
}
#[test]
fn test_log_phase_error_redacts_secrets() {
let token = test_support::github_pat();
log_phase_error(
"test-spec",
"requirements",
&format!("Failed with token {}", token),
1000,
);
}
#[test]
fn test_redaction_with_context() {
let mut logger = Logger::new(false);
logger.set_spec_id("test-spec".to_string());
logger.set_phase("requirements".to_string());
logger.set_runner_mode("native".to_string());
let token = test_support::github_pat();
logger.error(&format!("Error with secret: {}", token));
}
#[test]
fn test_no_environment_variables_in_logs() {
let logger = Logger::new(false);
let message = "Config: DATABASE_PASSWORD=secret123";
let sanitized = logger.sanitize(message);
assert!(!sanitized.contains("secret123"));
assert_eq!(sanitized, "[ENV_VAR_REDACTED]");
}
#[test]
fn test_error_context_without_sensitive_data() {
let logger = Logger::new(false);
let error_msg = "Authentication failed with API_KEY=secret123";
let sanitized = logger.sanitize(error_msg);
assert!(!sanitized.contains("secret123"));
assert_eq!(sanitized, "[ENV_VAR_REDACTED]");
}
#[test]
fn test_redaction_preserves_safe_content() {
let logger = Logger::new(false);
let message = "Processing completed successfully in 1.5 seconds";
let redacted = logger.redact(message);
assert_eq!(redacted, message);
}
#[test]
fn test_sanitize_slack_token() {
let logger = Logger::new(false);
let token = test_support::slack_bot_token();
let message = format!("Slack webhook: {}", token);
let sanitized = logger.sanitize(&message);
assert!(!sanitized.contains(&token));
assert!(sanitized.contains("[REDACTED:slack_token]"));
}
#[test]
fn test_verbose_fmt_redacts_secrets() {
let logger = Logger::new(true);
let token = test_support::github_pat();
logger.verbose_fmt(format_args!("Token: {}", token));
}
#[test]
fn test_to_title_case() {
assert_eq!(to_title_case("claude_path"), "Claude Path");
assert_eq!(to_title_case("claude_cli_version"), "Claude CLI Version");
assert_eq!(to_title_case("wsl_availability"), "WSL Availability");
assert_eq!(to_title_case("llm_provider"), "LLM Provider");
assert_eq!(
to_title_case("http_provider_config"),
"HTTP Provider Config"
);
assert_eq!(to_title_case("json_output"), "JSON Output");
assert_eq!(to_title_case("simple_test"), "Simple Test");
assert_eq!(to_title_case("api_key"), "API Key");
assert_eq!(to_title_case("gh_repo"), "GH Repo");
assert_eq!(to_title_case("fs_path"), "FS Path");
}
}