Go 实战:实现一个简单的日志库
编者按:本文实现的简单日志库不一定适合你,但可能会给你一些启发、借鉴
前言
一个完整的日志库不仅仅涵盖日志记录功能,还要包括日志 level、行号、文件切分,甚至包含统计与分析等,Go 语言中的日志库也是很多,其中知名度比较高的有:
| 库名 | star | 
|---|---|
| logrus[1] | 14940 | 
| zap[2] | 9827 | 
| zerolog[3] | 3386 | 
| seelog[4] | 1464 | 
备注:star 数获取时间为 2020-05-28 23:26:00
一千个人有一千个需求,不管是哪个开源日志库,用着总有不顺手的时候,没关系,那就自己实现一个吧,相信自己,来,就让咱们先从实现简单的日志记录功能开始吧~「手动狗头」
思路
功能设计
根据自己的需求,我想要的日志记录功能有:
按照 level 输出日志 能够同时输出到文件和控制台 控制台能够根据 level 将内容输出为不同颜色 日志文件根据大小进行分割 输出行号 API 设计
一般来说,根据 level 不同,设计有不同的 API,level 大概可以分为: trace、warn、error、fatal, 也就是说对外的 API 可以概括为: T(...inter), W(...), E(...), F(...)
type logger interface{
    T(format string, v ...interface{})
    W(format string, v ...interface{})
    E(format string, v ...interface{})
    F(format string, v ...interface{})
}
结构设计
根据需求,日志记录器 logger 的结构需要包含 writers、文件名、文件保存路径、文件分割大小 完整结构设计如下:
type myLog struct {
   sync.Once
   sync.Mutex                     //用于outs并发访问
   outs     map[logType]io.Writer //writer集合
   file     *os.File              //文件句柄
   fileName string                //日志名
   dir      string                //日志存放路径
   size     int64                 //单个日志文件的大小限制
}
关键方法实现
日志文件大小检测 
func (m *myLog) checkLogSize() {
   if m.file == nil {
       return
   }
   m.Lock()
   defer m.Unlock() //此处必须加锁,否则会出现并发问题
   fileInfo, err := m.file.Stat()
   if err != nil {
       panic(err)
   }
   if m.size > fileInfo.Size() {
       return
   }
   //需要分割,重新打开一个新的文件句柄替换老的,并关闭老的文件句柄,
   newName := path.Join(m.dir, time.Now().Format("2006_01_02_15:04:03")+".log")
   name := path.Join(m.dir, m.fileName)
   err = os.Rename(name, newName)
   if err != nil {
       panic(err)
   }
   file, err := os.OpenFile(name, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
   if err != nil {
       panic(err)
   }
   m.file.Close()
   m.file = file
   m.outs[logTypeFile] = file
   return
}
控制台带颜色输出内容 
func setColor(msg string, text int) string {
    return fmt.Sprintf("%c[%dm%s%c[0m", 0x1B, text, msg, 0x1B)
}
获取行号 
func shortFileName(file string) string {
   short := file
   for i := len(file) - 1; i > 0; i-- {
       if file[i] == '/' {
           short = file[i+1:]
           break
       }
   }
   return short
}
完整代码实现
package logUtil
import (
    "fmt"
    "io"
    "os"
    "path"
    "runtime"
    "strconv"
    "sync"
    "time"
)
const (
    colorRed    = 31
    colorYellow = 33
    colorBlue   = 34
    levelT = "[T] "
    levelE = "[E] "
    levelW = "[W] "
    defaultFileSize = 60 * 1024 * 1024
    minFileSize     = 1 * 1024 * 1024
    defaultLogDir   = "log"
    defaultLogName  = "default.log"
    logTypeStd logType = iota + 1
    logTypeFile
)
type (
    logType int
    LogOption func(log *myLog)
    myLog struct {
        sync.Once
        sync.Mutex
        outs     map[logType]io.Writer //writer集合
        file     *os.File              //文件句柄
        fileName string                //日志名
        dir      string                //日志存放路径
        size     int64                 //单个日志文件的大小限制
    }
)
var (
    defaultLogger = &myLog{}
)
func (m *myLog) init() {
    if m.dir == "" {
        m.dir = defaultLogDir
    }
    if m.fileName == "" {
        m.fileName = defaultLogName
    }
    if m.size == 0 {
        m.size = defaultFileSize
    } else {
        if m.size < minFileSize {
            panic(fmt.Sprintf("invalid size: %d", m.size))
        }
    }
    if m.outs == nil {
        m.outs = make(map[logType]io.Writer)
    }
    if !isExist(m.dir) {
        if err := os.Mkdir(m.dir, 0777); err != nil {
            panic(err)
        }
    }
    name := path.Join(m.dir, m.fileName)
    file, err := os.OpenFile(name, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
    if err != nil {
        panic(err)
    }
    m.file = file
    m.outs[logTypeStd] = os.Stdout
    m.outs[logTypeFile] = file
}
func (m *myLog) checkLogSize() {
    if m.file == nil {
        return
    }
    m.Lock()
    defer m.Unlock()
    fileInfo, err := m.file.Stat()
    if err != nil {
        panic(err)
    }
    if m.size > fileInfo.Size() {
        return
    }
    //需要分割
    newName := path.Join(m.dir, time.Now().Format("2006_01_02_15:04:03")+".log")
    name := path.Join(m.dir, m.fileName)
    err = os.Rename(name, newName)
    if err != nil {
        panic(err)
    }
    file, err := os.OpenFile(name, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
    if err != nil {
        panic(err)
    }
    m.file.Close()
    m.file = file
    m.outs[logTypeFile] = file
    return
}
func (m *myLog) write(level string, content string) {
    m.checkLogSize()
    var colorText int
    switch level {
    case levelT:
        colorText = colorBlue
    case levelW:
        colorText = colorYellow
    case levelE:
        colorText = colorRed
    }
    for k, wr := range m.outs {
        if k == logTypeStd {
            fmt.Fprintf(wr, setColor(content, colorText))
        } else {
            fmt.Fprintf(wr, content)
        }
    }
}
func WithSize(size int64) LogOption {
    return func(log *myLog) {
        log.size = size
    }
}
func WithLogDir(dir string) LogOption {
    return func(log *myLog) {
        log.dir = dir
    }
}
func WithFileName(name string) LogOption {
    return func(log *myLog) {
        log.fileName = name
    }
}
func InitLogger(args ...LogOption) {
    defaultLogger.Do(func() {
        for _, af := range args {
            af(defaultLogger)
        }
        defaultLogger.init()
    })
}
//Info
func T(format string, v ...interface{}) {
    _, file, line, _ := runtime.Caller(1)
    timeStr := time.Now().Format("2006-01-02 15:04:05.0000") + " "
    codeLine := "[" + timeStr + shortFileName(file) + ":" + strconv.Itoa(line) + "]"
    content := levelT + codeLine + fmt.Sprintf(format, v...) + "\n"
    defaultLogger.write(levelT, content)
}
//Error
func E(format string, v ...interface{}) {
    _, file, line, _ := runtime.Caller(1)
    timeStr := time.Now().Format("2006-01-02 15:04:05.0000") + " "
    codeLine := "[" + timeStr + shortFileName(file) + ":" + strconv.Itoa(line) + "]"
    content := levelE + codeLine + fmt.Sprintf(format, v...) + "\n"
    defaultLogger.write(levelE, content)
}
//Warn
func W(format string, v ...interface{}) {
    _, file, line, _ := runtime.Caller(1)
    timeStr := time.Now().Format("2006-01-02 15:04:05.0000") + " "
    codeLine := "[" + timeStr + shortFileName(file) + ":" + strconv.Itoa(line) + "]"
    content := levelW + codeLine + fmt.Sprintf(format, v...) + "\n"
    defaultLogger.write(levelW, content)
}
func isExist(path string) bool {
    _, err := os.Stat(path)
    if err != nil {
        if os.IsExist(err) {
            return true
        }
        return false
    }
    return true
}
func shortFileName(file string) string {
    short := file
    for i := len(file) - 1; i > 0; i-- {
        if file[i] == '/' {
            short = file[i+1:]
            break
        }
    }
    return short
}
func setColor(msg string, text int) string {
    return fmt.Sprintf("%c[%dm%s%c[0m", 0x1B, text, msg, 0x1B)
}
最后
如有不足,还请不吝指教!
附上代码地址:logUtil[5],欢迎指正!
参考资料
logrus: https://github.com/sirupsen/logrus
[2]zap: https://github.com/uber-go/zap
[3]zerolog: https://github.com/rs/zerolog
[4]seelog: https://github.com/cihub/seelog
[5]logUtil: https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fpyihe%2Futil%2Ftree%2Fmaster%2FlogUtil
本文作者:pyihe
原文链接:
https://pyihe.github.io/2020/05/31/Go%E8%AF%AD%E8%A8%80%E5%AE%9E%E7%8E%B0%E7%AE%80%E5%8D%95%E7%9A%84%E6%97%A5%E5%BF%97%E8%AE%B0%E5%BD%95%E5%8A%9F%E8%83%BD.html
推荐阅读
站长 polarisxu
自己的原创文章
不限于 Go 技术
职场和创业经验
Go语言中文网
每天为你
分享 Go 知识
Go爱好者值得关注
