Table of Contents generated with DocToc
zap的重点在于对日志内容的组装和控制,没有实现对于日志文件的管理。而lumberjack就是一个用来管理日志文件的利器,经常与各种日志组件配合起来使用,实现日志的老化压缩,文件分割等等功能
// [email protected]+incompatible/lumberjack.go
// If MaxBackups and MaxAge are both 0, no old log files will be deleted.
type Logger struct {
// Filename is the file to write logs to. It uses <processname>-lumberjack.log in
// os.TempDir() if empty.
// 日志输出的文件名,这个文件名是一个全路径,即日志文件会在Filename指定的文件的目录下,进行存储和老化。
// 备份文件会写到相同目录
Filename string `json:"filename" yaml:"filename"`
// :日志文件的最大占用空间,也就是日志文件达到多大时触发日志文件的分割。单位是MB。megabytes.
MaxSize int `json:"maxsize" yaml:"maxsize"`
// 已经被分割存储的日志文件最大的留存时间,单位是天. Note that a day is defined as 24
// hours and may not exactly correspond to calendar days due to daylight
// savings, leap seconds, etc. The default is not to remove old log files
// based on age.
MaxAge int `json:"maxage" yaml:"maxage"`
// 已经被分割存储的日志文件最多的留存个数,单位是个。这个和上面的MaxAge共同生效,满足二者中的一个条件就会触发日志文件的删除。
MaxBackups int `json:"maxbackups" yaml:"maxbackups"`
// 用来指定被分割的日志文件上的时间戳是否要使用本地时间戳,默认会使用UTC时间。.
LocalTime bool `json:"localtime" yaml:"localtime"`
// 指定被分割之后的文件是否要压缩gzip,默认不压缩
Compress bool `json:"compress" yaml:"compress"`
size int64
file *os.File
mu sync.Mutex
millCh chan bool
startMill sync.Once
}
Logger实现了标准库中的WriterCloser接口,也就是Writer和Closer接口。 还有一个用来主动触发日志分割的Rotate方法。
文件名:filename方法,在内部实现中调用,用来获取待输出的文件名,如果没有设置文件名,返回的将是进程名称拼接上“-lumberjack.log”,目录在系统的临时目录下。
func (l *Logger) filename() string {
if l.Filename != "" {
return l.Filename
}
name := filepath.Base(os.Args[0]) + "-lumberjack.log"
return filepath.Join(os.TempDir(), name)
}
max方法,获取触发日志分割的文件大小,如果用户没有配置,默认会是100MB。
func (l *Logger) max() int64 {
if l.MaxSize == 0 {
return int64(defaultMaxSize * megabyte)
}
return int64(l.MaxSize) * int64(megabyte)
}
func (l *Logger) Write(p []byte) (n int, err error) {
l.mu.Lock()
defer l.mu.Unlock()
writeLen := int64(len(p))
if writeLen > l.max() {
// 如果输入的长度大于触发文件分割的阈值,直接返回错误
return 0, fmt.Errorf(
"write length %d exceeds maximum file size %d", writeLen, l.max(),
)
}
if l.file == nil {
//打开或者新建日志文件
if err = l.openExistingOrNew(len(p)); err != nil {
return 0, err
}
}
if l.size+writeLen > l.max() {
if err := l.rotate(); err != nil {
return 0, err
}
}
n, err = l.file.Write(p)
l.size += int64(n)
return n, err
}
func (l *Logger) openExistingOrNew(writeLen int) error {
l.mill()
filename := l.filename()
info, err := os_Stat(filename)
if os.IsNotExist(err) {
// 老文件不存在
return l.openNew()
}
if err != nil {
return fmt.Errorf("error getting log file info: %s", err)
}
if info.Size()+int64(writeLen) >= l.max() {
// 老文件大小加上本次要写入的达到阈值
return l.rotate()
}
file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
// 老文件打开失败
// if we fail to open the old log file for some reason, just ignore
// it and open a new log file.
return l.openNew()
}
l.file = file
l.size = info.Size()
return nil
}
func (l *Logger) mill() {
// 是一个once对象,用来控制这里的通道只初始化一次,以及协程只启动一次
l.startMill.Do(func() {
//millCh用来传递进行日志压缩删除的信号
l.millCh = make(chan bool, 1)
go l.millRun()
})
select {
case l.millCh <- true:
default:
}
}
func (l *Logger) millRun() {
for _ = range l.millCh {
// 使用range关键字不断的从millCh通道中读取信号,每读到一次,就调用一次millRunOnce来真正实现压缩删除
_ = l.millRunOnce()
}
}
优秀设计:这里的思路和go的timer中的通道类似,使用容量为1的通道做通知以及去重.
- 如果millCh中没有元素,自然可以写入,并使得millRun读到,开始调用millRunOnce方法;
- 如果有元素,不能写入,那么就说明在millRun方法中还在执行millRunOnce方法,那也就没必要写入了,直接走default分支结束就好。
创建文件
// /Users/xiaxin/go/pkg/mod/github.com/natefinch/[email protected]+incompatible/lumberjack.go
func (l *Logger) openNew() error {
// 创建全目录
err := os.MkdirAll(l.dir(), 0744)
if err != nil {
return fmt.Errorf("can't make directories for new logfile: %s", err)
}
//判断老文件是否存在
name := l.filename()
mode := os.FileMode(0644)
info, err := os_Stat(name)
if err == nil {
// 如果存在
// Copy the mode off the old logfile.
mode = info.Mode()
// move the existing file
// 调用backupName函数,生成老的日志文件被分割后的文件名
newname := backupName(name, l.LocalTime)
// 重命名旧文件
if err := os.Rename(name, newname); err != nil {
return fmt.Errorf("can't rename log file: %s", err)
}
// this is a no-op anywhere but linux
if err := chown(name, info); err != nil {
return err
}
}
// 无论是否存在老文件,下来都要创建新文件,并重置Logger对象的文件句柄和已输出大小。
// we use truncate here because this should only get called when we've moved
// the file ourselves. if someone else creates the file in the meantime,
// just wipe out the contents.
f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
if err != nil {
return fmt.Errorf("can't open new logfile: %s", err)
}
l.file = f
l.size = 0
return nil
}
文件rotate
func (l *Logger) rotate() error {
// 1.关闭原有的文件句柄
if err := l.close(); err != nil {
return err
}
//2.创建新文件
if err := l.openNew(); err != nil {
return err
}
// 3. 调用mill进行日志文件的压缩删除
l.mill()
return nil
}
func (l *Logger) close() error {
if l.file == nil {
return nil
}
err := l.file.Close()
l.file = nil
return err
}
millRunOnce:压缩和没用的文件清除
// millRunOnce performs compression and removal of stale log files.
// Log files are compressed if enabled via configuration and old log
// files are removed, keeping at most l.MaxBackups files, as long as
// none of them are older than MaxAge.
func (l *Logger) millRunOnce() error {
if l.MaxBackups == 0 && l.MaxAge == 0 && !l.Compress {
return nil
}
files, err := l.oldLogFiles()
if err != nil {
return err
}
var compress, remove []logInfo
if l.MaxBackups > 0 && l.MaxBackups < len(files) {
preserved := make(map[string]bool)
var remaining []logInfo
for _, f := range files {
// Only count the uncompressed log file or the
// compressed log file, not both.
fn := f.Name()
if strings.HasSuffix(fn, compressSuffix) {
fn = fn[:len(fn)-len(compressSuffix)]
}
preserved[fn] = true
if len(preserved) > l.MaxBackups {
remove = append(remove, f)
} else {
remaining = append(remaining, f)
}
}
files = remaining
}
if l.MaxAge > 0 {
diff := time.Duration(int64(24*time.Hour) * int64(l.MaxAge))
cutoff := currentTime().Add(-1 * diff)
var remaining []logInfo
for _, f := range files {
if f.timestamp.Before(cutoff) {
remove = append(remove, f)
} else {
remaining = append(remaining, f)
}
}
files = remaining
}
if l.Compress {
for _, f := range files {
if !strings.HasSuffix(f.Name(), compressSuffix) {
compress = append(compress, f)
}
}
}
for _, f := range remove {
errRemove := os.Remove(filepath.Join(l.dir(), f.Name()))
if err == nil && errRemove != nil {
err = errRemove
}
}
for _, f := range compress {
fn := filepath.Join(l.dir(), f.Name())
errCompress := compressLogFile(fn, fn+compressSuffix)
if err == nil && errCompress != nil {
err = errCompress
}
}
return err
}
审计日志 log 后端
// https://github.com/kubernetes/kubernetes/blob/a380ef5c416194826b70ae75dc4e86776e1a3afe/staging/src/k8s.io/apiserver/pkg/server/options/audit.go
func (o *AuditLogOptions) getWriter() (io.Writer, error) {
if !o.enabled() {
return nil, nil
}
if o.Path == "-" {
return os.Stdout, nil
}
if err := o.ensureLogFile(); err != nil {
return nil, fmt.Errorf("ensureLogFile: %w", err)
}
return &lumberjack.Logger{
Filename: o.Path,
MaxAge: o.MaxAge,
MaxBackups: o.MaxBackups,
MaxSize: o.MaxSize,
Compress: o.Compress,
}, nil
}