baichun_framework_logger/appender/
rolling.rs1use std::{
2 fs::{self, File},
3 io::{BufWriter, Write},
4 path::{Path, PathBuf},
5 sync::Arc,
6 time::SystemTime,
7};
8
9use chrono::{DateTime, Datelike, Local};
10use parking_lot::Mutex;
11
12use crate::{
13 config::{LogLevel, SizeRollingConfig, TimeRollingConfig},
14 error::Result,
15};
16
17use super::LogAppender;
18
19#[derive(Debug, Clone)]
21pub enum RollingStrategy {
22 Time(TimeRollingConfig),
24 Size(SizeRollingConfig),
26}
27
28#[derive(Debug)]
30pub struct RollingFileAppender {
31 base_path: PathBuf,
33 current_path: Arc<Mutex<PathBuf>>,
35 writer: Arc<Mutex<BufWriter<File>>>,
37 strategy: RollingStrategy,
39 last_check: Arc<Mutex<SystemTime>>,
41 current_size: Arc<Mutex<u64>>,
43}
44
45impl RollingFileAppender {
46 pub fn new(base_path: PathBuf, strategy: RollingStrategy) -> Result<Self> {
48 if let Some(parent) = base_path.parent() {
50 fs::create_dir_all(parent)?;
51 }
52
53 let current_path = Self::get_current_path(&base_path, &strategy)?;
55
56 if let Some(parent) = current_path.parent() {
58 fs::create_dir_all(parent)?;
59 }
60
61 let file = Self::open_file(¤t_path)?;
63 let current_size = file.metadata()?.len();
64
65 Ok(Self {
66 base_path,
67 current_path: Arc::new(Mutex::new(current_path)),
68 writer: Arc::new(Mutex::new(BufWriter::new(file))),
69 strategy,
70 last_check: Arc::new(Mutex::new(SystemTime::now())),
71 current_size: Arc::new(Mutex::new(current_size)),
72 })
73 }
74
75 fn get_current_path(base_path: &Path, strategy: &RollingStrategy) -> Result<PathBuf> {
77 match strategy {
78 RollingStrategy::Time(config) => {
79 let now = Local::now();
80 let suffix = match config.period {
81 crate::config::TimePeriod::Hourly => now.format("%Y%m%d%H"),
82 crate::config::TimePeriod::Daily => now.format("%Y%m%d"),
83 crate::config::TimePeriod::Weekly => now.format("%Y%V"),
84 crate::config::TimePeriod::Monthly => now.format("%Y%m"),
85 };
86 let mut path = base_path.to_path_buf();
87 if let Some(ext) = path.extension() {
88 let mut new_ext = ext.to_string_lossy().to_string();
89 new_ext.push('.');
90 new_ext.push_str(&suffix.to_string());
91 path.set_extension(new_ext);
92 } else {
93 path.set_extension(suffix.to_string());
94 }
95 Ok(path)
96 }
97 RollingStrategy::Size(_) => Ok(base_path.to_path_buf()),
98 }
99 }
100
101 fn open_file(path: &Path) -> Result<File> {
103 if let Some(parent) = path.parent() {
105 fs::create_dir_all(parent)?;
106 }
107
108 Ok(std::fs::OpenOptions::new()
109 .create(true)
110 .write(true)
111 .append(true)
112 .open(path)?)
113 }
114
115 fn check_roll(&self) -> Result<bool> {
117 match &self.strategy {
118 RollingStrategy::Time(config) => {
119 let now = SystemTime::now();
120 let last_check = *self.last_check.lock();
121 let duration = now.duration_since(last_check).unwrap();
122
123 let should_roll = match config.period {
124 crate::config::TimePeriod::Hourly => duration.as_secs() >= 3600,
125 crate::config::TimePeriod::Daily => duration.as_secs() >= 86400,
126 crate::config::TimePeriod::Weekly => duration.as_secs() >= 604800,
127 crate::config::TimePeriod::Monthly => {
128 let now: DateTime<Local> = now.into();
129 let last: DateTime<Local> = last_check.into();
130 now.month() != last.month()
131 }
132 };
133
134 if should_roll {
135 *self.last_check.lock() = now;
136 }
137
138 Ok(should_roll)
139 }
140 RollingStrategy::Size(config) => {
141 let current_size = *self.current_size.lock();
142 Ok(current_size >= config.max_size)
143 }
144 }
145 }
146
147 fn roll(&self) -> Result<()> {
149 let new_path = Self::get_current_path(&self.base_path, &self.strategy)?;
151
152 let new_file = Self::open_file(&new_path)?;
154
155 {
157 let mut current_path = self.current_path.lock();
158 let mut writer = self.writer.lock();
159 let mut current_size = self.current_size.lock();
160
161 writer.flush()?;
163
164 *current_path = new_path;
166
167 *writer = BufWriter::new(new_file);
169
170 *current_size = 0;
172 }
173
174 self.cleanup()?;
176
177 Ok(())
178 }
179
180 fn cleanup(&self) -> Result<()> {
182 match &self.strategy {
183 RollingStrategy::Time(config) => {
184 let now = Local::now();
185 let keep_days = config.keep_days;
186
187 let dir = self.base_path.parent().unwrap_or_else(|| Path::new("."));
188 let prefix = self
189 .base_path
190 .file_name()
191 .unwrap()
192 .to_string_lossy()
193 .to_string();
194
195 for entry in fs::read_dir(dir)? {
196 let entry = entry?;
197 let path = entry.path();
198
199 if let Some(name) = path.file_name() {
201 let name = name.to_string_lossy();
202 if !name.starts_with(&prefix) {
203 continue;
204 }
205 }
206
207 let metadata = fs::metadata(&path)?;
209 let modified: DateTime<Local> = metadata.modified()?.into();
210
211 let days_old = now.signed_duration_since(modified).num_days();
213 if days_old > keep_days as i64 {
214 fs::remove_file(path)?;
215 }
216 }
217 }
218 RollingStrategy::Size(config) => {
219 let dir = self.base_path.parent().unwrap_or_else(|| Path::new("."));
220 let prefix = self
221 .base_path
222 .file_name()
223 .unwrap()
224 .to_string_lossy()
225 .to_string();
226
227 let mut files: Vec<_> = fs::read_dir(dir)?
228 .filter_map(|e| e.ok())
229 .filter(|e| {
230 if let Some(name) = e.path().file_name() {
231 name.to_string_lossy().starts_with(&prefix)
232 } else {
233 false
234 }
235 })
236 .collect();
237
238 if files.len() > config.max_files as usize {
239 files.sort_by_key(|f| f.metadata().unwrap().modified().unwrap());
241
242 for file in files.iter().take(files.len() - config.max_files as usize) {
244 fs::remove_file(file.path())?;
245 }
246 }
247 }
248 }
249
250 Ok(())
251 }
252}
253
254impl LogAppender for RollingFileAppender {
255 fn write(&self, _level: LogLevel, message: &str) -> Result<()> {
256 if self.check_roll()? {
258 self.roll()?;
259 }
260
261 let mut writer = self.writer.lock();
263 writer.write_all(message.as_bytes())?;
264 writer.write_all(b"\n")?;
265
266 *self.current_size.lock() += message.len() as u64 + 1;
268
269 Ok(())
270 }
271
272 fn flush(&self) -> Result<()> {
273 let mut writer = self.writer.lock();
274 writer.flush()?;
275 Ok(())
276 }
277}
278
279impl Clone for RollingFileAppender {
280 fn clone(&self) -> Self {
281 Self {
282 base_path: self.base_path.clone(),
283 current_path: Arc::clone(&self.current_path),
284 writer: Arc::clone(&self.writer),
285 strategy: self.strategy.clone(),
286 last_check: Arc::clone(&self.last_check),
287 current_size: Arc::clone(&self.current_size),
288 }
289 }
290}