1pub 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;
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 {
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
120pub fn logging_set_bypass() {
122 LOGGING_BYPASSED.store(true, Ordering::Relaxed);
123}
124
125pub fn logging_shutdown() {
127 crate::logging::logger::shutdown_graceful();
130}
131
132pub fn arm_shutdown_on_error(enabled: bool) {
134 crate::logging::logger::arm_shutdown_on_error(enabled);
135}
136
137pub fn disarm_shutdown_on_error() {
139 crate::logging::logger::disarm_shutdown_on_error();
140}
141
142pub fn take_shutdown_on_error_trigger() -> Option<crate::logging::logger::ShutdownOnErrorTrigger> {
144 crate::logging::logger::take_shutdown_on_error_trigger()
145}
146
147pub fn try_drain_shutdown_on_error_trigger<F>(drain: F) -> bool
149where
150 F: FnOnce(&crate::logging::logger::ShutdownOnErrorTrigger) -> bool,
151{
152 crate::logging::logger::try_drain_shutdown_on_error_trigger(drain)
153}
154
155pub fn logging_sync_to_disk() -> anyhow::Result<()> {
163 crate::logging::logger::sync_to_disk()
164}
165
166pub fn logging_is_colored() -> bool {
168 LOGGING_COLORED.load(Ordering::Relaxed)
169}
170
171pub fn logging_clock_set_realtime_mode() {
173 LOGGING_REALTIME.store(true, Ordering::Relaxed);
174}
175
176pub fn logging_clock_set_static_mode() {
178 LOGGING_REALTIME.store(false, Ordering::Relaxed);
179}
180
181pub fn logging_clock_set_static_time(time_ns: u64) {
183 let clock = get_atomic_clock_static();
184 clock.set_time(time_ns.into());
185}
186
187pub fn init_logging(
201 trader_id: TraderId,
202 instance_id: UUID4,
203 config: LoggerConfig,
204 file_config: FileWriterConfig,
205) -> anyhow::Result<LogGuard> {
206 Logger::init_with_config(trader_id, instance_id, config, file_config)
207}
208
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
221pub fn parse_level_filter_str(s: &str) -> anyhow::Result<LevelFilter> {
227 let mut log_level_str = s.to_string().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
235pub fn parse_component_levels(
241 original_map: Option<HashMap<String, serde_json::Value>>,
242) -> anyhow::Result<AHashMap<Ustr, LevelFilter>> {
243 match original_map {
244 Some(map) => {
245 let mut new_map = AHashMap::new();
246
247 for (key, value) in map {
248 let ustr_key = Ustr::from(&key);
249 let s = value.as_str().ok_or_else(|| {
250 anyhow::anyhow!(
251 "Component log level for '{key}' must be a string, was: {value}"
252 )
253 })?;
254 let lvl = parse_level_filter_str(s)?;
255 new_map.insert(ustr_key, lvl);
256 }
257 Ok(new_map)
258 }
259 None => Ok(AHashMap::new()),
260 }
261}
262
263pub fn log_task_started(task_name: &str) {
265 log::debug!("Started task '{task_name}'");
266}
267
268pub fn log_task_stopped(task_name: &str) {
270 log::debug!("Stopped task '{task_name}'");
271}
272
273pub fn log_task_awaiting(task_name: &str) {
275 log::debug!("Awaiting task '{task_name}'");
276}
277
278pub fn log_task_aborted(task_name: &str) {
280 log::debug!("Aborted task '{task_name}'");
281}
282
283pub fn log_task_error(task_name: &str, e: &anyhow::Error) {
285 log::error!("Error in task '{task_name}': {e}");
286}
287
288#[cfg(test)]
289mod tests {
290 use rstest::rstest;
291
292 use super::*;
293
294 #[rstest]
295 #[case("DEBUG", LevelFilter::Debug)]
296 #[case("debug", LevelFilter::Debug)]
297 #[case("Debug", LevelFilter::Debug)]
298 #[case("DeBuG", LevelFilter::Debug)]
299 #[case("INFO", LevelFilter::Info)]
300 #[case("info", LevelFilter::Info)]
301 #[case("WARNING", LevelFilter::Warn)]
302 #[case("warning", LevelFilter::Warn)]
303 #[case("WARN", LevelFilter::Warn)]
304 #[case("warn", LevelFilter::Warn)]
305 #[case("ERROR", LevelFilter::Error)]
306 #[case("error", LevelFilter::Error)]
307 #[case("OFF", LevelFilter::Off)]
308 #[case("off", LevelFilter::Off)]
309 #[case("TRACE", LevelFilter::Trace)]
310 #[case("trace", LevelFilter::Trace)]
311 fn test_parse_level_filter_str_case_insensitive(
312 #[case] input: &str,
313 #[case] expected: LevelFilter,
314 ) {
315 let result = parse_level_filter_str(input).unwrap();
316 assert_eq!(result, expected);
317 }
318
319 #[rstest]
320 #[case("INVALID")]
321 #[case("DEBG")]
322 #[case("WARNINGG")]
323 #[case("")]
324 #[case("INFO123")]
325 fn test_parse_level_filter_str_invalid_returns_error(#[case] invalid_input: &str) {
326 let result = parse_level_filter_str(invalid_input);
327
328 assert!(result.is_err());
329 assert!(
330 result
331 .unwrap_err()
332 .to_string()
333 .contains("Invalid log level")
334 );
335 }
336
337 #[rstest]
338 fn test_parse_component_levels_valid() {
339 let mut map = HashMap::new();
340 map.insert(
341 "Strategy1".to_string(),
342 serde_json::Value::String("DEBUG".to_string()),
343 );
344 map.insert(
345 "Strategy2".to_string(),
346 serde_json::Value::String("info".to_string()),
347 );
348
349 let result = parse_component_levels(Some(map)).unwrap();
350
351 assert_eq!(result.len(), 2);
352 assert_eq!(result[&Ustr::from("Strategy1")], LevelFilter::Debug);
353 assert_eq!(result[&Ustr::from("Strategy2")], LevelFilter::Info);
354 }
355
356 #[rstest]
357 fn test_parse_component_levels_non_string_value_returns_error() {
358 let mut map = HashMap::new();
359 map.insert(
360 "Strategy1".to_string(),
361 serde_json::Value::Number(123.into()),
362 );
363
364 let result = parse_component_levels(Some(map));
365
366 assert!(result.is_err());
367 assert!(result.unwrap_err().to_string().contains("must be a string"));
368 }
369
370 #[rstest]
371 fn test_parse_component_levels_invalid_level_returns_error() {
372 let mut map = HashMap::new();
373 map.insert(
374 "Strategy1".to_string(),
375 serde_json::Value::String("INVALID_LEVEL".to_string()),
376 );
377
378 let result = parse_component_levels(Some(map));
379
380 assert!(result.is_err());
381 assert!(
382 result
383 .unwrap_err()
384 .to_string()
385 .contains("Invalid log level")
386 );
387 }
388
389 #[rstest]
390 fn test_parse_component_levels_none_returns_empty() {
391 let result = parse_component_levels(None).unwrap();
392 assert_eq!(result.len(), 0);
393 }
394
395 #[rstest]
396 fn test_logging_clock_set_static_mode() {
397 logging_clock_set_static_mode();
398 assert!(!LOGGING_REALTIME.load(Ordering::Relaxed));
399 }
400
401 #[rstest]
402 fn test_logging_clock_set_realtime_mode() {
403 logging_clock_set_realtime_mode();
404 assert!(LOGGING_REALTIME.load(Ordering::Relaxed));
405 }
406
407 #[rstest]
408 fn test_logging_clock_set_static_time() {
409 let test_time: u64 = 1_700_000_000_000_000_000;
410 logging_clock_set_static_time(test_time);
411 let clock = get_atomic_clock_static();
412 assert_eq!(clock.get_time_ns(), test_time);
413 }
414
415 #[rstest]
416 fn test_logging_set_bypass() {
417 logging_set_bypass();
418 assert!(LOGGING_BYPASSED.load(Ordering::Relaxed));
419 }
420
421 #[rstest]
422 fn test_map_log_level_to_filter() {
423 assert_eq!(map_log_level_to_filter(LogLevel::Off), LevelFilter::Off);
424 assert_eq!(map_log_level_to_filter(LogLevel::Trace), LevelFilter::Trace);
425 assert_eq!(map_log_level_to_filter(LogLevel::Debug), LevelFilter::Debug);
426 assert_eq!(map_log_level_to_filter(LogLevel::Info), LevelFilter::Info);
427 assert_eq!(
428 map_log_level_to_filter(LogLevel::Warning),
429 LevelFilter::Warn
430 );
431 assert_eq!(map_log_level_to_filter(LogLevel::Error), LevelFilter::Error);
432 }
433
434 #[rstest]
435 fn test_ensure_logging_initialized_returns_consistent_value() {
436 let first_call = ensure_logging_initialized();
445 let second_call = ensure_logging_initialized();
446
447 assert_eq!(
448 first_call, second_call,
449 "ensure_logging_initialized must be idempotent"
450 );
451 assert_eq!(
452 first_call,
453 logging_is_initialized(),
454 "ensure_logging_initialized return value must match logging_is_initialized()"
455 );
456 }
457
458 #[rstest]
459 fn test_ensure_logging_initialized_fast_path() {
460 if logging_is_initialized() {
463 assert!(
464 ensure_logging_initialized(),
465 "Fast path should return true when already initialized"
466 );
467 }
468 }
471}