1use std::{
2 fs::File,
3 io::{self, BufWriter, Write},
4 path::{Path, PathBuf},
5 sync::Arc,
6};
7
8use parking_lot::{Mutex, MutexGuard};
9use time::{
10 format_description::{parse_owned, Component, OwnedFormatItem},
11 Date, Duration, OffsetDateTime, Time, UtcOffset,
12};
13use tracing_subscriber::fmt::MakeWriter;
14
15pub trait Checker: Sized {
16 type W: Write;
17 fn should_update(&self) -> bool;
18 fn new_writer(&self) -> io::Result<Self::W>;
19 fn buffered(self) -> Buffered<Self, Self::W> {
21 Buffered {
22 checker: self,
23 size: 4096,
24 }
25 }
26 fn buffer_with(self, size: usize) -> Buffered<Self, Self::W> {
28 Buffered {
29 checker: self,
30 size,
31 }
32 }
33
34 fn build(self) -> io::Result<(Rolling<Self, Self::W>, Token<Self::W>)> {
35 let fd = Arc::new(Mutex::new(self.new_writer()?));
36 let t = Token(fd.clone());
37 let r = Rolling::new(self, fd);
38 Ok((r, t))
39 }
40}
41
42#[must_use = "must manual drop to ensure flush file when process exits"]
43pub struct Token<W: Write>(Arc<Mutex<W>>);
44
45impl<W: Write> Drop for Token<W> {
46 fn drop(&mut self) {
47 if let Err(e) = self.0.lock().flush() {
48 eprintln!("drop writer {e}");
49 }
50 }
51}
52
53pub struct Rolling<C: Checker<W = W>, W: Write> {
54 writer: Arc<Mutex<W>>,
55 checker: C,
56}
57
58impl<C: Checker<W = W>, W: Write> Rolling<C, W> {
59 pub fn new(checker: C, writer: Arc<Mutex<W>>) -> Self {
60 Self { writer, checker }
61 }
62
63 fn update_writer(&self) -> io::Result<()> {
64 {
65 let mut writer = self.writer.lock();
66 writer.flush()?;
67 }
68 let writer = self.checker.new_writer()?;
69 *self.writer.lock() = writer;
70 Ok(())
71 }
72}
73
74impl<'a, C: Checker<W = W>, W: Write + 'a> MakeWriter<'a> for Rolling<C, W> {
75 type Writer = RollingWriter<'a, W>;
76
77 fn make_writer(&'a self) -> Self::Writer {
78 if self.checker.should_update() {
79 if let Err(e) = self.update_writer() {
80 eprintln!("can not update log file {e}")
81 }
82 }
83 RollingWriter(self.writer.lock())
84 }
85}
86
87pub struct RollingWriter<'a, W: Write>(MutexGuard<'a, W>);
88
89impl<'a, W: Write> Write for RollingWriter<'a, W> {
90 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
91 self.0.write(buf)
92 }
93
94 fn flush(&mut self) -> std::io::Result<()> {
95 self.0.flush()
96 }
97}
98
99pub trait Period {
100 fn previous_dt(&self) -> Result<OffsetDateTime, String>;
101 fn now(&self) -> OffsetDateTime;
102 fn new_path(&self) -> String;
103 fn duration(&self) -> &Duration;
104}
105
106impl<P: Period> Checker for P {
107 type W = File;
108 fn should_update(&self) -> bool {
109 let file_dt = match self.previous_dt() {
110 Ok(v) => v,
111 Err(e) => {
112 eprintln!("parse previous file failed: {e}");
113 return false;
114 }
115 };
116 self.now() - file_dt >= *self.duration()
117 }
118
119 fn new_writer(&self) -> io::Result<File> {
120 let path = self.new_path();
121 let file = File::options().append(true).create(true).open(path)?;
122 Ok(file)
123 }
124}
125
126pub struct Minute {
127 offset: UtcOffset,
128 fmt: OwnedFormatItem,
129 active: Mutex<String>,
130}
131
132impl Minute {
133 pub const DURATION: Duration = Duration::MINUTE;
134
135 pub fn new(path: impl AsRef<Path>, offset: impl Into<Option<UtcOffset>>) -> Self {
136 let ext = path
137 .as_ref()
138 .extension()
139 .and_then(|ext| ext.to_str())
140 .unwrap_or_default();
141 let fmt = path
142 .as_ref()
143 .with_extension(format!("[year]-[month]-[day]-[hour]-[minute].{ext}"));
144 let fmt = parse_owned::<1>(&format!("{}", fmt.display())).unwrap();
145 Self {
146 offset: offset.into().unwrap_or(UtcOffset::UTC),
147 fmt,
148 active: Default::default(),
149 }
150 }
151}
152
153impl Period for Minute {
154 fn previous_dt(&self) -> Result<OffsetDateTime, String> {
155 let file = self.active.lock();
156 let date = Date::parse(&file, &self.fmt).map_err(|e| e.to_string())?;
157 let time = Time::parse(&file, &self.fmt).map_err(|e| e.to_string())?;
158 Ok(date.with_time(time).assume_offset(self.offset))
159 }
160
161 fn now(&self) -> OffsetDateTime {
162 OffsetDateTime::now_utc().to_offset(self.offset)
163 }
164
165 fn new_path(&self) -> String {
166 let now = self.now();
167 let file = now.format(&self.fmt).unwrap();
168 *self.active.lock() = file.clone();
169 file
170 }
171
172 fn duration(&self) -> &Duration {
173 &Self::DURATION
174 }
175}
176
177pub struct Hourly {
178 offset: UtcOffset,
179 fmt: OwnedFormatItem,
180 hour_regex: regex::Regex,
181 active: Mutex<String>,
182}
183
184impl Hourly {
185 pub const DURATION: Duration = Duration::HOUR;
186
187 pub fn new(path: impl AsRef<Path>, offset: impl Into<Option<UtcOffset>>) -> Self {
188 let ext = path
189 .as_ref()
190 .extension()
191 .and_then(|ext| ext.to_str())
192 .unwrap_or_default();
193 let fmt = path
194 .as_ref()
195 .with_extension(format!("[year]-[month]-[day]-[hour].{ext}"));
196 let hour_regex =
197 regex::Regex::new(&format!(r".*\d{{4}}-\d{{2}}-\d{{2}}-(\d{{2}})\.{ext}")).unwrap();
198 let fmt = parse_owned::<1>(&format!("{}", fmt.display())).unwrap();
199 Self {
200 offset: offset.into().unwrap_or(UtcOffset::UTC),
201 fmt,
202 active: Default::default(),
203 hour_regex,
204 }
205 }
206}
207
208impl Period for Hourly {
209 fn previous_dt(&self) -> Result<OffsetDateTime, String> {
210 let file = self.active.lock();
211 let date = Date::parse(&file, &self.fmt).map_err(|e| e.to_string())?;
212 let hour = self
213 .hour_regex
214 .captures(&file)
215 .and_then(|cap| cap.get(1))
216 .and_then(|m| m.as_str().parse::<u8>().ok())
217 .ok_or_else(|| format!("invalid hour component of {file}"))?;
218 let time = Time::from_hms(hour, 0, 0).unwrap();
219 Ok(date.with_time(time).assume_offset(self.offset))
220 }
221
222 fn now(&self) -> OffsetDateTime {
223 OffsetDateTime::now_utc().to_offset(self.offset)
224 }
225
226 fn new_path(&self) -> String {
227 let now = self.now();
228 let file = now.format(&self.fmt).unwrap();
229 *self.active.lock() = file.clone();
230 file
231 }
232
233 fn duration(&self) -> &Duration {
234 &Self::DURATION
235 }
236}
237
238pub struct Daily {
239 offset: UtcOffset,
240 fmt: OwnedFormatItem,
241 active: Mutex<String>,
242}
243
244impl Daily {
245 pub const DURATION: Duration = Duration::DAY;
246
247 fn ensure_year_month_day(fmt: &OwnedFormatItem) {
248 match fmt {
249 OwnedFormatItem::Compound(items) => {
250 let mut year = false;
251 let mut month = false;
252 let mut day = false;
253 for item in &items[..] {
254 match item {
255 OwnedFormatItem::Component(Component::Year(_)) => {
256 year = !year;
257 }
258 OwnedFormatItem::Component(Component::Month(_)) => {
259 month = !month;
260 }
261 OwnedFormatItem::Component(Component::Day(_)) => {
262 day = !day;
263 }
264 _ => {}
265 }
266 }
267 if !(year && month && day) {
268 panic!("invalid daily format");
269 }
270 }
271 _ => panic!("expect compound format"),
272 }
273 }
274
275 pub fn new<S>(
280 path: impl AsRef<Path>,
281 fmt: impl Into<Option<S>>,
282 offset: impl Into<Option<UtcOffset>>,
283 ) -> Self
284 where
285 S: std::fmt::Display,
286 {
287 let ext = path
288 .as_ref()
289 .extension()
290 .and_then(|ext| ext.to_str())
291 .unwrap_or_default();
292
293 let file_stem = path
294 .as_ref()
295 .file_stem()
296 .and_then(|stem| stem.to_str())
297 .unwrap_or_default();
298
299 let file_name = fmt
300 .into()
301 .map(|f| format!("{file_stem}-{f}.{ext}"))
302 .unwrap_or_else(|| format!("{file_stem}-[year]-[month]-[day].{ext}"));
303 let fmt = parse_owned::<1>(&format!(
304 "{}",
305 path.as_ref().with_file_name(file_name).display()
306 ))
307 .unwrap();
308 Self::ensure_year_month_day(&fmt);
309 Self {
310 offset: offset.into().unwrap_or(UtcOffset::UTC),
311 fmt,
312 active: Default::default(),
313 }
314 }
315}
316
317impl Period for Daily {
318 fn previous_dt(&self) -> Result<OffsetDateTime, String> {
319 let file = self.active.lock();
320 let date = Date::parse(&file, &self.fmt).map_err(|e| e.to_string())?;
321 Ok(date
322 .with_time(time::macros::time!(0:0:0))
323 .assume_offset(self.offset))
324 }
325
326 fn now(&self) -> OffsetDateTime {
327 OffsetDateTime::now_utc().to_offset(self.offset)
328 }
329
330 fn new_path(&self) -> String {
331 let now = self.now();
332 let file = now.format(&self.fmt).unwrap();
333 *self.active.lock() = file.clone();
334 file
335 }
336
337 fn duration(&self) -> &Duration {
338 &Self::DURATION
339 }
340}
341
342pub struct Buffered<C: Checker<W = W>, W: Write> {
343 checker: C,
344 size: usize,
345}
346
347impl<C: Checker<W = W>, W: Write> Buffered<C, W> {
348 pub fn new(checker: C, size: usize) -> Self {
349 Self { checker, size }
350 }
351}
352
353impl<C: Checker<W = W>, W: Write> Checker for Buffered<C, W> {
354 type W = BufWriter<W>;
355 fn should_update(&self) -> bool {
356 self.checker.should_update()
357 }
358 fn new_writer(&self) -> io::Result<BufWriter<W>> {
359 Ok(BufWriter::with_capacity(
360 self.size,
361 self.checker.new_writer()?,
362 ))
363 }
364}
365
366pub struct ConstFile(PathBuf);
368
369impl Checker for ConstFile {
370 type W = File;
371
372 fn should_update(&self) -> bool {
373 false
374 }
375
376 fn new_writer(&self) -> io::Result<Self::W> {
377 File::options().append(true).create(true).open(&self.0)
378 }
379}
380
381impl ConstFile {
382 pub fn new(path: impl AsRef<Path>) -> Self {
383 Self(path.as_ref().to_path_buf())
384 }
385}