light_program_test/logging/
mod.rs

1//! Enhanced logging system for light-program-test
2//!
3//! This module provides Solana Explorer-like transaction logging with:
4//! - Hierarchical instruction display with inner instructions
5//! - Account changes tracking
6//! - Light Protocol specific parsing and formatting
7//! - Configurable verbosity levels
8//! - Color-coded output
9//!
10//! Logging behavior:
11//! - File logging: Always enabled when `enhanced_logging.enabled = true` (default)
12//! - Log file: Written to `target/light_program_test.log`
13//! - Console output: Only when `RUST_BACKTRACE` is set AND `log_events = true`
14//! - Log file is overwritten at session start, then appended for each transaction
15
16pub mod config;
17pub mod decoder;
18pub mod formatter;
19pub mod types;
20
21use std::{
22    fs::OpenOptions,
23    io::Write,
24    path::PathBuf,
25    time::{SystemTime, UNIX_EPOCH},
26};
27
28use chrono;
29pub use config::{EnhancedLoggingConfig, LogVerbosity};
30pub use formatter::TransactionFormatter;
31use litesvm::types::TransactionResult;
32use solana_sdk::{signature::Signature, transaction::Transaction};
33pub use types::{
34    AccountChange, EnhancedInstructionLog, EnhancedTransactionLog, ParsedInstructionData,
35    TransactionStatus,
36};
37
38use crate::program_test::config::ProgramTestConfig;
39
40static SESSION_STARTED: std::sync::Once = std::sync::Once::new();
41
42/// Get the log file path in target directory
43fn get_log_file_path() -> PathBuf {
44    // Always use cargo workspace target directory
45    use std::process::Command;
46    if let Ok(output) = Command::new("cargo")
47        .arg("metadata")
48        .arg("--format-version=1")
49        .arg("--no-deps")
50        .output()
51    {
52        if output.status.success() {
53            if let Ok(metadata) = String::from_utf8(output.stdout) {
54                if let Ok(json) = serde_json::from_str::<serde_json::Value>(&metadata) {
55                    if let Some(target_directory) = json["target_directory"].as_str() {
56                        let mut path = PathBuf::from(target_directory);
57                        path.push("light_program_test.log");
58                        return path;
59                    }
60                }
61            }
62        }
63    }
64
65    // Fallback to current directory's target
66    let mut path = PathBuf::from("target");
67    path.push("light_program_test.log");
68    path
69}
70
71/// Initialize log file with session header (called only once per session)
72fn initialize_log_file() {
73    SESSION_STARTED.call_once(|| {
74        let log_path = get_log_file_path();
75        let timestamp = SystemTime::now()
76            .duration_since(UNIX_EPOCH)
77            .unwrap_or_default()
78            .as_secs();
79
80        // Create new log file with session header
81        if let Ok(mut file) = OpenOptions::new()
82            .create(true)
83            .write(true)
84            .truncate(true)
85            .open(&log_path)
86        {
87            // Format timestamp as readable date
88            let datetime =
89                chrono::DateTime::from_timestamp(timestamp as i64, 0).unwrap_or(chrono::Utc::now());
90            let formatted_date = datetime.format("%Y-%m-%d %H:%M:%S UTC");
91
92            let _ = writeln!(
93                file,
94                "=== Light Program Test Session Started at {} ===\n",
95                formatted_date
96            );
97        }
98    });
99}
100
101/// Strip ANSI escape codes from string for plain text log files
102fn strip_ansi_codes(text: &str) -> String {
103    // Simple regex-free approach to remove ANSI escape sequences
104    let mut result = String::with_capacity(text.len());
105    let mut chars = text.chars();
106
107    while let Some(ch) = chars.next() {
108        if ch == '\x1b' {
109            // Found escape character, skip until we find 'm' (end of color code)
110            for next_ch in chars.by_ref() {
111                if next_ch == 'm' {
112                    break;
113                }
114            }
115        } else {
116            result.push(ch);
117        }
118    }
119
120    result
121}
122
123/// Write log entry to file (append to existing session log)
124fn write_to_log_file(content: &str) {
125    // Ensure session is initialized
126    initialize_log_file();
127
128    let log_path = get_log_file_path();
129
130    // Ensure parent directory exists
131    if let Some(parent) = log_path.parent() {
132        let _ = std::fs::create_dir_all(parent);
133    }
134
135    // Strip ANSI color codes for file output
136    let clean_content = strip_ansi_codes(content);
137
138    // Append transaction log to existing file
139    if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&log_path) {
140        let _ = writeln!(file, "{}", clean_content);
141    }
142}
143
144/// Main entry point for enhanced transaction logging
145pub fn log_transaction_enhanced(
146    config: &ProgramTestConfig,
147    transaction: &Transaction,
148    result: &TransactionResult,
149    signature: &Signature,
150    slot: u64,
151    transaction_counter: usize,
152) {
153    log_transaction_enhanced_with_console(
154        config,
155        transaction,
156        result,
157        signature,
158        slot,
159        transaction_counter,
160        false,
161    )
162}
163
164/// Enhanced transaction logging with console output control
165pub fn log_transaction_enhanced_with_console(
166    config: &ProgramTestConfig,
167    transaction: &Transaction,
168    result: &TransactionResult,
169    signature: &Signature,
170    slot: u64,
171    transaction_counter: usize,
172    print_to_console: bool,
173) {
174    if !config.enhanced_logging.enabled {
175        return;
176    }
177
178    let enhanced_log = EnhancedTransactionLog::from_transaction_result(
179        transaction,
180        result,
181        signature,
182        slot,
183        &config.enhanced_logging,
184    );
185
186    let formatter = TransactionFormatter::new(&config.enhanced_logging);
187    let formatted_log = formatter.format(&enhanced_log, transaction_counter);
188
189    // Always write to log file when enhanced logging is enabled
190    write_to_log_file(&formatted_log);
191
192    // Print to console if requested
193    if print_to_console {
194        println!("{}", formatted_log);
195    }
196}
197
198/// Check if enhanced logging should be used instead of basic logging
199pub fn should_use_enhanced_logging(config: &ProgramTestConfig) -> bool {
200    config.enhanced_logging.enabled && !config.no_logs
201}