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 pending file logs are synced when the last guard drops.
21//!
22//! # `LogGuard` reference counting
23//!
24//! The logging system maintains a global count of active `LogGuard` instances using an atomic
25//! counter (`LOGGING_GUARDS_ACTIVE`). When a `LogGuard` is created, the counter is incremented,
26//! and when dropped, it's decremented. When the last `LogGuard` is dropped (counter reaches zero),
27//! pending file logs are synchronously flushed and synced. The process-global logging thread stays
28//! alive for later guard acquisitions and is only terminated by [`logging_shutdown`].
29//!
30//! The system supports a maximum of 255 concurrent `LogGuard` instances. Attempting to create
31//! more will cause a panic.
32
33pub mod config;
34pub mod headers;
35pub mod logger;
36pub mod macros;
37pub mod writer;
38
39#[cfg(feature = "tracing-bridge")]
40pub mod bridge;
41
42use std::{
43    collections::HashMap,
44    env,
45    io::{self, Write},
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. If `NAUTILUS_LOG` enables file
97/// logging and the log file cannot be opened, the error is reported on stderr and
98/// logging continues to the console only.
99pub fn ensure_logging_initialized() -> bool {
100    if crate::logging::logger::is_running() {
101        return true;
102    }
103
104    LAZY_GUARD.get_or_init(|| {
105        let config = env::var("NAUTILUS_LOG")
106            .ok()
107            .and_then(|spec| LoggerConfig::from_spec(&spec).ok())
108            .unwrap_or_default();
109
110        let file_logging = config.fileout_level != LevelFilter::Off;
111        let result = Logger::init_with_config(
112            TraderId::default(),
113            UUID4::default(),
114            config.clone(),
115            FileWriterConfig::default(),
116        );
117
118        // Callers ignore the return value, so keep console logging rather than run with no logger
119        let result = match result {
120            Err(e) if file_logging => {
121                let _ = writeln!(io::stderr(), "Continuing without file logging: {e:#}");
122
123                let console_config = LoggerConfig {
124                    fileout_level: LevelFilter::Off,
125                    ..config
126                };
127
128                Logger::init_with_config(
129                    TraderId::default(),
130                    UUID4::default(),
131                    console_config,
132                    FileWriterConfig::default(),
133                )
134            }
135            result => result,
136        };
137
138        result
139            .inspect_err(|e| {
140                let _ = writeln!(io::stderr(), "Failed to initialize logging: {e:#}");
141            })
142            .ok()
143    });
144
145    crate::logging::logger::is_running()
146}
147
148/// Sets the logging subsystem to bypass mode.
149pub fn logging_set_bypass() {
150    LOGGING_BYPASSED.store(true, Ordering::Relaxed);
151}
152
153/// Shuts down the logging subsystem.
154pub fn logging_shutdown() {
155    // Perform a graceful shutdown: prevent new logs, signal Close, drain, and join.
156    // Delegates to logger implementation which has access to the internals.
157    crate::logging::logger::shutdown_graceful();
158}
159
160/// Arms shutdown-on-error handling for the current run.
161pub fn arm_shutdown_on_error(enabled: bool) {
162    crate::logging::logger::arm_shutdown_on_error(enabled);
163}
164
165/// Disarms shutdown-on-error handling.
166pub fn disarm_shutdown_on_error() {
167    crate::logging::logger::disarm_shutdown_on_error();
168}
169
170/// Returns and clears the pending shutdown-on-error trigger, if one was recorded.
171pub fn take_shutdown_on_error_trigger() -> Option<crate::logging::logger::ShutdownOnErrorTrigger> {
172    crate::logging::logger::take_shutdown_on_error_trigger()
173}
174
175/// Conditionally drains the pending shutdown-on-error trigger.
176pub fn try_drain_shutdown_on_error_trigger<F>(drain: F) -> bool
177where
178    F: FnOnce(&crate::logging::logger::ShutdownOnErrorTrigger) -> bool,
179{
180    crate::logging::logger::try_drain_shutdown_on_error_trigger(drain)
181}
182
183/// Flushes and syncs file logs to disk.
184///
185/// This is a no-op when logging is not initialized or file logging is disabled.
186///
187/// # Errors
188///
189/// Returns an error if the sync request cannot be delivered or acknowledged.
190pub fn logging_sync_to_disk() -> anyhow::Result<()> {
191    crate::logging::logger::sync_to_disk()
192}
193
194/// Returns whether the core logger is using ANSI colors.
195pub fn logging_is_colored() -> bool {
196    LOGGING_COLORED.load(Ordering::Relaxed)
197}
198
199/// Sets the global logging clock to real-time mode.
200pub fn logging_clock_set_realtime_mode() {
201    LOGGING_REALTIME.store(true, Ordering::Relaxed);
202}
203
204/// Sets the global logging clock to static mode.
205pub fn logging_clock_set_static_mode() {
206    LOGGING_REALTIME.store(false, Ordering::Relaxed);
207}
208
209/// Sets the global logging clock static time with the given UNIX timestamp (nanoseconds).
210pub fn logging_clock_set_static_time(time_ns: u64) {
211    let clock = get_atomic_clock_static();
212    clock.set_time(time_ns.into());
213}
214
215/// Initialize logging.
216///
217/// Logging should be used for Python and sync Rust logic which is most of
218/// the components in the [nautilus_trader](https://pypi.org/project/nautilus_trader) package.
219/// Logging can be configured to filter components and write up to a specific level only
220/// by passing a configuration using the `NAUTILUS_LOG` environment variable.
221///
222/// Should only be called once during an applications run, ideally at the
223/// beginning of the run.
224///
225/// # Errors
226///
227/// Returns an error if the logging subsystem fails to initialize.
228pub fn init_logging(
229    trader_id: TraderId,
230    instance_id: UUID4,
231    config: LoggerConfig,
232    file_config: FileWriterConfig,
233) -> anyhow::Result<LogGuard> {
234    Logger::init_with_config(trader_id, instance_id, config, file_config)
235}
236
237/// Maps a [`LogLevel`] to a [`LevelFilter`].
238#[must_use]
239pub const fn map_log_level_to_filter(log_level: LogLevel) -> LevelFilter {
240    match log_level {
241        LogLevel::Off => LevelFilter::Off,
242        LogLevel::Trace => LevelFilter::Trace,
243        LogLevel::Debug => LevelFilter::Debug,
244        LogLevel::Info => LevelFilter::Info,
245        LogLevel::Warning => LevelFilter::Warn,
246        LogLevel::Error => LevelFilter::Error,
247    }
248}
249
250/// Parses a string into a [`LevelFilter`].
251///
252/// # Errors
253///
254/// Returns an error if the provided string is not a valid `LevelFilter`.
255pub fn parse_level_filter_str(s: &str) -> anyhow::Result<LevelFilter> {
256    let mut log_level_str = s.to_uppercase();
257    if log_level_str == "WARNING" {
258        log_level_str = "WARN".to_string();
259    }
260    LevelFilter::from_str(&log_level_str)
261        .map_err(|_| anyhow::anyhow!("Invalid log level string: '{s}'"))
262}
263
264/// Parses component-specific log levels from a JSON value map.
265///
266/// # Errors
267///
268/// Returns an error if a JSON value in the map is not a string or is not a valid log level.
269pub fn parse_component_levels(
270    original_map: Option<HashMap<String, serde_json::Value>>,
271) -> anyhow::Result<AHashMap<Ustr, LevelFilter>> {
272    let mut new_map = AHashMap::new();
273
274    for (key, value) in original_map.unwrap_or_default() {
275        let ustr_key = Ustr::from(&key);
276        let s = value.as_str().ok_or_else(|| {
277            anyhow::anyhow!("Component log level for '{key}' must be a string, was: {value}")
278        })?;
279        let lvl = parse_level_filter_str(s)?;
280        new_map.insert(ustr_key, lvl);
281    }
282
283    Ok(new_map)
284}
285
286/// Logs that a task has started.
287pub fn log_task_started(task_name: &str) {
288    log::debug!("Started task '{task_name}'");
289}
290
291/// Logs that a task has stopped.
292pub fn log_task_stopped(task_name: &str) {
293    log::debug!("Stopped task '{task_name}'");
294}
295
296/// Logs that a task is being awaited.
297pub fn log_task_awaiting(task_name: &str) {
298    log::debug!("Awaiting task '{task_name}'");
299}
300
301/// Logs that a task was aborted.
302pub fn log_task_aborted(task_name: &str) {
303    log::debug!("Aborted task '{task_name}'");
304}
305
306/// Logs that there was an error in a task.
307pub fn log_task_error(task_name: &str, e: &anyhow::Error) {
308    log::error!("Error in task '{task_name}': {e}");
309}
310
311#[cfg(test)]
312mod tests {
313    use rstest::rstest;
314
315    use super::*;
316
317    #[rstest]
318    #[case("DEBUG", LevelFilter::Debug)]
319    #[case("debug", LevelFilter::Debug)]
320    #[case("Debug", LevelFilter::Debug)]
321    #[case("DeBuG", LevelFilter::Debug)]
322    #[case("INFO", LevelFilter::Info)]
323    #[case("info", LevelFilter::Info)]
324    #[case("WARNING", LevelFilter::Warn)]
325    #[case("warning", LevelFilter::Warn)]
326    #[case("WARN", LevelFilter::Warn)]
327    #[case("warn", LevelFilter::Warn)]
328    #[case("ERROR", LevelFilter::Error)]
329    #[case("error", LevelFilter::Error)]
330    #[case("OFF", LevelFilter::Off)]
331    #[case("off", LevelFilter::Off)]
332    #[case("TRACE", LevelFilter::Trace)]
333    #[case("trace", LevelFilter::Trace)]
334    fn test_parse_level_filter_str_case_insensitive(
335        #[case] input: &str,
336        #[case] expected: LevelFilter,
337    ) {
338        let result = parse_level_filter_str(input).unwrap();
339        assert_eq!(result, expected);
340    }
341
342    #[rstest]
343    #[case("INVALID")]
344    #[case("DEBG")]
345    #[case("WARNINGG")]
346    #[case("")]
347    #[case("INFO123")]
348    fn test_parse_level_filter_str_invalid_returns_error(#[case] invalid_input: &str) {
349        let result = parse_level_filter_str(invalid_input);
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_valid() {
362        let mut map = HashMap::new();
363        map.insert(
364            "Strategy1".to_string(),
365            serde_json::Value::String("DEBUG".to_string()),
366        );
367        map.insert(
368            "Strategy2".to_string(),
369            serde_json::Value::String("info".to_string()),
370        );
371
372        let result = parse_component_levels(Some(map)).unwrap();
373
374        assert_eq!(result.len(), 2);
375        assert_eq!(result[&Ustr::from("Strategy1")], LevelFilter::Debug);
376        assert_eq!(result[&Ustr::from("Strategy2")], LevelFilter::Info);
377    }
378
379    #[rstest]
380    fn test_parse_component_levels_non_string_value_returns_error() {
381        let mut map = HashMap::new();
382        map.insert(
383            "Strategy1".to_string(),
384            serde_json::Value::Number(123.into()),
385        );
386
387        let result = parse_component_levels(Some(map));
388
389        assert!(result.is_err());
390        assert!(result.unwrap_err().to_string().contains("must be a string"));
391    }
392
393    #[rstest]
394    fn test_parse_component_levels_invalid_level_returns_error() {
395        let mut map = HashMap::new();
396        map.insert(
397            "Strategy1".to_string(),
398            serde_json::Value::String("INVALID_LEVEL".to_string()),
399        );
400
401        let result = parse_component_levels(Some(map));
402
403        assert!(result.is_err());
404        assert!(
405            result
406                .unwrap_err()
407                .to_string()
408                .contains("Invalid log level")
409        );
410    }
411
412    #[rstest]
413    fn test_parse_component_levels_none_returns_empty() {
414        let result = parse_component_levels(None).unwrap();
415        assert_eq!(result.len(), 0);
416    }
417
418    #[rstest]
419    fn test_logging_clock_set_static_mode() {
420        logging_clock_set_static_mode();
421        assert!(!LOGGING_REALTIME.load(Ordering::Relaxed));
422    }
423
424    #[rstest]
425    fn test_logging_clock_set_realtime_mode() {
426        logging_clock_set_realtime_mode();
427        assert!(LOGGING_REALTIME.load(Ordering::Relaxed));
428    }
429
430    #[rstest]
431    fn test_logging_clock_set_static_time() {
432        let test_time: u64 = 1_700_000_000_000_000_000;
433        logging_clock_set_static_time(test_time);
434        let clock = get_atomic_clock_static();
435        assert_eq!(clock.get_time_ns(), test_time);
436    }
437
438    #[rstest]
439    fn test_logging_set_bypass() {
440        logging_set_bypass();
441        assert!(LOGGING_BYPASSED.load(Ordering::Relaxed));
442    }
443
444    #[rstest]
445    fn test_map_log_level_to_filter() {
446        assert_eq!(map_log_level_to_filter(LogLevel::Off), LevelFilter::Off);
447        assert_eq!(map_log_level_to_filter(LogLevel::Trace), LevelFilter::Trace);
448        assert_eq!(map_log_level_to_filter(LogLevel::Debug), LevelFilter::Debug);
449        assert_eq!(map_log_level_to_filter(LogLevel::Info), LevelFilter::Info);
450        assert_eq!(
451            map_log_level_to_filter(LogLevel::Warning),
452            LevelFilter::Warn
453        );
454        assert_eq!(map_log_level_to_filter(LogLevel::Error), LevelFilter::Error);
455    }
456
457    #[rstest]
458    fn test_ensure_logging_initialized_returns_consistent_value() {
459        // This test verifies ensure_logging_initialized() can be called safely.
460        // Due to Once semantics, we can only test one code path per process.
461        //
462        // With nextest (process isolation per test):
463        // - If NAUTILUS_LOG is unset, this returns false.
464        // - If NAUTILUS_LOG is set externally, it may return true.
465        //
466        // The key invariant: multiple calls return the same value.
467        let first_call = ensure_logging_initialized();
468        let second_call = ensure_logging_initialized();
469
470        assert_eq!(
471            first_call, second_call,
472            "ensure_logging_initialized must be idempotent"
473        );
474        assert_eq!(
475            first_call,
476            logging_is_initialized(),
477            "ensure_logging_initialized return value must match logging_is_initialized()"
478        );
479    }
480
481    #[rstest]
482    fn test_ensure_logging_initialized_fast_path() {
483        // If logging is already initialized, the fast path returns true immediately.
484        // This test documents the expected behavior.
485        if logging_is_initialized() {
486            assert!(
487                ensure_logging_initialized(),
488                "Fast path should return true when already initialized"
489            );
490        }
491        // If not initialized, we can't test the initialization path here
492        // without side effects that affect other tests.
493    }
494}