openzeppelin_monitor/utils/logging/
mod.rs1pub mod error;
11
12use chrono::Utc;
13use std::{
14 env,
15 fs::{create_dir_all, metadata},
16 path::Path,
17};
18use tracing::info;
19use tracing_appender;
20use tracing_subscriber::{filter::EnvFilter, fmt, prelude::*};
21
22use tracing::Subscriber;
23use tracing_subscriber::fmt::format::Writer;
24use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
25use tracing_subscriber::registry::LookupSpan;
26
27struct StripAnsiFormatter<T> {
29 inner: T,
30}
31
32impl<T> StripAnsiFormatter<T> {
33 fn new(inner: T) -> Self {
34 Self { inner }
35 }
36}
37
38impl<S, N, T> FormatEvent<S, N> for StripAnsiFormatter<T>
39where
40 S: Subscriber + for<'a> LookupSpan<'a>,
41 N: for<'a> FormatFields<'a> + 'static,
42 T: FormatEvent<S, N>,
43{
44 fn format_event(
45 &self,
46 ctx: &FmtContext<'_, S, N>,
47 mut writer: Writer<'_>,
48 event: &tracing::Event<'_>,
49 ) -> std::fmt::Result {
50 let mut buf = String::new();
52 let string_writer = Writer::new(&mut buf);
53
54 self.inner.format_event(ctx, string_writer, event)?;
56
57 let stripped = strip_ansi_escapes(&buf);
59
60 write!(writer, "{}", stripped)
62 }
63}
64
65fn strip_ansi_escapes(s: &str) -> String {
67 let re = regex::Regex::new(r"\x1b\[[0-9;]*[a-zA-Z]").unwrap();
70 re.replace_all(s, "").to_string()
71}
72
73pub fn compute_rolled_file_path(base_file_path: &str, date_str: &str, index: u32) -> String {
75 let trimmed = base_file_path
76 .strip_suffix(".log")
77 .unwrap_or(base_file_path);
78 format!("{}-{}.{}.log", trimmed, date_str, index)
79}
80
81pub fn space_based_rolling(
89 file_path: &str,
90 base_file_path: &str,
91 date_str: &str,
92 max_size: u64,
93) -> String {
94 let mut final_path = file_path.to_string();
95 let mut index = 1;
96 while let Ok(metadata) = metadata(&final_path) {
97 if metadata.len() > max_size {
98 final_path = compute_rolled_file_path(base_file_path, date_str, index);
99 index += 1;
100 } else {
101 break;
102 }
103 }
104 final_path
105}
106
107fn create_log_format(with_ansi: bool) -> fmt::format::Format<fmt::format::Compact> {
109 fmt::format()
110 .with_level(true)
111 .with_target(true)
112 .with_thread_ids(false)
113 .with_thread_names(false)
114 .with_ansi(with_ansi)
115 .compact()
116}
117
118pub fn setup_logging() -> Result<(), Box<dyn std::error::Error>> {
120 let log_mode = env::var("LOG_MODE").unwrap_or_else(|_| "stdout".to_string());
121 let log_level = env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string());
122
123 let level_filter = match log_level.to_lowercase().as_str() {
125 "trace" => tracing::Level::TRACE,
126 "debug" => tracing::Level::DEBUG,
127 "info" => tracing::Level::INFO,
128 "warn" => tracing::Level::WARN,
129 "error" => tracing::Level::ERROR,
130 _ => tracing::Level::INFO,
131 };
132
133 let with_ansi = log_mode.to_lowercase() != "file";
135 let format = create_log_format(with_ansi);
136
137 let subscriber = tracing_subscriber::registry().with(EnvFilter::new(level_filter.to_string()));
139
140 if log_mode.to_lowercase() == "file" {
141 info!("Logging to file: {}", log_level);
142
143 let log_dir = if env::var("IN_DOCKER")
145 .map(|val| val == "true")
146 .unwrap_or(false)
147 {
148 "logs/".to_string()
149 } else {
150 env::var("LOG_DATA_DIR").unwrap_or_else(|_| "logs/".to_string())
151 };
152
153 let log_dir = format!("{}/", log_dir.trim_end_matches('/'));
154 let now = Utc::now();
156 let date_str = now.format("%Y-%m-%d").to_string();
157
158 let base_file_path = format!("{}monitor.log", log_dir);
160
161 if Path::new(&base_file_path).exists() {
163 info!(
164 "Base Log file already exists: {}. Proceeding to compute rolled log file path.",
165 base_file_path
166 );
167 }
168
169 let time_based_path = compute_rolled_file_path(&base_file_path, &date_str, 1);
171
172 if let Some(parent) = Path::new(&time_based_path).parent() {
174 create_dir_all(parent).expect("Failed to create log directory");
175 }
176
177 let max_size = parse_log_max_size();
179
180 let final_path =
181 space_based_rolling(&time_based_path, &base_file_path, &date_str, max_size);
182
183 let file_appender = tracing_appender::rolling::never(
185 Path::new(&final_path).parent().unwrap_or(Path::new(".")),
186 Path::new(&final_path).file_name().unwrap_or_default(),
187 );
188
189 let ansi_stripped_format = StripAnsiFormatter::new(format);
190
191 subscriber
192 .with(
193 fmt::layer()
194 .event_format(ansi_stripped_format)
195 .with_writer(file_appender)
196 .fmt_fields(fmt::format::PrettyFields::new()),
197 )
198 .init();
199 } else {
200 subscriber
202 .with(
203 fmt::layer()
204 .event_format(format)
205 .fmt_fields(fmt::format::PrettyFields::new()),
206 )
207 .init();
208 }
209
210 info!("Logging is successfully configured (mode: {})", log_mode);
211 Ok(())
212}
213
214fn parse_log_max_size() -> u64 {
215 env::var("LOG_MAX_SIZE")
216 .map(|s| {
217 s.parse::<u64>()
218 .expect("LOG_MAX_SIZE must be a valid u64 if set")
219 })
220 .unwrap_or(1_073_741_824)
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use std::fs::File;
227 use std::io::Write;
228 use tempfile::tempdir;
229
230 #[test]
231 fn test_strip_ansi_escapes() {
232 let input = "\x1b[31mRed text\x1b[0m and \x1b[32mgreen text\x1b[0m";
233 let expected = "Red text and green text";
234 assert_eq!(strip_ansi_escapes(input), expected);
235 }
236
237 #[test]
238 fn test_compute_rolled_file_path() {
239 let result = compute_rolled_file_path("app.log", "2023-01-01", 1);
241 assert_eq!(result, "app-2023-01-01.1.log");
242
243 let result = compute_rolled_file_path("app", "2023-01-01", 2);
245 assert_eq!(result, "app-2023-01-01.2.log");
246
247 let result = compute_rolled_file_path("logs/app.log", "2023-01-01", 3);
249 assert_eq!(result, "logs/app-2023-01-01.3.log");
250 }
251
252 #[test]
253 fn test_space_based_rolling() {
254 let dir = tempdir().expect("Failed to create temp directory");
256 let base_path = dir.path().join("test.log").to_str().unwrap().to_string();
257 let date_str = "2023-01-01";
258
259 let initial_path = compute_rolled_file_path(&base_path, date_str, 1);
261 {
262 let mut file = File::create(&initial_path).expect("Failed to create test file");
263 file.write_all(&[0; 100])
265 .expect("Failed to write to test file");
266 }
267
268 let result = space_based_rolling(&initial_path, &base_path, date_str, 50);
270 assert_eq!(result, compute_rolled_file_path(&base_path, date_str, 2));
271
272 let result = space_based_rolling(&initial_path, &base_path, date_str, 200);
274 assert_eq!(result, initial_path);
275 }
276
277 #[test]
279 #[should_panic(expected = "LOG_MAX_SIZE must be a valid u64 if set")]
280 fn test_invalid_log_max_size_panics() {
281 std::env::set_var("LOG_MAX_SIZE", "not_a_number");
282 let _ = parse_log_max_size(); }
284}