package logx import ( "compress/gzip" "errors" "fmt" "git.diulo.com/mogfee/kit/core/lang" "io" "log" "os" "path" "path/filepath" "sort" "strings" "sync" "syscall" "time" ) const ( dateFormat = "2006-01-02" fileTimeFormat = time.RFC3339 hoursPerDay = 24 bufferSize = 100 defaultDirMode = 0o755 defaultFileMode = 0o600 gzipExt = ".gz" megaBytes = 1 << 20 ) var ErrLogFileClosed = errors.New("error: log file closed") type ( RotateRule interface { BackupFileName() string MarkRotated() OutdatedFiles() []string ShallRotate(size int64) bool } RotateLogger struct { filename string backup string fp *os.File channel chan []byte done chan lang.PlaceholderType rule RotateRule compress bool waitGroup sync.WaitGroup closeOnce sync.Once currentSize int64 } DailyRotateRule struct { rotatedTime string filename string delimiter string days int gzip bool } SizeLimitRotateRule struct { DailyRotateRule maxSize int64 maxBackups int } ) func (d *DailyRotateRule) BackupFileName() string { return fmt.Sprintf("%s%s%s", d.filename, d.delimiter, getNowDate()) } func (d *DailyRotateRule) MarkRotated() { d.rotatedTime = getNowDate() } func (r *DailyRotateRule) OutdatedFiles() []string { if r.days <= 0 { return nil } var pattern string if r.gzip { pattern = fmt.Sprintf("%s%s*%s", r.filename, r.delimiter, gzipExt) pattern = fmt.Sprintf("%s%s*", r.filename, r.delimiter) } files, err := filepath.Glob(pattern) if err != nil { Errorf("failed to delete outdated log files, error: %s", err) return nil } var buf strings.Builder boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(dateFormat) buf.WriteString(r.filename) buf.WriteString(r.delimiter) buf.WriteString(boundary) if r.gzip { buf.WriteString(gzipExt) } boundaryFile := buf.String() var outdates []string for _, file := range files { if file < boundaryFile { outdates = append(outdates, file) } } return outdates } func (r *DailyRotateRule) ShallRotate(size int64) bool { return len(r.rotatedTime) > 0 && getNowDate() != r.rotatedTime } func DefaultRotateRule(filename, delimiter string, days int, gzip bool) RotateRule { return &DailyRotateRule{ rotatedTime: getNowDate(), filename: filename, delimiter: delimiter, days: days, gzip: gzip, } } func compressLogFile(file string) { start := time.Now() Infof("compressing log file: %s", file) if err := gzipFile(file); err != nil { Errorf("compress error: %s", err) } else { Infof("compressed log file: %s, took %s", file, time.Since(start)) } } func getNowDate() string { return time.Now().Format(dateFormat) } func getNowDateInRFC3339Format() string { return time.Now().Format(fileTimeFormat) } func gzipFile(file string) error { in, err := os.Open(file) if err != nil { return err } defer in.Close() out, err := os.Create(fmt.Sprintf("%s%s", file, gzipExt)) if err != nil { return err } defer out.Close() w := gzip.NewWriter(out) if _, err = io.Copy(w, in); err != nil { return err } else if err = w.Close(); err != nil { return err } return os.Remove(file) } func NewSizeLimitRotateRule(filename string, delimiter string, days, maxSize, maxBackups int, gzip bool) RotateRule { return &SizeLimitRotateRule{ DailyRotateRule: DailyRotateRule{ rotatedTime: getNowDateInRFC3339Format(), filename: filename, delimiter: delimiter, days: days, gzip: gzip, }, maxSize: int64(maxSize) * megaBytes, maxBackups: maxBackups, } } func (r *SizeLimitRotateRule) BackupFileName() string { dir := filepath.Dir(r.filename) prefix, ext := r.parseFilename() timestamp := getNowDateInRFC3339Format() return filepath.Join(dir, fmt.Sprintf("%s%s%s%s", prefix, r.delimiter, timestamp, ext)) } func (r *SizeLimitRotateRule) MarkRotated() { r.rotatedTime = getNowDateInRFC3339Format() } func (r *SizeLimitRotateRule) OutdatedFiles() []string { dir := filepath.Dir(r.filename) prefix, ext := r.parseFilename() var pattern string if r.gzip { pattern = fmt.Sprintf("%s%s%s%s*%s%s", dir, string(filepath.Separator), prefix, r.delimiter, ext, gzipExt) } else { pattern = fmt.Sprintf("%s%s%s%s*%s", dir, string(filepath.Separator), prefix, r.delimiter, ext) } files, err := filepath.Glob(pattern) if err != nil { Errorf("failed to delete outdated log files, error: %s", err) return nil } sort.Strings(files) outdated := make(map[string]lang.PlaceholderType) if r.maxBackups > 0 && len(files) > r.maxBackups { for _, f := range files[:len(files)-r.maxBackups] { outdated[f] = lang.Placeholder } files = files[len(files)-r.maxBackups:] } if r.days > 0 { boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(fileTimeFormat) boundaryFile := filepath.Join(dir, fmt.Sprintf("%s%s%s%s", prefix, r.delimiter, boundary, ext)) if r.gzip { boundaryFile += gzipExt } for _, f := range files { if f >= boundaryFile { break } outdated[f] = lang.Placeholder } } var result []string for k := range outdated { result = append(result, k) } return result } func (r *SizeLimitRotateRule) ShallRotate(size int64) bool { return r.maxSize > 0 && r.maxSize < size } func (r *SizeLimitRotateRule) parseFilename() (prefix, ext string) { logName := filepath.Base(r.filename) ext = filepath.Ext(r.filename) prefix = logName[:len(logName)-len(ext)] return } func NewLogger(filename string, rule RotateRule, compress bool) (*RotateLogger, error) { l := &RotateLogger{ filename: filename, channel: make(chan []byte, bufferSize), done: make(chan lang.PlaceholderType), rule: rule, compress: compress, } if err := l.init(); err != nil { return nil, err } l.startWorker() return l, nil } func (l *RotateLogger) startWorker() { l.waitGroup.Add(1) go func() { defer l.waitGroup.Done() for { select { case event := <-l.channel: l.Write(event) case <-l.done: return } } }() } func (l *RotateLogger) Close() error { var err error l.closeOnce.Do(func() { close(l.done) l.waitGroup.Wait() if err = l.fp.Sync(); err != nil { return } err = l.fp.Close() }) return err } func (l *RotateLogger) Write(data []byte) (int, error) { select { case l.channel <- data: return len(data), nil case <-l.done: log.Println(string(data)) return 0, ErrLogFileClosed } } func (l *RotateLogger) getBackupFilename() string { if len(l.backup) == 0 { return l.rule.BackupFileName() } return l.backup } func (l *RotateLogger) init() error { l.backup = l.rule.BackupFileName() if fileInfo, err := os.Stat(l.filename); err != nil { basePath := path.Dir(l.filename) if _, err = os.Stat(basePath); err != nil { if err = os.MkdirAll(basePath, defaultDirMode); err != nil { return err } } if l.fp, err = os.Create(l.filename); err != nil { return err } } else { if l.fp, err = os.OpenFile(l.filename, os.O_APPEND|os.O_WRONLY, defaultFileMode); err != nil { return err } l.currentSize = fileInfo.Size() } if l.fp != nil { syscall.CloseOnExec(int(l.fp.Fd())) } return nil } func (l *RotateLogger) maybeCompressFile(file string) { if !l.compress { return } defer func() { if r := recover(); r != nil { ErrorStack(r) } }() if _, err := os.Stat(file); err != nil { return } compressLogFile(file) } func (l *RotateLogger) maybeDeleteOutdatedFiles() { files := l.rule.OutdatedFiles() for _, file := range files { if err := os.Remove(file); err != nil { Errorf("failed to remove outdated file: %s", file) } } } func (l *RotateLogger) postRotate(file string) { go func() { l.maybeCompressFile(file) l.maybeDeleteOutdatedFiles() }() }