You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
347 lines
7.7 KiB
347 lines
7.7 KiB
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() |
|
}() |
|
}
|
|
|