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