1use getset::Getters;
2use getset::WithSetters;
3use log::LevelFilter;
4use log4rs::append::console::ConsoleAppender;
5use log4rs::append::rolling_file::RollingFileAppender;
6use log4rs::append::rolling_file::policy::compound::CompoundPolicy;
7use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller;
8use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger;
9use log4rs::config::{Appender, Config, Logger, Root};
10use log4rs::encode::pattern::PatternEncoder;
11use orion_conf::ErrorWith;
12use orion_conf::ToStructError;
13use orion_conf::error::ConfIOReason;
14#[cfg(feature = "std")]
15use orion_conf::error::OrionConfResult;
16use serde::{Deserialize, Serialize};
17use std::collections::BTreeMap;
18use std::fmt::{Display, Formatter};
19use std::path::PathBuf;
20use std::str::FromStr;
21use strum_macros::Display;
22#[derive(PartialEq, Deserialize, Serialize, Clone, Debug)]
23#[serde(deny_unknown_fields)]
24pub struct FileLogConf {
25 pub path: String,
26}
27
28#[derive(PartialEq, Deserialize, Serialize, Clone, Debug, WithSetters, Getters)]
29#[serde(deny_unknown_fields)]
30#[get = "pub"]
31pub struct LogConf {
32 pub level: String,
33 #[serde(default)]
34 pub levels: Option<BTreeMap<String, String>>, #[set_with = "pub"]
36 pub output: Output,
37 #[serde(default)]
38 pub file: Option<FileLogConf>, }
40
41#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize, Display)]
42pub enum Output {
43 Console,
44 File,
45 Both,
46}
47
48impl Default for LogConf {
49 fn default() -> Self {
50 LogConf {
51 level: String::from("warn,ctrl=info,data=error,matrc=error,dfx=warn,kdb=warn"),
52 levels: None,
53 output: Output::File,
54 file: Some(FileLogConf {
55 path: "./data/logs/".to_string(),
56 }),
57 }
58 }
59}
60
61impl Display for LogConf {
62 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
63 if let Some(map) = &self.levels {
64 writeln!(f, "levels: {:?}", map)?;
66 } else {
67 writeln!(f, "level: {}", self.level)?;
68 }
69 writeln!(f, "output: {}", self.output)?;
70 writeln!(f, "path: {:?}", self.file.as_ref().map(|x| x.path.clone()))
71 }
72}
73impl FromStr for LogConf {
76 type Err = anyhow::Error;
77 fn from_str(debug: &str) -> Result<Self, Self::Err> {
78 Ok(LogConf {
79 level: debug.to_string(),
80 levels: None,
81 output: Output::File,
82 file: Some(FileLogConf {
83 path: "./logs".to_string(),
84 }),
85 })
86 }
87}
88
89impl LogConf {
90 pub fn log_to_console(debug: &str) -> Self {
91 LogConf {
92 level: debug.to_string(),
93 levels: None,
94 output: Output::Console,
95 file: None,
96 }
97 }
98}
99
100pub const PRINT_STAT: &str = "PRINT_STAT";
101
102#[cfg(feature = "std")]
103pub fn log_init(conf: &LogConf) -> OrionConfResult<()> {
104 use orion_conf::ErrorOwe;
105
106 let (root_level, target_levels) = parse_level_spec(&conf.level)?;
107
108 let enc = PatternEncoder::new("{d(%Y-%m-%d %H:%M:%S.%f)} [{l:5}] [{t:7}] {m}{n}");
110
111 let mut config = Config::builder();
112 let mut root = Root::builder();
113
114 match conf.output {
116 Output::Console => {
117 let stdout = ConsoleAppender::builder().encoder(Box::new(enc)).build();
118 config = config.appender(Appender::builder().build("stdout", Box::new(stdout)));
119 root = root.appender("stdout");
120 }
121 Output::File => {
122 use orion_conf::ErrorOwe;
123
124 let file_path = resolve_log_file(conf)?;
125 if let Some(p) = std::path::Path::new(&file_path).parent() {
127 let _ = std::fs::create_dir_all(p);
128 }
129 let pattern = format!("{}.{{}}.gz", &file_path);
131 let roller = FixedWindowRoller::builder()
132 .base(0)
133 .build(&pattern, 10)
134 .owe_logic()
135 .with(pattern.as_str())?;
136 let trigger = SizeTrigger::new(10 * 1024 * 1024);
137 let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller));
138 let file = RollingFileAppender::builder()
139 .encoder(Box::new(enc))
140 .build(&file_path, Box::new(policy))
141 .owe_res()
142 .with(file_path.as_str())?;
143 config = config.appender(Appender::builder().build("file", Box::new(file)));
144 root = root.appender("file");
145 }
146 Output::Both => {
147 use orion_conf::ErrorOwe;
148
149 let file_path = resolve_log_file(conf)?;
150 if let Some(p) = std::path::Path::new(&file_path).parent() {
151 let _ = std::fs::create_dir_all(p);
152 }
153 let stdout = ConsoleAppender::builder()
154 .encoder(Box::new(enc.clone()))
155 .build();
156 config = config.appender(Appender::builder().build("stdout", Box::new(stdout)));
157 let pattern = format!("{}.{{}}.gz", &file_path);
158 let roller = FixedWindowRoller::builder()
159 .base(0)
160 .build(&pattern, 10)
161 .owe_logic()
162 .want(pattern.as_str())?;
163 let trigger = SizeTrigger::new(10 * 1024 * 1024);
164 let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller));
165 let file = RollingFileAppender::builder()
166 .encoder(Box::new(enc))
167 .build(&file_path, Box::new(policy))
168 .owe_res()
169 .with(file_path.as_str())?;
170 config = config.appender(Appender::builder().build("file", Box::new(file)));
171 root = root.appender("stdout").appender("file");
172 }
173 }
174
175 for (name, lv) in target_levels {
176 config = config.logger(Logger::builder().build(name.as_str(), lv));
177 }
178
179 let cfg = config
180 .build(root.build(root_level))
181 .owe_logic()
182 .want("build log cfg")?;
183 log4rs::init_config(cfg)
184 .owe_logic()
185 .want("init log config")?;
186 Ok(())
187}
188
189#[cfg(feature = "std")]
190pub fn log_for_test() -> OrionConfResult<()> {
191 let conf = LogConf {
192 level: "debug".into(),
193 levels: None,
194 output: Output::Console,
195 file: None,
196 };
197 log_init(&conf)
198}
199
200#[cfg(feature = "std")]
201pub fn log_for_test_level(level: &str) -> OrionConfResult<()> {
202 let conf = LogConf {
203 level: level.into(),
204 levels: None,
205 output: Output::Console,
206 file: None,
207 };
208 log_init(&conf)
209}
210
211fn parse_level_spec(spec: &str) -> OrionConfResult<(LevelFilter, Vec<(String, LevelFilter)>)> {
212 let mut root = LevelFilter::Info;
213 let mut targets = Vec::new();
214 for part in spec.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
215 if let Some((k, v)) = part.split_once('=') {
216 targets.push((k.trim().to_string(), parse_lv(v.trim())?));
217 } else {
218 root = parse_lv(part)?;
219 }
220 }
221 Ok((root, targets))
222}
223
224fn resolve_log_file(conf: &LogConf) -> OrionConfResult<String> {
225 let dir = conf
226 .file
227 .as_ref()
228 .map(|f| f.path.clone())
229 .unwrap_or_else(|| "./logs".to_string());
230 let arg0 = std::env::args().next().unwrap_or_else(|| "app".to_string());
231 let stem = std::path::Path::new(&arg0)
232 .file_stem()
233 .and_then(|s| s.to_str())
234 .unwrap_or("app");
235 let mut p = PathBuf::from(dir);
236 p.push(format!("{}.log", stem));
237 Ok(p.display().to_string())
238}
239
240fn parse_lv(s: &str) -> OrionConfResult<LevelFilter> {
241 match s.to_ascii_lowercase().as_str() {
242 "off" => Ok(LevelFilter::Off),
243 "error" => Ok(LevelFilter::Error),
244 "warn" => Ok(LevelFilter::Warn),
245 "info" => Ok(LevelFilter::Info),
246 "debug" => Ok(LevelFilter::Debug),
247 "trace" => Ok(LevelFilter::Trace),
248 _ => ConfIOReason::Other("unknow log level".into())
249 .err_result()
250 .with(s),
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_output_display() {
260 assert_eq!(Output::Console.to_string(), "Console");
261 assert_eq!(Output::File.to_string(), "File");
262 assert_eq!(Output::Both.to_string(), "Both");
263 }
264
265 #[test]
266 fn test_output_serde() {
267 let console: Output = serde_json::from_str(r#""Console""#).unwrap();
268 assert_eq!(console, Output::Console);
269
270 let file: Output = serde_json::from_str(r#""File""#).unwrap();
271 assert_eq!(file, Output::File);
272
273 let both: Output = serde_json::from_str(r#""Both""#).unwrap();
274 assert_eq!(both, Output::Both);
275
276 assert_eq!(
277 serde_json::to_string(&Output::Console).unwrap(),
278 r#""Console""#
279 );
280 assert_eq!(serde_json::to_string(&Output::File).unwrap(), r#""File""#);
281 assert_eq!(serde_json::to_string(&Output::Both).unwrap(), r#""Both""#);
282 }
283
284 #[test]
285 fn test_log_conf_default() {
286 let conf = LogConf::default();
287 assert!(conf.level.contains("warn"));
288 assert!(conf.level.contains("ctrl=info"));
289 assert_eq!(conf.output, Output::File);
290 assert!(conf.file.is_some());
291 assert_eq!(conf.file.as_ref().unwrap().path, "./data/logs/");
292 assert!(conf.levels.is_none());
293 }
294
295 #[test]
296 fn test_log_conf_from_str() {
297 let conf: LogConf = "debug".parse().unwrap();
298 assert_eq!(conf.level, "debug");
299 assert_eq!(conf.output, Output::File);
300 assert!(conf.file.is_some());
301 assert_eq!(conf.file.as_ref().unwrap().path, "./logs");
302 assert!(conf.levels.is_none());
303 }
304
305 #[test]
306 fn test_log_conf_log_to_console() {
307 let conf = LogConf::log_to_console("info");
308 assert_eq!(conf.level, "info");
309 assert_eq!(conf.output, Output::Console);
310 assert!(conf.file.is_none());
311 assert!(conf.levels.is_none());
312 }
313
314 #[test]
315 fn test_log_conf_display_without_levels() {
316 let conf = LogConf {
317 level: "debug".into(),
318 levels: None,
319 output: Output::Console,
320 file: Some(FileLogConf {
321 path: "/tmp/logs".into(),
322 }),
323 };
324 let display = conf.to_string();
325 assert!(display.contains("level: debug"));
326 assert!(display.contains("output: Console"));
327 assert!(display.contains("/tmp/logs"));
328 }
329
330 #[test]
331 fn test_log_conf_display_with_levels() {
332 let mut levels = BTreeMap::new();
333 levels.insert("global".into(), "warn".into());
334 levels.insert("ctrl".into(), "info".into());
335
336 let conf = LogConf {
337 level: "warn".into(),
338 levels: Some(levels),
339 output: Output::File,
340 file: None,
341 };
342 let display = conf.to_string();
343 assert!(display.contains("levels:"));
344 assert!(display.contains("global"));
345 assert!(display.contains("ctrl"));
346 assert!(display.contains("output: File"));
347 }
348
349 #[test]
350 fn test_log_conf_serde_basic() {
351 let json = r#"{
352 "level": "info",
353 "output": "Console"
354 }"#;
355 let conf: LogConf = serde_json::from_str(json).unwrap();
356 assert_eq!(conf.level, "info");
357 assert_eq!(conf.output, Output::Console);
358 assert!(conf.file.is_none());
359 }
360
361 #[test]
362 fn test_log_conf_serde_with_file() {
363 let json = r#"{
364 "level": "debug",
365 "output": "File",
366 "file": { "path": "/var/log/app" }
367 }"#;
368 let conf: LogConf = serde_json::from_str(json).unwrap();
369 assert_eq!(conf.level, "debug");
370 assert_eq!(conf.output, Output::File);
371 assert!(conf.file.is_some());
372 assert_eq!(conf.file.as_ref().unwrap().path, "/var/log/app");
373 }
374
375 #[test]
376 fn test_log_conf_serde_with_levels() {
377 let json = r#"{
378 "level": "warn",
379 "levels": { "ctrl": "info", "source": "debug" },
380 "output": "Both"
381 }"#;
382 let conf: LogConf = serde_json::from_str(json).unwrap();
383 assert_eq!(conf.level, "warn");
384 assert!(conf.levels.is_some());
385 let levels = conf.levels.as_ref().unwrap();
386 assert_eq!(levels.get("ctrl"), Some(&"info".to_string()));
387 assert_eq!(levels.get("source"), Some(&"debug".to_string()));
388 assert_eq!(conf.output, Output::Both);
389 }
390
391 #[test]
392 fn test_log_conf_serde_roundtrip() {
393 let conf = LogConf::default();
394 let json = serde_json::to_string(&conf).unwrap();
395 let parsed: LogConf = serde_json::from_str(&json).unwrap();
396 assert_eq!(conf, parsed);
397 }
398
399 #[test]
400 fn test_log_conf_reject_deprecated_output_path() {
401 let json = r#"{
402 "level": "info",
403 "output": "Console",
404 "output_path": "/old/path"
405 }"#;
406 let result: Result<LogConf, _> = serde_json::from_str(json);
407 assert!(result.is_err());
408 let err = result.unwrap_err().to_string();
409 assert!(err.contains("output_path"));
410 }
411
412 #[test]
413 fn test_log_conf_deny_unknown_fields() {
414 let json = r#"{
415 "level": "info",
416 "output": "Console",
417 "unknown_field": "value"
418 }"#;
419 let result: Result<LogConf, _> = serde_json::from_str(json);
420 assert!(result.is_err());
421 }
422
423 #[test]
424 fn test_file_log_conf_serde() {
425 let json = r#"{ "path": "/var/log" }"#;
426 let conf: FileLogConf = serde_json::from_str(json).unwrap();
427 assert_eq!(conf.path, "/var/log");
428
429 let serialized = serde_json::to_string(&conf).unwrap();
430 assert!(serialized.contains("/var/log"));
431 }
432
433 #[test]
434 fn test_parse_lv_all_levels() {
435 assert_eq!(parse_lv("off").unwrap(), LevelFilter::Off);
436 assert_eq!(parse_lv("error").unwrap(), LevelFilter::Error);
437 assert_eq!(parse_lv("warn").unwrap(), LevelFilter::Warn);
438 assert_eq!(parse_lv("info").unwrap(), LevelFilter::Info);
439 assert_eq!(parse_lv("debug").unwrap(), LevelFilter::Debug);
440 assert_eq!(parse_lv("trace").unwrap(), LevelFilter::Trace);
441 }
442
443 #[test]
444 fn test_parse_lv_case_insensitive() {
445 assert_eq!(parse_lv("DEBUG").unwrap(), LevelFilter::Debug);
446 assert_eq!(parse_lv("Info").unwrap(), LevelFilter::Info);
447 assert_eq!(parse_lv("WARN").unwrap(), LevelFilter::Warn);
448 assert_eq!(parse_lv("ErRoR").unwrap(), LevelFilter::Error);
449 }
450
451 #[test]
452 fn test_parse_lv_invalid() {
453 assert!(parse_lv("invalid").is_err());
454 assert!(parse_lv("").is_err());
455 assert!(parse_lv("warning").is_err());
456 }
457
458 #[test]
459 fn test_parse_level_spec_single_level() {
460 let (root, targets) = parse_level_spec("info").unwrap();
461 assert_eq!(root, LevelFilter::Info);
462 assert!(targets.is_empty());
463 }
464
465 #[test]
466 fn test_parse_level_spec_with_targets() {
467 let (root, targets) = parse_level_spec("warn,ctrl=info,source=debug").unwrap();
468 assert_eq!(root, LevelFilter::Warn);
469 assert_eq!(targets.len(), 2);
470 assert!(
471 targets
472 .iter()
473 .any(|(k, v)| k == "ctrl" && *v == LevelFilter::Info)
474 );
475 assert!(
476 targets
477 .iter()
478 .any(|(k, v)| k == "source" && *v == LevelFilter::Debug)
479 );
480 }
481
482 #[test]
483 fn test_parse_level_spec_with_whitespace() {
484 let (root, targets) = parse_level_spec("warn , ctrl = info , source = debug").unwrap();
485 assert_eq!(root, LevelFilter::Warn);
486 assert_eq!(targets.len(), 2);
487 }
488
489 #[test]
490 fn test_parse_level_spec_empty_parts() {
491 let (root, targets) = parse_level_spec("warn,,ctrl=info,").unwrap();
492 assert_eq!(root, LevelFilter::Warn);
493 assert_eq!(targets.len(), 1);
494 }
495
496 #[test]
497 fn test_parse_level_spec_default_like() {
498 let spec = "warn,ctrl=info,launch=info,source=info,sink=info,stat=info,runtime=warn";
499 let (root, targets) = parse_level_spec(spec).unwrap();
500 assert_eq!(root, LevelFilter::Warn);
501 assert_eq!(targets.len(), 6);
502 }
503
504 #[test]
505 fn test_log_conf_with_setters() {
506 let conf = LogConf::default().with_output(Output::Console);
507 assert_eq!(conf.output, Output::Console);
508 }
509
510 #[test]
511 fn test_log_conf_getters() {
512 let conf = LogConf::default();
513 assert_eq!(conf.level(), &conf.level);
514 assert_eq!(conf.output(), &conf.output);
515 assert_eq!(conf.file(), &conf.file);
516 assert_eq!(conf.levels(), &conf.levels);
517 }
518}