Skip to main content

rskit_logging/
config.rs

1//! Logging configuration vocabulary.
2//!
3//! These are plain `serde` data types describing *what* logging should do — the level, output format,
4//! and sink. They carry no `tracing` dependency and are always available,
5//! even when the `setup` feature (the subscriber-building layer) is disabled.
6//! This lets configuration crates compose the logging vocabulary without linking the `tracing` subscriber stack.
7
8use serde::Deserialize;
9
10/// Logging configuration.
11#[derive(Debug, Clone, Deserialize)]
12pub struct LoggingConfig {
13    /// Minimum log level: `trace`, `debug`, `info`, `warn`, `error`.
14    #[serde(default = "LoggingConfig::default_level")]
15    pub level: String,
16
17    /// Log output format (JSON or console).
18    #[serde(default)]
19    pub format: LogFormat,
20
21    /// Override service name in log output (defaults to the service identity).
22    pub service_name: Option<String>,
23
24    /// Where to write log output.
25    #[serde(default)]
26    pub output: LogOutput,
27
28    /// Include `file:line` caller location in every log line.
29    #[serde(default)]
30    pub with_caller: bool,
31}
32
33impl Default for LoggingConfig {
34    fn default() -> Self {
35        Self {
36            level: Self::default_level(),
37            format: LogFormat::default(),
38            service_name: None,
39            output: LogOutput::default(),
40            with_caller: false,
41        }
42    }
43}
44
45impl LoggingConfig {
46    fn default_level() -> String {
47        "info".to_string()
48    }
49}
50
51/// Log output format.
52#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
53#[non_exhaustive]
54#[serde(rename_all = "lowercase")]
55pub enum LogFormat {
56    /// Machine-readable JSON (use in production).
57    Json,
58    /// Human-readable coloured output (default, use in development).
59    #[default]
60    Console,
61}
62
63/// Where log output is written.
64#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
65#[non_exhaustive]
66#[serde(tag = "type", rename_all = "lowercase")]
67pub enum LogOutput {
68    /// Write to standard output (default).
69    #[default]
70    Stdout,
71    /// Write to standard error.
72    Stderr,
73    /// Write to a file at the given path.
74    File {
75        /// Absolute or relative path to the log file.
76        path: String,
77    },
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn logging_config_default_level() {
86        let cfg = LoggingConfig::default();
87        assert_eq!(cfg.level, "info");
88    }
89
90    #[test]
91    fn logging_config_default_format() {
92        let cfg = LoggingConfig::default();
93        assert_eq!(cfg.format, LogFormat::Console);
94    }
95
96    #[test]
97    fn logging_config_default_output() {
98        let cfg = LoggingConfig::default();
99        assert_eq!(cfg.output, LogOutput::Stdout);
100    }
101
102    #[test]
103    fn logging_config_default_service_name_is_none() {
104        let cfg = LoggingConfig::default();
105        assert!(cfg.service_name.is_none());
106    }
107
108    #[test]
109    fn logging_config_default_with_caller_false() {
110        let cfg = LoggingConfig::default();
111        assert!(!cfg.with_caller);
112    }
113
114    #[test]
115    fn log_format_default_is_console() {
116        assert_eq!(LogFormat::default(), LogFormat::Console);
117    }
118
119    #[test]
120    fn log_format_json_variant() {
121        let fmt = LogFormat::Json;
122        assert_ne!(fmt, LogFormat::Console);
123    }
124
125    #[test]
126    fn log_format_deserialize_json() {
127        let fmt: LogFormat = serde_json::from_str(r#""json""#).unwrap();
128        assert_eq!(fmt, LogFormat::Json);
129    }
130
131    #[test]
132    fn log_format_deserialize_console() {
133        let fmt: LogFormat = serde_json::from_str(r#""console""#).unwrap();
134        assert_eq!(fmt, LogFormat::Console);
135    }
136
137    #[test]
138    fn log_output_default_is_stdout() {
139        assert_eq!(LogOutput::default(), LogOutput::Stdout);
140    }
141
142    #[test]
143    fn log_output_stderr_variant() {
144        let out = LogOutput::Stderr;
145        assert_ne!(out, LogOutput::Stdout);
146    }
147
148    #[test]
149    fn log_output_file_variant() {
150        let out = LogOutput::File {
151            path: "/var/log/app.log".to_string(),
152        };
153        assert_eq!(
154            out,
155            LogOutput::File {
156                path: "/var/log/app.log".to_string()
157            }
158        );
159    }
160
161    #[test]
162    fn log_output_deserialize_stdout() {
163        let out: LogOutput = serde_json::from_str(r#"{"type":"stdout"}"#).unwrap();
164        assert_eq!(out, LogOutput::Stdout);
165    }
166
167    #[test]
168    fn log_output_deserialize_stderr() {
169        let out: LogOutput = serde_json::from_str(r#"{"type":"stderr"}"#).unwrap();
170        assert_eq!(out, LogOutput::Stderr);
171    }
172
173    #[test]
174    fn log_output_deserialize_file() {
175        let out: LogOutput =
176            serde_json::from_str(r#"{"type":"file","path":"/logs/app.log"}"#).unwrap();
177        assert_eq!(
178            out,
179            LogOutput::File {
180                path: "/logs/app.log".to_string()
181            }
182        );
183    }
184}