1use ahash::AHashMap;
17use log::LevelFilter;
18use nautilus_core::{UUID4, python::to_pyvalue_err};
19use nautilus_model::identifiers::TraderId;
20use pyo3::prelude::*;
21use ustr::Ustr;
22
23use crate::{
24 enums::{LogColor, LogLevel},
25 logging::{
26 self, headers,
27 logger::{self, LogGuard, LoggerConfig},
28 logging_clock_set_realtime_mode, logging_clock_set_static_mode,
29 logging_clock_set_static_time, logging_set_bypass, map_log_level_to_filter,
30 parse_level_filter_str,
31 writer::FileWriterConfig,
32 },
33 python::config_error_to_pyvalue_err,
34};
35
36#[pymethods]
37#[pyo3_stub_gen::derive::gen_stub_pymethods]
38impl LoggerConfig {
39 #[new]
41 #[pyo3(signature = (
42 stdout_level=None,
43 fileout_level=None,
44 component_levels=None,
45 is_colored=None,
46 print_config=None,
47 bypass_logging=None,
48 log_components_only=None,
49 file_config=None,
50 clear_log_file=None,
51 fileout_sync_on_flush=None,
52 buffered_stdout=None,
53 ))]
54 #[expect(
55 clippy::too_many_arguments,
56 reason = "PyO3 constructor mirrors LoggerConfig keyword arguments"
57 )]
58 fn py_new(
59 stdout_level: Option<LogLevel>,
60 fileout_level: Option<LogLevel>,
61 component_levels: Option<std::collections::HashMap<String, String>>,
62 is_colored: Option<bool>,
63 print_config: Option<bool>,
64 bypass_logging: Option<bool>,
65 log_components_only: Option<bool>,
66 file_config: Option<FileWriterConfig>,
67 clear_log_file: Option<bool>,
68 fileout_sync_on_flush: Option<bool>,
69 buffered_stdout: Option<bool>,
70 ) -> PyResult<Self> {
71 let component_levels = parse_component_levels(component_levels).map_err(to_pyvalue_err)?;
72 let mut config = Self::new(
73 stdout_level.map_or(LevelFilter::Info, map_log_level_to_filter),
74 fileout_level.map_or(LevelFilter::Off, map_log_level_to_filter),
75 component_levels,
76 AHashMap::new(),
77 log_components_only.unwrap_or(false),
78 is_colored.unwrap_or(true),
79 print_config.unwrap_or(false),
80 false,
81 bypass_logging.unwrap_or(false),
82 file_config,
83 clear_log_file.unwrap_or(false),
84 );
85 config.fileout_sync_on_flush = fileout_sync_on_flush.unwrap_or(true);
86 config.buffered_stdout = buffered_stdout.unwrap_or(false);
87 config.validate().map_err(config_error_to_pyvalue_err)?;
88 Ok(config)
89 }
90
91 #[staticmethod]
104 #[pyo3(name = "from_spec")]
105 pub fn py_from_spec(spec: &str) -> PyResult<Self> {
106 Self::from_spec(spec).map_err(to_pyvalue_err)
107 }
108}
109
110#[pymethods]
111#[pyo3_stub_gen::derive::gen_stub_pymethods]
112impl FileWriterConfig {
113 #[new]
115 #[pyo3(signature = (directory=None, file_name=None, file_format=None, file_rotate=None))]
116 #[must_use]
117 pub fn py_new(
118 directory: Option<String>,
119 file_name: Option<String>,
120 file_format: Option<String>,
121 file_rotate: Option<(u64, u32)>,
122 ) -> Self {
123 Self::new(directory, file_name, file_format, file_rotate)
124 }
125}
126
127#[pyfunction]
137#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
138#[pyo3(name = "init_logging")]
139#[expect(clippy::too_many_arguments)]
140#[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))]
141pub fn py_init_logging(
142 trader_id: TraderId,
143 instance_id: UUID4,
144 level_stdout: LogLevel,
145 level_file: Option<LogLevel>,
146 component_levels: Option<std::collections::HashMap<String, String>>,
147 directory: Option<String>,
148 file_name: Option<String>,
149 file_format: Option<String>,
150 file_rotate: Option<(u64, u32)>,
151 is_colored: Option<bool>,
152 is_bypassed: Option<bool>,
153 print_config: Option<bool>,
154 log_components_only: Option<bool>,
155 fileout_sync_on_flush: Option<bool>,
156 buffered_stdout: Option<bool>,
157) -> PyResult<LogGuard> {
158 let level_file = level_file.map_or(LevelFilter::Off, map_log_level_to_filter);
159
160 let component_levels = parse_component_levels(component_levels).map_err(to_pyvalue_err)?;
161
162 let file_config = FileWriterConfig::new(directory, file_name, file_format, file_rotate);
163 file_config
164 .validate()
165 .map_err(config_error_to_pyvalue_err)?;
166
167 let mut config = LoggerConfig::new(
168 map_log_level_to_filter(level_stdout),
169 level_file,
170 component_levels,
171 AHashMap::new(), log_components_only.unwrap_or(false),
173 is_colored.unwrap_or(true),
174 print_config.unwrap_or(false),
175 false, is_bypassed.unwrap_or(false), None, false, );
180 config.fileout_sync_on_flush = fileout_sync_on_flush.unwrap_or(true);
181 config.buffered_stdout = buffered_stdout.unwrap_or(false);
182
183 if config.bypass_logging {
184 logging_set_bypass();
185 }
186
187 logging::init_logging(trader_id, instance_id, config, file_config).map_err(to_pyvalue_err)
188}
189
190#[pyfunction()]
191#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
192#[pyo3(name = "logger_flush")]
193pub fn py_logger_flush() {
194 log::logger().flush();
195}
196
197#[pyfunction()]
205#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
206#[pyo3(name = "logging_sync_to_disk")]
207pub fn py_logging_sync_to_disk() -> PyResult<bool> {
208 logging::logging_sync_to_disk()
209 .map(|()| true)
210 .map_err(to_pyvalue_err)
211}
212
213fn parse_component_levels(
214 original_map: Option<std::collections::HashMap<String, String>>,
215) -> anyhow::Result<AHashMap<Ustr, LevelFilter>> {
216 match original_map {
217 Some(map) => {
218 let mut new_map = AHashMap::new();
219
220 for (key, value) in map {
221 let ustr_key = Ustr::from(&key);
222 let level = parse_level_filter_str(&value)?;
223 new_map.insert(ustr_key, level);
224 }
225 Ok(new_map)
226 }
227 None => Ok(AHashMap::new()),
228 }
229}
230
231#[pyfunction]
233#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
234#[pyo3(name = "logger_log")]
235pub fn py_logger_log(level: LogLevel, color: LogColor, component: &str, message: &str) {
236 logger::log(level, color, Ustr::from(component), message);
237}
238
239#[pyfunction]
241#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
242#[pyo3(name = "log_header")]
243pub fn py_log_header(trader_id: TraderId, machine_id: &str, instance_id: UUID4, component: &str) {
244 headers::log_header(trader_id, machine_id, instance_id, Ustr::from(component));
245}
246
247#[pyfunction]
249#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
250#[pyo3(name = "log_sysinfo")]
251pub fn py_log_sysinfo(component: &str) {
252 headers::log_sysinfo(Ustr::from(component));
253}
254
255#[pyfunction]
257#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
258#[pyo3(name = "logging_clock_set_static_mode")]
259pub fn py_logging_clock_set_static_mode() {
260 logging_clock_set_static_mode();
261}
262
263#[pyfunction]
265#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
266#[pyo3(name = "logging_clock_set_realtime_mode")]
267pub fn py_logging_clock_set_realtime_mode() {
268 logging_clock_set_realtime_mode();
269}
270
271#[pyfunction]
273#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
274#[pyo3(name = "logging_clock_set_static_time")]
275pub fn py_logging_clock_set_static_time(time_ns: u64) {
276 logging_clock_set_static_time(time_ns);
277}
278
279#[cfg(feature = "tracing-bridge")]
281#[pyfunction]
282#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
283#[pyo3(name = "tracing_is_initialized")]
284#[must_use]
285pub fn py_tracing_is_initialized() -> bool {
286 crate::logging::bridge::tracing_is_initialized()
287}
288
289#[cfg(feature = "tracing-bridge")]
305#[pyfunction]
306#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
307#[pyo3(name = "init_tracing")]
308pub fn py_init_tracing() -> PyResult<()> {
309 crate::logging::bridge::init_tracing().map_err(to_pyvalue_err)
310}
311
312#[pyclass(
319 module = "nautilus_trader.core.nautilus_pyo3.common",
320 name = "Logger",
321 unsendable,
322 from_py_object
323)]
324#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")]
325#[derive(Debug, Clone)]
326pub struct PyLogger {
327 name: Ustr,
328}
329
330impl PyLogger {
331 pub fn new(name: &str) -> Self {
332 Self {
333 name: Ustr::from(name),
334 }
335 }
336
337 fn log_message(&self, level: LogLevel, color: Option<LogColor>, message: &str) {
338 let color = color.unwrap_or(LogColor::Normal);
339 logger::log(level, color, self.name, message);
340 }
341}
342
343#[pymethods]
344#[pyo3_stub_gen::derive::gen_stub_pymethods]
345impl PyLogger {
346 #[new]
348 #[pyo3(signature = (name="Python"))]
349 fn py_new(name: &str) -> Self {
350 Self::new(name)
351 }
352
353 #[getter]
355 fn name(&self) -> &str {
356 &self.name
357 }
358
359 #[pyo3(name = "trace")]
361 #[pyo3(signature = (message, color=None))]
362 fn py_trace(&self, message: &str, color: Option<LogColor>) {
363 self.log_message(LogLevel::Trace, color, message);
364 }
365
366 #[pyo3(name = "debug")]
368 #[pyo3(signature = (message, color=None))]
369 fn py_debug(&self, message: &str, color: Option<LogColor>) {
370 self.log_message(LogLevel::Debug, color, message);
371 }
372
373 #[pyo3(name = "info")]
375 #[pyo3(signature = (message, color=None))]
376 fn py_info(&self, message: &str, color: Option<LogColor>) {
377 self.log_message(LogLevel::Info, color, message);
378 }
379
380 #[pyo3(name = "warning")]
382 #[pyo3(signature = (message, color=None))]
383 fn py_warning(&self, message: &str, color: Option<LogColor>) {
384 self.log_message(LogLevel::Warning, color, message);
385 }
386
387 #[pyo3(name = "error")]
389 #[pyo3(signature = (message, color=None))]
390 fn py_error(&self, message: &str, color: Option<LogColor>) {
391 self.log_message(LogLevel::Error, color, message);
392 }
393
394 #[pyo3(name = "exception")]
396 #[pyo3(signature = (message="", color=None))]
397 fn py_exception(&self, py: Python, message: &str, color: Option<LogColor>) {
398 let mut full_msg = message.to_owned();
399
400 if pyo3::PyErr::occurred(py) {
401 let err = PyErr::fetch(py);
402 let err_str = err.to_string();
403
404 if full_msg.is_empty() {
405 full_msg = err_str;
406 } else {
407 full_msg = format!("{full_msg}: {err_str}");
408 }
409 }
410
411 self.log_message(LogLevel::Error, color, &full_msg);
412 }
413
414 #[pyo3(name = "flush")]
416 fn py_flush(&self) {
417 log::logger().flush();
418 }
419
420 #[pyo3(name = "_log")]
422 #[pyo3(signature = (level, color=None, message=""))]
423 fn py_log(&self, level: LogLevel, color: Option<LogColor>, message: &str) {
424 self.log_message(level, color, message);
425 }
426}