edb_common/
logging.rs

1// EDB - Ethereum Debugger
2// Copyright (C) 2024 Zhuo Zhang and Wuqi Zhang
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! Fancy logging configuration for EDB components
18//!
19//! Provides centralized logging setup with:
20//! - Colorful console output with structured formatting
21//! - File logging to temporary directory
22//! - Environment variable support (RUST_LOG)
23//! - Default INFO level with beautiful styling
24
25use eyre::Result;
26use std::{env, fs, path::PathBuf, sync::Once};
27use tracing::Level;
28use tracing_appender::{non_blocking, rolling};
29use tracing_subscriber::{
30    fmt::{self, format::FmtSpan, time::LocalTime},
31    layer::SubscriberExt,
32    util::SubscriberInitExt,
33    EnvFilter, Layer,
34};
35
36/// Initialize fancy logging for EDB components
37///
38/// This function sets up:
39/// - Colorful, structured console logging with timestamps
40/// - File logging to a temporary directory with daily rotation
41/// - Environment variable support for log levels (RUST_LOG)
42/// - Default INFO level if no RUST_LOG is set
43/// - Beautiful formatting with component names and spans
44///
45/// # Arguments
46/// * `component_name` - Name of the component (e.g., "edb", "edb-rpc-proxy")
47/// * `enable_file_logging` - Whether to enable file logging (default: true)
48///
49/// # Returns
50/// * `Result<()>` - Success or error from logging initialization
51///
52/// # Examples
53/// ```rust
54/// use edb_common::logging;
55///
56/// #[tokio::main]
57/// async fn main() -> eyre::Result<()> {
58///     // Initialize logging for the main EDB binary
59///     logging::init_logging("edb", true)?;
60///     
61///     tracing::info!("Application started");
62///     Ok(())
63/// }
64/// ```
65pub fn init_logging(component_name: &str, enable_file_logging: bool) -> Result<()> {
66    // Create environment filter with default INFO level
67    let env_filter = EnvFilter::try_from_default_env()
68        .or_else(|_| EnvFilter::try_new("info"))
69        .expect("Failed to create environment filter");
70
71    // Create beautiful console layer with colors and formatting
72    let console_layer = fmt::layer()
73        .with_target(true)
74        .with_thread_ids(true)
75        .with_thread_names(true)
76        .with_file(true)
77        .with_line_number(true)
78        .with_span_events(FmtSpan::CLOSE)
79        .with_timer(LocalTime::rfc_3339())
80        .with_ansi(true) // Enable colors
81        .pretty(); // Use pretty formatting
82
83    if enable_file_logging {
84        // Create log directory in temp folder
85        let log_dir = create_log_directory(component_name)?;
86
87        // Create file appender with daily rotation
88        let file_appender = rolling::daily(&log_dir, format!("{component_name}.log"));
89        let (non_blocking_appender, guard) = non_blocking(file_appender);
90
91        // Store guard to prevent it from being dropped
92        // In a real application, you'd want to store this somewhere persistent
93        std::mem::forget(guard);
94
95        // Create file layer (without colors for file output)
96        let file_layer = fmt::layer()
97            .with_target(true)
98            .with_thread_ids(true)
99            .with_thread_names(true)
100            .with_file(true)
101            .with_line_number(true)
102            .with_span_events(FmtSpan::CLOSE)
103            .with_timer(LocalTime::rfc_3339())
104            .with_ansi(false) // No colors in files
105            .with_writer(non_blocking_appender);
106
107        // Initialize subscriber with both console and file layers
108        tracing_subscriber::registry()
109            .with(env_filter)
110            .with(console_layer.with_filter(filter_for_console()))
111            .with(file_layer.with_filter(filter_for_file()))
112            .try_init()
113            .map_err(|e| eyre::eyre!("Failed to initialize tracing subscriber: {}", e))?;
114
115        tracing::info!(
116            component = component_name,
117            log_dir = %log_dir.display(),
118            "Logging initialized with console and file output"
119        );
120    } else {
121        // Initialize subscriber with only console layer
122        tracing_subscriber::registry()
123            .with(env_filter)
124            .with(console_layer)
125            .try_init()
126            .map_err(|e| eyre::eyre!("Failed to initialize tracing subscriber: {}", e))?;
127
128        tracing::info!(component = component_name, "Logging initialized with console output only");
129    }
130
131    // Log some useful information
132    log_environment_info(component_name);
133
134    Ok(())
135}
136
137/// Create log directory in system temp folder
138fn create_log_directory(component_name: &str) -> Result<PathBuf> {
139    let temp_dir = env::temp_dir();
140    let log_dir = temp_dir.join("edb-logs").join(component_name);
141
142    fs::create_dir_all(&log_dir)?;
143
144    Ok(log_dir)
145}
146
147/// Filter for console output - show everything
148fn filter_for_console() -> EnvFilter {
149    EnvFilter::from_default_env()
150        .add_directive("tower_http=warn".parse().unwrap()) // Reduce HTTP noise
151        .add_directive("hyper=warn".parse().unwrap()) // Reduce HTTP noise
152        .add_directive("reqwest=warn".parse().unwrap()) // Reduce HTTP noise
153}
154
155/// Filter for file output - be more verbose for debugging
156fn filter_for_file() -> EnvFilter {
157    EnvFilter::from_default_env()
158}
159
160/// Log useful environment and system information
161fn log_environment_info(component_name: &str) {
162    let rust_log = env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
163    let args: Vec<String> = env::args().collect();
164
165    tracing::info!(
166        component = component_name,
167        rust_log = %rust_log,
168        args = ?args,
169        "Environment information"
170    );
171
172    if let Ok(current_dir) = env::current_dir() {
173        tracing::debug!(
174            working_directory = %current_dir.display(),
175            "Working directory"
176        );
177    }
178}
179
180/// Initialize file-only logging for TUI applications
181///
182/// This function sets up logging that only writes to a file, not to stdout/stderr.
183/// This is essential for TUI applications that need full control over the terminal.
184///
185/// # Arguments
186/// * `component_name` - Name of the component (e.g., "edb-tui")
187///
188/// # Returns
189/// * `Result<PathBuf>` - The path to the log file on success
190///
191/// # Examples
192/// ```rust
193/// use edb_common::logging;
194///
195/// #[tokio::main]
196/// async fn main() -> eyre::Result<()> {
197///     // Initialize file-only logging for TUI
198///     let log_path = logging::init_file_only_logging("edb-tui")?;
199///     eprintln!("Logs are being written to: {}", log_path.display());
200///     
201///     tracing::info!("TUI started");
202///     Ok(())
203/// }
204/// ```
205pub fn init_file_only_logging(component_name: &str) -> Result<PathBuf> {
206    // Create environment filter with default INFO level
207    let env_filter = EnvFilter::try_from_default_env()
208        .or_else(|_| EnvFilter::try_new("info"))
209        .expect("Failed to create environment filter");
210
211    // Create log directory in temp folder
212    let log_dir = create_log_directory(component_name)?;
213    let log_file_path = log_dir.join(format!("{component_name}.log"));
214
215    // Create file appender with daily rotation
216    let file_appender = rolling::daily(&log_dir, format!("{component_name}.log"));
217    let (non_blocking_appender, guard) = non_blocking(file_appender);
218
219    // Store guard to prevent it from being dropped
220    std::mem::forget(guard);
221
222    // Create file-only layer (no console output)
223    let file_layer = fmt::layer()
224        .with_target(true)
225        .with_thread_ids(false) // Less verbose for TUI logs
226        .with_thread_names(false)
227        .with_file(true)
228        .with_line_number(true)
229        .with_span_events(FmtSpan::CLOSE)
230        .with_timer(LocalTime::rfc_3339())
231        .with_ansi(false) // No colors in files
232        .with_writer(non_blocking_appender);
233
234    // Initialize subscriber with only file layer (no console output)
235    tracing_subscriber::registry()
236        .with(env_filter)
237        .with(file_layer)
238        .try_init()
239        .map_err(|e| eyre::eyre!("Failed to initialize TUI file logging: {}", e))?;
240
241    tracing::info!(
242        component = component_name,
243        log_file = %log_file_path.display(),
244        "File-only logging initialized"
245    );
246
247    // Log some useful information
248    log_environment_info(component_name);
249
250    Ok(log_file_path)
251}
252
253/// Initialize simple logging (console only, no fancy formatting)
254///
255/// This is useful for tests or simple utilities that don't need
256/// the full fancy logging setup.
257///
258/// # Arguments  
259/// * `level` - The default log level to use
260pub fn init_simple_logging(level: Level) -> Result<()> {
261    let env_filter = EnvFilter::try_from_default_env()
262        .or_else(|_| EnvFilter::try_new(level.as_str()))
263        .expect("Failed to create environment filter");
264
265    tracing_subscriber::fmt()
266        .with_env_filter(env_filter)
267        .with_target(false)
268        .compact()
269        .try_init()
270        .map_err(|e| eyre::eyre!("Failed to initialize simple logging: {}", e))?;
271
272    Ok(())
273}
274
275// Global test logging initialization - ensures logging is only set up once across all tests
276static TEST_LOGGING_INIT: Once = Once::new();
277
278/// Safe logging initialization for tests - can be called multiple times without crashing
279///
280/// This function provides a safe way for tests to enable logging without worrying about
281/// whether a tracing subscriber has already been initialized. It uses `std::sync::Once`
282/// to ensure initialization happens only once per test process.
283///
284/// Features:
285/// - Console-only output (no file logging for tests)
286/// - DEBUG level by default, but respects RUST_LOG environment variable
287/// - Can be called from any test file safely
288/// - Idempotent - multiple calls are safe and efficient
289///
290/// # Usage
291/// ```rust,ignore
292/// use edb_common::logging;
293/// use tracing::info;
294///
295/// #[test]
296/// fn my_test() {
297///     logging::ensure_test_logging();
298///     info!("This will work safely in any test!");
299///     // ... rest of test
300/// }
301/// ```
302pub fn ensure_test_logging(default_level: Option<Level>) {
303    TEST_LOGGING_INIT.call_once(|| {
304        // Initialize simple console-only logging for tests
305        // Default to INFO but respect RUST_LOG if set
306        let default_level = default_level.unwrap_or(Level::INFO);
307        let _ = init_simple_logging(default_level);
308        // Ignore any errors - if initialization fails, that's usually because
309        // a subscriber is already set up, which is fine for tests
310    });
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use tracing::{debug, error, info, warn};
317
318    // Use the public ensure_test_logging function
319    fn init_test_logging() {
320        ensure_test_logging(None);
321    }
322
323    #[test]
324    fn test_logging_functions_work() {
325        // This test ensures logging functions work without panicking
326        init_test_logging();
327
328        // Test that we can log without errors
329        info!("Test info message");
330        warn!("Test warning message");
331        debug!("Test debug message");
332        error!("Test error message");
333
334        // Test passes if no panic occurs
335    }
336
337    #[test]
338    fn test_log_directory_creation() {
339        // Test that we can create log directories
340        let result = create_log_directory("test-component");
341        assert!(result.is_ok());
342
343        let log_dir = result.unwrap();
344        assert!(log_dir.exists());
345        assert!(log_dir.to_string_lossy().contains("edb-logs"));
346        assert!(log_dir.to_string_lossy().contains("test-component"));
347    }
348
349    #[test]
350    fn test_environment_filters() {
351        // Test that filters can be created without errors
352        let console_filter = filter_for_console();
353        let file_filter = filter_for_file();
354
355        // Both should be valid filters (non-empty string representation)
356        assert!(!console_filter.to_string().is_empty());
357        assert!(!file_filter.to_string().is_empty());
358    }
359
360    #[test]
361    fn test_fancy_logging_initialization_safety() {
362        // Test that fancy logging handles multiple initialization attempts gracefully
363        init_test_logging(); // Ensure something is already initialized
364
365        // These calls should not panic, even if subscriber is already initialized
366        let result1 = init_logging("test-fancy-1", false);
367        let result2 = init_logging("test-fancy-2", false);
368
369        // One or both may fail due to already initialized subscriber, but should not panic
370        // The important thing is that the function handles the error case gracefully
371        match (result1, result2) {
372            (Ok(_), _) => {}       // First succeeded
373            (Err(_), Ok(_)) => {}  // Second succeeded
374            (Err(_), Err(_)) => {} // Both failed gracefully
375        }
376
377        // Verify logging still works after initialization attempts
378        info!("Test logging after fancy init attempts");
379    }
380}