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_DATA_DIR: when using file mode, the path of the log file (default "logs/relayer.log")
7
8use chrono::Utc;
9use log::info;
10use simplelog::{Config, LevelFilter, SimpleLogger, WriteLogger};
11use std::{
12    env,
13    fs::{create_dir_all, metadata, File, OpenOptions},
14    path::Path,
15};
16
17/// Computes the path of the rolled log file given the base file path and the date string.
18pub fn compute_rolled_file_path(base_file_path: &str, date_str: &str, index: u32) -> String {
19    if base_file_path.ends_with(".log") {
20        let trimmed = base_file_path.strip_suffix(".log").unwrap();
21        format!("{}-{}.{}.log", trimmed, date_str, index)
22    } else {
23        format!("{}-{}.{}.log", base_file_path, date_str, index)
24    }
25}
26
27/// Generates a time-based log file name.
28/// This is simply a wrapper around `compute_rolled_file_path` for clarity.
29pub fn time_based_rolling(base_file_path: &str, date_str: &str, index: u32) -> String {
30    compute_rolled_file_path(base_file_path, date_str, index)
31}
32
33/// Checks if the given log file exceeds the maximum allowed size (in bytes).
34/// If so, it appends a sequence number to generate a new file name.
35/// Returns the final log file path to use.
36/// - `file_path`: the initial time-based log file path.
37/// - `base_file_path`: the original base log file path.
38/// - `date_str`: the current date string.
39/// - `max_size`: maximum file size in bytes (e.g., 1GB).
40pub fn space_based_rolling(
41    file_path: &str,
42    base_file_path: &str,
43    date_str: &str,
44    max_size: u64,
45) -> String {
46    let mut final_path = file_path.to_string();
47    let mut index = 1;
48    while let Ok(metadata) = metadata(&final_path) {
49        if metadata.len() > max_size {
50            final_path = compute_rolled_file_path(base_file_path, date_str, index);
51            index += 1;
52        } else {
53            break;
54        }
55    }
56    final_path
57}
58
59/// Sets up logging by reading configuration from environment variables.
60pub fn setup_logging() {
61    let log_mode = env::var("LOG_MODE").unwrap_or_else(|_| "stdout".to_string());
62    let log_level = env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string());
63    // Parse the log level into LevelFilter
64    let level_filter = match log_level.to_lowercase().as_str() {
65        "trace" => LevelFilter::Trace,
66        "debug" => LevelFilter::Debug,
67        "info" => LevelFilter::Info,
68        "warn" => LevelFilter::Warn,
69        "error" => LevelFilter::Error,
70        _ => LevelFilter::Info,
71    };
72
73    // Only run if log_mode is "file"
74    if log_mode.to_lowercase() == "file" {
75        info!("Logging to file: {}", log_level);
76
77        // Use logs/ directly in container path, otherwise use LOG_DATA_DIR or default to logs/ for host path
78        let log_dir = if env::var("IN_DOCKER")
79            .map(|val| val == "true")
80            .unwrap_or(false)
81        {
82            "logs/".to_string()
83        } else {
84            env::var("LOG_DATA_DIR").unwrap_or_else(|_| "./logs".to_string())
85        };
86
87        let log_dir = format!("{}/", log_dir.trim_end_matches('/'));
88        // set dates
89        let now = Utc::now();
90        let date_str = now.format("%Y-%m-%d").to_string();
91
92        // Get log file path from environment or use default
93        let base_file_path = format!("{}relayer.log", log_dir);
94
95        // verify the log file already exists
96        if Path::new(&base_file_path).exists() {
97            info!(
98                "Base Log file already exists: {}. Proceeding to compute rolled log file path.",
99                base_file_path
100            );
101        }
102
103        // Time-based rolling: compute file name based on the current UTC date.
104        let time_based_path = time_based_rolling(&base_file_path, &date_str, 1);
105
106        // Ensure parent directory exists.
107        if let Some(parent) = Path::new(&time_based_path).parent() {
108            create_dir_all(parent).expect("Failed to create log directory");
109        }
110
111        // Space-based rolling: if an existing log file exceeds 1GB, adopt a new file name.
112        let max_size: u64 = env::var("LOG_MAX_SIZE")
113            .map(|s| {
114                s.parse::<u64>()
115                    .expect("LOG_MAX_SIZE must be a valid u64 if set")
116            })
117            .unwrap_or(1_073_741_824);
118
119        let final_path =
120            space_based_rolling(&time_based_path, &base_file_path, &date_str, max_size);
121
122        // Open the log file. Append to it if it exists and is under threshold; otherwise, create
123        // it.
124        let log_file = if Path::new(&final_path).exists() {
125            OpenOptions::new()
126                .append(true)
127                .open(&final_path)
128                .unwrap_or_else(|e| panic!("Unable to open log file {}: {}", final_path, e))
129        } else {
130            File::create(&final_path)
131                .unwrap_or_else(|e| panic!("Unable to create log file {}: {}", final_path, e))
132        };
133        WriteLogger::init(level_filter, Config::default(), log_file)
134            .expect("Failed to initialize file logger");
135    } else {
136        // Default to stdout logging
137        SimpleLogger::init(level_filter, Config::default())
138            .expect("Failed to initialize simple logger");
139    }
140
141    info!("Logging is successfully configured (mode: {})", log_mode);
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use std::fs::File;
148    use std::io::Write;
149    use std::sync::Once;
150    use tempfile::tempdir;
151
152    // Use this to ensure logger is only initialized once across all tests
153    static INIT_LOGGER: Once = Once::new();
154
155    #[test]
156    fn test_compute_rolled_file_path() {
157        // Test with .log extension
158        let result = compute_rolled_file_path("app.log", "2023-01-01", 1);
159        assert_eq!(result, "app-2023-01-01.1.log");
160
161        // Test without .log extension
162        let result = compute_rolled_file_path("app", "2023-01-01", 2);
163        assert_eq!(result, "app-2023-01-01.2.log");
164
165        // Test with path
166        let result = compute_rolled_file_path("logs/app.log", "2023-01-01", 3);
167        assert_eq!(result, "logs/app-2023-01-01.3.log");
168    }
169
170    #[test]
171    fn test_time_based_rolling() {
172        // This is just a wrapper around compute_rolled_file_path
173        let result = time_based_rolling("app.log", "2023-01-01", 1);
174        assert_eq!(result, "app-2023-01-01.1.log");
175    }
176
177    #[test]
178    fn test_space_based_rolling() {
179        // Create a temporary directory for testing
180        let temp_dir = tempdir().expect("Failed to create temp directory");
181        let base_path = temp_dir
182            .path()
183            .join("test.log")
184            .to_str()
185            .unwrap()
186            .to_string();
187
188        // Test when file doesn't exist
189        let result = space_based_rolling(&base_path, &base_path, "2023-01-01", 100);
190        assert_eq!(result, base_path);
191
192        // Create a file larger than max_size
193        {
194            let mut file = File::create(&base_path).expect("Failed to create test file");
195            file.write_all(&[0; 200])
196                .expect("Failed to write to test file");
197        }
198
199        // Test when file exists and is larger than max_size
200        let expected_path = compute_rolled_file_path(&base_path, "2023-01-01", 1);
201        let result = space_based_rolling(&base_path, &base_path, "2023-01-01", 100);
202        assert_eq!(result, expected_path);
203
204        // Create multiple files to test sequential numbering
205        {
206            let mut file = File::create(&expected_path).expect("Failed to create test file");
207            file.write_all(&[0; 200])
208                .expect("Failed to write to test file");
209        }
210
211        // Test sequential numbering
212        let expected_path2 = compute_rolled_file_path(&base_path, "2023-01-01", 2);
213        let result = space_based_rolling(&base_path, &base_path, "2023-01-01", 100);
214        assert_eq!(result, expected_path2);
215    }
216
217    #[test]
218    fn test_logging_configuration() {
219        // We'll test both configurations in a single test to avoid multiple logger initializations
220
221        // First test stdout configuration
222        {
223            // Set environment variables for testing
224            env::set_var("LOG_MODE", "stdout");
225            env::set_var("LOG_LEVEL", "debug");
226
227            // Initialize logger only once across all tests
228            INIT_LOGGER.call_once(|| {
229                setup_logging();
230            });
231
232            // Clean up
233            env::remove_var("LOG_MODE");
234            env::remove_var("LOG_LEVEL");
235        }
236
237        // Now test file configuration without reinitializing the logger
238        {
239            // Create a temporary directory for testing
240            let temp_dir = tempdir().expect("Failed to create temp directory");
241            let log_path = temp_dir
242                .path()
243                .join("test_logs")
244                .to_str()
245                .unwrap()
246                .to_string();
247
248            // Set environment variables for testing
249            env::set_var("LOG_MODE", "file");
250            env::set_var("LOG_LEVEL", "info");
251            env::set_var("LOG_DATA_DIR", &log_path);
252            env::set_var("LOG_MAX_SIZE", "1024"); // 1KB for testing
253
254            // We don't call setup_logging() again, but we can test the directory creation logic
255            if let Some(parent) = Path::new(&format!("{}/relayer.log", log_path)).parent() {
256                create_dir_all(parent).expect("Failed to create log directory");
257            }
258
259            // Verify the log directory was created
260            assert!(Path::new(&log_path).exists());
261
262            // Clean up
263            env::remove_var("LOG_MODE");
264            env::remove_var("LOG_LEVEL");
265            env::remove_var("LOG_DATA_DIR");
266            env::remove_var("LOG_MAX_SIZE");
267        }
268    }
269}