1use std::{env, str::FromStr};
59
60use ahash::AHashMap;
61use log::LevelFilter;
62use ustr::Ustr;
63
64use super::writer::FileWriterConfig;
65
66#[cfg_attr(
68 feature = "python",
69 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.common", from_py_object)
70)]
71#[cfg_attr(
72 feature = "python",
73 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")
74)]
75#[derive(Debug, Clone, PartialEq, Eq, bon::Builder)]
76pub struct LoggerConfig {
77 #[builder(default = LevelFilter::Info)]
79 pub stdout_level: LevelFilter,
80 #[builder(default = LevelFilter::Off)]
82 pub fileout_level: LevelFilter,
83 #[builder(default)]
85 pub component_level: AHashMap<Ustr, LevelFilter>,
86 #[builder(default)]
88 pub module_level: AHashMap<Ustr, LevelFilter>,
89 #[builder(default)]
91 pub log_components_only: bool,
92 #[builder(default = true)]
94 pub is_colored: bool,
95 #[builder(default)]
97 pub print_config: bool,
98 #[builder(default)]
100 pub use_tracing: bool,
101 #[builder(default)]
103 pub bypass_logging: bool,
104 pub file_config: Option<FileWriterConfig>,
106 #[builder(default)]
108 pub clear_log_file: bool,
109}
110
111impl Default for LoggerConfig {
112 fn default() -> Self {
113 Self::builder().build()
114 }
115}
116
117impl LoggerConfig {
118 #[must_use]
120 #[expect(clippy::too_many_arguments)]
121 pub fn new(
122 stdout_level: LevelFilter,
123 fileout_level: LevelFilter,
124 component_level: AHashMap<Ustr, LevelFilter>,
125 module_level: AHashMap<Ustr, LevelFilter>,
126 log_components_only: bool,
127 is_colored: bool,
128 print_config: bool,
129 use_tracing: bool,
130 bypass_logging: bool,
131 file_config: Option<FileWriterConfig>,
132 clear_log_file: bool,
133 ) -> Self {
134 Self {
135 stdout_level,
136 fileout_level,
137 component_level,
138 module_level,
139 log_components_only,
140 is_colored,
141 print_config,
142 use_tracing,
143 bypass_logging,
144 file_config,
145 clear_log_file,
146 }
147 }
148
149 pub fn from_spec(spec: &str) -> anyhow::Result<Self> {
162 let mut config = Self::default();
163
164 for kv in spec.split(';') {
165 let kv = kv.trim();
166 if kv.is_empty() {
167 continue;
168 }
169
170 let kv_lower = kv.to_lowercase();
171
172 if !kv.contains('=') {
174 match kv_lower.as_str() {
175 "log_components_only" => config.log_components_only = true,
176 "is_colored" => config.is_colored = true,
177 "print_config" => config.print_config = true,
178 "use_tracing" => config.use_tracing = true,
179 "bypass_logging" => config.bypass_logging = true,
180 _ => anyhow::bail!("Invalid spec pair: {kv}"),
181 }
182 continue;
183 }
184
185 let parts: Vec<&str> = kv.splitn(2, '=').collect();
186 if parts.len() != 2 {
187 anyhow::bail!("Invalid spec pair: {kv}");
188 }
189
190 let k = parts[0].trim();
191 let v = parts[1].trim();
192 let k_lower = k.to_lowercase();
193
194 match k_lower.as_str() {
195 "is_colored" => {
196 config.is_colored = parse_bool_value(v);
197 }
198 "log_components_only" => {
199 config.log_components_only = parse_bool_value(v);
200 }
201 "print_config" => {
202 config.print_config = parse_bool_value(v);
203 }
204 "use_tracing" => {
205 config.use_tracing = parse_bool_value(v);
206 }
207 "bypass_logging" => {
208 config.bypass_logging = parse_bool_value(v);
209 }
210 "stdout" => {
211 config.stdout_level = parse_level(v)?;
212 }
213 "fileout" => {
214 config.fileout_level = parse_level(v)?;
215 }
216 _ => {
217 let lvl = parse_level(v)?;
218
219 if k.contains("::") {
220 config.module_level.insert(Ustr::from(k), lvl);
221 } else {
222 config.component_level.insert(Ustr::from(k), lvl);
223 }
224 }
225 }
226 }
227
228 Ok(config)
229 }
230
231 pub fn from_env() -> anyhow::Result<Self> {
237 let spec = env::var("NAUTILUS_LOG")?;
238 Self::from_spec(&spec)
239 }
240}
241
242fn parse_bool_value(v: &str) -> bool {
246 !matches!(v.to_lowercase().as_str(), "false" | "0" | "no")
247}
248
249fn parse_level(v: &str) -> anyhow::Result<LevelFilter> {
251 LevelFilter::from_str(v).map_err(|_| anyhow::anyhow!("Invalid log level: {v}"))
252}
253
254#[cfg(test)]
255mod tests {
256 use rstest::rstest;
257
258 use super::*;
259
260 #[rstest]
261 fn test_default_config() {
262 let config = LoggerConfig::default();
263 assert_eq!(config.stdout_level, LevelFilter::Info);
264 assert_eq!(config.fileout_level, LevelFilter::Off);
265 assert!(config.component_level.is_empty());
266 assert!(!config.log_components_only);
267 assert!(config.is_colored);
268 assert!(!config.print_config);
269 assert!(!config.bypass_logging);
270 assert!(config.file_config.is_none());
271 assert!(!config.clear_log_file);
272 }
273
274 #[rstest]
275 fn test_from_spec_stdout_and_fileout() {
276 let config = LoggerConfig::from_spec("stdout=Debug;fileout=Error").unwrap();
277 assert_eq!(config.stdout_level, LevelFilter::Debug);
278 assert_eq!(config.fileout_level, LevelFilter::Error);
279 }
280
281 #[rstest]
282 fn test_from_spec_case_insensitive_levels() {
283 let config = LoggerConfig::from_spec("stdout=debug;fileout=ERROR").unwrap();
284 assert_eq!(config.stdout_level, LevelFilter::Debug);
285 assert_eq!(config.fileout_level, LevelFilter::Error);
286 }
287
288 #[rstest]
289 fn test_from_spec_case_insensitive_keys() {
290 let config = LoggerConfig::from_spec("STDOUT=Info;FILEOUT=Debug").unwrap();
291 assert_eq!(config.stdout_level, LevelFilter::Info);
292 assert_eq!(config.fileout_level, LevelFilter::Debug);
293 }
294
295 #[rstest]
296 fn test_from_spec_empty_string() {
297 let config = LoggerConfig::from_spec("").unwrap();
298 assert_eq!(config, LoggerConfig::default());
299 }
300
301 #[rstest]
302 fn test_from_spec_with_whitespace() {
303 let config = LoggerConfig::from_spec(" stdout = Info ; fileout = Debug ").unwrap();
304 assert_eq!(config.stdout_level, LevelFilter::Info);
305 assert_eq!(config.fileout_level, LevelFilter::Debug);
306 }
307
308 #[rstest]
309 fn test_from_spec_trailing_semicolon() {
310 let config = LoggerConfig::from_spec("stdout=Warn;").unwrap();
311 assert_eq!(config.stdout_level, LevelFilter::Warn);
312 }
313
314 #[rstest]
315 fn test_from_spec_bare_is_colored() {
316 let config = LoggerConfig::from_spec("is_colored").unwrap();
317 assert!(config.is_colored);
318 }
319
320 #[rstest]
321 fn test_from_spec_is_colored_true() {
322 let config = LoggerConfig::from_spec("is_colored=true").unwrap();
323 assert!(config.is_colored);
324 }
325
326 #[rstest]
327 fn test_from_spec_is_colored_false() {
328 let config = LoggerConfig::from_spec("is_colored=false").unwrap();
329 assert!(!config.is_colored);
330 }
331
332 #[rstest]
333 fn test_from_spec_is_colored_zero() {
334 let config = LoggerConfig::from_spec("is_colored=0").unwrap();
335 assert!(!config.is_colored);
336 }
337
338 #[rstest]
339 fn test_from_spec_is_colored_no() {
340 let config = LoggerConfig::from_spec("is_colored=no").unwrap();
341 assert!(!config.is_colored);
342 }
343
344 #[rstest]
345 fn test_from_spec_is_colored_case_insensitive() {
346 let config = LoggerConfig::from_spec("IS_COLORED=FALSE").unwrap();
347 assert!(!config.is_colored);
348 }
349
350 #[rstest]
351 fn test_from_spec_print_config() {
352 let config = LoggerConfig::from_spec("print_config").unwrap();
353 assert!(config.print_config);
354 }
355
356 #[rstest]
357 fn test_from_spec_print_config_false() {
358 let config = LoggerConfig::from_spec("print_config=false").unwrap();
359 assert!(!config.print_config);
360 }
361
362 #[rstest]
363 fn test_from_spec_log_components_only() {
364 let config = LoggerConfig::from_spec("log_components_only").unwrap();
365 assert!(config.log_components_only);
366 }
367
368 #[rstest]
369 fn test_from_spec_log_components_only_false() {
370 let config = LoggerConfig::from_spec("log_components_only=false").unwrap();
371 assert!(!config.log_components_only);
372 }
373
374 #[rstest]
375 fn test_from_spec_component_level() {
376 let config = LoggerConfig::from_spec("RiskEngine=Error;DataEngine=Debug").unwrap();
377 assert_eq!(
378 config.component_level[&Ustr::from("RiskEngine")],
379 LevelFilter::Error
380 );
381 assert_eq!(
382 config.component_level[&Ustr::from("DataEngine")],
383 LevelFilter::Debug
384 );
385 }
386
387 #[rstest]
388 fn test_from_spec_component_preserves_case() {
389 let config = LoggerConfig::from_spec("MyComponent=Info").unwrap();
391 assert!(
392 config
393 .component_level
394 .contains_key(&Ustr::from("MyComponent"))
395 );
396 assert!(
397 !config
398 .component_level
399 .contains_key(&Ustr::from("mycomponent"))
400 );
401 }
402
403 #[rstest]
404 fn test_from_spec_full_example() {
405 let config = LoggerConfig::from_spec(
406 "stdout=Info;fileout=Debug;RiskEngine=Error;is_colored;print_config",
407 )
408 .unwrap();
409
410 assert_eq!(config.stdout_level, LevelFilter::Info);
411 assert_eq!(config.fileout_level, LevelFilter::Debug);
412 assert_eq!(
413 config.component_level[&Ustr::from("RiskEngine")],
414 LevelFilter::Error
415 );
416 assert!(config.is_colored);
417 assert!(config.print_config);
418 }
419
420 #[rstest]
421 fn test_from_spec_disabled_colors() {
422 let config = LoggerConfig::from_spec("stdout=Info;is_colored=false;fileout=Debug").unwrap();
423 assert!(!config.is_colored);
424 assert_eq!(config.stdout_level, LevelFilter::Info);
425 assert_eq!(config.fileout_level, LevelFilter::Debug);
426 }
427
428 #[rstest]
429 fn test_from_spec_invalid_level() {
430 let result = LoggerConfig::from_spec("stdout=InvalidLevel");
431 assert!(result.is_err());
432 assert!(
433 result
434 .unwrap_err()
435 .to_string()
436 .contains("Invalid log level")
437 );
438 }
439
440 #[rstest]
441 fn test_from_spec_invalid_bare_flag() {
442 let result = LoggerConfig::from_spec("unknown_flag");
443 assert!(result.is_err());
444 assert!(
445 result
446 .unwrap_err()
447 .to_string()
448 .contains("Invalid spec pair")
449 );
450 }
451
452 #[rstest]
453 fn test_from_spec_missing_value() {
454 let result = LoggerConfig::from_spec("stdout=");
456 assert!(result.is_err());
457 }
458
459 #[rstest]
460 #[case("Off", LevelFilter::Off)]
461 #[case("Error", LevelFilter::Error)]
462 #[case("Warn", LevelFilter::Warn)]
463 #[case("Info", LevelFilter::Info)]
464 #[case("Debug", LevelFilter::Debug)]
465 #[case("Trace", LevelFilter::Trace)]
466 fn test_all_log_levels(#[case] level_str: &str, #[case] expected: LevelFilter) {
467 let config = LoggerConfig::from_spec(&format!("stdout={level_str}")).unwrap();
468 assert_eq!(config.stdout_level, expected);
469 }
470
471 #[rstest]
472 fn test_from_spec_single_module_path() {
473 let config = LoggerConfig::from_spec("nautilus_okx::websocket=Debug").unwrap();
474 assert_eq!(
475 config.module_level[&Ustr::from("nautilus_okx::websocket")],
476 LevelFilter::Debug
477 );
478 assert!(config.component_level.is_empty());
479 }
480
481 #[rstest]
482 fn test_from_spec_multiple_module_paths() {
483 let config =
484 LoggerConfig::from_spec("nautilus_okx::websocket=Debug;nautilus_binance::data=Trace")
485 .unwrap();
486 assert_eq!(
487 config.module_level[&Ustr::from("nautilus_okx::websocket")],
488 LevelFilter::Debug
489 );
490 assert_eq!(
491 config.module_level[&Ustr::from("nautilus_binance::data")],
492 LevelFilter::Trace
493 );
494 assert!(config.component_level.is_empty());
495 }
496
497 #[rstest]
498 fn test_from_spec_mixed_module_and_component() {
499 let config = LoggerConfig::from_spec(
500 "nautilus_okx::websocket=Debug;RiskEngine=Error;nautilus_network::data=Trace",
501 )
502 .unwrap();
503
504 assert_eq!(
505 config.module_level[&Ustr::from("nautilus_okx::websocket")],
506 LevelFilter::Debug
507 );
508 assert_eq!(
509 config.module_level[&Ustr::from("nautilus_network::data")],
510 LevelFilter::Trace
511 );
512 assert_eq!(config.module_level.len(), 2);
513 assert_eq!(
514 config.component_level[&Ustr::from("RiskEngine")],
515 LevelFilter::Error
516 );
517 assert_eq!(config.component_level.len(), 1);
518 }
519
520 #[rstest]
521 fn test_from_spec_deeply_nested_module_path() {
522 let config =
523 LoggerConfig::from_spec("nautilus_okx::websocket::handler::auth=Trace").unwrap();
524 assert_eq!(
525 config.module_level[&Ustr::from("nautilus_okx::websocket::handler::auth")],
526 LevelFilter::Trace
527 );
528 }
529
530 #[rstest]
531 fn test_from_spec_module_path_with_underscores() {
532 let config =
533 LoggerConfig::from_spec("nautilus_trader::adapters::interactive_brokers=Debug")
534 .unwrap();
535 assert_eq!(
536 config.module_level[&Ustr::from("nautilus_trader::adapters::interactive_brokers")],
537 LevelFilter::Debug
538 );
539 }
540
541 #[rstest]
542 fn test_from_spec_full_example_with_modules() {
543 let config = LoggerConfig::from_spec(
544 "stdout=Info;fileout=Debug;RiskEngine=Error;nautilus_okx::websocket=Trace;is_colored",
545 )
546 .unwrap();
547
548 assert_eq!(config.stdout_level, LevelFilter::Info);
549 assert_eq!(config.fileout_level, LevelFilter::Debug);
550 assert_eq!(
551 config.component_level[&Ustr::from("RiskEngine")],
552 LevelFilter::Error
553 );
554 assert_eq!(
555 config.module_level[&Ustr::from("nautilus_okx::websocket")],
556 LevelFilter::Trace
557 );
558 assert!(config.is_colored);
559 }
560
561 #[rstest]
562 fn test_from_spec_module_path_preserves_case() {
563 let config = LoggerConfig::from_spec("MyModule::SubModule=Info").unwrap();
564 assert!(
565 config
566 .module_level
567 .contains_key(&Ustr::from("MyModule::SubModule"))
568 );
569 }
570
571 #[rstest]
572 fn test_from_spec_single_colon_is_component() {
573 let config = LoggerConfig::from_spec("Component:Name=Info").unwrap();
575 assert!(config.module_level.is_empty());
576 assert!(
577 config
578 .component_level
579 .contains_key(&Ustr::from("Component:Name"))
580 );
581 }
582
583 #[rstest]
584 fn test_default_module_level_is_empty() {
585 let config = LoggerConfig::default();
586 assert!(config.module_level.is_empty());
587 }
588
589 #[rstest]
590 fn test_from_spec_bypass_logging_bare() {
591 let config = LoggerConfig::from_spec("bypass_logging").unwrap();
592 assert!(config.bypass_logging);
593 }
594
595 #[rstest]
596 fn test_from_spec_bypass_logging_true() {
597 let config = LoggerConfig::from_spec("bypass_logging=true").unwrap();
598 assert!(config.bypass_logging);
599 }
600
601 #[rstest]
602 fn test_from_spec_bypass_logging_false() {
603 let config = LoggerConfig::from_spec("bypass_logging=false").unwrap();
604 assert!(!config.bypass_logging);
605 }
606}