openzeppelin_relayer/logging/
mod.rs

1//! ## Sets up logging by reading configuration from environment variables.
2//!
3//! Environment variables used:
4//! - LOG_MODE: "stdout" (default) or "file"
5//! - LOG_LEVEL: log level ("trace", "debug", "info", "warn", "error"); default is "info"
6//! - LOG_FORMAT: output format ("compact" (default), "pretty", "json")
7//! - LOG_DATA_DIR: when using file mode, the path of the log file (default "logs/relayer.log")
8
9use chrono::Utc;
10use std::{
11    env,
12    fs::{create_dir_all, metadata, File, OpenOptions},
13    path::Path,
14};
15use tracing::info;
16use tracing_appender::non_blocking;
17use tracing_error::ErrorLayer;
18use tracing_subscriber::{fmt, prelude::*, EnvFilter};
19
20use crate::constants::{
21    DEFAULT_LOG_DIR, DEFAULT_LOG_FORMAT, DEFAULT_LOG_LEVEL, DEFAULT_LOG_MODE,
22    DEFAULT_MAX_LOG_FILE_SIZE, DOCKER_LOG_DIR, LOG_FILE_NAME,
23};
24
25/// Computes the path of the rolled log file given the base file path and the date string.
26pub fn compute_rolled_file_path(base_file_path: &str, date_str: &str, index: u32) -> String {
27    if base_file_path.ends_with(".log") {
28        let trimmed = base_file_path.strip_suffix(".log").unwrap();
29        format!("{trimmed}-{date_str}.{index}.log")
30    } else {
31        format!("{base_file_path}-{date_str}.{index}.log")
32    }
33}
34
35/// Generates a time-based log file name.
36/// This is simply a wrapper around `compute_rolled_file_path` for clarity.
37pub fn time_based_rolling(base_file_path: &str, date_str: &str, index: u32) -> String {
38    compute_rolled_file_path(base_file_path, date_str, index)
39}
40
41/// Checks if the given log file exceeds the maximum allowed size (in bytes).
42/// If so, it appends a sequence number to generate a new file name.
43/// Returns the final log file path to use.
44/// - `file_path`: the initial time-based log file path.
45/// - `base_file_path`: the original base log file path.
46/// - `date_str`: the current date string.
47/// - `max_size`: maximum file size in bytes (e.g., 1GB).
48pub fn space_based_rolling(
49    file_path: &str,
50    base_file_path: &str,
51    date_str: &str,
52    max_size: u64,
53) -> String {
54    let mut final_path = file_path.to_string();
55    let mut index = 1;
56    while let Ok(metadata) = metadata(&final_path) {
57        if metadata.len() > max_size {
58            final_path = compute_rolled_file_path(base_file_path, date_str, index);
59            index += 1;
60        } else {
61            break;
62        }
63    }
64    final_path
65}
66
67/// Sets up logging by reading configuration from environment variables.
68pub fn setup_logging() {
69    // Set RUST_LOG from LOG_LEVEL if RUST_LOG is not already set
70    if std::env::var_os("RUST_LOG").is_none() {
71        if let Ok(level) = env::var("LOG_LEVEL") {
72            std::env::set_var("RUST_LOG", level);
73        }
74    }
75
76    // Configure filter, format, and mode from environment
77    let env_filter =
78        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(DEFAULT_LOG_LEVEL));
79    let format = env::var("LOG_FORMAT").unwrap_or_else(|_| DEFAULT_LOG_FORMAT.to_string());
80    let log_mode = env::var("LOG_MODE").unwrap_or_else(|_| DEFAULT_LOG_MODE.to_string());
81
82    // Set up logging based on mode
83    if log_mode.eq_ignore_ascii_case("file") {
84        // File logging setup
85        let log_dir = if env::var("IN_DOCKER").ok().as_deref() == Some("true") {
86            DOCKER_LOG_DIR.to_string()
87        } else {
88            env::var("LOG_DATA_DIR").unwrap_or_else(|_| DEFAULT_LOG_DIR.to_string())
89        };
90        let log_dir = format!("{}/", log_dir.trim_end_matches('/'));
91
92        let now = Utc::now();
93        let date_str = now.format("%Y-%m-%d").to_string();
94        let base_file_path = format!("{log_dir}{LOG_FILE_NAME}");
95
96        if let Some(parent) = Path::new(&base_file_path).parent() {
97            create_dir_all(parent).expect("Failed to create log directory");
98        }
99
100        let time_based_path = time_based_rolling(&base_file_path, &date_str, 1);
101        let max_size = match env::var("LOG_MAX_SIZE") {
102            Ok(value) => value.parse().unwrap_or_else(|_| {
103                panic!("LOG_MAX_SIZE must be a valid u64 if set");
104            }),
105            Err(_) => DEFAULT_MAX_LOG_FILE_SIZE,
106        };
107        let final_path =
108            space_based_rolling(&time_based_path, &base_file_path, &date_str, max_size);
109
110        let file = if Path::new(&final_path).exists() {
111            OpenOptions::new()
112                .append(true)
113                .open(&final_path)
114                .expect("Failed to open log file")
115        } else {
116            File::create(&final_path).expect("Failed to create log file")
117        };
118
119        let (non_blocking_writer, guard) = non_blocking(file);
120        Box::leak(Box::new(guard)); // Keep guard alive for the lifetime of the program
121
122        match format.as_str() {
123            "pretty" => {
124                tracing_subscriber::registry()
125                    .with(env_filter)
126                    .with(ErrorLayer::default())
127                    .with(
128                        fmt::layer()
129                            .with_writer(non_blocking_writer)
130                            .with_ansi(false)
131                            .pretty()
132                            .with_thread_ids(true)
133                            .with_file(true)
134                            .with_line_number(true),
135                    )
136                    .init();
137            }
138            "json" => {
139                tracing_subscriber::registry()
140                    .with(env_filter)
141                    .with(ErrorLayer::default())
142                    .with(
143                        fmt::layer()
144                            .with_writer(non_blocking_writer)
145                            .with_ansi(false)
146                            .json()
147                            .with_current_span(true)
148                            .with_span_list(true)
149                            .with_thread_ids(true)
150                            .with_file(true)
151                            .with_line_number(true),
152                    )
153                    .init();
154            }
155            _ => {
156                // compact is default
157                tracing_subscriber::registry()
158                    .with(env_filter)
159                    .with(ErrorLayer::default())
160                    .with(
161                        fmt::layer()
162                            .with_writer(non_blocking_writer)
163                            .with_ansi(false)
164                            .compact()
165                            .with_target(false),
166                    )
167                    .init();
168            }
169        }
170    } else {
171        // Stdout logging
172        match format.as_str() {
173            "pretty" => {
174                tracing_subscriber::registry()
175                    .with(env_filter)
176                    .with(ErrorLayer::default())
177                    .with(
178                        fmt::layer()
179                            .pretty()
180                            .with_thread_ids(true)
181                            .with_file(true)
182                            .with_line_number(true),
183                    )
184                    .init();
185            }
186            "json" => {
187                tracing_subscriber::registry()
188                    .with(env_filter)
189                    .with(ErrorLayer::default())
190                    .with(
191                        fmt::layer()
192                            .json()
193                            .with_current_span(true)
194                            .with_span_list(true)
195                            .with_thread_ids(true)
196                            .with_file(true)
197                            .with_line_number(true),
198                    )
199                    .init();
200            }
201            _ => {
202                // compact is default
203                tracing_subscriber::registry()
204                    .with(env_filter)
205                    .with(ErrorLayer::default())
206                    .with(fmt::layer().compact().with_target(false))
207                    .init();
208            }
209        }
210    }
211
212    info!(mode=%log_mode, format=%format, "logging configured");
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use std::fs::File;
219    use std::io::Write;
220    use std::sync::Once;
221    use tempfile::tempdir;
222
223    // Use this to ensure logger is only initialized once across all tests
224    static INIT_LOGGER: Once = Once::new();
225
226    #[test]
227    fn test_compute_rolled_file_path() {
228        // Test with .log extension
229        let result = compute_rolled_file_path("app.log", "2023-01-01", 1);
230        assert_eq!(result, "app-2023-01-01.1.log");
231
232        // Test without .log extension
233        let result = compute_rolled_file_path("app", "2023-01-01", 2);
234        assert_eq!(result, "app-2023-01-01.2.log");
235
236        // Test with path
237        let result = compute_rolled_file_path("logs/app.log", "2023-01-01", 3);
238        assert_eq!(result, "logs/app-2023-01-01.3.log");
239    }
240
241    #[test]
242    fn test_time_based_rolling() {
243        // This is just a wrapper around compute_rolled_file_path
244        let result = time_based_rolling("app.log", "2023-01-01", 1);
245        assert_eq!(result, "app-2023-01-01.1.log");
246    }
247
248    #[test]
249    fn test_space_based_rolling() {
250        // Create a temporary directory for testing
251        let temp_dir = tempdir().expect("Failed to create temp directory");
252        let base_path = temp_dir
253            .path()
254            .join("test.log")
255            .to_str()
256            .unwrap()
257            .to_string();
258
259        // Test when file doesn't exist
260        let result = space_based_rolling(&base_path, &base_path, "2023-01-01", 100);
261        assert_eq!(result, base_path);
262
263        // Create a file larger than max_size
264        {
265            let mut file = File::create(&base_path).expect("Failed to create test file");
266            file.write_all(&[0; 200])
267                .expect("Failed to write to test file");
268        }
269
270        // Test when file exists and is larger than max_size
271        let expected_path = compute_rolled_file_path(&base_path, "2023-01-01", 1);
272        let result = space_based_rolling(&base_path, &base_path, "2023-01-01", 100);
273        assert_eq!(result, expected_path);
274
275        // Create multiple files to test sequential numbering
276        {
277            let mut file = File::create(&expected_path).expect("Failed to create test file");
278            file.write_all(&[0; 200])
279                .expect("Failed to write to test file");
280        }
281
282        // Test sequential numbering
283        let expected_path2 = compute_rolled_file_path(&base_path, "2023-01-01", 2);
284        let result = space_based_rolling(&base_path, &base_path, "2023-01-01", 100);
285        assert_eq!(result, expected_path2);
286    }
287
288    #[test]
289    fn test_logging_configuration() {
290        // We'll test both configurations in a single test to avoid multiple logger initializations
291
292        // First test stdout configuration
293        {
294            // Set environment variables for testing
295            env::set_var("LOG_MODE", "stdout");
296            env::set_var("LOG_LEVEL", "debug");
297
298            // Initialize logger only once across all tests
299            INIT_LOGGER.call_once(|| {
300                setup_logging();
301            });
302
303            // Clean up
304            env::remove_var("LOG_MODE");
305            env::remove_var("LOG_LEVEL");
306        }
307
308        // Now test file configuration without reinitializing the logger
309        {
310            // Create a temporary directory for testing
311            let temp_dir = tempdir().expect("Failed to create temp directory");
312            let log_path = temp_dir
313                .path()
314                .join("test_logs")
315                .to_str()
316                .unwrap()
317                .to_string();
318
319            // Set environment variables for testing
320            env::set_var("LOG_MODE", "file");
321            env::set_var("LOG_LEVEL", "info");
322            env::set_var("LOG_DATA_DIR", &log_path);
323            env::set_var("LOG_MAX_SIZE", "1024"); // 1KB for testing
324
325            // We don't call setup_logging() again, but we can test the directory creation logic
326            if let Some(parent) = Path::new(&format!("{}/relayer.log", log_path)).parent() {
327                create_dir_all(parent).expect("Failed to create log directory");
328            }
329
330            // Verify the log directory was created
331            assert!(Path::new(&log_path).exists());
332
333            // Clean up
334            env::remove_var("LOG_MODE");
335            env::remove_var("LOG_LEVEL");
336            env::remove_var("LOG_DATA_DIR");
337            env::remove_var("LOG_MAX_SIZE");
338        }
339    }
340}