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}