zap源码阅读(2)

白沙云影

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
}
阅读 188
1 声望
1 粉丝
0 条评论
你知道吗?

1 声望
1 粉丝
宣传栏