Go 实战:实现一个简单的日志库

Go语言精选

共 6671字,需浏览 14分钟

 ·

2020-09-02 05:03

点击上方蓝色“Go语言中文网”关注我们,领全套Go资料,每天学习 Go 语言

编者按:本文实现的简单日志库不一定适合你,但可能会给你一些启发、借鉴

前言

一个完整的日志库不仅仅涵盖日志记录功能,还要包括日志 level、行号、文件切分,甚至包含统计与分析等,Go 语言中的日志库也是很多,其中知名度比较高的有:

库名star
logrus[1]14940
zap[2]9827
zerolog[3]3386
seelog[4]1464

备注:star 数获取时间为 2020-05-28 23:26:00

一千个人有一千个需求,不管是哪个开源日志库,用着总有不顺手的时候,没关系,那就自己实现一个吧,相信自己,来,就让咱们先从实现简单的日志记录功能开始吧~「手动狗头」

思路

  1. 功能设计

    根据自己的需求,我想要的日志记录功能有:

    • 按照 level 输出日志
    • 能够同时输出到文件和控制台
    • 控制台能够根据 level 将内容输出为不同颜色
    • 日志文件根据大小进行分割
    • 输出行号
  2. 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{})
}
  1. 结构设计

    根据需求,日志记录器 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                 //单个日志文件的大小限制
}
  1. 关键方法实现

    • 日志文件大小检测
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],欢迎指正!

参考资料

[1]

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




推荐阅读



学习交流 Go 语言,扫码回复「进群」即可


站长 polarisxu

自己的原创文章

不限于 Go 技术

职场和创业经验


Go语言中文网

每天为你

分享 Go 知识

Go爱好者值得关注


浏览 80
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报