1use std::fmt::Display;
2
3use clap::ValueEnum;
4use provcfg::{ClapArgs, Configurable};
5use serde::{Deserialize, Deserializer, Serialize};
6
7#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Configurable, ClapArgs)]
8#[serde(default)]
9pub struct Log {
10 #[arg(long = "log-format", value_enum)]
11 pub format: LogFormat,
12 #[arg(long = "log-level", short = 'l', value_enum)]
13 pub level: LogLevel,
14 #[arg(long = "log-level-web-server", value_enum)]
15 pub level_web_server: LogLevel,
16}
17
18impl Default for Log {
19 fn default() -> Self {
20 Self {
21 format: LogFormat::Compact,
22 level: LogLevel::Info,
23 level_web_server: LogLevel::Warn,
24 }
25 }
26}
27
28#[derive(Debug, PartialEq, Eq, Clone, Copy, ValueEnum, Default)]
29pub enum LogFormat {
30 #[default]
31 Compact,
32 Pretty,
33 Json,
34}
35
36impl<'de> Deserialize<'de> for LogFormat {
37 fn deserialize<D>(de: D) -> Result<Self, D::Error>
38 where
39 D: Deserializer<'de>,
40 {
41 match String::deserialize(de)?.to_lowercase().as_str() {
42 "compact" => Ok(Self::Compact),
43 "pretty" => Ok(Self::Pretty),
44 "json" => Ok(Self::Json),
45 other => Err(serde::de::Error::custom(format!(
46 "unknown log format {other:?}; expected \"compact\", \"pretty\", or \"json\""
47 ))),
48 }
49 }
50}
51
52impl Serialize for LogFormat {
53 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
54 where
55 S: serde::Serializer,
56 {
57 serializer.serialize_str(&self.to_string())
58 }
59}
60
61impl Display for LogFormat {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 match self {
64 LogFormat::Compact => write!(f, "compact"),
65 LogFormat::Pretty => write!(f, "pretty"),
66 LogFormat::Json => write!(f, "json"),
67 }
68 }
69}
70
71#[derive(Debug, PartialEq, Eq, Clone, Copy, ValueEnum, Default)]
72pub enum LogLevel {
73 Trace,
74 Debug,
75 #[default]
76 Info,
77 Warn,
78 Error,
79}
80
81impl<'de> Deserialize<'de> for LogLevel {
82 fn deserialize<D>(de: D) -> Result<Self, D::Error>
83 where
84 D: Deserializer<'de>,
85 {
86 match String::deserialize(de)?.to_lowercase().as_str() {
87 "trace" => Ok(Self::Trace),
88 "debug" => Ok(Self::Debug),
89 "info" => Ok(Self::Info),
90 "warn" => Ok(Self::Warn),
91 "error" => Ok(Self::Error),
92 other => Err(serde::de::Error::custom(format!(
93 "unknown log level {other:?}; expected one of trace, debug, info, warn, error"
94 ))),
95 }
96 }
97}
98
99impl Display for LogLevel {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 match self {
102 LogLevel::Trace => write!(f, "trace"),
103 LogLevel::Debug => write!(f, "debug"),
104 LogLevel::Info => write!(f, "info"),
105 LogLevel::Warn => write!(f, "warn"),
106 LogLevel::Error => write!(f, "error"),
107 }
108 }
109}
110
111impl Serialize for LogLevel {
112 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
113 where
114 S: serde::Serializer,
115 {
116 serializer.serialize_str(&self.to_string())
117 }
118}
119
120impl From<LogLevel> for tracing::Level {
121 fn from(value: LogLevel) -> Self {
122 match value {
123 LogLevel::Trace => tracing::Level::TRACE,
124 LogLevel::Debug => tracing::Level::DEBUG,
125 LogLevel::Info => tracing::Level::INFO,
126 LogLevel::Warn => tracing::Level::WARN,
127 LogLevel::Error => tracing::Level::ERROR,
128 }
129 }
130}
131
132impl From<LogLevel> for tracing::level_filters::LevelFilter {
133 fn from(value: LogLevel) -> Self {
134 Self::from_level(value.into())
135 }
136}
137
138#[cfg(test)]
139mod log_format_tests {
140 use serde::Deserialize;
141
142 use super::*;
143
144 #[derive(Debug, Deserialize)]
145 struct Settings {
146 log_format: LogFormat,
147 }
148
149 #[test]
150 fn test_deserialize_log_format_compact() {
151 let toml = r#"
152 log_format = "compact"
153 "#;
154
155 let settings: Settings = toml::from_str(toml).unwrap();
156 assert_eq!(settings.log_format, LogFormat::Compact);
157 }
158
159 #[test]
160 fn test_deserialize_log_format_pretty() {
161 let toml = r#"
162 log_format = "pretty"
163 "#;
164
165 let settings: Settings = toml::from_str(toml).unwrap();
166 assert_eq!(settings.log_format, LogFormat::Pretty);
167 }
168
169 #[test]
170 fn test_deserialize_log_format_json() {
171 let toml = r#"
172 log_format = "json"
173 "#;
174
175 let settings: Settings = toml::from_str(toml).unwrap();
176 assert_eq!(settings.log_format, LogFormat::Json);
177 }
178
179 #[test]
180 fn test_deserialize_log_format_invalid() {
181 let toml = r#"
182 log_level = "no_log_format"
183 "#;
184
185 let settings: Result<Settings, toml::de::Error> = toml::from_str(toml);
186 assert!(settings.is_err());
187 }
188}
189
190#[cfg(test)]
191mod log_level_tests {
192 use serde::Deserialize;
193
194 use super::*;
195
196 #[derive(Debug, Deserialize)]
197 struct Settings {
198 log_level: LogLevel,
199 }
200
201 #[test]
202 fn test_deserialize_log_level_trace() {
203 let toml = r#"
204 log_level = "trace"
205 "#;
206
207 let settings: Settings = toml::from_str(toml).unwrap();
208 assert_eq!(settings.log_level, LogLevel::Trace);
209 }
210
211 #[test]
212 fn test_deserialize_log_level_debug() {
213 let toml = r#"
214 log_level = "debug"
215 "#;
216
217 let settings: Settings = toml::from_str(toml).unwrap();
218 assert_eq!(settings.log_level, LogLevel::Debug);
219 }
220
221 #[test]
222 fn test_deserialize_log_level_info() {
223 let toml = r#"
224 log_level = "info"
225 "#;
226
227 let settings: Settings = toml::from_str(toml).unwrap();
228 assert_eq!(settings.log_level, LogLevel::Info);
229 }
230
231 #[test]
232 fn test_deserialize_log_level_warn() {
233 let toml = r#"
234 log_level = "warn"
235 "#;
236
237 let settings: Settings = toml::from_str(toml).unwrap();
238 assert_eq!(settings.log_level, LogLevel::Warn);
239 }
240
241 #[test]
242 fn test_deserialize_log_level_error() {
243 let toml = r#"
244 log_level = "error"
245 "#;
246
247 let settings: Settings = toml::from_str(toml).unwrap();
248 assert_eq!(settings.log_level, LogLevel::Error);
249 }
250
251 #[test]
252 fn test_deserialize_log_level_uppercase() {
253 let toml = r#"
254 log_level = "DEBUG"
255 "#;
256
257 let settings: Settings = toml::from_str(toml).unwrap();
258 assert_eq!(settings.log_level, LogLevel::Debug);
259 }
260
261 #[test]
262 fn test_deserialize_log_level_invalid() {
263 let toml = r#"
264 log_level = "no_log_level"
265 "#;
266
267 let settings: Result<Settings, toml::de::Error> = toml::from_str(toml);
268 assert!(settings.is_err());
269 }
270}
271
272#[cfg(test)]
273mod log_tests {
274 use super::*;
275
276 #[derive(Debug, Deserialize)]
277 struct Settings {
278 pub log: Log,
279 }
280
281 #[test]
282 fn test_deserialize_whole_log() {
283 let toml = r#"
284 [no_log]
285 foo = "bar"
286
287 [log]
288 level = "trace"
289 level_web_server = "debug"
290 format = "compact"
291 "#;
292
293 let settings: Settings = toml::from_str(toml).unwrap();
294 assert_eq!(settings.log.level, LogLevel::Trace);
295 assert_eq!(settings.log.level_web_server, LogLevel::Debug);
296 assert_eq!(settings.log.format, LogFormat::Compact);
297 }
298}