nautilus_common/python/
logging.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::collections::HashMap;
17
18use log::LevelFilter;
19use nautilus_core::{UUID4, python::to_pyvalue_err};
20use nautilus_model::identifiers::TraderId;
21use pyo3::prelude::*;
22use ustr::Ustr;
23
24use crate::{
25    enums::{LogColor, LogLevel},
26    logging::{
27        self, headers,
28        logger::{self, LogGuard, LoggerConfig},
29        logging_clock_set_realtime_mode, logging_clock_set_static_mode,
30        logging_clock_set_static_time, logging_set_bypass, map_log_level_to_filter,
31        parse_level_filter_str,
32        writer::FileWriterConfig,
33    },
34};
35
36#[pymethods]
37impl LoggerConfig {
38    /// Creates a [`LoggerConfig`] from a spec string.
39    ///
40    /// # Errors
41    ///
42    /// Returns a Python exception if the spec string is invalid.
43    #[staticmethod]
44    #[pyo3(name = "from_spec")]
45    pub fn py_from_spec(spec: String) -> PyResult<Self> {
46        Self::from_spec(&spec).map_err(to_pyvalue_err)
47    }
48}
49
50#[pymethods]
51impl FileWriterConfig {
52    #[new]
53    #[pyo3(signature = (directory=None, file_name=None, file_format=None, file_rotate=None))]
54    #[must_use]
55    pub fn py_new(
56        directory: Option<String>,
57        file_name: Option<String>,
58        file_format: Option<String>,
59        file_rotate: Option<(u64, u32)>,
60    ) -> Self {
61        Self::new(directory, file_name, file_format, file_rotate)
62    }
63}
64
65/// Initialize tracing.
66///
67/// Tracing is meant to be used to trace/debug async Rust code. It can be
68/// configured to filter modules and write up to a specific level only using
69/// by passing a configuration using the `RUST_LOG` environment variable.
70///
71/// # Safety
72///
73/// Should only be called once during an applications run, ideally at the
74/// beginning of the run.
75///
76/// # Errors
77///
78/// Returns an error if tracing subscriber fails to initialize.
79#[pyfunction()]
80#[pyo3(name = "init_tracing")]
81pub fn py_init_tracing() -> PyResult<()> {
82    logging::init_tracing().map_err(to_pyvalue_err)
83}
84
85/// Initialize logging.
86///
87/// Logging should be used for Python and sync Rust logic which is most of
88/// the components in the [nautilus_trader](https://pypi.org/project/nautilus_trader) package.
89/// Logging can be configured to filter components and write up to a specific level only
90/// by passing a configuration using the `NAUTILUS_LOG` environment variable.
91///
92/// # Safety
93///
94/// Should only be called once during an applications run, ideally at the
95/// beginning of the run.
96/// Initializes logging via Python interface.
97///
98/// # Errors
99///
100/// Returns a Python exception if logger initialization fails.
101#[pyfunction]
102#[pyo3(name = "init_logging")]
103#[allow(clippy::too_many_arguments)]
104#[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))]
105pub fn py_init_logging(
106    trader_id: TraderId,
107    instance_id: UUID4,
108    level_stdout: LogLevel,
109    level_file: Option<LogLevel>,
110    component_levels: Option<HashMap<String, String>>,
111    directory: Option<String>,
112    file_name: Option<String>,
113    file_format: Option<String>,
114    file_rotate: Option<(u64, u32)>,
115    is_colored: Option<bool>,
116    is_bypassed: Option<bool>,
117    print_config: Option<bool>,
118    log_components_only: Option<bool>,
119) -> PyResult<LogGuard> {
120    let level_file = level_file.map_or(LevelFilter::Off, map_log_level_to_filter);
121
122    let component_levels = parse_component_levels(component_levels).map_err(to_pyvalue_err)?;
123
124    let config = LoggerConfig::new(
125        map_log_level_to_filter(level_stdout),
126        level_file,
127        component_levels,
128        log_components_only.unwrap_or(false),
129        is_colored.unwrap_or(true),
130        print_config.unwrap_or(false),
131    );
132
133    let file_config = FileWriterConfig::new(directory, file_name, file_format, file_rotate);
134
135    if is_bypassed.unwrap_or(false) {
136        logging_set_bypass();
137    }
138
139    logging::init_logging(trader_id, instance_id, config, file_config).map_err(to_pyvalue_err)
140}
141
142#[pyfunction()]
143#[pyo3(name = "logger_flush")]
144pub fn py_logger_flush() {
145    log::logger().flush();
146}
147
148fn parse_component_levels(
149    original_map: Option<HashMap<String, String>>,
150) -> anyhow::Result<HashMap<Ustr, LevelFilter>> {
151    match original_map {
152        Some(map) => {
153            let mut new_map = HashMap::new();
154            for (key, value) in map {
155                let ustr_key = Ustr::from(&key);
156                let level = parse_level_filter_str(&value)?;
157                new_map.insert(ustr_key, level);
158            }
159            Ok(new_map)
160        }
161        None => Ok(HashMap::new()),
162    }
163}
164
165/// Create a new log event.
166#[pyfunction]
167#[pyo3(name = "logger_log")]
168pub fn py_logger_log(level: LogLevel, color: LogColor, component: &str, message: &str) {
169    logger::log(level, color, Ustr::from(component), message);
170}
171
172/// Logs the standard Nautilus system header.
173#[pyfunction]
174#[pyo3(name = "log_header")]
175pub fn py_log_header(trader_id: TraderId, machine_id: &str, instance_id: UUID4, component: &str) {
176    headers::log_header(trader_id, machine_id, instance_id, Ustr::from(component));
177}
178
179/// Logs system information.
180#[pyfunction]
181#[pyo3(name = "log_sysinfo")]
182pub fn py_log_sysinfo(component: &str) {
183    headers::log_sysinfo(Ustr::from(component));
184}
185
186#[pyfunction]
187#[pyo3(name = "logging_clock_set_static_mode")]
188pub fn py_logging_clock_set_static_mode() {
189    logging_clock_set_static_mode();
190}
191
192#[pyfunction]
193#[pyo3(name = "logging_clock_set_realtime_mode")]
194pub fn py_logging_clock_set_realtime_mode() {
195    logging_clock_set_realtime_mode();
196}
197
198#[pyfunction]
199#[pyo3(name = "logging_clock_set_static_time")]
200pub fn py_logging_clock_set_static_time(time_ns: u64) {
201    logging_clock_set_static_time(time_ns);
202}
203
204/// A thin wrapper around the global Rust logger which exposes ergonomic
205/// logging helpers for Python code.
206///
207/// It mirrors the familiar Python `logging` interface while forwarding
208/// all records through the Nautilus logging infrastructure so that log levels
209/// and formatting remain consistent across Rust and Python.
210#[pyclass(
211    module = "nautilus_trader.core.nautilus_pyo3.common",
212    name = "Logger",
213    unsendable
214)]
215#[derive(Debug, Clone)]
216pub struct PyLogger {
217    name: Ustr,
218}
219
220impl PyLogger {
221    pub fn new(name: &str) -> Self {
222        Self {
223            name: Ustr::from(name),
224        }
225    }
226}
227
228#[pymethods]
229impl PyLogger {
230    /// Create a new `Logger` instance.
231    #[new]
232    #[pyo3(signature = (name="Python"))]
233    fn py_new(name: &str) -> Self {
234        Self::new(name)
235    }
236
237    /// The component identifier carried by this logger.
238    #[getter]
239    fn name(&self) -> &str {
240        &self.name
241    }
242
243    /// Emit a TRACE level record.
244    #[pyo3(name = "trace")]
245    fn py_trace(&self, message: &str, color: Option<LogColor>) {
246        self._log(LogLevel::Trace, color, message);
247    }
248
249    /// Emit a DEBUG level record.
250    #[pyo3(name = "debug")]
251    fn py_debug(&self, message: &str, color: Option<LogColor>) {
252        self._log(LogLevel::Debug, color, message);
253    }
254
255    /// Emit an INFO level record.
256    #[pyo3(name = "info")]
257    fn py_info(&self, message: &str, color: Option<LogColor>) {
258        self._log(LogLevel::Info, color, message);
259    }
260
261    /// Emit a WARNING level record.
262    #[pyo3(name = "warning")]
263    fn py_warning(&self, message: &str, color: Option<LogColor>) {
264        self._log(LogLevel::Warning, color, message);
265    }
266
267    /// Emit an ERROR level record.
268    #[pyo3(name = "error")]
269    fn py_error(&self, message: &str, color: Option<LogColor>) {
270        self._log(LogLevel::Error, color, message);
271    }
272
273    /// Emit an ERROR level record with the active Python exception info.
274    #[pyo3(name = "exception")]
275    #[pyo3(signature = (message="", color=None))]
276    fn py_exception(&self, py: Python, message: &str, color: Option<LogColor>) {
277        let mut full_msg = message.to_owned();
278
279        if pyo3::PyErr::occurred(py) {
280            let err = PyErr::fetch(py);
281            let err_str = err.to_string();
282            if full_msg.is_empty() {
283                full_msg = err_str;
284            } else {
285                full_msg = format!("{full_msg}: {err_str}");
286            }
287        }
288
289        self._log(LogLevel::Error, color, &full_msg);
290    }
291
292    /// Flush buffered log records.
293    #[pyo3(name = "flush")]
294    fn py_flush(&self) {
295        log::logger().flush();
296    }
297
298    fn _log(&self, level: LogLevel, color: Option<LogColor>, message: &str) {
299        let color = color.unwrap_or(LogColor::Normal);
300        logger::log(level, color, self.name, message);
301    }
302}