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爱好者值得关注