1use std::collections::HashMap;
17
18use ahash::AHashMap;
19use log::LevelFilter;
20use nautilus_core::{UUID4, python::to_pyvalue_err};
21use nautilus_model::identifiers::TraderId;
22use pyo3::prelude::*;
23use ustr::Ustr;
24
25use crate::{
26 enums::{LogColor, LogLevel},
27 logging::{
28 self, headers,
29 logger::{self, LogGuard, LoggerConfig},
30 logging_clock_set_realtime_mode, logging_clock_set_static_mode,
31 logging_clock_set_static_time, logging_set_bypass, map_log_level_to_filter,
32 parse_level_filter_str,
33 writer::FileWriterConfig,
34 },
35 python::config_error_to_pyvalue_err,
36};
37
38#[pyo3_stub_gen::derive::gen_stub_pymethods]
39#[pymethods]
40impl LoggerConfig {
41 #[new]
43 #[pyo3(signature = (
44 stdout_level=None,
45 fileout_level=None,
46 component_levels=None,
47 is_colored=None,
48 print_config=None,
49 bypass_logging=None,
50 log_components_only=None,
51 file_config=None,
52 clear_log_file=None,
53 fileout_sync_on_flush=None,
54 buffered_stdout=None,
55 ))]
56 #[expect(
57 clippy::too_many_arguments,
58 reason = "PyO3 constructor mirrors LoggerConfig keyword arguments"
59 )]
60 fn py_new(
61 stdout_level: Option<LogLevel>,
62 fileout_level: Option<LogLevel>,
63 component_levels: Option<std::collections::HashMap<String, String>>,
64 is_colored: Option<bool>,
65 print_config: Option<bool>,
66 bypass_logging: Option<bool>,
67 log_components_only: Option<bool>,
68 file_config: Option<FileWriterConfig>,
69 clear_log_file: Option<bool>,
70 fileout_sync_on_flush: Option<bool>,
71 buffered_stdout: Option<bool>,
72 ) -> PyResult<Self> {
73 let component_levels = parse_component_levels(component_levels).map_err(to_pyvalue_err)?;
74 let mut config = Self::new(
75 stdout_level.map_or(LevelFilter::Info, map_log_level_to_filter),
76 fileout_level.map_or(LevelFilter::Off, map_log_level_to_filter),
77 component_levels,
78 AHashMap::new(),
79 log_components_only.unwrap_or(false),
80 is_colored.unwrap_or(true),
81 print_config.unwrap_or(false),
82 false,
83 bypass_logging.unwrap_or(false),
84 file_config,
85 clear_log_file.unwrap_or(false),
86 );
87 config.fileout_sync_on_flush = fileout_sync_on_flush.unwrap_or(true);
88 config.buffered_stdout = buffered_stdout.unwrap_or(false);
89 config.validate().map_err(config_error_to_pyvalue_err)?;
90 Ok(config)
91 }
92
93 #[getter]
94 #[pyo3(name = "stdout_level")]
95 fn py_stdout_level(&self) -> LogLevel {
96 level_filter_to_log_level(self.stdout_level)
97 }
98
99 #[getter]
100 #[pyo3(name = "fileout_level")]
101 fn py_fileout_level(&self) -> LogLevel {
102 level_filter_to_log_level(self.fileout_level)
103 }
104
105 #[getter]
106 #[pyo3(name = "component_levels")]
107 fn py_component_levels(&self) -> HashMap<String, String> {
108 self.component_level
109 .iter()
110 .map(|(component, level)| (component.to_string(), level.to_string()))
111 .collect()
112 }
113
114 #[getter]
115 #[pyo3(name = "is_colored")]
116 const fn py_is_colored(&self) -> bool {
117 self.is_colored
118 }
119
120 #[getter]
121 #[pyo3(name = "print_config")]
122 const fn py_print_config(&self) -> bool {
123 self.print_config
124 }
125
126 #[getter]
127 #[pyo3(name = "bypass_logging")]
128 const fn py_bypass_logging(&self) -> bool {
129 self.bypass_logging
130 }
131
132 #[getter]
133 #[pyo3(name = "log_components_only")]
134 const fn py_log_components_only(&self) -> bool {
135 self.log_components_only
136 }
137
138 #[getter]
139 #[pyo3(name = "file_config")]
140 fn py_file_config(&self) -> Option<FileWriterConfig> {
141 self.file_config.clone()
142 }
143
144 #[getter]
145 #[pyo3(name = "clear_log_file")]
146 const fn py_clear_log_file(&self) -> bool {
147 self.clear_log_file
148 }
149
150 #[getter]
151 #[pyo3(name = "fileout_sync_on_flush")]
152 const fn py_fileout_sync_on_flush(&self) -> bool {
153 self.fileout_sync_on_flush
154 }
155
156 #[getter]
157 #[pyo3(name = "buffered_stdout")]
158 const fn py_buffered_stdout(&self) -> bool {
159 self.buffered_stdout
160 }
161
162 #[staticmethod]
175 #[pyo3(name = "from_spec")]
176 pub fn py_from_spec(spec: &str) -> PyResult<Self> {
177 Self::from_spec(spec).map_err(to_pyvalue_err)
178 }
179}
180
181#[pyo3_stub_gen::derive::gen_stub_pymethods]
182#[pymethods]
183impl FileWriterConfig {
184 #[new]
186 #[pyo3(signature = (directory=None, file_name=None, file_format=None, file_rotate=None))]
187 #[must_use]
188 pub fn py_new(
189 directory: Option<String>,
190 file_name: Option<String>,
191 file_format: Option<String>,
192 file_rotate: Option<(u64, u32)>,
193 ) -> Self {
194 Self::new(directory, file_name, file_format, file_rotate)
195 }
196
197 #[getter]
198 #[pyo3(name = "directory")]
199 fn py_directory(&self) -> Option<&str> {
200 self.directory.as_deref()
201 }
202
203 #[getter]
204 #[pyo3(name = "file_name")]
205 fn py_file_name(&self) -> Option<&str> {
206 self.file_name.as_deref()
207 }
208
209 #[getter]
210 #[pyo3(name = "file_format")]
211 fn py_file_format(&self) -> Option<&str> {
212 self.file_format.as_deref()
213 }
214
215 #[getter]
216 #[pyo3(name = "file_rotate")]
217 fn py_file_rotate(&self) -> Option<(u64, u32)> {
218 self.file_rotate
219 .as_ref()
220 .map(|rotate| (rotate.max_file_size, rotate.max_backup_count))
221 }
222}
223
224#[pyfunction]
238#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
239#[pyo3(name = "init_logging")]
240#[expect(clippy::too_many_arguments)]
241#[pyo3(signature = (trader_id, instance_id, level_stdout, level_file=None, component_levels=None, directory=None, file_name=None, file_format=None, file_rotate=None, is_colored=None, is_bypassed=None, print_config=None, log_components_only=None, fileout_sync_on_flush=None, buffered_stdout=None))]
242pub fn py_init_logging(
243 trader_id: TraderId,
244 instance_id: UUID4,
245 level_stdout: LogLevel,
246 level_file: Option<LogLevel>,
247 component_levels: Option<std::collections::HashMap<String, String>>,
248 directory: Option<String>,
249 file_name: Option<String>,
250 file_format: Option<String>,
251 file_rotate: Option<(u64, u32)>,
252 is_colored: Option<bool>,
253 is_bypassed: Option<bool>,
254 print_config: Option<bool>,
255 log_components_only: Option<bool>,
256 fileout_sync_on_flush: Option<bool>,
257 buffered_stdout: Option<bool>,
258) -> PyResult<LogGuard> {
259 let level_file = level_file.map_or(LevelFilter::Off, map_log_level_to_filter);
260
261 let component_levels = parse_component_levels(component_levels).map_err(to_pyvalue_err)?;
262
263 let file_config = FileWriterConfig::new(directory, file_name, file_format, file_rotate);
264 file_config
265 .validate()
266 .map_err(config_error_to_pyvalue_err)?;
267
268 let mut config = LoggerConfig::new(
269 map_log_level_to_filter(level_stdout),
270 level_file,
271 component_levels,
272 AHashMap::new(), log_components_only.unwrap_or(false),
274 is_colored.unwrap_or(true),
275 print_config.unwrap_or(false),
276 false, is_bypassed.unwrap_or(false), None, false, );
281 config.fileout_sync_on_flush = fileout_sync_on_flush.unwrap_or(true);
282 config.buffered_stdout = buffered_stdout.unwrap_or(false);
283
284 if config.bypass_logging {
285 logging_set_bypass();
286 }
287
288 logging::init_logging(trader_id, instance_id, config, file_config).map_err(to_pyvalue_err)
289}
290
291#[pyfunction()]
292#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
293#[pyo3(name = "logger_flush")]
294pub fn py_logger_flush() {
295 log::logger().flush();
296}
297
298#[pyfunction()]
306#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
307#[pyo3(name = "logging_sync_to_disk")]
308pub fn py_logging_sync_to_disk() -> PyResult<bool> {
309 logging::logging_sync_to_disk()
310 .map(|()| true)
311 .map_err(to_pyvalue_err)
312}
313
314fn parse_component_levels(
315 original_map: Option<std::collections::HashMap<String, String>>,
316) -> anyhow::Result<AHashMap<Ustr, LevelFilter>> {
317 match original_map {
318 Some(map) => {
319 let mut new_map = AHashMap::new();
320
321 for (key, value) in map {
322 let ustr_key = Ustr::from(&key);
323 let level = parse_level_filter_str(&value)?;
324 new_map.insert(ustr_key, level);
325 }
326 Ok(new_map)
327 }
328 None => Ok(AHashMap::new()),
329 }
330}
331
332const fn level_filter_to_log_level(level: LevelFilter) -> LogLevel {
333 match level {
334 LevelFilter::Off => LogLevel::Off,
335 LevelFilter::Error => LogLevel::Error,
336 LevelFilter::Warn => LogLevel::Warning,
337 LevelFilter::Info => LogLevel::Info,
338 LevelFilter::Debug => LogLevel::Debug,
339 LevelFilter::Trace => LogLevel::Trace,
340 }
341}
342
343#[pyfunction]
345#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
346#[pyo3(name = "logger_log")]
347pub fn py_logger_log(level: LogLevel, color: LogColor, component: &str, message: &str) {
348 logger::log(level, color, Ustr::from(component), message);
349}
350
351#[pyfunction]
353#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
354#[pyo3(name = "log_header")]
355pub fn py_log_header(trader_id: TraderId, machine_id: &str, instance_id: UUID4, component: &str) {
356 headers::log_header(trader_id, machine_id, instance_id, Ustr::from(component));
357}
358
359#[pyfunction]
361#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
362#[pyo3(name = "log_sysinfo")]
363pub fn py_log_sysinfo(component: &str) {
364 headers::log_sysinfo(Ustr::from(component));
365}
366
367#[pyfunction]
369#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
370#[pyo3(name = "logging_clock_set_static_mode")]
371pub fn py_logging_clock_set_static_mode() {
372 logging_clock_set_static_mode();
373}
374
375#[pyfunction]
377#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
378#[pyo3(name = "logging_clock_set_realtime_mode")]
379pub fn py_logging_clock_set_realtime_mode() {
380 logging_clock_set_realtime_mode();
381}
382
383#[pyfunction]
385#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
386#[pyo3(name = "logging_clock_set_static_time")]
387pub fn py_logging_clock_set_static_time(time_ns: u64) {
388 logging_clock_set_static_time(time_ns);
389}
390
391#[cfg(feature = "tracing-bridge")]
393#[pyfunction]
394#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
395#[pyo3(name = "tracing_is_initialized")]
396#[must_use]
397pub fn py_tracing_is_initialized() -> bool {
398 crate::logging::bridge::tracing_is_initialized()
399}
400
401#[cfg(feature = "tracing-bridge")]
417#[pyfunction]
418#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
419#[pyo3(name = "init_tracing")]
420pub fn py_init_tracing() -> PyResult<()> {
421 crate::logging::bridge::init_tracing().map_err(to_pyvalue_err)
422}
423
424#[pyclass(
431 module = "nautilus_trader.common",
432 name = "Logger",
433 unsendable,
434 from_py_object
435)]
436#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")]
437#[derive(Debug, Clone)]
438pub struct PyLogger {
439 name: Ustr,
440}
441
442impl PyLogger {
443 pub fn new(name: &str) -> Self {
444 Self {
445 name: Ustr::from(name),
446 }
447 }
448
449 fn log_message(&self, level: LogLevel, color: Option<LogColor>, message: &str) {
450 let color = color.unwrap_or(LogColor::Normal);
451 logger::log(level, color, self.name, message);
452 }
453}
454
455#[pymethods]
456#[pyo3_stub_gen::derive::gen_stub_pymethods]
457impl PyLogger {
458 #[new]
460 #[pyo3(signature = (name="Python"))]
461 fn py_new(name: &str) -> Self {
462 Self::new(name)
463 }
464
465 #[getter]
467 fn name(&self) -> &str {
468 &self.name
469 }
470
471 #[pyo3(name = "trace")]
473 #[pyo3(signature = (message, color=None))]
474 fn py_trace(&self, message: &str, color: Option<LogColor>) {
475 self.log_message(LogLevel::Trace, color, message);
476 }
477
478 #[pyo3(name = "debug")]
480 #[pyo3(signature = (message, color=None))]
481 fn py_debug(&self, message: &str, color: Option<LogColor>) {
482 self.log_message(LogLevel::Debug, color, message);
483 }
484
485 #[pyo3(name = "info")]
487 #[pyo3(signature = (message, color=None))]
488 fn py_info(&self, message: &str, color: Option<LogColor>) {
489 self.log_message(LogLevel::Info, color, message);
490 }
491
492 #[pyo3(name = "warning")]
494 #[pyo3(signature = (message, color=None))]
495 fn py_warning(&self, message: &str, color: Option<LogColor>) {
496 self.log_message(LogLevel::Warning, color, message);
497 }
498
499 #[pyo3(name = "error")]
501 #[pyo3(signature = (message, color=None))]
502 fn py_error(&self, message: &str, color: Option<LogColor>) {
503 self.log_message(LogLevel::Error, color, message);
504 }
505
506 #[pyo3(name = "exception")]
508 #[pyo3(signature = (message="", color=None))]
509 fn py_exception(&self, py: Python, message: &str, color: Option<LogColor>) {
510 let mut full_msg = message.to_owned();
511
512 if pyo3::PyErr::occurred(py) {
513 let err = PyErr::fetch(py);
514 let err_str = err.to_string();
515
516 if full_msg.is_empty() {
517 full_msg = err_str;
518 } else {
519 full_msg = format!("{full_msg}: {err_str}");
520 }
521 }
522
523 self.log_message(LogLevel::Error, color, &full_msg);
524 }
525
526 #[pyo3(name = "flush")]
528 fn py_flush(&self) {
529 log::logger().flush();
530 }
531
532 #[pyo3(name = "_log")]
534 #[pyo3(signature = (level, color=None, message=""))]
535 fn py_log(&self, level: LogLevel, color: Option<LogColor>, message: &str) {
536 self.log_message(level, color, message);
537 }
538}