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