1mod rotate_method;
74
75use std::{
76 error::Error,
77 fmt::{Display, Error as FmtError, Formatter},
78 fs::{self, File, OpenOptions},
79 io::{self, Read, Write},
80 path::{Path, PathBuf},
81 thread,
82 time::Duration,
83};
84
85use chrono::{DateTime, Utc};
86use path_absolutize::*;
87use regex::Regex;
88pub use rotate_method::RotateMethod;
89use xz2::write::XzEncoder;
90
91const BUFFER_SIZE: usize = 4096 * 4;
92const FILE_WAIT_MILLI_SECONDS: u64 = 30;
93
94#[derive(Debug)]
97pub enum PipeLoggerBuilderError {
98 RotateFileSizeTooSmall,
100 CountTooSmall,
102 IOError(io::Error),
104 FileIsDirectory(PathBuf),
106}
107
108impl Display for PipeLoggerBuilderError {
109 #[inline]
110 fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> {
111 match self {
112 PipeLoggerBuilderError::RotateFileSizeTooSmall => {
113 f.write_str("A valid rotated file size needs bigger than 1.")
114 },
115 PipeLoggerBuilderError::CountTooSmall => {
116 f.write_str("A valid count of log files needs bigger than 0.")
117 },
118 PipeLoggerBuilderError::IOError(err) => Display::fmt(err, f),
119 PipeLoggerBuilderError::FileIsDirectory(path) => f.write_fmt(format_args!(
120 "A log file cannot be a directory. The path of that file is `{}`.",
121 path.to_string_lossy()
122 )),
123 }
124 }
125}
126
127impl Error for PipeLoggerBuilderError {}
128
129impl From<io::Error> for PipeLoggerBuilderError {
130 #[inline]
131 fn from(err: io::Error) -> Self {
132 PipeLoggerBuilderError::IOError(err)
133 }
134}
135
136impl From<PathBuf> for PipeLoggerBuilderError {
137 #[inline]
138 fn from(err: PathBuf) -> Self {
139 PipeLoggerBuilderError::FileIsDirectory(err)
140 }
141}
142
143#[derive(Debug, Clone)]
144pub enum Tee {
146 Stdout,
148 Stderr,
150}
151
152#[derive(Debug)]
153pub struct PipeLoggerBuilder<P: AsRef<Path>> {
155 rotate: Option<RotateMethod>,
156 count: Option<usize>,
157 log_path: P,
158 compress: bool,
159 tee: Option<Tee>,
160}
161
162impl<P: AsRef<Path>> PipeLoggerBuilder<P> {
163 pub fn new(log_path: P) -> PipeLoggerBuilder<P> {
165 PipeLoggerBuilder {
166 rotate: None,
167 count: None,
168 log_path,
169 compress: false,
170 tee: None,
171 }
172 }
173
174 pub fn rotate(&self) -> &Option<RotateMethod> {
175 &self.rotate
176 }
177
178 pub fn count(&self) -> &Option<usize> {
179 &self.count
180 }
181
182 pub fn log_path(&self) -> &P {
183 &self.log_path
184 }
185
186 pub fn compress(&self) -> bool {
188 self.compress
189 }
190
191 pub fn tee(&self) -> &Option<Tee> {
192 &self.tee
193 }
194
195 pub fn set_rotate(&mut self, rotate: Option<RotateMethod>) -> &mut Self {
196 self.rotate = rotate;
197 self
198 }
199
200 pub fn set_count(&mut self, count: Option<usize>) -> &mut Self {
201 self.count = count;
202 self
203 }
204
205 pub fn set_compress(&mut self, compress: bool) -> &mut Self {
207 self.compress = compress;
208 self
209 }
210
211 pub fn set_tee(&mut self, tee: Option<Tee>) -> &mut Self {
212 self.tee = tee;
213 self
214 }
215
216 pub fn build(self) -> Result<PipeLogger, PipeLoggerBuilderError> {
218 if let Some(rotate) = &self.rotate {
219 match rotate {
220 RotateMethod::FileSize(file_size) => {
221 if *file_size < 2 {
222 return Err(PipeLoggerBuilderError::RotateFileSizeTooSmall);
223 }
224 },
225 }
226
227 if let Some(count) = &self.count {
228 if *count < 1 {
229 return Err(PipeLoggerBuilderError::CountTooSmall);
230 }
231 }
232 }
233
234 let file_path = self.log_path.as_ref().absolutize()?;
235
236 let file_size;
237
238 let folder_path = match file_path.metadata() {
239 Ok(metadata) => {
240 if metadata.is_dir() {
241 return Err(PipeLoggerBuilderError::FileIsDirectory(file_path.into_owned()));
242 }
243
244 let p = metadata.permissions();
245
246 if p.readonly() {
247 return Err(PipeLoggerBuilderError::IOError(io::Error::new(
248 io::ErrorKind::PermissionDenied,
249 format!("`{}` is readonly.", file_path.to_str().unwrap()),
250 )));
251 }
252
253 file_size = metadata.len();
254
255 match file_path.parent() {
256 Some(parent) => {
257 if self.rotate.is_some() {
258 match fs::metadata(parent) {
259 Ok(m) => {
260 let p = m.permissions();
261 if p.readonly() {
262 return Err(PipeLoggerBuilderError::IOError(
263 io::Error::new(
264 io::ErrorKind::PermissionDenied,
265 format!(
266 "`{}` is readonly.",
267 parent.to_str().unwrap()
268 ),
269 ),
270 ));
271 }
272 },
273 Err(err) => {
274 return Err(PipeLoggerBuilderError::IOError(err));
275 },
276 }
277 }
278 parent
279 },
280 None => unreachable!(),
281 }
282 },
283 Err(_) => {
284 file_size = 0;
285
286 match file_path.parent() {
287 Some(parent) => match fs::metadata(parent) {
288 Ok(m) => {
289 let p = m.permissions();
290 if p.readonly() {
291 return Err(PipeLoggerBuilderError::IOError(io::Error::new(
292 io::ErrorKind::PermissionDenied,
293 format!("`{}` is readonly.", parent.to_str().unwrap()),
294 )));
295 }
296 parent
297 },
298 Err(err) => {
299 return Err(PipeLoggerBuilderError::IOError(err));
300 },
301 },
302 None => {
303 return Err(PipeLoggerBuilderError::IOError(io::Error::new(
304 io::ErrorKind::NotFound,
305 format!("`{}`'s parent does not exist.", file_path.to_str().unwrap()),
306 )));
307 },
308 }
309 },
310 }
311 .to_path_buf();
312
313 let file_name =
314 Path::new(file_path.as_ref()).file_name().unwrap().to_str().unwrap().to_string();
315
316 let file_name_point_index = match file_name.rfind('.') {
317 Some(index) => index,
318 None => file_name.len(),
319 };
320
321 let rotated_log_file_names = {
322 let mut rotated_log_file_names = Vec::new();
323
324 let re = Regex::new("^-[1-2][0-9]{3}(-[0-5][0-9]){5}-[0-9]{3}$").unwrap(); let file_name_without_extension = &file_name[..file_name_point_index];
327
328 for entry in folder_path.read_dir().unwrap().filter_map(|entry| entry.ok()) {
329 let rotated_log_file_path = entry.path();
330
331 if !rotated_log_file_path.is_file() {
332 continue;
333 }
334
335 let rotated_log_file_name =
336 Path::new(&rotated_log_file_path).file_name().unwrap().to_str().unwrap();
337
338 if !rotated_log_file_name.starts_with(file_name_without_extension) {
339 continue;
340 }
341
342 let rotated_log_file_name_point_index = match rotated_log_file_name.rfind('.') {
343 Some(index) => index,
344 None => rotated_log_file_name.len(),
345 };
346
347 if rotated_log_file_name_point_index < file_name_point_index + 24 {
348 continue;
350 }
351
352 let file_name_without_extension_len = file_name_without_extension.len();
353
354 if !re.is_match(
355 &rotated_log_file_name
356 [file_name_without_extension_len..file_name_without_extension_len + 24],
357 ) {
358 continue;
360 }
361
362 let ext = &rotated_log_file_name[rotated_log_file_name_point_index..];
363
364 if ext.eq(&file_name[file_name_point_index..]) {
365 rotated_log_file_names.push(rotated_log_file_name.to_string());
366 } else if ext.eq(".xz")
367 && rotated_log_file_name[..rotated_log_file_name_point_index]
368 .ends_with(&file_name[file_name_point_index..])
369 {
370 rotated_log_file_names.push(
371 rotated_log_file_name[..rotated_log_file_name_point_index].to_string(),
372 );
373 }
374 }
375
376 rotated_log_file_names.sort_unstable();
377
378 rotated_log_file_names
379 };
380
381 let file =
382 OpenOptions::new().create(true).write(true).append(true).open(file_path.as_ref())?;
383
384 Ok(PipeLogger {
385 rotate: self.rotate,
386 count: self.count,
387 file: Some(file),
388 file_name,
389 file_name_point_index,
390 file_path: file_path.into_owned(),
391 file_size,
392 folder_path,
393 rotated_log_file_names,
394 compress: self.compress,
395 tee: self.tee,
396 last_rotated_time: 0,
397 })
398 }
399}
400
401pub struct PipeLogger {
407 rotate: Option<RotateMethod>,
408 count: Option<usize>,
409 file: Option<File>,
410 file_name: String,
411 file_name_point_index: usize,
412 file_path: PathBuf,
413 file_size: u64,
414 folder_path: PathBuf,
415 rotated_log_file_names: Vec<String>,
416 compress: bool,
417 tee: Option<Tee>,
418 last_rotated_time: i64,
419}
420
421impl Write for PipeLogger {
422 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
424 PipeLogger::write(self, String::from_utf8_lossy(buf))?;
425
426 Ok(buf.len())
427 }
428
429 fn flush(&mut self) -> io::Result<()> {
430 match self.file {
431 Some(ref mut file) => file.flush(),
432 None => unreachable!(),
433 }
434 }
435}
436
437impl PipeLogger {
438 pub fn builder<P: AsRef<Path>>(log_path: P) -> PipeLoggerBuilder<P> {
440 PipeLoggerBuilder::new(log_path)
441 }
442
443 pub fn write<S: AsRef<str>>(&mut self, text: S) -> io::Result<Option<PathBuf>> {
445 let s = text.as_ref();
446
447 let buf = s.as_bytes();
448
449 let len = buf.len();
450
451 if len == 0 {
452 return Ok(None);
453 }
454
455 self.print(s);
456
457 let mut file = self.file.take().unwrap();
458
459 let n = file.write(buf)?;
460
461 self.file_size += n as u64;
462
463 let mut new_file = None;
464
465 if let Some(rotate) = &self.rotate {
466 match rotate {
467 RotateMethod::FileSize(size) => {
468 if self.file_size >= *size {
469 let utc: DateTime<Utc> = {
470 let mut utc: DateTime<Utc> = Utc::now();
471 let mut millisecond = utc.timestamp_millis();
472 while self.last_rotated_time == millisecond {
473 thread::sleep(Duration::from_millis(FILE_WAIT_MILLI_SECONDS));
475 utc = Utc::now();
476 millisecond = utc.timestamp_millis();
477 }
478 self.last_rotated_time = millisecond;
479 utc
480 };
481
482 let timestamp = utc.format("%Y-%m-%d-%H-%M-%S").to_string();
483 let millisecond = utc.format("%.3f").to_string();
484
485 file.flush()?;
486
487 file.sync_all()?;
488
489 drop(file);
490
491 let rotated_log_file_name = format!(
492 "{}-{}-{}{}",
493 &self.file_name[..self.file_name_point_index],
494 timestamp,
495 &millisecond[1..],
496 &self.file_name[self.file_name_point_index..]
497 );
498
499 let rotated_log_file =
500 Path::join(&self.folder_path, Path::new(&rotated_log_file_name));
501
502 fs::copy(&self.file_path, &rotated_log_file)?;
503
504 if self.compress {
505 let rotated_log_file_name_compressed =
506 format!("{}.xz", rotated_log_file_name);
507 let rotated_log_file_compressed = Path::join(
508 &self.folder_path,
509 Path::new(&rotated_log_file_name_compressed),
510 );
511 let rotated_log_file = rotated_log_file.clone();
512
513 let tee = self.tee.clone();
514
515 let print_err = move |s| match tee {
516 Some(tee) => match tee {
517 Tee::Stdout => {
518 eprintln!("{}", s);
519 },
520 Tee::Stderr => {
521 println!("{}", s);
522 },
523 },
524 None => {
525 eprintln!("{}", s);
526 },
527 };
528
529 thread::spawn(move || {
530 match File::create(&rotated_log_file_compressed) {
531 Ok(file_w) => {
532 match File::open(&rotated_log_file) {
533 Ok(mut file_r) => {
534 let mut compressor = XzEncoder::new(file_w, 9);
535 let mut buffer = [0u8; BUFFER_SIZE];
536 loop {
537 match file_r.read(&mut buffer) {
538 Ok(c) => {
539 if c == 0 {
540 drop(file_r);
541 if fs::remove_file(
542 &rotated_log_file,
543 )
544 .is_err()
545 {
546 }
547 break;
548 }
549 match compressor.write(&buffer[..c]) {
550 Ok(cc) => {
551 if c != cc {
552 print_err(
553 "The space is not \
554 enough."
555 .to_string(),
556 );
557 break;
558 }
559 },
560 Err(err) => {
561 print_err(err.to_string());
562 break;
563 },
564 }
565 },
566 Err(ref err)
567 if err.kind()
568 == io::ErrorKind::NotFound =>
569 {
570 drop(compressor);
572 if fs::remove_file(
573 &rotated_log_file_compressed,
574 )
575 .is_err()
576 {
577 }
578 break;
579 },
580 Err(err) => {
581 print_err(err.to_string());
582 break;
583 },
584 }
585 }
586 },
587 Err(ref err)
588 if err.kind() == io::ErrorKind::NotFound =>
589 {
590 drop(file_w);
592 if fs::remove_file(&rotated_log_file_compressed)
593 .is_err()
594 {
595 }
596 },
597 Err(err) => {
598 print_err(err.to_string());
599 },
600 }
601 },
602 Err(err) => {
603 print_err(err.to_string());
604 },
605 };
606 });
607 }
608
609 self.rotated_log_file_names.push(rotated_log_file_name);
610
611 if let Some(count) = self.count {
612 while self.rotated_log_file_names.len() >= count {
613 let mut rotated_log_file_name =
614 self.rotated_log_file_names.remove(0);
615 if fs::remove_file(Path::join(
616 &self.folder_path,
617 Path::new(&rotated_log_file_name),
618 ))
619 .is_err()
620 {}
621
622 let p_compressed_name = {
623 rotated_log_file_name.push_str(".xz");
624
625 rotated_log_file_name
626 };
627
628 let p_compressed =
629 Path::join(&self.folder_path, Path::new(&p_compressed_name));
630 if fs::remove_file(p_compressed).is_err() {}
631 }
632 }
633
634 file =
635 OpenOptions::new().write(true).truncate(true).open(&self.file_path)?;
636
637 self.file_size = 0;
638
639 new_file = if self.compress {
640 let mut s = rotated_log_file.into_os_string();
641 s.push(".xz");
642 Some(PathBuf::from(s))
643 } else {
644 Some(rotated_log_file)
645 };
646 }
647 },
648 }
649 }
650
651 if n != len {
652 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "The space is not enough."));
653 }
654
655 self.file = Some(file);
656
657 Ok(new_file)
658 }
659
660 pub fn write_line<S: AsRef<str>>(&mut self, text: S) -> io::Result<Option<PathBuf>> {
662 let new_file = self.write(text)?;
663
664 if new_file.is_none() {
665 match self.file {
666 Some(ref mut file) => {
667 let n = file.write(b"\n")?;
668
669 if n != 1 {
670 return Err(io::Error::new(
671 io::ErrorKind::BrokenPipe,
672 "The space is not enough.",
673 ));
674 }
675
676 self.file_size += 1u64;
677 },
678 None => unreachable!(),
679 }
680 self.print("\n");
681 }
682
683 Ok(new_file)
684 }
685
686 fn print<S: AsRef<str>>(&self, text: S) {
687 let s = text.as_ref();
688
689 match &self.tee {
690 Some(tee) => match tee {
691 Tee::Stdout => {
692 print!("{}", s);
693 },
694 Tee::Stderr => {
695 eprint!("{}", s);
696 },
697 },
698 None => (),
699 }
700 }
701}
702
703