Skip to main content

nautilus_common/logging/
config.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//! Logging configuration types and parsing.
17//!
18//! This module provides configuration for the Nautilus logging subsystem via
19//! the `LoggerConfig` and `FileWriterConfig` types.
20//!
21//! # Spec String Format
22//!
23//! The `NAUTILUS_LOG` environment variable uses a semicolon-separated format:
24//!
25//! ```text
26//! stdout=Info;fileout=Debug;RiskEngine=Error;my_crate::module=Debug;is_colored
27//! ```
28//!
29//! ## Supported Keys
30//!
31//! | Key                     | Type      | Description                                  |
32//! |-------------------------|-----------|----------------------------------------------|
33//! | `stdout`                | Log level | Maximum level for stdout output.             |
34//! | `fileout`               | Log level | Maximum level for file output.               |
35//! | `is_colored`            | Boolean   | Enable ANSI colors (default: true).          |
36//! | `print_config`          | Boolean   | Print config to stdout at startup.           |
37//! | `log_components_only`   | Boolean   | Only log components with explicit filters.   |
38//! | `use_tracing`           | Boolean   | Enable tracing subscriber for external libs. |
39//! | `fileout_sync_on_flush` | Boolean   | Sync file logs on every flush (default: true). |
40//! | `buffered_stdout`       | Boolean   | Buffer stdout output (default: false).       |
41//! | `<component>`           | Log level | Component-specific log level (exact match).  |
42//! | `<module::path>`        | Log level | Module-specific log level (prefix match).    |
43//!
44//! ## Log Levels
45//!
46//! All log levels are case-insensitive.
47//!
48//! - `Off`
49//! - `Error`
50//! - `Warn`
51//! - `Info`
52//! - `Debug`
53//! - `Trace`
54//!
55//! ## Boolean Values
56//!
57//! - Bare flag: `is_colored` → true
58//! - Explicit: `is_colored=true`, `is_colored=false`, `is_colored=0`, `is_colored=no`
59
60use std::{env, str::FromStr};
61
62use ahash::AHashMap;
63use log::LevelFilter;
64use serde::{Deserialize, Serialize};
65use ustr::Ustr;
66
67use super::writer::FileWriterConfig;
68use crate::config::ConfigResult;
69
70/// Configuration for the Nautilus logger.
71#[cfg_attr(
72    feature = "python",
73    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.common", from_py_object)
74)]
75#[cfg_attr(
76    feature = "python",
77    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")
78)]
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
80#[builder(finish_fn(name = build_inner, vis = ""))]
81#[serde(default, deny_unknown_fields)]
82pub struct LoggerConfig {
83    /// Maximum log level for stdout output.
84    #[builder(default = LevelFilter::Info)]
85    pub stdout_level: LevelFilter,
86    /// Maximum log level for file output (`Off` disables file logging).
87    #[builder(default = LevelFilter::Off)]
88    pub fileout_level: LevelFilter,
89    /// Per-component log level overrides (exact match).
90    #[builder(default)]
91    pub component_level: AHashMap<Ustr, LevelFilter>,
92    /// Per-module path log level overrides (prefix match).
93    #[builder(default)]
94    pub module_level: AHashMap<Ustr, LevelFilter>,
95    /// Log only components with explicit level filters.
96    #[builder(default)]
97    pub log_components_only: bool,
98    /// Use ANSI color codes in output.
99    #[builder(default = true)]
100    pub is_colored: bool,
101    /// Print configuration to stdout at startup.
102    #[builder(default)]
103    pub print_config: bool,
104    /// Initialize the tracing subscriber for external Rust crate logs.
105    #[builder(default)]
106    pub use_tracing: bool,
107    /// If all logging should be bypassed.
108    #[builder(default)]
109    pub bypass_logging: bool,
110    /// File writer configuration for log file output.
111    pub file_config: Option<FileWriterConfig>,
112    /// If the log file should be cleared before use.
113    #[builder(default)]
114    pub clear_log_file: bool,
115    /// If file log flushes should also sync data to disk.
116    #[builder(default = true)]
117    pub fileout_sync_on_flush: bool,
118    /// If stdout writes should be buffered until flush or buffer capacity.
119    #[builder(default)]
120    pub buffered_stdout: bool,
121}
122
123impl<S: logger_config_builder::IsComplete> LoggerConfigBuilder<S> {
124    /// Validates and builds the [`LoggerConfig`].
125    ///
126    /// # Errors
127    ///
128    /// Returns a [`ConfigError`](crate::config::ConfigError) if any field fails validation
129    /// (see [`LoggerConfig::validate`]).
130    pub fn build(self) -> ConfigResult<LoggerConfig> {
131        let config = self.build_inner();
132        config.validate()?;
133        Ok(config)
134    }
135}
136
137impl Default for LoggerConfig {
138    fn default() -> Self {
139        Self::builder()
140            .build()
141            .expect("default `LoggerConfig` should be valid")
142    }
143}
144
145impl LoggerConfig {
146    /// Creates a new [`LoggerConfig`] instance.
147    #[must_use]
148    #[expect(clippy::too_many_arguments)]
149    pub fn new(
150        stdout_level: LevelFilter,
151        fileout_level: LevelFilter,
152        component_level: AHashMap<Ustr, LevelFilter>,
153        module_level: AHashMap<Ustr, LevelFilter>,
154        log_components_only: bool,
155        is_colored: bool,
156        print_config: bool,
157        use_tracing: bool,
158        bypass_logging: bool,
159        file_config: Option<FileWriterConfig>,
160        clear_log_file: bool,
161    ) -> Self {
162        Self {
163            stdout_level,
164            fileout_level,
165            component_level,
166            module_level,
167            log_components_only,
168            is_colored,
169            print_config,
170            use_tracing,
171            bypass_logging,
172            file_config,
173            clear_log_file,
174            fileout_sync_on_flush: true,
175            buffered_stdout: false,
176        }
177    }
178
179    /// Validates the logger configuration.
180    ///
181    /// # Errors
182    ///
183    /// Returns a [`ConfigError`](crate::config::ConfigError) if the file writer configuration
184    /// is invalid (see [`FileWriterConfig::validate`]).
185    pub fn validate(&self) -> ConfigResult<()> {
186        if let Some(file_config) = &self.file_config {
187            file_config.validate()?;
188        }
189
190        Ok(())
191    }
192
193    /// Parses a configuration from a spec string.
194    ///
195    /// # Format
196    ///
197    /// Semicolon-separated key-value pairs or bare flags:
198    /// ```text
199    /// stdout=Info;fileout=Debug;RiskEngine=Error;my_crate::module=Debug;is_colored
200    /// ```
201    ///
202    /// # Errors
203    ///
204    /// Returns an error if the spec string contains invalid syntax or log levels.
205    pub fn from_spec(spec: &str) -> anyhow::Result<Self> {
206        let mut config = Self::default();
207
208        for kv in spec.split(';') {
209            let kv = kv.trim();
210            if kv.is_empty() {
211                continue;
212            }
213
214            let kv_lower = kv.to_lowercase();
215
216            // Handle bare flags (without =)
217            if !kv.contains('=') {
218                match kv_lower.as_str() {
219                    "log_components_only" => config.log_components_only = true,
220                    "is_colored" => config.is_colored = true,
221                    "print_config" => config.print_config = true,
222                    "use_tracing" => config.use_tracing = true,
223                    "bypass_logging" => config.bypass_logging = true,
224                    "fileout_sync_on_flush" => config.fileout_sync_on_flush = true,
225                    "buffered_stdout" => config.buffered_stdout = true,
226                    _ => anyhow::bail!("Invalid spec pair: {kv}"),
227                }
228                continue;
229            }
230
231            let parts: Vec<&str> = kv.splitn(2, '=').collect();
232            if parts.len() != 2 {
233                anyhow::bail!("Invalid spec pair: {kv}");
234            }
235
236            let k = parts[0].trim();
237            let v = parts[1].trim();
238            let k_lower = k.to_lowercase();
239
240            match k_lower.as_str() {
241                "is_colored" => {
242                    config.is_colored = parse_bool_value(v);
243                }
244                "log_components_only" => {
245                    config.log_components_only = parse_bool_value(v);
246                }
247                "print_config" => {
248                    config.print_config = parse_bool_value(v);
249                }
250                "use_tracing" => {
251                    config.use_tracing = parse_bool_value(v);
252                }
253                "bypass_logging" => {
254                    config.bypass_logging = parse_bool_value(v);
255                }
256                "fileout_sync_on_flush" => {
257                    config.fileout_sync_on_flush = parse_bool_value(v);
258                }
259                "buffered_stdout" => {
260                    config.buffered_stdout = parse_bool_value(v);
261                }
262                "stdout" => {
263                    config.stdout_level = parse_level(v)?;
264                }
265                "fileout" => {
266                    config.fileout_level = parse_level(v)?;
267                }
268                _ => {
269                    let lvl = parse_level(v)?;
270
271                    if k.contains("::") {
272                        config.module_level.insert(Ustr::from(k), lvl);
273                    } else {
274                        config.component_level.insert(Ustr::from(k), lvl);
275                    }
276                }
277            }
278        }
279
280        Ok(config)
281    }
282
283    /// Parses configuration from the `NAUTILUS_LOG` environment variable.
284    ///
285    /// # Errors
286    ///
287    /// Returns an error if the variable is unset or contains invalid syntax.
288    pub fn from_env() -> anyhow::Result<Self> {
289        let spec = env::var("NAUTILUS_LOG")?;
290        Self::from_spec(&spec)
291    }
292}
293
294/// Parses a boolean value from a string.
295///
296/// Returns `true` unless the value is explicitly "false", "0", or "no" (case-insensitive).
297fn parse_bool_value(v: &str) -> bool {
298    !matches!(v.to_lowercase().as_str(), "false" | "0" | "no")
299}
300
301/// Parses a log level from a string.
302fn parse_level(v: &str) -> anyhow::Result<LevelFilter> {
303    LevelFilter::from_str(v).map_err(|_| anyhow::anyhow!("Invalid log level: {v}"))
304}
305
306#[cfg(test)]
307mod tests {
308    use rstest::rstest;
309
310    use super::*;
311    use crate::config::ConfigError;
312
313    #[rstest]
314    fn test_zero_rotation_max_file_size_rejected() {
315        let file_config = FileWriterConfig::new(None, None, None, Some((0, 5)));
316        let result = LoggerConfig::builder().file_config(file_config).build();
317        assert!(
318            matches!(result, Err(ConfigError::Range { field, .. }) if field == "file_config.file_rotate.max_file_size")
319        );
320    }
321
322    #[rstest]
323    fn test_positive_rotation_max_file_size_accepted() {
324        let file_config = FileWriterConfig::new(None, None, None, Some((1_048_576, 5)));
325        let result = LoggerConfig::builder().file_config(file_config).build();
326        assert!(result.is_ok());
327    }
328
329    #[rstest]
330    fn test_default_config() {
331        let config = LoggerConfig::default();
332        assert_eq!(config.stdout_level, LevelFilter::Info);
333        assert_eq!(config.fileout_level, LevelFilter::Off);
334        assert!(config.component_level.is_empty());
335        assert!(!config.log_components_only);
336        assert!(config.is_colored);
337        assert!(!config.print_config);
338        assert!(!config.bypass_logging);
339        assert!(config.file_config.is_none());
340        assert!(!config.clear_log_file);
341        assert!(config.fileout_sync_on_flush);
342        assert!(!config.buffered_stdout);
343    }
344
345    #[rstest]
346    fn test_from_spec_stdout_and_fileout() {
347        let config = LoggerConfig::from_spec("stdout=Debug;fileout=Error").unwrap();
348        assert_eq!(config.stdout_level, LevelFilter::Debug);
349        assert_eq!(config.fileout_level, LevelFilter::Error);
350    }
351
352    #[rstest]
353    fn test_from_spec_case_insensitive_levels() {
354        let config = LoggerConfig::from_spec("stdout=debug;fileout=ERROR").unwrap();
355        assert_eq!(config.stdout_level, LevelFilter::Debug);
356        assert_eq!(config.fileout_level, LevelFilter::Error);
357    }
358
359    #[rstest]
360    fn test_from_spec_case_insensitive_keys() {
361        let config = LoggerConfig::from_spec("STDOUT=Info;FILEOUT=Debug").unwrap();
362        assert_eq!(config.stdout_level, LevelFilter::Info);
363        assert_eq!(config.fileout_level, LevelFilter::Debug);
364    }
365
366    #[rstest]
367    fn test_from_spec_empty_string() {
368        let config = LoggerConfig::from_spec("").unwrap();
369        assert_eq!(config, LoggerConfig::default());
370    }
371
372    #[rstest]
373    fn test_from_spec_with_whitespace() {
374        let config = LoggerConfig::from_spec("  stdout = Info ; fileout = Debug  ").unwrap();
375        assert_eq!(config.stdout_level, LevelFilter::Info);
376        assert_eq!(config.fileout_level, LevelFilter::Debug);
377    }
378
379    #[rstest]
380    fn test_from_spec_trailing_semicolon() {
381        let config = LoggerConfig::from_spec("stdout=Warn;").unwrap();
382        assert_eq!(config.stdout_level, LevelFilter::Warn);
383    }
384
385    #[rstest]
386    fn test_from_spec_bare_is_colored() {
387        let config = LoggerConfig::from_spec("is_colored").unwrap();
388        assert!(config.is_colored);
389    }
390
391    #[rstest]
392    fn test_from_spec_is_colored_true() {
393        let config = LoggerConfig::from_spec("is_colored=true").unwrap();
394        assert!(config.is_colored);
395    }
396
397    #[rstest]
398    fn test_from_spec_is_colored_false() {
399        let config = LoggerConfig::from_spec("is_colored=false").unwrap();
400        assert!(!config.is_colored);
401    }
402
403    #[rstest]
404    fn test_from_spec_is_colored_zero() {
405        let config = LoggerConfig::from_spec("is_colored=0").unwrap();
406        assert!(!config.is_colored);
407    }
408
409    #[rstest]
410    fn test_from_spec_is_colored_no() {
411        let config = LoggerConfig::from_spec("is_colored=no").unwrap();
412        assert!(!config.is_colored);
413    }
414
415    #[rstest]
416    fn test_from_spec_is_colored_case_insensitive() {
417        let config = LoggerConfig::from_spec("IS_COLORED=FALSE").unwrap();
418        assert!(!config.is_colored);
419    }
420
421    #[rstest]
422    fn test_from_spec_print_config() {
423        let config = LoggerConfig::from_spec("print_config").unwrap();
424        assert!(config.print_config);
425    }
426
427    #[rstest]
428    fn test_from_spec_print_config_false() {
429        let config = LoggerConfig::from_spec("print_config=false").unwrap();
430        assert!(!config.print_config);
431    }
432
433    #[rstest]
434    fn test_from_spec_log_components_only() {
435        let config = LoggerConfig::from_spec("log_components_only").unwrap();
436        assert!(config.log_components_only);
437    }
438
439    #[rstest]
440    fn test_from_spec_log_components_only_false() {
441        let config = LoggerConfig::from_spec("log_components_only=false").unwrap();
442        assert!(!config.log_components_only);
443    }
444
445    #[rstest]
446    fn test_from_spec_fileout_sync_on_flush_false() {
447        let config = LoggerConfig::from_spec("fileout_sync_on_flush=false").unwrap();
448        assert!(!config.fileout_sync_on_flush);
449    }
450
451    #[rstest]
452    fn test_from_spec_buffered_stdout() {
453        let config = LoggerConfig::from_spec("buffered_stdout").unwrap();
454        assert!(config.buffered_stdout);
455    }
456
457    #[rstest]
458    fn test_from_spec_component_level() {
459        let config = LoggerConfig::from_spec("RiskEngine=Error;DataEngine=Debug").unwrap();
460        assert_eq!(
461            config.component_level[&Ustr::from("RiskEngine")],
462            LevelFilter::Error
463        );
464        assert_eq!(
465            config.component_level[&Ustr::from("DataEngine")],
466            LevelFilter::Debug
467        );
468    }
469
470    #[rstest]
471    fn test_from_spec_component_preserves_case() {
472        // Component names should preserve their original case
473        let config = LoggerConfig::from_spec("MyComponent=Info").unwrap();
474        assert!(
475            config
476                .component_level
477                .contains_key(&Ustr::from("MyComponent"))
478        );
479        assert!(
480            !config
481                .component_level
482                .contains_key(&Ustr::from("mycomponent"))
483        );
484    }
485
486    #[rstest]
487    fn test_from_spec_full_example() {
488        let config = LoggerConfig::from_spec(
489            "stdout=Info;fileout=Debug;RiskEngine=Error;is_colored;print_config",
490        )
491        .unwrap();
492
493        assert_eq!(config.stdout_level, LevelFilter::Info);
494        assert_eq!(config.fileout_level, LevelFilter::Debug);
495        assert_eq!(
496            config.component_level[&Ustr::from("RiskEngine")],
497            LevelFilter::Error
498        );
499        assert!(config.is_colored);
500        assert!(config.print_config);
501    }
502
503    #[rstest]
504    fn test_from_spec_disabled_colors() {
505        let config = LoggerConfig::from_spec("stdout=Info;is_colored=false;fileout=Debug").unwrap();
506        assert!(!config.is_colored);
507        assert_eq!(config.stdout_level, LevelFilter::Info);
508        assert_eq!(config.fileout_level, LevelFilter::Debug);
509    }
510
511    #[rstest]
512    fn test_from_spec_invalid_level() {
513        let result = LoggerConfig::from_spec("stdout=InvalidLevel");
514        assert!(result.is_err());
515        assert!(
516            result
517                .unwrap_err()
518                .to_string()
519                .contains("Invalid log level")
520        );
521    }
522
523    #[rstest]
524    fn test_from_spec_invalid_bare_flag() {
525        let result = LoggerConfig::from_spec("unknown_flag");
526        assert!(result.is_err());
527        assert!(
528            result
529                .unwrap_err()
530                .to_string()
531                .contains("Invalid spec pair")
532        );
533    }
534
535    #[rstest]
536    fn test_from_spec_missing_value() {
537        // "stdout=" with no value is technically valid empty string, which is invalid level
538        let result = LoggerConfig::from_spec("stdout=");
539        assert!(result.is_err());
540    }
541
542    #[rstest]
543    #[case("Off", LevelFilter::Off)]
544    #[case("Error", LevelFilter::Error)]
545    #[case("Warn", LevelFilter::Warn)]
546    #[case("Info", LevelFilter::Info)]
547    #[case("Debug", LevelFilter::Debug)]
548    #[case("Trace", LevelFilter::Trace)]
549    fn test_all_log_levels(#[case] level_str: &str, #[case] expected: LevelFilter) {
550        let config = LoggerConfig::from_spec(&format!("stdout={level_str}")).unwrap();
551        assert_eq!(config.stdout_level, expected);
552    }
553
554    #[rstest]
555    fn test_from_spec_single_module_path() {
556        let config = LoggerConfig::from_spec("nautilus_okx::websocket=Debug").unwrap();
557        assert_eq!(
558            config.module_level[&Ustr::from("nautilus_okx::websocket")],
559            LevelFilter::Debug
560        );
561        assert!(config.component_level.is_empty());
562    }
563
564    #[rstest]
565    fn test_from_spec_multiple_module_paths() {
566        let config =
567            LoggerConfig::from_spec("nautilus_okx::websocket=Debug;nautilus_binance::data=Trace")
568                .unwrap();
569        assert_eq!(
570            config.module_level[&Ustr::from("nautilus_okx::websocket")],
571            LevelFilter::Debug
572        );
573        assert_eq!(
574            config.module_level[&Ustr::from("nautilus_binance::data")],
575            LevelFilter::Trace
576        );
577        assert!(config.component_level.is_empty());
578    }
579
580    #[rstest]
581    fn test_from_spec_mixed_module_and_component() {
582        let config = LoggerConfig::from_spec(
583            "nautilus_okx::websocket=Debug;RiskEngine=Error;nautilus_network::data=Trace",
584        )
585        .unwrap();
586
587        assert_eq!(
588            config.module_level[&Ustr::from("nautilus_okx::websocket")],
589            LevelFilter::Debug
590        );
591        assert_eq!(
592            config.module_level[&Ustr::from("nautilus_network::data")],
593            LevelFilter::Trace
594        );
595        assert_eq!(config.module_level.len(), 2);
596        assert_eq!(
597            config.component_level[&Ustr::from("RiskEngine")],
598            LevelFilter::Error
599        );
600        assert_eq!(config.component_level.len(), 1);
601    }
602
603    #[rstest]
604    fn test_from_spec_deeply_nested_module_path() {
605        let config =
606            LoggerConfig::from_spec("nautilus_okx::websocket::handler::auth=Trace").unwrap();
607        assert_eq!(
608            config.module_level[&Ustr::from("nautilus_okx::websocket::handler::auth")],
609            LevelFilter::Trace
610        );
611    }
612
613    #[rstest]
614    fn test_from_spec_module_path_with_underscores() {
615        let config =
616            LoggerConfig::from_spec("nautilus_trader::adapters::interactive_brokers=Debug")
617                .unwrap();
618        assert_eq!(
619            config.module_level[&Ustr::from("nautilus_trader::adapters::interactive_brokers")],
620            LevelFilter::Debug
621        );
622    }
623
624    #[rstest]
625    fn test_from_spec_full_example_with_modules() {
626        let config = LoggerConfig::from_spec(
627            "stdout=Info;fileout=Debug;RiskEngine=Error;nautilus_okx::websocket=Trace;is_colored",
628        )
629        .unwrap();
630
631        assert_eq!(config.stdout_level, LevelFilter::Info);
632        assert_eq!(config.fileout_level, LevelFilter::Debug);
633        assert_eq!(
634            config.component_level[&Ustr::from("RiskEngine")],
635            LevelFilter::Error
636        );
637        assert_eq!(
638            config.module_level[&Ustr::from("nautilus_okx::websocket")],
639            LevelFilter::Trace
640        );
641        assert!(config.is_colored);
642    }
643
644    #[rstest]
645    fn test_from_spec_module_path_preserves_case() {
646        let config = LoggerConfig::from_spec("MyModule::SubModule=Info").unwrap();
647        assert!(
648            config
649                .module_level
650                .contains_key(&Ustr::from("MyModule::SubModule"))
651        );
652    }
653
654    #[rstest]
655    fn test_from_spec_single_colon_is_component() {
656        // Single colon is NOT a module path separator in Rust
657        let config = LoggerConfig::from_spec("Component:Name=Info").unwrap();
658        assert!(config.module_level.is_empty());
659        assert!(
660            config
661                .component_level
662                .contains_key(&Ustr::from("Component:Name"))
663        );
664    }
665
666    #[rstest]
667    fn test_default_module_level_is_empty() {
668        let config = LoggerConfig::default();
669        assert!(config.module_level.is_empty());
670    }
671
672    #[rstest]
673    fn test_from_spec_bypass_logging_bare() {
674        let config = LoggerConfig::from_spec("bypass_logging").unwrap();
675        assert!(config.bypass_logging);
676    }
677
678    #[rstest]
679    fn test_from_spec_bypass_logging_true() {
680        let config = LoggerConfig::from_spec("bypass_logging=true").unwrap();
681        assert!(config.bypass_logging);
682    }
683
684    #[rstest]
685    fn test_from_spec_bypass_logging_false() {
686        let config = LoggerConfig::from_spec("bypass_logging=false").unwrap();
687        assert!(!config.bypass_logging);
688    }
689
690    #[rstest]
691    fn test_toml_deserialize_minimal() {
692        let config: LoggerConfig = toml::from_str(
693            r#"
694stdout_level = "INFO"
695fileout_level = "DEBUG"
696is_colored = false
697
698[component_level]
699RiskEngine = "ERROR"
700
701[file_config]
702directory = "/var/log/nautilus"
703"#,
704        )
705        .unwrap();
706
707        assert_eq!(config.stdout_level, LevelFilter::Info);
708        assert_eq!(config.fileout_level, LevelFilter::Debug);
709        assert!(!config.is_colored);
710        assert_eq!(
711            config.component_level[&Ustr::from("RiskEngine")],
712            LevelFilter::Error
713        );
714        assert_eq!(
715            config.file_config.as_ref().unwrap().directory.as_deref(),
716            Some("/var/log/nautilus"),
717        );
718    }
719}