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