nautilus_common/logging/
writer.rs1use std::{
17 fs::{create_dir_all, File},
18 io::{self, BufWriter, Stderr, Stdout, Write},
19 path::PathBuf,
20 sync::OnceLock,
21};
22
23use chrono::{DateTime, Utc};
24use log::LevelFilter;
25use regex::Regex;
26
27use crate::logging::logger::LogLine;
28
29static ANSI_RE: OnceLock<Regex> = OnceLock::new();
30
31pub trait LogWriter {
32 fn write(&mut self, line: &str);
34 fn flush(&mut self);
36 fn enabled(&self, line: &LogLine) -> bool;
38}
39
40#[derive(Debug)]
41pub struct StdoutWriter {
42 pub is_colored: bool,
43 io: Stdout,
44 level: LevelFilter,
45}
46
47impl StdoutWriter {
48 #[must_use]
50 pub fn new(level: LevelFilter, is_colored: bool) -> Self {
51 Self {
52 io: io::stdout(),
53 level,
54 is_colored,
55 }
56 }
57}
58
59impl LogWriter for StdoutWriter {
60 fn write(&mut self, line: &str) {
61 match self.io.write_all(line.as_bytes()) {
62 Ok(()) => {}
63 Err(e) => eprintln!("Error writing to stdout: {e:?}"),
64 }
65 }
66
67 fn flush(&mut self) {
68 match self.io.flush() {
69 Ok(()) => {}
70 Err(e) => eprintln!("Error flushing stdout: {e:?}"),
71 }
72 }
73
74 fn enabled(&self, line: &LogLine) -> bool {
75 line.level > LevelFilter::Error && line.level <= self.level
77 }
78}
79
80#[derive(Debug)]
81pub struct StderrWriter {
82 pub is_colored: bool,
83 io: Stderr,
84}
85
86impl StderrWriter {
87 #[must_use]
89 pub fn new(is_colored: bool) -> Self {
90 Self {
91 io: io::stderr(),
92 is_colored,
93 }
94 }
95}
96
97impl LogWriter for StderrWriter {
98 fn write(&mut self, line: &str) {
99 match self.io.write_all(line.as_bytes()) {
100 Ok(()) => {}
101 Err(e) => eprintln!("Error writing to stderr: {e:?}"),
102 }
103 }
104
105 fn flush(&mut self) {
106 match self.io.flush() {
107 Ok(()) => {}
108 Err(e) => eprintln!("Error flushing stderr: {e:?}"),
109 }
110 }
111
112 fn enabled(&self, line: &LogLine) -> bool {
113 line.level == LevelFilter::Error
114 }
115}
116
117#[cfg_attr(
118 feature = "python",
119 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.common")
120)]
121#[derive(Debug, Clone, Default)]
122pub struct FileWriterConfig {
123 pub directory: Option<String>,
124 pub file_name: Option<String>,
125 pub file_format: Option<String>,
126}
127
128impl FileWriterConfig {
129 #[must_use]
131 pub const fn new(
132 directory: Option<String>,
133 file_name: Option<String>,
134 file_format: Option<String>,
135 ) -> Self {
136 Self {
137 directory,
138 file_name,
139 file_format,
140 }
141 }
142}
143
144#[derive(Debug)]
145pub struct FileWriter {
146 pub json_format: bool,
147 buf: BufWriter<File>,
148 path: PathBuf,
149 file_config: FileWriterConfig,
150 trader_id: String,
151 instance_id: String,
152 level: LevelFilter,
153}
154
155impl FileWriter {
156 pub fn new(
158 trader_id: String,
159 instance_id: String,
160 file_config: FileWriterConfig,
161 fileout_level: LevelFilter,
162 ) -> Option<Self> {
163 let json_format = match file_config.file_format.as_ref().map(|s| s.to_lowercase()) {
165 Some(ref format) if format == "json" => true,
166 None => false,
167 Some(ref unrecognized) => {
168 tracing::error!(
169 "Unrecognized log file format: {unrecognized}. Using plain text format as default."
170 );
171 false
172 }
173 };
174
175 let file_path =
176 Self::create_log_file_path(&file_config, &trader_id, &instance_id, json_format);
177
178 match File::options()
179 .create(true)
180 .append(true)
181 .open(file_path.clone())
182 {
183 Ok(file) => Some(Self {
184 json_format,
185 buf: BufWriter::new(file),
186 path: file_path,
187 file_config,
188 trader_id,
189 instance_id,
190 level: fileout_level,
191 }),
192 Err(e) => {
193 tracing::error!("Error creating log file: {e}");
194 None
195 }
196 }
197 }
198
199 fn create_log_file_path(
200 file_config: &FileWriterConfig,
201 trader_id: &str,
202 instance_id: &str,
203 is_json_format: bool,
204 ) -> PathBuf {
205 let basename = if let Some(file_name) = file_config.file_name.as_ref() {
206 file_name.clone()
207 } else {
208 let current_date_utc = Utc::now().format("%Y-%m-%d");
210 format!("{trader_id}_{current_date_utc}_{instance_id}")
211 };
212
213 let suffix = if is_json_format { "json" } else { "log" };
214 let mut file_path = PathBuf::new();
215
216 if let Some(directory) = file_config.directory.as_ref() {
217 file_path.push(directory);
218 create_dir_all(&file_path).expect("Failed to create directories for log file");
219 }
220
221 file_path.push(basename);
222 file_path.set_extension(suffix);
223 file_path
224 }
225
226 #[must_use]
227 pub fn should_rotate_file(&self) -> bool {
228 let current_date_utc = Utc::now().date_naive();
229 let metadata = self
230 .path
231 .metadata()
232 .expect("Failed to read log file metadata");
233 let creation_time = metadata
234 .created()
235 .expect("Failed to get log file creation time");
236
237 let creation_time_utc: DateTime<Utc> = creation_time.into();
238 let creation_date_utc = creation_time_utc.date_naive();
239
240 current_date_utc != creation_date_utc
241 }
242}
243
244impl LogWriter for FileWriter {
245 fn write(&mut self, line: &str) {
246 if self.should_rotate_file() {
247 self.flush();
248
249 let file_path = Self::create_log_file_path(
250 &self.file_config,
251 &self.trader_id,
252 &self.instance_id,
253 self.json_format,
254 );
255
256 match File::options()
257 .create(true)
258 .append(true)
259 .open(file_path.clone())
260 {
261 Ok(file) => {
262 self.buf = BufWriter::new(file);
263 self.path = file_path;
264 }
265 Err(e) => tracing::error!("Error creating log file: {e}"),
266 }
267 }
268
269 let line = strip_ansi_codes(line);
270
271 match self.buf.write_all(line.as_bytes()) {
272 Ok(()) => {}
273 Err(e) => tracing::error!("Error writing to file: {e:?}"),
274 }
275 }
276
277 fn flush(&mut self) {
278 match self.buf.flush() {
279 Ok(()) => {}
280 Err(e) => tracing::error!("Error flushing file: {e:?}"),
281 }
282 }
283
284 fn enabled(&self, line: &LogLine) -> bool {
285 line.level <= self.level
286 }
287}
288
289fn strip_nonprinting_except_newline(s: &str) -> String {
290 s.chars()
291 .filter(|&c| c == '\n' || (!c.is_control() && c != '\u{7F}'))
292 .collect()
293}
294
295fn strip_ansi_codes(s: &str) -> String {
296 let re = ANSI_RE.get_or_init(|| Regex::new(r"\x1B\[[0-9;?=]*[A-Za-z]|\x1B\].*?\x07").unwrap());
297 let no_controls = strip_nonprinting_except_newline(s);
298 re.replace_all(&no_controls, "").to_string()
299}