1use flexi_logger::{
2 Age, Cleanup, Criterion, DeferredNow, FileSpec, Logger as FlexiLogger, Naming, WriteMode,
3};
4
5use crate::config::LoggerConfig;
6
7pub struct Logger;
9
10impl Logger {
11 pub fn init() {
16 Self::init_with_config(LoggerConfig::default()).expect("Logger init failed");
17 }
18
19 pub fn init_with_level(level: String) {
27 let level = level
28 .parse::<log::LevelFilter>()
29 .expect("Invalid log level");
30
31 let config = LoggerConfig {
32 level,
33 duplicate_to_stdout: if level == log::LevelFilter::Debug {
34 crate::config::Duplicate::All
35 } else {
36 crate::config::Duplicate::None
37 },
38 ..LoggerConfig::default()
39 };
40
41 Self::init_with_config(config).expect("Logger init failed");
42 }
43
44 pub fn init_with_config(config: LoggerConfig) -> Result<(), Box<dyn std::error::Error>> {
53 let mut logger = FlexiLogger::try_with_env_or_str(config.level.as_str())?
54 .log_to_file(
55 FileSpec::default()
56 .directory(config.directory)
57 .basename(config.basename)
58 .suffix(config.suffix),
59 )
60 .rotate(
61 Criterion::AgeOrSize(Age::Day, config.rotate_size),
62 Naming::Timestamps,
63 Cleanup::KeepLogAndCompressedFiles(3, 90),
64 )
65 .write_mode(WriteMode::BufferAndFlush)
66 .duplicate_to_stdout(config.duplicate_to_stdout.into());
67
68 logger = logger.format_for_files(detailed_format);
69 logger = logger.format_for_stdout(console_format);
70
71 logger.start()?;
72 log::info!("Logger initialized, log level: {}", log::max_level());
73 Ok(())
74 }
75}
76
77fn detailed_format(
84 w: &mut dyn std::io::Write,
85 now: &mut DeferredNow,
86 record: &log::Record,
87) -> std::io::Result<()> {
88 let path = record
89 .file_static()
90 .unwrap_or(record.module_path().unwrap_or("<unnamed>"));
91 let path = extract_crate_path(path);
92
93 write!(
94 w,
95 "[{}] {:<5} [{:<55}] : {}",
96 now.now().format("%Y-%m-%d %H:%M:%S%.6f %:z"),
97 record.level(),
98 format!("{}:{}", path, record.line().unwrap_or(0)),
99 &record.args()
100 )
101}
102
103fn console_format(
110 w: &mut dyn std::io::Write,
111 now: &mut DeferredNow,
112 record: &log::Record,
113) -> std::io::Result<()> {
114 let level_str = match record.level() {
115 log::Level::Error => "\x1b[31mERROR\x1b[0m",
116 log::Level::Warn => "\x1b[33mWARN\x1b[0m",
117 log::Level::Info => "\x1b[32mINFO\x1b[0m",
118 log::Level::Debug => "\x1b[34mDEBUG\x1b[0m",
119 log::Level::Trace => "\x1b[36mTRACE\x1b[0m",
120 };
121
122 let path = record
123 .file_static()
124 .unwrap_or(record.module_path().unwrap_or("<unnamed>"));
125
126 let path = extract_crate_path(path);
127
128 write!(
129 w,
130 "[{}] {:<15} [{:<55}] {}",
131 now.now().format("%Y-%m-%d %H:%M:%S"),
132 level_str,
133 format!("{}:{}", path, record.line().unwrap_or(0)),
134 record.args()
135 )
136}
137
138fn extract_crate_path(full_path: &str) -> &str {
139 let src_tags = ["\\src\\", "/src/"];
140 for src_tag in &src_tags {
141 if let Some(src_pos) = full_path.rfind(src_tag) {
142 let before_src = &full_path[..src_pos];
143 let seps: Vec<_> = before_src
144 .char_indices()
145 .filter(|&(_, c)| c == '\\' || c == '/')
146 .map(|(i, _)| i)
147 .collect();
148 let start = if seps.len() >= 2 {
149 seps[seps.len() - 1] + 1
150 } else if seps.len() == 1 {
151 seps[0] + 1
152 } else {
153 0
154 };
155 return &full_path[start..];
156 }
157 }
158 full_path
159}