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 str::FromStr,
46 sync::{
47 OnceLock,
48 atomic::{AtomicBool, AtomicU8, Ordering},
49 },
50};
51
52use ahash::AHashMap;
53use log::LevelFilter;
54pub 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
82pub fn logging_is_initialized() -> bool {
84 LOGGING_INITIALIZED.load(Ordering::Relaxed)
85}
86
87pub 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
119pub fn logging_set_bypass() {
121 LOGGING_BYPASSED.store(true, Ordering::Relaxed);
122}
123
124pub fn logging_shutdown() {
126 crate::logging::logger::shutdown_graceful();
129}
130
131pub fn arm_shutdown_on_error(enabled: bool) {
133 crate::logging::logger::arm_shutdown_on_error(enabled);
134}
135
136pub fn disarm_shutdown_on_error() {
138 crate::logging::logger::disarm_shutdown_on_error();
139}
140
141pub fn take_shutdown_on_error_trigger() -> Option<crate::logging::logger::ShutdownOnErrorTrigger> {
143 crate::logging::logger::take_shutdown_on_error_trigger()
144}
145
146pub 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
154pub fn logging_sync_to_disk() -> anyhow::Result<()> {
162 crate::logging::logger::sync_to_disk()
163}
164
165pub fn logging_is_colored() -> bool {
167 LOGGING_COLORED.load(Ordering::Relaxed)
168}
169
170pub fn logging_clock_set_realtime_mode() {
172 LOGGING_REALTIME.store(true, Ordering::Relaxed);
173}
174
175pub fn logging_clock_set_static_mode() {
177 LOGGING_REALTIME.store(false, Ordering::Relaxed);
178}
179
180pub 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
186pub 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#[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_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 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
257pub fn log_task_started(task_name: &str) {
259 log::debug!("Started task '{task_name}'");
260}
261
262pub fn log_task_stopped(task_name: &str) {
264 log::debug!("Stopped task '{task_name}'");
265}
266
267pub fn log_task_awaiting(task_name: &str) {
269 log::debug!("Awaiting task '{task_name}'");
270}
271
272pub fn log_task_aborted(task_name: &str) {
274 log::debug!("Aborted task '{task_name}'");
275}
276
277pub 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 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_initialized() {
457 assert!(
458 ensure_logging_initialized(),
459 "Fast path should return true when already initialized"
460 );
461 }
462 }
465}