1use anyhow::Result;
2use flexi_logger::{
3 Age, Cleanup, Criterion, DeferredNow, Duplicate, FileSpec, LogSpecification, Logger, Naming,
4 Record,
5};
6use log::LevelFilter;
7use serde::{Deserialize, Serialize};
8const TIME_FORMAT: &str = "%Y.%m.%d %H:%M:%S%.3f";
9#[derive(Clone, Debug, Serialize, Deserialize)]
10pub enum LogStyle {
11 Default,
12 Line,
13 Module,
14 Full,
15}
16
17#[derive(Clone, Debug, Serialize, Deserialize)]
18pub struct LogConfig {
19 #[serde(default = "default_level")]
21 pub level: u8,
22 #[serde(default = "default_usize")]
24 pub size_mb: u64,
25 #[serde(default = "default_console")]
27 pub console: bool,
28 #[serde(default = "default_log_dir")]
30 pub dir: String,
31
32 pub keep_day: Option<usize>,
34 pub keep_file_count: Option<usize>,
36
37 pub filters: Option<Vec<String>>,
39 pub log_name: Option<String>,
41 pub style: LogStyle,
43 pub cleanup_sync: Option<bool>,
45}
46
47fn default_usize() -> u64 {
48 3
49}
50fn default_console() -> bool {
51 true
52}
53
54fn default_level() -> u8 {
55 4
56}
57fn default_log_dir() -> String {
58 "./logs".to_string()
59}
60
61impl LogConfig {
62 pub fn to_level(&self) -> LevelFilter {
63 let level = match self.level {
64 1 => LevelFilter::Error,
65 2 => LevelFilter::Warn,
66 3 => LevelFilter::Info,
67 4 => LevelFilter::Debug,
68 5 => LevelFilter::Trace,
69 _ => LevelFilter::Debug,
70 };
71 level
72 }
73}
74
75impl Default for LogConfig {
76 fn default() -> Self {
77 Self {
78 level: default_level(),
79 size_mb: default_usize(),
80 console: default_console(),
81 dir: default_log_dir(),
82 keep_day: None,
83 keep_file_count: Some(3),
84 filters: None,
85 log_name: None,
86 style: LogStyle::Default,
87 cleanup_sync: Some(false),
88 }
89 }
90}
91
92pub fn string_to_level(level: &str) -> u8 {
93 let v = match level {
94 "trace" | "TRACE" => 5,
95 "debug" | "DEBUG" => 4,
96 "info" | "INFO" => 3,
97 "warn" | "WARN" => 2,
98 "error" | "ERROR" => 1,
99 _ => 4,
100 };
101 v
102}
103
104pub fn _default(
105 w: &mut dyn std::io::Write,
106 now: &mut DeferredNow,
107 record: &Record,
108) -> Result<(), std::io::Error> {
109 if record.level() == LevelFilter::Error || record.level() == LevelFilter::Warn {
110 write!(
111 w,
112 "{} [{}] {}:{}: ",
113 now.format(TIME_FORMAT),
114 record.level(),
115 record.file().unwrap_or("<unnamed>"),
116 record.line().unwrap_or(0),
117 )?;
118 } else {
119 write!(w, "{} [{}]: ", now.format(TIME_FORMAT), record.level(),)?;
120 }
121 write!(w, "{}", &record.args())
122}
123
124pub fn _line(
125 w: &mut dyn std::io::Write,
126 now: &mut DeferredNow,
127 record: &Record,
128) -> Result<(), std::io::Error> {
129 write!(
130 w,
131 "{} [{}] {}:{}: ",
132 now.format(TIME_FORMAT),
133 record.level(),
134 record.file().unwrap_or("<unnamed>"),
135 record.line().unwrap_or(0),
136 )?;
137 write!(w, "{}", &record.args())
138}
139
140pub fn _module(
141 w: &mut dyn std::io::Write,
142 now: &mut DeferredNow,
143 record: &Record,
144) -> Result<(), std::io::Error> {
145 write!(
146 w,
147 "{} [{}] [{}]: ",
148 now.format(TIME_FORMAT),
149 record.level(),
150 record.module_path().unwrap_or("<unnamed>"),
151 )?;
152 write!(w, "{}", &record.args())
153}
154
155pub fn _full(
156 w: &mut dyn std::io::Write,
157 now: &mut DeferredNow,
158 record: &Record,
159) -> Result<(), std::io::Error> {
160 write!(
161 w,
162 "{} [{}] [{}] {}:{}: ",
163 now.format(TIME_FORMAT),
164 record.level(),
165 record.module_path().unwrap_or("<unnamed>"),
166 record.file().unwrap_or("<unnamed>"),
167 record.line().unwrap_or(0),
168 )?;
169 write!(w, "{}", &record.args())
170}
171
172pub fn setup(cfg: LogConfig) -> Result<()> {
174 let level = match cfg.level {
175 1 => LevelFilter::Error,
176 2 => LevelFilter::Warn,
177 3 => LevelFilter::Info,
178 4 => LevelFilter::Debug,
179 5 => LevelFilter::Trace,
180 _ => LevelFilter::Debug,
181 };
182
183 let mut log_spec = LogSpecification::builder();
184 log_spec.default(level);
185 if let Some(items) = &cfg.filters {
186 for m in items {
187 log_spec.module(m, LevelFilter::Warn);
188 }
189 }
190
191 let hand = Logger::with(log_spec.build());
192
193 let hand = match cfg.style {
195 LogStyle::Line => hand.format(_line),
196 LogStyle::Module => hand.format(_module),
197 LogStyle::Full => hand.format(_full),
198 _ => hand.format(_default),
199 };
200
201 let basename = match &cfg.log_name {
202 Some(name) => name,
203 None => "",
204 };
205 let filespec = FileSpec::default().directory(cfg.dir).basename(basename);
206
207 let mut crit = Criterion::Size(cfg.size_mb * 1024 * 1024);
208 let mut cleanup = Cleanup::KeepLogFiles(cfg.keep_file_count.unwrap_or(3));
209 let mut naming = Naming::Numbers;
210 if let Some(v) = cfg.keep_day {
211 crit = Criterion::AgeOrSize(Age::Day, cfg.size_mb * 1024 * 1024);
212 cleanup = Cleanup::KeepForDays(v);
213 naming = Naming::TimestampsCustomFormat {
214 current_infix: Some("rCURRENT"),
215 format: "r%Y-%m-%d_%H-%M-%S",
216 };
217 }
218
219 let mut hand = hand
220 .rotate(crit, naming, cleanup)
221 .cleanup_in_background_thread(cfg.cleanup_sync.unwrap_or(false))
222 .log_to_file(filespec);
223
224 if cfg.console {
225 let d = match cfg.level {
226 1 => Duplicate::Error,
227 2 => Duplicate::Warn,
228 3 => Duplicate::Info,
229 4 => Duplicate::Debug,
230 5 => Duplicate::Trace,
231 _ => Duplicate::Debug,
232 };
233 hand = hand.duplicate_to_stdout(d)
234 }
235 hand.start()?;
236
237 Ok(())
238}