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.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 Some((k, v)) = kv.split_once('=') else {
215                // Handle bare flags (without =)
216                match kv.to_lowercase().as_str() {
217                    "log_components_only" => config.log_components_only = true,
218                    "is_colored" => config.is_colored = true,
219                    "print_config" => config.print_config = true,
220                    "use_tracing" => config.use_tracing = true,
221                    "bypass_logging" => config.bypass_logging = true,
222                    "fileout_sync_on_flush" => config.fileout_sync_on_flush = true,
223                    "buffered_stdout" => config.buffered_stdout = true,
224                    _ => anyhow::bail!("Invalid spec pair: {kv}"),
225                }
226                continue;
227            };
228
229            let k = k.trim();
230            let v = v.trim();
231            let k_lower = k.to_lowercase();
232
233            match k_lower.as_str() {
234                "is_colored" => {
235                    config.is_colored = parse_bool_value(v);
236                }
237                "log_components_only" => {
238                    config.log_components_only = parse_bool_value(v);
239                }
240                "print_config" => {
241                    config.print_config = parse_bool_value(v);
242                }
243                "use_tracing" => {
244                    config.use_tracing = parse_bool_value(v);
245                }
246                "bypass_logging" => {
247                    config.bypass_logging = parse_bool_value(v);
248                }
249                "fileout_sync_on_flush" => {
250                    config.fileout_sync_on_flush = parse_bool_value(v);
251                }
252                "buffered_stdout" => {
253                    config.buffered_stdout = parse_bool_value(v);
254                }
255                "stdout" => {
256                    config.stdout_level = parse_level(v)?;
257                }
258                "fileout" => {
259                    config.fileout_level = parse_level(v)?;
260                }
261                _ => {
262                    let lvl = parse_level(v)?;
263
264                    if k.contains("::") {
265                        config.module_level.insert(Ustr::from(k), lvl);
266                    } else {
267                        config.component_level.insert(Ustr::from(k), lvl);
268                    }
269                }
270            }
271        }
272
273        Ok(config)
274    }
275
276    /// Parses configuration from the `NAUTILUS_LOG` environment variable.
277    ///
278    /// # Errors
279    ///
280    /// Returns an error if the variable is unset or contains invalid syntax.
281    pub fn from_env() -> anyhow::Result<Self> {
282        let spec = env::var("NAUTILUS_LOG")?;
283        Self::from_spec(&spec)
284    }
285}
286
287/// Parses a boolean value from a string.
288///
289/// Returns `true` unless the value is explicitly "false", "0", or "no" (case-insensitive).
290fn parse_bool_value(v: &str) -> bool {
291    !matches!(v.to_lowercase().as_str(), "false" | "0" | "no")
292}
293
294/// Parses a log level from a string.
295fn parse_level(v: &str) -> anyhow::Result<LevelFilter> {
296    LevelFilter::from_str(v).map_err(|_| anyhow::anyhow!("Invalid log level: {v}"))
297}
298
299#[cfg(test)]
300mod tests {
301    use rstest::rstest;
302
303    use super::*;
304    use crate::config::ConfigError;
305
306    #[rstest]
307    fn test_zero_rotation_max_file_size_rejected() {
308        let file_config = FileWriterConfig::new(None, None, None, Some((0, 5)));
309        let result = LoggerConfig::builder().file_config(file_config).build();
310        assert!(
311            matches!(result, Err(ConfigError::Range { field, .. }) if field == "file_config.file_rotate.max_file_size")
312        );
313    }
314
315    #[rstest]
316    fn test_positive_rotation_max_file_size_accepted() {
317        let file_config = FileWriterConfig::new(None, None, None, Some((1_048_576, 5)));
318        let result = LoggerConfig::builder().file_config(file_config).build();
319        assert!(result.is_ok());
320    }
321
322    #[rstest]
323    fn test_default_config() {
324        let config = LoggerConfig::default();
325        assert_eq!(config.stdout_level, LevelFilter::Info);
326        assert_eq!(config.fileout_level, LevelFilter::Off);
327        assert!(config.component_level.is_empty());
328        assert!(!config.log_components_only);
329        assert!(config.is_colored);
330        assert!(!config.print_config);
331        assert!(!config.bypass_logging);
332        assert!(config.file_config.is_none());
333        assert!(!config.clear_log_file);
334        assert!(config.fileout_sync_on_flush);
335        assert!(!config.buffered_stdout);
336    }
337
338    #[rstest]
339    fn test_from_spec_stdout_and_fileout() {
340        let config = LoggerConfig::from_spec("stdout=Debug;fileout=Error").unwrap();
341        assert_eq!(config.stdout_level, LevelFilter::Debug);
342        assert_eq!(config.fileout_level, LevelFilter::Error);
343    }
344
345    #[rstest]
346    fn test_from_spec_case_insensitive_levels() {
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_keys() {
354        let config = LoggerConfig::from_spec("STDOUT=Info;FILEOUT=Debug").unwrap();
355        assert_eq!(config.stdout_level, LevelFilter::Info);
356        assert_eq!(config.fileout_level, LevelFilter::Debug);
357    }
358
359    #[rstest]
360    fn test_from_spec_empty_string() {
361        let config = LoggerConfig::from_spec("").unwrap();
362        assert_eq!(config, LoggerConfig::default());
363    }
364
365    #[rstest]
366    fn test_from_spec_with_whitespace() {
367        let config = LoggerConfig::from_spec("  stdout = Info ; fileout = Debug  ").unwrap();
368        assert_eq!(config.stdout_level, LevelFilter::Info);
369        assert_eq!(config.fileout_level, LevelFilter::Debug);
370    }
371
372    #[rstest]
373    fn test_from_spec_trailing_semicolon() {
374        let config = LoggerConfig::from_spec("stdout=Warn;").unwrap();
375        assert_eq!(config.stdout_level, LevelFilter::Warn);
376    }
377
378    #[rstest]
379    fn test_from_spec_bare_is_colored() {
380        let config = LoggerConfig::from_spec("is_colored").unwrap();
381        assert!(config.is_colored);
382    }
383
384    #[rstest]
385    fn test_from_spec_is_colored_true() {
386        let config = LoggerConfig::from_spec("is_colored=true").unwrap();
387        assert!(config.is_colored);
388    }
389
390    #[rstest]
391    fn test_from_spec_is_colored_false() {
392        let config = LoggerConfig::from_spec("is_colored=false").unwrap();
393        assert!(!config.is_colored);
394    }
395
396    #[rstest]
397    fn test_from_spec_is_colored_zero() {
398        let config = LoggerConfig::from_spec("is_colored=0").unwrap();
399        assert!(!config.is_colored);
400    }
401
402    #[rstest]
403    fn test_from_spec_is_colored_no() {
404        let config = LoggerConfig::from_spec("is_colored=no").unwrap();
405        assert!(!config.is_colored);
406    }
407
408    #[rstest]
409    fn test_from_spec_is_colored_case_insensitive() {
410        let config = LoggerConfig::from_spec("IS_COLORED=FALSE").unwrap();
411        assert!(!config.is_colored);
412    }
413
414    #[rstest]
415    fn test_from_spec_print_config() {
416        let config = LoggerConfig::from_spec("print_config").unwrap();
417        assert!(config.print_config);
418    }
419
420    #[rstest]
421    fn test_from_spec_print_config_false() {
422        let config = LoggerConfig::from_spec("print_config=false").unwrap();
423        assert!(!config.print_config);
424    }
425
426    #[rstest]
427    fn test_from_spec_log_components_only() {
428        let config = LoggerConfig::from_spec("log_components_only").unwrap();
429        assert!(config.log_components_only);
430    }
431
432    #[rstest]
433    fn test_from_spec_log_components_only_false() {
434        let config = LoggerConfig::from_spec("log_components_only=false").unwrap();
435        assert!(!config.log_components_only);
436    }
437
438    #[rstest]
439    fn test_from_spec_fileout_sync_on_flush_false() {
440        let config = LoggerConfig::from_spec("fileout_sync_on_flush=false").unwrap();
441        assert!(!config.fileout_sync_on_flush);
442    }
443
444    #[rstest]
445    fn test_from_spec_buffered_stdout() {
446        let config = LoggerConfig::from_spec("buffered_stdout").unwrap();
447        assert!(config.buffered_stdout);
448    }
449
450    #[rstest]
451    fn test_from_spec_component_level() {
452        let config = LoggerConfig::from_spec("RiskEngine=Error;DataEngine=Debug").unwrap();
453        assert_eq!(
454            config.component_level[&Ustr::from("RiskEngine")],
455            LevelFilter::Error
456        );
457        assert_eq!(
458            config.component_level[&Ustr::from("DataEngine")],
459            LevelFilter::Debug
460        );
461    }
462
463    #[rstest]
464    fn test_from_spec_component_preserves_case() {
465        // Component names should preserve their original case
466        let config = LoggerConfig::from_spec("MyComponent=Info").unwrap();
467        assert!(
468            config
469                .component_level
470                .contains_key(&Ustr::from("MyComponent"))
471        );
472        assert!(
473            !config
474                .component_level
475                .contains_key(&Ustr::from("mycomponent"))
476        );
477    }
478
479    #[rstest]
480    fn test_from_spec_full_example() {
481        let config = LoggerConfig::from_spec(
482            "stdout=Info;fileout=Debug;RiskEngine=Error;is_colored;print_config",
483        )
484        .unwrap();
485
486        assert_eq!(config.stdout_level, LevelFilter::Info);
487        assert_eq!(config.fileout_level, LevelFilter::Debug);
488        assert_eq!(
489            config.component_level[&Ustr::from("RiskEngine")],
490            LevelFilter::Error
491        );
492        assert!(config.is_colored);
493        assert!(config.print_config);
494    }
495
496    #[rstest]
497    fn test_from_spec_disabled_colors() {
498        let config = LoggerConfig::from_spec("stdout=Info;is_colored=false;fileout=Debug").unwrap();
499        assert!(!config.is_colored);
500        assert_eq!(config.stdout_level, LevelFilter::Info);
501        assert_eq!(config.fileout_level, LevelFilter::Debug);
502    }
503
504    #[rstest]
505    fn test_from_spec_invalid_level() {
506        let result = LoggerConfig::from_spec("stdout=InvalidLevel");
507        assert!(result.is_err());
508        assert!(
509            result
510                .unwrap_err()
511                .to_string()
512                .contains("Invalid log level")
513        );
514    }
515
516    #[rstest]
517    fn test_from_spec_invalid_bare_flag() {
518        let result = LoggerConfig::from_spec("unknown_flag");
519        assert!(result.is_err());
520        assert!(
521            result
522                .unwrap_err()
523                .to_string()
524                .contains("Invalid spec pair")
525        );
526    }
527
528    #[rstest]
529    fn test_from_spec_missing_value() {
530        // "stdout=" with no value is technically valid empty string, which is invalid level
531        let result = LoggerConfig::from_spec("stdout=");
532        assert!(result.is_err());
533    }
534
535    #[rstest]
536    #[case("Off", LevelFilter::Off)]
537    #[case("Error", LevelFilter::Error)]
538    #[case("Warn", LevelFilter::Warn)]
539    #[case("Info", LevelFilter::Info)]
540    #[case("Debug", LevelFilter::Debug)]
541    #[case("Trace", LevelFilter::Trace)]
542    fn test_all_log_levels(#[case] level_str: &str, #[case] expected: LevelFilter) {
543        let config = LoggerConfig::from_spec(&format!("stdout={level_str}")).unwrap();
544        assert_eq!(config.stdout_level, expected);
545    }
546
547    #[rstest]
548    fn test_from_spec_single_module_path() {
549        let config = LoggerConfig::from_spec("nautilus_okx::websocket=Debug").unwrap();
550        assert_eq!(
551            config.module_level[&Ustr::from("nautilus_okx::websocket")],
552            LevelFilter::Debug
553        );
554        assert!(config.component_level.is_empty());
555    }
556
557    #[rstest]
558    fn test_from_spec_multiple_module_paths() {
559        let config =
560            LoggerConfig::from_spec("nautilus_okx::websocket=Debug;nautilus_binance::data=Trace")
561                .unwrap();
562        assert_eq!(
563            config.module_level[&Ustr::from("nautilus_okx::websocket")],
564            LevelFilter::Debug
565        );
566        assert_eq!(
567            config.module_level[&Ustr::from("nautilus_binance::data")],
568            LevelFilter::Trace
569        );
570        assert!(config.component_level.is_empty());
571    }
572
573    #[rstest]
574    fn test_from_spec_mixed_module_and_component() {
575        let config = LoggerConfig::from_spec(
576            "nautilus_okx::websocket=Debug;RiskEngine=Error;nautilus_network::data=Trace",
577        )
578        .unwrap();
579
580        assert_eq!(
581            config.module_level[&Ustr::from("nautilus_okx::websocket")],
582            LevelFilter::Debug
583        );
584        assert_eq!(
585            config.module_level[&Ustr::from("nautilus_network::data")],
586            LevelFilter::Trace
587        );
588        assert_eq!(config.module_level.len(), 2);
589        assert_eq!(
590            config.component_level[&Ustr::from("RiskEngine")],
591            LevelFilter::Error
592        );
593        assert_eq!(config.component_level.len(), 1);
594    }
595
596    #[rstest]
597    fn test_from_spec_deeply_nested_module_path() {
598        let config =
599            LoggerConfig::from_spec("nautilus_okx::websocket::handler::auth=Trace").unwrap();
600        assert_eq!(
601            config.module_level[&Ustr::from("nautilus_okx::websocket::handler::auth")],
602            LevelFilter::Trace
603        );
604    }
605
606    #[rstest]
607    fn test_from_spec_module_path_with_underscores() {
608        let config =
609            LoggerConfig::from_spec("nautilus_trader::adapters::interactive_brokers=Debug")
610                .unwrap();
611        assert_eq!(
612            config.module_level[&Ustr::from("nautilus_trader::adapters::interactive_brokers")],
613            LevelFilter::Debug
614        );
615    }
616
617    #[rstest]
618    fn test_from_spec_full_example_with_modules() {
619        let config = LoggerConfig::from_spec(
620            "stdout=Info;fileout=Debug;RiskEngine=Error;nautilus_okx::websocket=Trace;is_colored",
621        )
622        .unwrap();
623
624        assert_eq!(config.stdout_level, LevelFilter::Info);
625        assert_eq!(config.fileout_level, LevelFilter::Debug);
626        assert_eq!(
627            config.component_level[&Ustr::from("RiskEngine")],
628            LevelFilter::Error
629        );
630        assert_eq!(
631            config.module_level[&Ustr::from("nautilus_okx::websocket")],
632            LevelFilter::Trace
633        );
634        assert!(config.is_colored);
635    }
636
637    #[rstest]
638    fn test_from_spec_module_path_preserves_case() {
639        let config = LoggerConfig::from_spec("MyModule::SubModule=Info").unwrap();
640        assert!(
641            config
642                .module_level
643                .contains_key(&Ustr::from("MyModule::SubModule"))
644        );
645    }
646
647    #[rstest]
648    fn test_from_spec_single_colon_is_component() {
649        // Single colon is NOT a module path separator in Rust
650        let config = LoggerConfig::from_spec("Component:Name=Info").unwrap();
651        assert!(config.module_level.is_empty());
652        assert!(
653            config
654                .component_level
655                .contains_key(&Ustr::from("Component:Name"))
656        );
657    }
658
659    #[rstest]
660    fn test_default_module_level_is_empty() {
661        let config = LoggerConfig::default();
662        assert!(config.module_level.is_empty());
663    }
664
665    #[rstest]
666    fn test_from_spec_bypass_logging_bare() {
667        let config = LoggerConfig::from_spec("bypass_logging").unwrap();
668        assert!(config.bypass_logging);
669    }
670
671    #[rstest]
672    fn test_from_spec_bypass_logging_true() {
673        let config = LoggerConfig::from_spec("bypass_logging=true").unwrap();
674        assert!(config.bypass_logging);
675    }
676
677    #[rstest]
678    fn test_from_spec_bypass_logging_false() {
679        let config = LoggerConfig::from_spec("bypass_logging=false").unwrap();
680        assert!(!config.bypass_logging);
681    }
682
683    #[rstest]
684    fn test_toml_deserialize_minimal() {
685        let config: LoggerConfig = toml::from_str(
686            r#"
687stdout_level = "INFO"
688fileout_level = "DEBUG"
689is_colored = false
690
691[component_level]
692RiskEngine = "ERROR"
693
694[file_config]
695directory = "/var/log/nautilus"
696"#,
697        )
698        .unwrap();
699
700        assert_eq!(config.stdout_level, LevelFilter::Info);
701        assert_eq!(config.fileout_level, LevelFilter::Debug);
702        assert!(!config.is_colored);
703        assert_eq!(
704            config.component_level[&Ustr::from("RiskEngine")],
705            LevelFilter::Error
706        );
707        assert_eq!(
708            config.file_config.as_ref().unwrap().directory.as_deref(),
709            Some("/var/log/nautilus"),
710        );
711    }
712}