Skip to main content

nautilus_common/python/
logging.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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 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};
34
35#[pymethods]
36#[pyo3_stub_gen::derive::gen_stub_pymethods]
37impl LoggerConfig {
38    /// Parses a configuration from a spec string.
39    ///
40    /// # Format
41    ///
42    /// Semicolon-separated key-value pairs or bare flags:
43    /// ```text
44    /// stdout=Info;fileout=Debug;RiskEngine=Error;my_crate::module=Debug;is_colored
45    /// ```
46    ///
47    /// # Errors
48    ///
49    /// Returns an error if the spec string contains invalid syntax or log levels.
50    #[staticmethod]
51    #[pyo3(name = "from_spec")]
52    pub fn py_from_spec(spec: &str) -> PyResult<Self> {
53        Self::from_spec(spec).map_err(to_pyvalue_err)
54    }
55}
56
57#[pymethods]
58#[pyo3_stub_gen::derive::gen_stub_pymethods]
59impl FileWriterConfig {
60    /// Creates a new `FileWriterConfig` instance.
61    #[new]
62    #[pyo3(signature = (directory=None, file_name=None, file_format=None, file_rotate=None))]
63    #[must_use]
64    pub fn py_new(
65        directory: Option<String>,
66        file_name: Option<String>,
67        file_format: Option<String>,
68        file_rotate: Option<(u64, u32)>,
69    ) -> Self {
70        Self::new(directory, file_name, file_format, file_rotate)
71    }
72}
73
74/// Initialize logging.
75///
76/// Logging should be used for Python and sync Rust logic which is most of
77/// the components in the [nautilus_trader](https://pypi.org/project/nautilus_trader) package.
78/// Logging can be configured to filter components and write up to a specific level only
79/// by passing a configuration using the `NAUTILUS_LOG` environment variable.
80///
81/// Should only be called once during an applications run, ideally at the
82/// beginning of the run.
83#[pyfunction]
84#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
85#[pyo3(name = "init_logging")]
86#[expect(clippy::too_many_arguments)]
87#[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))]
88pub fn py_init_logging(
89    trader_id: TraderId,
90    instance_id: UUID4,
91    level_stdout: LogLevel,
92    level_file: Option<LogLevel>,
93    component_levels: Option<std::collections::HashMap<String, String>>,
94    directory: Option<String>,
95    file_name: Option<String>,
96    file_format: Option<String>,
97    file_rotate: Option<(u64, u32)>,
98    is_colored: Option<bool>,
99    is_bypassed: Option<bool>,
100    print_config: Option<bool>,
101    log_components_only: Option<bool>,
102) -> PyResult<LogGuard> {
103    let level_file = level_file.map_or(LevelFilter::Off, map_log_level_to_filter);
104
105    let component_levels = parse_component_levels(component_levels).map_err(to_pyvalue_err)?;
106
107    let file_config = FileWriterConfig::new(directory, file_name, file_format, file_rotate);
108
109    let config = LoggerConfig::new(
110        map_log_level_to_filter(level_stdout),
111        level_file,
112        component_levels,
113        AHashMap::new(), // module_level - not exposed to Python
114        log_components_only.unwrap_or(false),
115        is_colored.unwrap_or(true),
116        print_config.unwrap_or(false),
117        false,                        // use_tracing - Python handles this separately in kernel
118        is_bypassed.unwrap_or(false), // bypass_logging
119        None,                         // file_config - passed separately to init_logging
120        false,                        // clear_log_file
121    );
122
123    if config.bypass_logging {
124        logging_set_bypass();
125    }
126
127    logging::init_logging(trader_id, instance_id, config, file_config).map_err(to_pyvalue_err)
128}
129
130#[pyfunction()]
131#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
132#[pyo3(name = "logger_flush")]
133pub fn py_logger_flush() {
134    log::logger().flush();
135}
136
137fn parse_component_levels(
138    original_map: Option<std::collections::HashMap<String, String>>,
139) -> anyhow::Result<AHashMap<Ustr, LevelFilter>> {
140    match original_map {
141        Some(map) => {
142            let mut new_map = AHashMap::new();
143
144            for (key, value) in map {
145                let ustr_key = Ustr::from(&key);
146                let level = parse_level_filter_str(&value)?;
147                new_map.insert(ustr_key, level);
148            }
149            Ok(new_map)
150        }
151        None => Ok(AHashMap::new()),
152    }
153}
154
155/// Create a new log event.
156#[pyfunction]
157#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
158#[pyo3(name = "logger_log")]
159pub fn py_logger_log(level: LogLevel, color: LogColor, component: &str, message: &str) {
160    logger::log(level, color, Ustr::from(component), message);
161}
162
163/// Logs the standard Nautilus system header.
164#[pyfunction]
165#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
166#[pyo3(name = "log_header")]
167pub fn py_log_header(trader_id: TraderId, machine_id: &str, instance_id: UUID4, component: &str) {
168    headers::log_header(trader_id, machine_id, instance_id, Ustr::from(component));
169}
170
171/// Logs system information.
172#[pyfunction]
173#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
174#[pyo3(name = "log_sysinfo")]
175pub fn py_log_sysinfo(component: &str) {
176    headers::log_sysinfo(Ustr::from(component));
177}
178
179/// Sets the global logging clock to static mode.
180#[pyfunction]
181#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
182#[pyo3(name = "logging_clock_set_static_mode")]
183pub fn py_logging_clock_set_static_mode() {
184    logging_clock_set_static_mode();
185}
186
187/// Sets the global logging clock to real-time mode.
188#[pyfunction]
189#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
190#[pyo3(name = "logging_clock_set_realtime_mode")]
191pub fn py_logging_clock_set_realtime_mode() {
192    logging_clock_set_realtime_mode();
193}
194
195/// Sets the global logging clock static time with the given UNIX timestamp (nanoseconds).
196#[pyfunction]
197#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
198#[pyo3(name = "logging_clock_set_static_time")]
199pub fn py_logging_clock_set_static_time(time_ns: u64) {
200    logging_clock_set_static_time(time_ns);
201}
202
203/// Returns whether the tracing subscriber has been initialized.
204#[cfg(feature = "tracing-bridge")]
205#[pyfunction]
206#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
207#[pyo3(name = "tracing_is_initialized")]
208#[must_use]
209pub fn py_tracing_is_initialized() -> bool {
210    crate::logging::bridge::tracing_is_initialized()
211}
212
213/// Initializes a tracing subscriber for external Rust crate logging.
214///
215/// This sets up a standard tracing subscriber that outputs to stdout with
216/// the format controlled by `RUST_LOG` environment variable. The output
217/// format uses nanosecond timestamps to align with Nautilus logging.
218///
219/// # Environment Variables
220///
221/// - `RUST_LOG`: Controls which modules emit tracing events and at what level.
222///   - Example: `RUST_LOG=hyper=debug,tokio=warn`.
223///   - Default: `warn` (if not set).
224///
225/// # Errors
226///
227/// Returns an error if the tracing subscriber has already been initialized.
228#[cfg(feature = "tracing-bridge")]
229#[pyfunction]
230#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.common")]
231#[pyo3(name = "init_tracing")]
232pub fn py_init_tracing() -> PyResult<()> {
233    crate::logging::bridge::init_tracing().map_err(to_pyvalue_err)
234}
235
236/// A thin wrapper around the global Rust logger which exposes ergonomic
237/// logging helpers for Python code.
238///
239/// It mirrors the familiar Python `logging` interface while forwarding
240/// all records through the Nautilus logging infrastructure so that log levels
241/// and formatting remain consistent across Rust and Python.
242#[pyclass(
243    module = "nautilus_trader.core.nautilus_pyo3.common",
244    name = "Logger",
245    unsendable,
246    from_py_object
247)]
248#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")]
249#[derive(Debug, Clone)]
250pub struct PyLogger {
251    name: Ustr,
252}
253
254impl PyLogger {
255    pub fn new(name: &str) -> Self {
256        Self {
257            name: Ustr::from(name),
258        }
259    }
260}
261
262#[pymethods]
263#[pyo3_stub_gen::derive::gen_stub_pymethods]
264impl PyLogger {
265    /// Create a new `Logger` instance.
266    #[new]
267    #[pyo3(signature = (name="Python"))]
268    fn py_new(name: &str) -> Self {
269        Self::new(name)
270    }
271
272    /// The component identifier carried by this logger.
273    #[getter]
274    fn name(&self) -> &str {
275        &self.name
276    }
277
278    /// Emit a TRACE level record.
279    #[pyo3(name = "trace")]
280    fn py_trace(&self, message: &str, color: Option<LogColor>) {
281        self._log(LogLevel::Trace, color, message);
282    }
283
284    /// Emit a DEBUG level record.
285    #[pyo3(name = "debug")]
286    fn py_debug(&self, message: &str, color: Option<LogColor>) {
287        self._log(LogLevel::Debug, color, message);
288    }
289
290    /// Emit an INFO level record.
291    #[pyo3(name = "info")]
292    fn py_info(&self, message: &str, color: Option<LogColor>) {
293        self._log(LogLevel::Info, color, message);
294    }
295
296    /// Emit a WARNING level record.
297    #[pyo3(name = "warning")]
298    fn py_warning(&self, message: &str, color: Option<LogColor>) {
299        self._log(LogLevel::Warning, color, message);
300    }
301
302    /// Emit an ERROR level record.
303    #[pyo3(name = "error")]
304    fn py_error(&self, message: &str, color: Option<LogColor>) {
305        self._log(LogLevel::Error, color, message);
306    }
307
308    /// Emit an ERROR level record with the active Python exception info.
309    #[pyo3(name = "exception")]
310    #[pyo3(signature = (message="", color=None))]
311    fn py_exception(&self, py: Python, message: &str, color: Option<LogColor>) {
312        let mut full_msg = message.to_owned();
313
314        if pyo3::PyErr::occurred(py) {
315            let err = PyErr::fetch(py);
316            let err_str = err.to_string();
317
318            if full_msg.is_empty() {
319                full_msg = err_str;
320            } else {
321                full_msg = format!("{full_msg}: {err_str}");
322            }
323        }
324
325        self._log(LogLevel::Error, color, &full_msg);
326    }
327
328    /// Flush buffered log records.
329    #[pyo3(name = "flush")]
330    fn py_flush(&self) {
331        log::logger().flush();
332    }
333
334    fn _log(&self, level: LogLevel, color: Option<LogColor>, message: &str) {
335        let color = color.unwrap_or(LogColor::Normal);
336        logger::log(level, color, self.name, message);
337    }
338}