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 = env::var("IN_DOCKER")
145 .map(|val| val == "true")
146 .unwrap_or(false)
147 .then(|| "logs/".to_string())
148 .unwrap_or_else(|| env::var("LOG_DATA_DIR").unwrap_or_else(|_| "logs/".to_string()));
149
150 let log_dir = format!("{}/", log_dir.trim_end_matches('/'));
151 let now = Utc::now();
153 let date_str = now.format("%Y-%m-%d").to_string();
154
155 let base_file_path = format!("{}monitor.log", log_dir);
157
158 if Path::new(&base_file_path).exists() {
160 info!(
161 "Base Log file already exists: {}. Proceeding to compute rolled log file path.",
162 base_file_path
163 );
164 }
165
166 let time_based_path = compute_rolled_file_path(&base_file_path, &date_str, 1);
168
169 if let Some(parent) = Path::new(&time_based_path).parent() {
171 create_dir_all(parent).expect("Failed to create log directory");
172 }
173
174 let max_size = parse_log_max_size();
176
177 let final_path =
178 space_based_rolling(&time_based_path, &base_file_path, &date_str, max_size);
179
180 let file_appender = tracing_appender::rolling::never(
182 Path::new(&final_path).parent().unwrap_or(Path::new(".")),
183 Path::new(&final_path).file_name().unwrap_or_default(),
184 );
185
186 let ansi_stripped_format = StripAnsiFormatter::new(format);
187
188 subscriber
189 .with(
190 fmt::layer()
191 .event_format(ansi_stripped_format)
192 .with_writer(file_appender)
193 .fmt_fields(fmt::format::PrettyFields::new()),
194 )
195 .init();
196 } else {
197 subscriber
199 .with(
200 fmt::layer()
201 .event_format(format)
202 .fmt_fields(fmt::format::PrettyFields::new()),
203 )
204 .init();
205 }
206
207 info!("Logging is successfully configured (mode: {})", log_mode);
208 Ok(())
209}
210
211fn parse_log_max_size() -> u64 {
212 env::var("LOG_MAX_SIZE")
213 .map(|s| {
214 s.parse::<u64>()
215 .expect("LOG_MAX_SIZE must be a valid u64 if set")
216 })
217 .unwrap_or(1_073_741_824)
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use std::fs::File;
224 use std::io::Write;
225 use tempfile::tempdir;
226
227 #[test]
228 fn test_strip_ansi_escapes() {
229 let input = "\x1b[31mRed text\x1b[0m and \x1b[32mgreen text\x1b[0m";
230 let expected = "Red text and green text";
231 assert_eq!(strip_ansi_escapes(input), expected);
232 }
233
234 #[test]
235 fn test_compute_rolled_file_path() {
236 let result = compute_rolled_file_path("app.log", "2023-01-01", 1);
238 assert_eq!(result, "app-2023-01-01.1.log");
239
240 let result = compute_rolled_file_path("app", "2023-01-01", 2);
242 assert_eq!(result, "app-2023-01-01.2.log");
243
244 let result = compute_rolled_file_path("logs/app.log", "2023-01-01", 3);
246 assert_eq!(result, "logs/app-2023-01-01.3.log");
247 }
248
249 #[test]
250 fn test_space_based_rolling() {
251 let dir = tempdir().expect("Failed to create temp directory");
253 let base_path = dir.path().join("test.log").to_str().unwrap().to_string();
254 let date_str = "2023-01-01";
255
256 let initial_path = compute_rolled_file_path(&base_path, date_str, 1);
258 {
259 let mut file = File::create(&initial_path).expect("Failed to create test file");
260 file.write_all(&[0; 100])
262 .expect("Failed to write to test file");
263 }
264
265 let result = space_based_rolling(&initial_path, &base_path, date_str, 50);
267 assert_eq!(result, compute_rolled_file_path(&base_path, date_str, 2));
268
269 let result = space_based_rolling(&initial_path, &base_path, date_str, 200);
271 assert_eq!(result, initial_path);
272 }
273
274 #[test]
276 #[should_panic(expected = "LOG_MAX_SIZE must be a valid u64 if set")]
277 fn test_invalid_log_max_size_panics() {
278 std::env::set_var("LOG_MAX_SIZE", "not_a_number");
279 let _ = parse_log_max_size(); }
281}