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    python::config_error_to_pyvalue_err,
34};
35
36#[pymethods]
37#[pyo3_stub_gen::derive::gen_stub_pymethods]
38impl LoggerConfig {
39    /// Configuration for the Nautilus logger.
40    #[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    /// Parses a configuration from a spec string.
92    ///
93    /// # Format
94    ///
95    /// Semicolon-separated key-value pairs or bare flags:
96    /// ```text
97    /// stdout=Info;fileout=Debug;RiskEngine=Error;my_crate::module=Debug;is_colored
98    /// ```
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if the spec string contains invalid syntax or log levels.
103    #[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    /// Creates a new `FileWriterConfig` instance.
114    #[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/// Initialize logging.
128///
129/// Logging should be used for Python and sync Rust logic which is most of
130/// the components in the [nautilus_trader](https://pypi.org/project/nautilus_trader) package.
131/// Logging can be configured to filter components and write up to a specific level only
132/// by passing a configuration using the `NAUTILUS_LOG` environment variable.
133///
134/// Should only be called once during an applications run, ideally at the
135/// beginning of the run.
136#[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(), // module_level - not exposed to Python
172        log_components_only.unwrap_or(false),
173        is_colored.unwrap_or(true),
174        print_config.unwrap_or(false),
175        false,                        // use_tracing - Python handles this separately in kernel
176        is_bypassed.unwrap_or(false), // bypass_logging
177        None,                         // file_config - passed separately to init_logging
178        false,                        // clear_log_file
179    );
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/// Flushes and syncs file logs to disk.
198///
199/// This is a no-op when logging is not initialized or file logging is disabled.
200///
201/// # Errors
202///
203/// Returns an error if the sync request cannot be delivered or acknowledged.
204#[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/// Create a new log event.
232#[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/// Logs the standard Nautilus system header.
240#[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/// Logs system information.
248#[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/// Sets the global logging clock to static mode.
256#[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/// Sets the global logging clock to real-time mode.
264#[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/// Sets the global logging clock static time with the given UNIX timestamp (nanoseconds).
272#[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/// Returns whether the tracing subscriber has been initialized.
280#[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/// Initializes a tracing subscriber for external Rust crate logging.
290///
291/// This sets up a standard tracing subscriber that outputs to stdout with
292/// the format controlled by `RUST_LOG` environment variable. The output
293/// format uses nanosecond timestamps to align with Nautilus logging.
294///
295/// # Environment Variables
296///
297/// - `RUST_LOG`: Controls which modules emit tracing events and at what level.
298///   - Example: `RUST_LOG=hyper=debug,tokio=warn`.
299///   - Default: `warn` (if not set).
300///
301/// # Errors
302///
303/// Returns an error if the tracing subscriber has already been initialized.
304#[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/// A thin wrapper around the global Rust logger which exposes ergonomic
313/// logging helpers for Python code.
314///
315/// It mirrors the familiar Python `logging` interface while forwarding
316/// all records through the Nautilus logging infrastructure so that log levels
317/// and formatting remain consistent across Rust and Python.
318#[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    /// Create a new `Logger` instance.
347    #[new]
348    #[pyo3(signature = (name="Python"))]
349    fn py_new(name: &str) -> Self {
350        Self::new(name)
351    }
352
353    /// The component identifier carried by this logger.
354    #[getter]
355    fn name(&self) -> &str {
356        &self.name
357    }
358
359    /// Emit a TRACE level record.
360    #[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    /// Emit a DEBUG level record.
367    #[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    /// Emit an INFO level record.
374    #[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    /// Emit a WARNING level record.
381    #[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    /// Emit an ERROR level record.
388    #[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    /// Emit an ERROR level record with the active Python exception info.
395    #[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    /// Flush buffered log records.
415    #[pyo3(name = "flush")]
416    fn py_flush(&self) {
417        log::logger().flush();
418    }
419
420    /// Emit a log record at the given level (Python-facing helper).
421    #[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}