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