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