1pub 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;
55pub 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
83pub fn logging_is_initialized() -> bool {
85 LOGGING_INITIALIZED.load(Ordering::Relaxed)
86}
87
88pub 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 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
148pub fn logging_set_bypass() {
150 LOGGING_BYPASSED.store(true, Ordering::Relaxed);
151}
152
153pub fn logging_shutdown() {
155 crate::logging::logger::shutdown_graceful();
158}
159
160pub fn arm_shutdown_on_error(enabled: bool) {
162 crate::logging::logger::arm_shutdown_on_error(enabled);
163}
164
165pub fn disarm_shutdown_on_error() {
167 crate::logging::logger::disarm_shutdown_on_error();
168}
169
170pub fn take_shutdown_on_error_trigger() -> Option<crate::logging::logger::ShutdownOnErrorTrigger> {
172 crate::logging::logger::take_shutdown_on_error_trigger()
173}
174
175pub 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
183pub fn logging_sync_to_disk() -> anyhow::Result<()> {
191 crate::logging::logger::sync_to_disk()
192}
193
194pub fn logging_is_colored() -> bool {
196 LOGGING_COLORED.load(Ordering::Relaxed)
197}
198
199pub fn logging_clock_set_realtime_mode() {
201 LOGGING_REALTIME.store(true, Ordering::Relaxed);
202}
203
204pub fn logging_clock_set_static_mode() {
206 LOGGING_REALTIME.store(false, Ordering::Relaxed);
207}
208
209pub 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
215pub 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#[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
250pub 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
264pub 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
286pub fn log_task_started(task_name: &str) {
288 log::debug!("Started task '{task_name}'");
289}
290
291pub fn log_task_stopped(task_name: &str) {
293 log::debug!("Stopped task '{task_name}'");
294}
295
296pub fn log_task_awaiting(task_name: &str) {
298 log::debug!("Awaiting task '{task_name}'");
299}
300
301pub fn log_task_aborted(task_name: &str) {
303 log::debug!("Aborted task '{task_name}'");
304}
305
306pub 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 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_initialized() {
486 assert!(
487 ensure_logging_initialized(),
488 "Fast path should return true when already initialized"
489 );
490 }
491 }
494}