go-zero/core/logx/rotatelogger.go

466 lines
10 KiB
Go
Raw Normal View History

2020-07-26 17:09:05 +08:00
package logx
import (
"compress/gzip"
"errors"
"fmt"
"log"
"os"
"path"
"path/filepath"
"sort"
2020-07-26 17:09:05 +08:00
"strings"
"sync"
"time"
"github.com/zeromicro/go-zero/core/fs"
"github.com/zeromicro/go-zero/core/lang"
2020-07-26 17:09:05 +08:00
)
const (
2022-07-22 23:16:38 +08:00
dateFormat = "2006-01-02"
fileTimeFormat = time.RFC3339
hoursPerDay = 24
bufferSize = 100
defaultDirMode = 0o755
defaultFileMode = 0o600
gzipExt = ".gz"
megaBytes = 1 << 20
2020-07-26 17:09:05 +08:00
)
2021-02-20 22:45:58 +08:00
// ErrLogFileClosed is an error that indicates the log file is already closed.
2020-07-26 17:09:05 +08:00
var ErrLogFileClosed = errors.New("error: log file closed")
type (
2021-02-20 22:45:58 +08:00
// A RotateRule interface is used to define the log rotating rules.
2020-07-26 17:09:05 +08:00
RotateRule interface {
BackupFileName() string
MarkRotated()
OutdatedFiles() []string
2022-07-22 22:28:01 +08:00
ShallRotate(size int64) bool
2020-07-26 17:09:05 +08:00
}
2021-02-20 22:45:58 +08:00
// A RotateLogger is a Logger that can rotate log files with given rules.
2020-07-26 17:09:05 +08:00
RotateLogger struct {
filename string
backup string
fp *os.File
channel chan []byte
done chan lang.PlaceholderType
rule RotateRule
compress bool
// can't use threading.RoutineGroup because of cycle import
2022-07-22 22:28:01 +08:00
waitGroup sync.WaitGroup
closeOnce sync.Once
currentSize int64
2020-07-26 17:09:05 +08:00
}
2021-02-20 22:45:58 +08:00
// A DailyRotateRule is a rule to daily rotate the log files.
2020-07-26 17:09:05 +08:00
DailyRotateRule struct {
rotatedTime string
filename string
delimiter string
days int
gzip bool
}
// SizeLimitRotateRule a rotation rule that make the log file rotated base on size
SizeLimitRotateRule struct {
DailyRotateRule
2022-07-22 22:28:01 +08:00
maxSize int64
maxBackups int
}
2020-07-26 17:09:05 +08:00
)
2021-02-20 22:45:58 +08:00
// DefaultRotateRule is a default log rotating rule, currently DailyRotateRule.
2020-07-26 17:09:05 +08:00
func DefaultRotateRule(filename, delimiter string, days int, gzip bool) RotateRule {
return &DailyRotateRule{
rotatedTime: getNowDate(),
filename: filename,
delimiter: delimiter,
days: days,
gzip: gzip,
}
}
2021-02-20 22:45:58 +08:00
// BackupFileName returns the backup filename on rotating.
2020-07-26 17:09:05 +08:00
func (r *DailyRotateRule) BackupFileName() string {
return fmt.Sprintf("%s%s%s", r.filename, r.delimiter, getNowDate())
}
2021-02-20 22:45:58 +08:00
// MarkRotated marks the rotated time of r to be the current time.
2020-07-26 17:09:05 +08:00
func (r *DailyRotateRule) MarkRotated() {
r.rotatedTime = getNowDate()
}
2021-02-20 22:45:58 +08:00
// OutdatedFiles returns the files that exceeded the keeping days.
2020-07-26 17:09:05 +08:00
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)
2020-07-26 17:09:05 +08:00
} else {
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)
2022-09-20 23:51:58 +08:00
buf.WriteString(r.filename)
buf.WriteString(r.delimiter)
buf.WriteString(boundary)
2020-07-26 17:09:05 +08:00
if r.gzip {
buf.WriteString(gzipExt)
2020-07-26 17:09:05 +08:00
}
boundaryFile := buf.String()
var outdates []string
for _, file := range files {
if file < boundaryFile {
outdates = append(outdates, file)
}
}
return outdates
}
2021-02-20 22:45:58 +08:00
// ShallRotate checks if the file should be rotated.
2022-07-22 22:28:01 +08:00
func (r *DailyRotateRule) ShallRotate(_ int64) bool {
2020-07-26 17:09:05 +08:00
return len(r.rotatedTime) > 0 && getNowDate() != r.rotatedTime
}
// NewSizeLimitRotateRule returns the rotation rule with size limit
func NewSizeLimitRotateRule(filename, delimiter string, days, maxSize, maxBackups int, gzip bool) RotateRule {
return &SizeLimitRotateRule{
DailyRotateRule: DailyRotateRule{
rotatedTime: getNowDateInRFC3339Format(),
filename: filename,
delimiter: delimiter,
days: days,
gzip: gzip,
},
2022-07-22 22:28:01 +08:00
maxSize: int64(maxSize) * megaBytes,
maxBackups: maxBackups,
}
}
func (r *SizeLimitRotateRule) BackupFileName() string {
dir := filepath.Dir(r.filename)
2022-07-22 22:28:01 +08:00
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 {
2022-07-22 22:28:01 +08:00
dir := filepath.Dir(r.filename)
prefix, ext := r.parseFilename()
var pattern string
if r.gzip {
2022-07-22 22:28:01 +08:00
pattern = fmt.Sprintf("%s%s%s%s*%s%s", dir, string(filepath.Separator),
prefix, r.delimiter, ext, gzipExt)
} else {
2022-07-22 22:28:01 +08:00
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)
// test if too many backups
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:]
}
// test if any too old backups
if r.days > 0 {
2022-07-22 23:16:38 +08:00
boundary := time.Now().Add(-time.Hour * time.Duration(hoursPerDay*r.days)).Format(fileTimeFormat)
2022-07-22 22:28:01 +08:00
boundaryFile := filepath.Join(dir, fmt.Sprintf("%s%s%s%s", prefix, r.delimiter, boundary, ext))
if r.gzip {
2022-07-22 22:28:01 +08:00
boundaryFile += gzipExt
}
for _, f := range files {
2022-07-22 22:28:01 +08:00
if f >= boundaryFile {
break
}
2022-07-22 22:28:01 +08:00
outdated[f] = lang.Placeholder
}
}
var result []string
for k := range outdated {
result = append(result, k)
}
return result
}
2022-07-22 22:28:01 +08:00
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
}
2021-02-20 22:45:58 +08:00
// NewLogger returns a RotateLogger with given filename and rule, etc.
2020-07-26 17:09:05 +08:00
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,
}
2023-03-12 20:42:50 +08:00
if err := l.initialize(); err != nil {
2020-07-26 17:09:05 +08:00
return nil, err
}
l.startWorker()
return l, nil
}
2021-02-20 22:45:58 +08:00
// Close closes l.
2020-07-26 17:09:05 +08:00
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()
}
2021-02-09 13:50:21 +08:00
return l.backup
2020-07-26 17:09:05 +08:00
}
2023-03-12 20:42:50 +08:00
func (l *RotateLogger) initialize() error {
2020-07-26 17:09:05 +08:00
l.backup = l.rule.BackupFileName()
if fileInfo, err := os.Stat(l.filename); err != nil {
2020-07-26 17:09:05 +08:00
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()
2020-07-26 17:09:05 +08:00
}
fs.CloseOnExec(l.fp)
return nil
}
func (l *RotateLogger) maybeCompressFile(file string) {
2020-10-01 16:49:39 +08:00
if !l.compress {
return
2020-07-26 17:09:05 +08:00
}
2020-10-01 16:49:39 +08:00
defer func() {
if r := recover(); r != nil {
ErrorStack(r)
}
}()
if _, err := os.Stat(file); err != nil {
// file not exists or other error, ignore compression
return
}
2020-10-01 16:49:39 +08:00
compressLogFile(file)
2020-07-26 17:09:05 +08:00
}
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() {
// we cannot use threading.GoSafe here, because of import cycle.
l.maybeCompressFile(file)
l.maybeDeleteOutdatedFiles()
}()
}
func (l *RotateLogger) rotate() error {
if l.fp != nil {
err := l.fp.Close()
l.fp = nil
if err != nil {
return err
}
}
_, err := os.Stat(l.filename)
if err == nil && len(l.backup) > 0 {
backupFilename := l.getBackupFilename()
err = os.Rename(l.filename, backupFilename)
if err != nil {
return err
}
l.postRotate(backupFilename)
}
l.backup = l.rule.BackupFileName()
if l.fp, err = os.Create(l.filename); err == nil {
fs.CloseOnExec(l.fp)
}
return err
}
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:
// avoid losing logs before closing.
for {
select {
case event := <-l.channel:
l.write(event)
default:
return
}
}
2020-07-26 17:09:05 +08:00
}
}
}()
}
func (l *RotateLogger) write(v []byte) {
2022-07-22 22:28:01 +08:00
if l.rule.ShallRotate(l.currentSize + int64(len(v))) {
2020-07-26 17:09:05 +08:00
if err := l.rotate(); err != nil {
log.Println(err)
} else {
l.rule.MarkRotated()
l.currentSize = 0
2020-07-26 17:09:05 +08:00
}
}
if l.fp != nil {
l.fp.Write(v)
2022-07-22 22:28:01 +08:00
l.currentSize += int64(len(v))
2020-07-26 17:09:05 +08:00
}
}
func compressLogFile(file string) {
start := time.Now()
2020-07-26 17:09:05 +08:00
Infof("compressing log file: %s", file)
2023-06-09 22:50:59 +08:00
if err := gzipFile(file, fileSys); err != nil {
Errorf("compress error: %s", err)
2020-07-26 17:09:05 +08:00
} else {
Infof("compressed log file: %s, took %s", file, time.Since(start))
2020-07-26 17:09:05 +08:00
}
}
func getNowDate() string {
return time.Now().Format(dateFormat)
}
func getNowDateInRFC3339Format() string {
2022-07-22 23:16:38 +08:00
return time.Now().Format(fileTimeFormat)
}
2023-06-09 22:50:59 +08:00
func gzipFile(file string, fsys fileSystem) (err error) {
in, err := fsys.Open(file)
2020-07-26 17:09:05 +08:00
if err != nil {
return err
}
2023-06-09 22:50:59 +08:00
defer func() {
if e := fsys.Close(in); e != nil {
Errorf("failed to close file: %s, error: %v", file, e)
}
if err == nil {
// only remove the original file when compression is successful
err = fsys.Remove(file)
}
}()
2020-07-26 17:09:05 +08:00
2023-06-09 22:50:59 +08:00
out, err := fsys.Create(fmt.Sprintf("%s%s", file, gzipExt))
2020-07-26 17:09:05 +08:00
if err != nil {
return err
}
2023-06-09 22:50:59 +08:00
defer func() {
e := fsys.Close(out)
if err == nil {
err = e
}
}()
2020-07-26 17:09:05 +08:00
w := gzip.NewWriter(out)
2023-06-09 22:50:59 +08:00
if _, err = fsys.Copy(w, in); err != nil {
// failed to copy, no need to close w
2020-07-26 17:09:05 +08:00
return err
}
2023-06-09 22:50:59 +08:00
return fsys.Close(w)
2020-07-26 17:09:05 +08:00
}