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(
430 module = "nautilus_trader.common",
431 name = "Logger",
432 unsendable,
433 from_py_object
434)]
435#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")]
436#[derive(Debug, Clone)]
437pub struct PyLogger {
438 name: Ustr,
439}
440
441impl PyLogger {
442 pub fn new(name: &str) -> Self {
443 Self {
444 name: Ustr::from(name),
445 }
446 }
447
448 pub fn log_callback_error(&self, method: &str, result: PyResult<()>) {
450 if let Err(e) = result {
451 let exception = format_exception(&e);
452 self.log_message(
453 LogLevel::Error,
454 Some(LogColor::Red),
455 &format!("Python {method} failed:\n{exception}"),
456 );
457 }
458 }
459
460 fn log_message(&self, level: LogLevel, color: Option<LogColor>, message: &str) {
461 let color = color.unwrap_or(LogColor::Normal);
462 logger::log(level, color, self.name, message);
463 }
464}
465
466#[pymethods]
467#[pyo3_stub_gen::derive::gen_stub_pymethods]
468impl PyLogger {
469 #[new]
471 #[pyo3(signature = (name="Python"))]
472 fn py_new(name: &str) -> Self {
473 Self::new(name)
474 }
475
476 #[getter]
478 fn name(&self) -> &str {
479 &self.name
480 }
481
482 #[pyo3(name = "trace")]
484 #[pyo3(signature = (message, color=None))]
485 fn py_trace(&self, message: &str, color: Option<LogColor>) {
486 self.log_message(LogLevel::Trace, color, message);
487 }
488
489 #[pyo3(name = "debug")]
491 #[pyo3(signature = (message, color=None))]
492 fn py_debug(&self, message: &str, color: Option<LogColor>) {
493 self.log_message(LogLevel::Debug, color, message);
494 }
495
496 #[pyo3(name = "info")]
498 #[pyo3(signature = (message, color=None))]
499 fn py_info(&self, message: &str, color: Option<LogColor>) {
500 self.log_message(LogLevel::Info, color, message);
501 }
502
503 #[pyo3(name = "warning")]
505 #[pyo3(signature = (message, color=None))]
506 fn py_warning(&self, message: &str, color: Option<LogColor>) {
507 self.log_message(LogLevel::Warning, color, message);
508 }
509
510 #[pyo3(name = "error")]
512 #[pyo3(signature = (message, color=None))]
513 fn py_error(&self, message: &str, color: Option<LogColor>) {
514 self.log_message(LogLevel::Error, color, message);
515 }
516
517 #[pyo3(name = "exception")]
519 #[pyo3(signature = (message="", color=None))]
520 fn py_exception(&self, py: Python, message: &str, color: Option<LogColor>) {
521 let mut full_msg = message.to_owned();
522
523 if pyo3::PyErr::occurred(py) {
524 let err = PyErr::fetch(py);
525 let err_str = err.to_string();
526
527 if full_msg.is_empty() {
528 full_msg = err_str;
529 } else {
530 full_msg = format!("{full_msg}: {err_str}");
531 }
532 }
533
534 self.log_message(LogLevel::Error, color, &full_msg);
535 }
536
537 #[pyo3(name = "flush")]
539 fn py_flush(&self) {
540 log::logger().flush();
541 }
542
543 #[pyo3(name = "_log")]
545 #[pyo3(signature = (level, color=None, message=""))]
546 fn py_log(&self, level: LogLevel, color: Option<LogColor>, message: &str) {
547 self.log_message(level, color, message);
548 }
549}
550
551#[must_use]
555pub fn format_exception(e: &PyErr) -> String {
556 Python::attach(|py| {
557 py.import("traceback")
558 .and_then(|module| {
559 module.call_method1(
560 "format_exception",
561 (e.get_type(py), e.value(py), e.traceback(py)),
562 )
563 })
564 .and_then(|lines| lines.extract::<Vec<String>>())
565 .map_or_else(|_| e.to_string(), |lines| lines.concat())
566 })
567}
568
569#[cfg(test)]
570mod tests {
571 use nautilus_core::python::to_pyruntime_err;
572 use pyo3::ffi::c_str;
573 use rstest::rstest;
574
575 use super::*;
576
577 #[rstest]
578 fn test_format_exception_traceback_and_cause() {
579 Python::initialize();
580 Python::attach(|py| {
581 let module = PyModule::from_code(
582 py,
583 c_str!(
584 r#"
585def callback():
586 try:
587 fail()
588 except ValueError as e:
589 raise RuntimeError("callback failure") from e
590
591def fail():
592 raise ValueError("original failure")
593"#
594 ),
595 c_str!("strategy_callback.py"),
596 c_str!("strategy_callback"),
597 )
598 .unwrap();
599 let e = module.call_method0("callback").unwrap_err();
600 let formatted = format_exception(&e);
601
602 assert!(formatted.contains("File \"strategy_callback.py\", line 9, in fail"));
603 assert!(formatted.contains("File \"strategy_callback.py\", line 6, in callback"));
604 assert!(formatted.contains("ValueError: original failure"));
605 assert!(formatted.contains("The above exception was the direct cause"));
606 assert!(formatted.ends_with("RuntimeError: callback failure\n"));
607 });
608 }
609
610 #[rstest]
611 fn test_format_exception_without_traceback() {
612 Python::initialize();
613 let e = to_pyruntime_err("callback failure");
614 assert_eq!(format_exception(&e), "RuntimeError: callback failure\n");
615 }
616
617 #[rstest]
618 fn test_format_exception_fallback() {
619 Python::initialize();
620 Python::attach(|py| {
621 let traceback = py.import("traceback").unwrap();
622 let original = traceback.getattr("format_exception").unwrap();
623 traceback.setattr("format_exception", py.None()).unwrap();
624 let e = to_pyruntime_err("callback failure");
625 let formatted = format_exception(&e);
626 traceback.setattr("format_exception", original).unwrap();
627
628 assert_eq!(formatted, "RuntimeError: callback failure");
629 });
630 }
631}