Skip to main content

nautilus_common/logging/
mod.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
16//! The logging framework for Nautilus systems.
17//!
18//! This module implements a high-performance logging subsystem that operates in a separate thread
19//! using an MPSC channel for log message delivery. The system uses reference counting to track
20//! active `LogGuard` instances, ensuring the logging thread completes all pending writes before
21//! termination.
22//!
23//! # LogGuard Reference Counting
24//!
25//! The logging system maintains a global count of active `LogGuard` instances using an atomic
26//! counter (`LOGGING_GUARDS_ACTIVE`). When a `LogGuard` is created, the counter is incremented,
27//! and when dropped, it's decremented. When the last `LogGuard` is dropped (counter reaches zero),
28//! the logging thread is properly joined to ensure all buffered log messages are written to their
29//! destinations before the process terminates.
30//!
31//! The system supports a maximum of 255 concurrent `LogGuard` instances. Attempting to create
32//! more will cause a panic.
33
34pub mod config;
35pub mod headers;
36pub mod logger;
37pub mod macros;
38pub mod writer;
39
40#[cfg(feature = "tracing-bridge")]
41pub mod bridge;
42
43use std::{
44    collections::HashMap,
45    env,
46    str::FromStr,
47    sync::{
48        OnceLock,
49        atomic::{AtomicBool, AtomicU8, Ordering},
50    },
51};
52
53use ahash::AHashMap;
54use log::LevelFilter;
55// Re-exports
56pub use macros::{log_debug, log_error, log_info, log_trace, log_warn};
57use nautilus_core::{UUID4, time::get_atomic_clock_static};
58use nautilus_model::identifiers::TraderId;
59use ustr::Ustr;
60
61use self::{
62    logger::{LogGuard, Logger, LoggerConfig},
63    writer::FileWriterConfig,
64};
65use crate::enums::LogLevel;
66
67pub const RECV: &str = "<--";
68pub const SEND: &str = "-->";
69pub const CMD: &str = "[CMD]";
70pub const EVT: &str = "[EVT]";
71pub const DOC: &str = "[DOC]";
72pub const RPT: &str = "[RPT]";
73pub const REQ: &str = "[REQ]";
74pub const RES: &str = "[RES]";
75
76static LOGGING_INITIALIZED: AtomicBool = AtomicBool::new(false);
77static LOGGING_BYPASSED: AtomicBool = AtomicBool::new(false);
78static LOGGING_REALTIME: AtomicBool = AtomicBool::new(true);
79static LOGGING_COLORED: AtomicBool = AtomicBool::new(true);
80static LOGGING_GUARDS_ACTIVE: AtomicU8 = AtomicU8::new(0);
81static LAZY_GUARD: OnceLock<Option<LogGuard>> = OnceLock::new();
82
83/// Returns whether the core logger is enabled.
84pub fn logging_is_initialized() -> bool {
85    LOGGING_INITIALIZED.load(Ordering::Relaxed)
86}
87
88/// Ensures logging is initialized on first use.
89///
90/// If `NAUTILUS_LOG` is set, initializes the logger with the specified config.
91/// Otherwise, initializes with INFO level to stdout. This enables lazy
92/// initialization for Rust-only binaries that don't go through the Python
93/// kernel initialization.
94///
95/// Returns `true` if logging is available (either already initialized or
96/// successfully lazy-initialized), `false` otherwise.
97pub fn ensure_logging_initialized() -> bool {
98    if LOGGING_INITIALIZED.load(Ordering::SeqCst) {
99        return true;
100    }
101
102    LAZY_GUARD.get_or_init(|| {
103        let config = env::var("NAUTILUS_LOG")
104            .ok()
105            .and_then(|spec| LoggerConfig::from_spec(&spec).ok())
106            .unwrap_or_default();
107
108        Logger::init_with_config(
109            TraderId::default(),
110            UUID4::default(),
111            config,
112            FileWriterConfig::default(),
113        )
114        .ok()
115    });
116
117    LOGGING_INITIALIZED.load(Ordering::SeqCst)
118}
119
120/// Sets the logging subsystem to bypass mode.
121pub fn logging_set_bypass() {
122    LOGGING_BYPASSED.store(true, Ordering::Relaxed);
123}
124
125/// Shuts down the logging subsystem.
126pub fn logging_shutdown() {
127    // Perform a graceful shutdown: prevent new logs, signal Close, drain and join.
128    // Delegates to logger implementation which has access to the internals.
129    crate::logging::logger::shutdown_graceful();
130}
131
132/// Returns whether the core logger is using ANSI colors.
133pub fn logging_is_colored() -> bool {
134    LOGGING_COLORED.load(Ordering::Relaxed)
135}
136
137/// Sets the global logging clock to real-time mode.
138pub fn logging_clock_set_realtime_mode() {
139    LOGGING_REALTIME.store(true, Ordering::Relaxed);
140}
141
142/// Sets the global logging clock to static mode.
143pub fn logging_clock_set_static_mode() {
144    LOGGING_REALTIME.store(false, Ordering::Relaxed);
145}
146
147/// Sets the global logging clock static time with the given UNIX timestamp (nanoseconds).
148pub fn logging_clock_set_static_time(time_ns: u64) {
149    let clock = get_atomic_clock_static();
150    clock.set_time(time_ns.into());
151}
152
153/// Initialize logging.
154///
155/// Logging should be used for Python and sync Rust logic which is most of
156/// the components in the [nautilus_trader](https://pypi.org/project/nautilus_trader) package.
157/// Logging can be configured to filter components and write up to a specific level only
158/// by passing a configuration using the `NAUTILUS_LOG` environment variable.
159///
160/// # Safety
161///
162/// Should only be called once during an applications run, ideally at the
163/// beginning of the run.
164///
165/// Logging should be used for Python and sync Rust logic which is most of
166/// the components in the `nautilus_trader` package.
167/// Logging can be configured via the `NAUTILUS_LOG` environment variable.
168///
169/// # Errors
170///
171/// Returns an error if the logging subsystem fails to initialize.
172pub fn init_logging(
173    trader_id: TraderId,
174    instance_id: UUID4,
175    config: LoggerConfig,
176    file_config: FileWriterConfig,
177) -> anyhow::Result<LogGuard> {
178    Logger::init_with_config(trader_id, instance_id, config, file_config)
179}
180
181#[must_use]
182pub const fn map_log_level_to_filter(log_level: LogLevel) -> LevelFilter {
183    match log_level {
184        LogLevel::Off => LevelFilter::Off,
185        LogLevel::Trace => LevelFilter::Trace,
186        LogLevel::Debug => LevelFilter::Debug,
187        LogLevel::Info => LevelFilter::Info,
188        LogLevel::Warning => LevelFilter::Warn,
189        LogLevel::Error => LevelFilter::Error,
190    }
191}
192
193/// Parses a string into a [`LevelFilter`].
194///
195/// # Errors
196///
197/// Returns an error if the provided string is not a valid `LevelFilter`.
198pub fn parse_level_filter_str(s: &str) -> anyhow::Result<LevelFilter> {
199    let mut log_level_str = s.to_string().to_uppercase();
200    if log_level_str == "WARNING" {
201        log_level_str = "WARN".to_string();
202    }
203    LevelFilter::from_str(&log_level_str)
204        .map_err(|_| anyhow::anyhow!("Invalid log level string: '{s}'"))
205}
206
207/// Parses component-specific log levels from a JSON value map.
208///
209/// # Errors
210///
211/// Returns an error if a JSON value in the map is not a string or is not a valid log level.
212pub fn parse_component_levels(
213    original_map: Option<HashMap<String, serde_json::Value>>,
214) -> anyhow::Result<AHashMap<Ustr, LevelFilter>> {
215    match original_map {
216        Some(map) => {
217            let mut new_map = AHashMap::new();
218            for (key, value) in map {
219                let ustr_key = Ustr::from(&key);
220                let s = value.as_str().ok_or_else(|| {
221                    anyhow::anyhow!(
222                        "Component log level for '{key}' must be a string, was: {value}"
223                    )
224                })?;
225                let lvl = parse_level_filter_str(s)?;
226                new_map.insert(ustr_key, lvl);
227            }
228            Ok(new_map)
229        }
230        None => Ok(AHashMap::new()),
231    }
232}
233
234/// Logs that a task has started.
235pub fn log_task_started(task_name: &str) {
236    log::debug!("Started task '{task_name}'");
237}
238
239/// Logs that a task has stopped.
240pub fn log_task_stopped(task_name: &str) {
241    log::debug!("Stopped task '{task_name}'");
242}
243
244/// Logs that a task is being awaited.
245pub fn log_task_awaiting(task_name: &str) {
246    log::debug!("Awaiting task '{task_name}'");
247}
248
249/// Logs that a task was aborted.
250pub fn log_task_aborted(task_name: &str) {
251    log::debug!("Aborted task '{task_name}'");
252}
253
254/// Logs that there was an error in a task.
255pub fn log_task_error(task_name: &str, e: &anyhow::Error) {
256    log::error!("Error in task '{task_name}': {e}");
257}
258
259#[cfg(test)]
260mod tests {
261    use rstest::rstest;
262
263    use super::*;
264
265    #[rstest]
266    #[case("DEBUG", LevelFilter::Debug)]
267    #[case("debug", LevelFilter::Debug)]
268    #[case("Debug", LevelFilter::Debug)]
269    #[case("DeBuG", LevelFilter::Debug)]
270    #[case("INFO", LevelFilter::Info)]
271    #[case("info", LevelFilter::Info)]
272    #[case("WARNING", LevelFilter::Warn)]
273    #[case("warning", LevelFilter::Warn)]
274    #[case("WARN", LevelFilter::Warn)]
275    #[case("warn", LevelFilter::Warn)]
276    #[case("ERROR", LevelFilter::Error)]
277    #[case("error", LevelFilter::Error)]
278    #[case("OFF", LevelFilter::Off)]
279    #[case("off", LevelFilter::Off)]
280    #[case("TRACE", LevelFilter::Trace)]
281    #[case("trace", LevelFilter::Trace)]
282    fn test_parse_level_filter_str_case_insensitive(
283        #[case] input: &str,
284        #[case] expected: LevelFilter,
285    ) {
286        let result = parse_level_filter_str(input).unwrap();
287        assert_eq!(result, expected);
288    }
289
290    #[rstest]
291    #[case("INVALID")]
292    #[case("DEBG")]
293    #[case("WARNINGG")]
294    #[case("")]
295    #[case("INFO123")]
296    fn test_parse_level_filter_str_invalid_returns_error(#[case] invalid_input: &str) {
297        let result = parse_level_filter_str(invalid_input);
298
299        assert!(result.is_err());
300        assert!(
301            result
302                .unwrap_err()
303                .to_string()
304                .contains("Invalid log level")
305        );
306    }
307
308    #[rstest]
309    fn test_parse_component_levels_valid() {
310        let mut map = HashMap::new();
311        map.insert(
312            "Strategy1".to_string(),
313            serde_json::Value::String("DEBUG".to_string()),
314        );
315        map.insert(
316            "Strategy2".to_string(),
317            serde_json::Value::String("info".to_string()),
318        );
319
320        let result = parse_component_levels(Some(map)).unwrap();
321
322        assert_eq!(result.len(), 2);
323        assert_eq!(result[&Ustr::from("Strategy1")], LevelFilter::Debug);
324        assert_eq!(result[&Ustr::from("Strategy2")], LevelFilter::Info);
325    }
326
327    #[rstest]
328    fn test_parse_component_levels_non_string_value_returns_error() {
329        let mut map = HashMap::new();
330        map.insert(
331            "Strategy1".to_string(),
332            serde_json::Value::Number(123.into()),
333        );
334
335        let result = parse_component_levels(Some(map));
336
337        assert!(result.is_err());
338        assert!(result.unwrap_err().to_string().contains("must be a string"));
339    }
340
341    #[rstest]
342    fn test_parse_component_levels_invalid_level_returns_error() {
343        let mut map = HashMap::new();
344        map.insert(
345            "Strategy1".to_string(),
346            serde_json::Value::String("INVALID_LEVEL".to_string()),
347        );
348
349        let result = parse_component_levels(Some(map));
350
351        assert!(result.is_err());
352        assert!(
353            result
354                .unwrap_err()
355                .to_string()
356                .contains("Invalid log level")
357        );
358    }
359
360    #[rstest]
361    fn test_parse_component_levels_none_returns_empty() {
362        let result = parse_component_levels(None).unwrap();
363        assert_eq!(result.len(), 0);
364    }
365
366    #[rstest]
367    fn test_logging_clock_set_static_mode() {
368        logging_clock_set_static_mode();
369        assert!(!LOGGING_REALTIME.load(Ordering::Relaxed));
370    }
371
372    #[rstest]
373    fn test_logging_clock_set_realtime_mode() {
374        logging_clock_set_realtime_mode();
375        assert!(LOGGING_REALTIME.load(Ordering::Relaxed));
376    }
377
378    #[rstest]
379    fn test_logging_clock_set_static_time() {
380        let test_time: u64 = 1_700_000_000_000_000_000;
381        logging_clock_set_static_time(test_time);
382        let clock = get_atomic_clock_static();
383        assert_eq!(clock.get_time_ns(), test_time);
384    }
385
386    #[rstest]
387    fn test_logging_set_bypass() {
388        logging_set_bypass();
389        assert!(LOGGING_BYPASSED.load(Ordering::Relaxed));
390    }
391
392    #[rstest]
393    fn test_map_log_level_to_filter() {
394        assert_eq!(map_log_level_to_filter(LogLevel::Off), LevelFilter::Off);
395        assert_eq!(map_log_level_to_filter(LogLevel::Trace), LevelFilter::Trace);
396        assert_eq!(map_log_level_to_filter(LogLevel::Debug), LevelFilter::Debug);
397        assert_eq!(map_log_level_to_filter(LogLevel::Info), LevelFilter::Info);
398        assert_eq!(
399            map_log_level_to_filter(LogLevel::Warning),
400            LevelFilter::Warn
401        );
402        assert_eq!(map_log_level_to_filter(LogLevel::Error), LevelFilter::Error);
403    }
404
405    #[rstest]
406    fn test_ensure_logging_initialized_returns_consistent_value() {
407        // This test verifies ensure_logging_initialized() can be called safely.
408        // Due to Once semantics, we can only test one code path per process.
409        //
410        // With nextest (process isolation per test):
411        // - If NAUTILUS_LOG is unset, this returns false.
412        // - If NAUTILUS_LOG is set externally, it may return true.
413        //
414        // The key invariant: multiple calls return the same value.
415        let first_call = ensure_logging_initialized();
416        let second_call = ensure_logging_initialized();
417
418        assert_eq!(
419            first_call, second_call,
420            "ensure_logging_initialized must be idempotent"
421        );
422        assert_eq!(
423            first_call,
424            logging_is_initialized(),
425            "ensure_logging_initialized return value must match logging_is_initialized()"
426        );
427    }
428
429    #[rstest]
430    fn test_ensure_logging_initialized_fast_path() {
431        // If logging is already initialized, the fast path returns true immediately.
432        // This test documents the expected behavior.
433        if logging_is_initialized() {
434            assert!(
435                ensure_logging_initialized(),
436                "Fast path should return true when already initialized"
437            );
438        }
439        // If not initialized, we can't test the initialization path here
440        // without side effects that affect other tests.
441    }
442}