zap源码阅读(2)
上篇文章主要是介绍了zap库的结构和性能优势,但是其提供的只是基础的日志写入功能,实际工作需要日志具备大小切割,日志压缩,保存指定天数等功能,此时就需要使用另外一个开源库lumberjack(https://github.com/natefinch/...
1. lumberjack 库介绍
Logger结构体
type Logger struct {
Filename string `json:"filename" yaml:"filename"` //文件名
MaxSize int `json:"maxsize" yaml:"maxsize"` // 最大保存文件大小,默认100M
MaxAge int `json:"maxage" yaml:"maxage"` // 最大保存天数
MaxBackups int `json:"maxbackups" yaml:"maxbackups"` // 最大保存个数
LocalTime bool `json:"localtime" yaml:"localtime"` //文件以时间命名的格式
Compress bool `json:"compress" yaml:"compress"` // 是否压缩
size int64 // 当前文件大小
file *os.File
mu sync.Mutex
millCh chan bool
startMill sync.Once
}
该库对外提供的接口比较简单,主要是Write函数。
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
}
openExistingOrNew 函数
func (l *Logger) openExistingOrNew(writeLen int) error {
l.mill()
filename := l.filename()//写入日志的文件,带有路径
info, err := osStat(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
}
openNew函数,创建新的文件
func (l *Logger) openNew() error {
err := os.MkdirAll(l.dir(), 0755)
if err != nil {
return fmt.Errorf("can't make directories for new logfile: %s", err)
}
name := l.filename()
mode := os.FileMode(0600)
info, err := osStat(name)
if err == nil {
// Copy the mode off the old logfile.
mode = info.Mode()
// move the existing file
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
}
}
// 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,
// 重新打开文件
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
}
var osChown = os.Chown
func chown(name string, info os.FileInfo) error {
f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode())
if err != nil {
return err
}
f.Close()// 关闭文件
stat := info.Sys().(*syscall.Stat_t)
return osChown(name, int(stat.Uid), int(stat.Gid)) //分配组
}
rotate 函数:
func (l *Logger) rotate() error { // 该函数并不是每个文件必须写满MaxSize, 当要写的日志大于MaxSize, 之前的日志文件不会再写,之后的文件以最新的文件开始写.
if err := l.close(); err != nil { // 先关掉之前的文件
return err
}
if err := l.openNew(); err != nil { // 创建新的文件
return err
}
l.mill()
return nil
}
mill 函数:
func (l *Logger) mill() {
l.startMill.Do(func() {
l.millCh = make(chan bool, 1)
go l.millRun() // 开启协程
})
select {
case l.millCh <- true: // 每次Write都会通知协程做事情。
default:
}
}
func (l *Logger) millRun() {
for range l.millCh { // 读millCh channel事件
_ = l.millRunOnce()
}
}
日志切割核心函数millRunOnce:
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
}
func (l *Logger) oldLogFiles() ([]logInfo, error) {
files, err := ioutil.ReadDir(l.dir()) //目录下的所有文件
if err != nil {
return nil, fmt.Errorf("can't read log file directory: %s", err)
}
logFiles := []logInfo{}
prefix, ext := l.prefixAndExt()
for _, f := range files {
if f.IsDir() {
continue
}
if t, err := l.timeFromName(f.Name(), prefix, ext); err == nil {
logFiles = append(logFiles, logInfo{t, f}) //符合条件的文件
continue
}
if t, err := l.timeFromName(f.Name(), prefix, ext+compressSuffix); err == nil {
logFiles = append(logFiles, logInfo{t, f}) //符合条件的压缩文件
continue
}
// error parsing means that the suffix at the end was not generated
// by lumberjack, and therefore it's not a backup file.
}
sort.Sort(byFormatTime(logFiles)) //以最近时间的日志排在前面
return logFiles, nil
}
压缩方法compressLogFile:
func compressLogFile(src, dst string) (err error) {
f, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open log file: %v", err)
}
defer f.Close()
fi, err := osStat(src)
if err != nil {
return fmt.Errorf("failed to stat log file: %v", err)
}
if err := chown(dst, fi); err != nil {
return fmt.Errorf("failed to chown compressed log file: %v", err)
}
// If this file already exists, we presume it was created by
// a previous attempt to compress the log file.
gzf, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, fi.Mode())
if err != nil {
return fmt.Errorf("failed to open compressed log file: %v", err)
}
defer gzf.Close()
gz := gzip.NewWriter(gzf)
defer func() {
if err != nil {
os.Remove(dst)
err = fmt.Errorf("failed to compress log file: %v", err)
}
}()
if _, err := io.Copy(gz, f); err != nil {
return err
}
if err := gz.Close(); err != nil {
return err
}
if err := gzf.Close(); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
if err := os.Remove(src); err != nil {
return err
}
return nil
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。