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