文章篇幅较长,可以先收藏防止迷路~
在许多Go语言项目中,我们需要一个好的日志记录器能够提供下面这些功能:
- 能够将事件记录到文件中,而不是应用程序控制台;
- 日志切割-能够根据文件大小、时间或间隔等来切割日志文件;
- 支持不同的日志级别。例如INFO,DEBUG,ERROR等;
- 能够打印基本信息,如调用文件/函数名和行号,日志时间等;
1. why zap?
-
比较全的日志级别
-
支持结构化日志
-
性能
2. 简单使用
go get -u go.uber.org/zap
Sugared LoggerLogger
func main() {
// sugared
sugar := zap.NewExample().Sugar()
sugar.Infof("hello! name:%s,age:%d", "xiaomin", 20) // printf 风格,易用性
// logger
logger := zap.NewExample()
logger.Info("hello!", zap.String("name", "xiaomin"), zap.Int("age", 20)) // 强调性能
}
// output
{"level":"info","msg":"hello! name:xiaomin,age:20"}
{"level":"info","msg":"hello!","name":"xiaomin","age":20}
example,development,production
func main() {
// example
logger := zap.NewExample()
logger.Info("example")
// Development
logger,_ = zap.NewDevelopment()
logger.Info("Development")
// Production
logger,_ = zap.NewProduction()
logger.Info("Production")
}
可以看出:日志等级,日志输出格式,默认字段都有所差异。
也可以自定义 logger,如下:
func main() {
encoder := getEncoder()
sync := getWriteSync()
core := zapcore.NewCore(encoder, sync, zapcore.InfoLevel)
logger := zap.New(core)
logger.Info("info 日志",zap.Int("line", 1))
logger.Error("info 日志", zap.Int("line", 2))
}
// 负责设置 encoding 的日志格式
func getEncoder() zapcore.Encoder {
return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}
// 负责日志写入的位置
func getWriteSync() zapcore.WriteSyncer {
file, _ := os.OpenFile("./log.txt", os.O_CREATE|os.O_APPEND|os.O_RDWR, os.ModePerm)
syncFile := zapcore.AddSync(file)
syncConsole := zapcore.AddSync(os.Stderr)
return zapcore.NewMultiWriteSyncer(syncConsole, syncFile)
}
运行结果:
// output
// 创建 log.txt,追加日志
// console 打印日志
//{"level":"info","ts":1636471657.16419,"msg":"info 日志","line":1}
//{"level":"error","ts":1636471657.1643898,"msg":"info 日志","line":2}
New(core zapcore.Core, options ...Option) *Loggerzapcore.Core
NewCore(enc Encoder, ws WriteSyncer, enab LevelEnabler) CoreEncoderws WriteSyncerLevelEnabler
3. 自定义logger例子
./util/zap.go
package util
import (
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type LogConfig struct {
Level string `json:"level"` // Level 最低日志等级,DEBUG<INFO<WARN<ERROR<FATAL 例如:info-->收集info等级以上的日志
FileName string `json:"file_name"` // FileName 日志文件位置
MaxSize int `json:"max_size"` // MaxSize 进行切割之前,日志文件的最大大小(MB为单位),默认为100MB
MaxAge int `json:"max_age"` // MaxAge 是根据文件名中编码的时间戳保留旧日志文件的最大天数。
MaxBackups int `json:"max_backups"` // MaxBackups 是要保留的旧日志文件的最大数量。默认是保留所有旧的日志文件(尽管 MaxAge 可能仍会导致它们被删除。)
}
var logger *zap.Logger
// 负责设置 encoding 的日志格式
func getEncoder() zapcore.Encoder {
// 获取一个指定的的EncoderConfig,进行自定义
encodeConfig := zap.NewProductionEncoderConfig()
// 设置每个日志条目使用的键。如果有任何键为空,则省略该条目的部分。
// 序列化时间。eg: 2022-09-01T19:11:35.921+0800
encodeConfig.EncodeTime = zapcore.ISO8601TimeEncoder
// "time":"2022-09-01T19:11:35.921+0800"
encodeConfig.TimeKey = "time"
// 将Level序列化为全大写字符串。例如,将info level序列化为INFO。
encodeConfig.EncodeLevel = zapcore.CapitalLevelEncoder
// 以 package/file:行 的格式 序列化调用程序,从完整路径中删除除最后一个目录外的所有目录。
encodeConfig.EncodeCaller = zapcore.ShortCallerEncoder
return zapcore.NewJSONEncoder(encodeConfig)
}
// 负责日志写入的位置
func getLogWriter(filename string, maxsize, maxBackup, maxAge int) zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: filename, // 文件位置
MaxSize: maxsize, // 进行切割之前,日志文件的最大大小(MB为单位)
MaxAge: maxAge, // 保留旧文件的最大天数
MaxBackups: maxBackup, // 保留旧文件的最大个数
Compress: false, // 是否压缩/归档旧文件
}
// AddSync 将 io.Writer 转换为 WriteSyncer。
// 它试图变得智能:如果 io.Writer 的具体类型实现了 WriteSyncer,我们将使用现有的 Sync 方法。
// 如果没有,我们将添加一个无操作同步。
return zapcore.AddSync(lumberJackLogger)
}
// InitLogger 初始化Logger
func InitLogger(lCfg LogConfig) (err error) {
// 获取日志写入位置
writeSyncer := getLogWriter(lCfg.FileName, lCfg.MaxSize, lCfg.MaxBackups, lCfg.MaxAge)
// 获取日志编码格式
encoder := getEncoder()
// 获取日志最低等级,即>=该等级,才会被写入。
var l = new(zapcore.Level)
err = l.UnmarshalText([]byte(lCfg.Level))
if err != nil {
return
}
// 创建一个将日志写入 WriteSyncer 的核心。
core := zapcore.NewCore(encoder, writeSyncer, l)
logger = zap.New(core, zap.AddCaller())
// 替换zap包中全局的logger实例,后续在其他包中只需使用zap.L()调用即可
zap.ReplaceGlobals(logger)
return
}
encodeConfig := zap.NewProductionEncoderConfig()
// 打印格式: {"level":"info","ts":1662032576.6267354,"msg":"test","line":1}
encodeConfig.EncodeTime = zapcore.ISO8601TimeEncoder
// 打印格式:{"level":"info","ts":"2022-09-01T19:43:07.178+0800","msg":"test","line":1}
encodeConfig.TimeKey = "time"
// 打印格式:{"level":"info","time":"2022-09-01T19:43:20.558+0800","msg":"test","line":1}
encodeConfig.EncodeLevel = zapcore.CapitalLevelEncoder
// 打印格式:{"level":"INFO","time":"2022-09-01T19:43:41.192+0800","msg":"test","line":1}
encodeConfig.EncodeCaller = zapcore.ShortCallerEncoder
// 打印格式:{"level":"INFO","time":"2022-09-01T19:41:39.819+0800","caller":"test/test.go:20","msg":"test","line":1}
// 这个需要注意,是要结合 logger := zap.New(core, zap.AddCaller()),一起使用的
syncFile := zapcore.AddSync(lumberJackLogger) // 打印到文件
syncConsole := zapcore.AddSync(os.Stderr) // 打印到控制台
return zapcore.NewMultiWriteSyncer(syncFile, syncConsole)
type Level int8
const (
DebugLevel Level = iota - 1
InfoLevel
WarnLevel
...
)
{"level":"INFO","time":"2022-09-01T19:41:39.819+0800","caller":"test/test.go:20","msg":"test","line":1}
测试:
main.go
package main
import (
"fmt"
"ginstudy02/util"
"net/http"
"go.uber.org/zap"
)
func main() {
lc := util.LogConfig{
Level: "debug",
FileName: fmt.Sprintf("./log/%v.log", time.Now().Unix()),
MaxSize: 1,
MaxBackups: 5,
MaxAge: 30,
}
err := util.InitLogger(lc)
if err != nil {
fmt.Println(err)
}
// L():获取全局logger
logger := zap.L()
// 调用内核的Sync方法,刷新所有缓冲的日志条目。
// 应用程序应该注意在退出之前调用Sync。
defer logger.Sync()
simpleHttpGet(logger, "www.sogo.com")
simpleHttpGet(logger, "http://www.sogo.com")
}
func simpleHttpGet(logger *zap.Logger, url string) {
sugarLogger := logger.Sugar()
sugarLogger.Debugf("Trying to hit GET request for %s", url)
resp, err := http.Get(url)
if err != nil {
sugarLogger.Errorf("Error fetching URL %s : Error = %s", url, err)
} else {
sugarLogger.Infof("Success! statusCode = %s for URL %s", resp.Status, url)
resp.Body.Close()
}
}
执行结果:
./log/1662027710.log
{"level":"DEBUG","time":"2022-09-01T17:46:58.378+0800","caller":"ginstudy02/main.go:53","msg":"Trying to hit GET request for www.sogo.com"}
{"level":"ERROR","time":"2022-09-01T17:46:58.393+0800","caller":"ginstudy02/main.go:56","msg":"Error fetching URL www.sogo.com : Error = Get \"www.sogo.com\": unsupported protocol scheme \"\""}
{"level":"DEBUG","time":"2022-09-01T17:46:58.393+0800","caller":"ginstudy02/main.go:53","msg":"Trying to hit GET request for http://www.sogo.com"}
{"level":"INFO","time":"2022-09-01T17:46:58.681+0800","caller":"ginstudy02/main.go:58","msg":"Success! statusCode = 200 OK for URL http://www.sogo.com"}
getEncoder()JSON EncoderNewJSONEncoder()NewConsoleEncoder()
//return zapcore.NewJSONEncoder(encodeConfig)
return zapcore.NewConsoleEncoder(encodeConfig)
再次运行:
2022-09-01T17:53:09.870+0800 DEBUG ginstudy02/main.go:53 Trying to hit GET request for www.sogo.com
2022-09-01T17:53:09.887+0800 ERROR ginstudy02/main.go:56 Error fetching URL www.sogo.com : Error = Get "www.sogo.com": unsupported protocol scheme ""
2022-09-01T17:53:09.887+0800 DEBUG ginstudy02/main.go:53 Trying to hit GET request for http://www.sogo.com
2022-09-01T17:53:10.145+0800 INFO ginstudy02/main.go:58 Success! statusCode = 200 OK for URL http://www.sogo.com
4. Gin项目使用zap
util/zap.go
在上面的代码的基础上,添加下面两个中间件:
// GinLogger 接收gin框架默认的日志
func GinLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path // 请求路径 eg: /test
query := c.Request.URL.RawQuery //query类型的请求参数:?name=1&password=2
// 挂起当前中间件,执行下一个中间件
c.Next()
cost := time.Since(start)
// Field 是 Field 的别名。给这个类型起别名极大地提高了这个包的 API 文档的可导航性。
// type Field struct {
// Key string
// Type FieldType // 类型,数字对应具体类型,eg: 15--->string
// Integer int64
// String string
// Interface interface{}
//}
logger.Info(path,
zap.Int("status", c.Writer.Status()), // 状态码 eg: 200
zap.String("method", c.Request.Method), // 请求方法类型 eg: GET
zap.String("path", path), // 请求路径 eg: /test
zap.String("query", query), // 请求参数 eg: name=1&password=2
zap.String("ip", c.ClientIP()), // 返回真实的客户端IP eg: ::1(这个就是本机IP,ipv6地址)
zap.String("user-agent", c.Request.UserAgent()), // 返回客户端的用户代理。 eg: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36
zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()), // 返回Errors 切片中ErrorTypePrivate类型的错误
zap.Duration("cost", cost), // 返回花费时间
)
}
}
// GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志
func GinRecovery(stack bool) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); interface{}(err) != nil {
// 检查断开的连接,因为它不是保证紧急堆栈跟踪的真正条件。
var brokenPipe bool
// OpError 是 net 包中的函数通常返回的错误类型。它描述了错误的操作、网络类型和地址。
if ne, ok := interface{}(err).(*net.OpError); ok {
// SyscallError 记录来自特定系统调用的错误。
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") {
brokenPipe = true
}
}
}
// DumpRequest 以 HTTP/1.x 连线形式返回给定的请求
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
logger.Error(c.Request.URL.Path,
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
// 如果连接死了,我们就不能给它写状态
c.Error(interface{}(err).(error))
c.Abort() // 终止该中间件
return
}
if stack {
logger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
zap.String("starck", string(debug.Stack())), // 返回调用它的goroutine的格式化堆栈跟踪。
)
} else {
logger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
}
// 调用 `Abort()` 并使用指定的状态代码写入标头。
// StatusInternalServerError:500
c.AbortWithStatus(http.StatusInternalServerError)
}
}()
c.Next()
}
}
测试:
./main.go
func main() {
r := gin.New()
r.Use(util.GinLogger(), util.GinRecovery(false))
r.GET("./test", func(c *gin.Context) {
lc := util.LogConfig{
Level: "debug",
FileName: fmt.Sprintf("./log/%v.log", time.Now().Unix()),
MaxSize: 1,
MaxBackups: 5,
MaxAge: 30,
}
err := util.InitLogger(lc)
if err != nil {
fmt.Println(err)
}
logger := zap.L().Sugar()
// 调用内核的Sync方法,刷新所有缓冲的日志条目。应用程序应该注意在退出之前调用Sync。
defer logger.Sync()
})
r.Run()
}
请求地址:http://localhost:8080/test
测试结果:
./log/test.log
{"level":"INFO","time":"2022-09-01T18:11:47.600+0800","caller":"util/zap.go:105","msg":"/test","status":200,"method":"GET","path":"/test","query":"","ip":"::1","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36","errors":"","cost":0.0001746}
6. lumberjack 日志切割组件
Golang 语言标准库的 log 包和 zap 日志库 不支持日志切割,然而如果我们业务每天产生海量日志,日志文件就会越来越大,甚至会触发磁盘空间不足的报警,此时如果我们移动或者删除日志文件,需要先将业务停止写日志,很不方便。
而且大日志文件也不方便查询,多少有点失去日志的意义。所以实际业务开发中,我们通常会按照日志文件大小或者日期进行日志切割。
lumberjack
lumberjack gopkg.in
go get -u github.com/natefinch/lumberjack
import "gopkg.in/natefinch/lumberjack.v2"
log.SetOutput(&lumberjack.Logger{
Filename: "./log/test.log",
MaxSize: 1, // 单位: MB
MaxBackups: 3,
MaxAge: 28, //单位: 天
Compress: true, // 默认情况下禁用
})
func getLogWriter(filename string, maxsize, maxBackup, maxAge int) zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: filename, // 文件位置
MaxSize: maxsize, // 进行切割之前,日志文件的最大大小(MB为单位)
MaxAge: maxAge, // 保留旧文件的最大天数
MaxBackups: maxBackup, // 保留旧文件的最大个数
Compress: false, // 是否压缩/归档旧文件
}
// AddSync 将 io.Writer 转换为 WriteSyncer。
// 它试图变得智能:如果 io.Writer 的具体类型实现了 WriteSyncer,我们将使用现有的 Sync 方法。
// 如果没有,我们将添加一个无操作同步。
return zapcore.AddSync(lumberJackLogger)
}
type Logger struct {
// Filename 写入日志的文件。备份的日志文件将保留在同一目录下。
// 如果为空,则在os.TempDir()中使用-lumberjack.log。
Filename string `json:"filename" yaml:"filename"`
// MaxSize 是日志文件在轮换之前的最大大小(以 MB 为单位)。默认为 100 兆字节。
MaxSize int `json:"maxsize" yaml:"maxsize"`
// MaxAge 是根据文件名中编码的时间戳保留旧日志文件的最大天数。
// 请注意,一天被定义为 24 小时,由于夏令时、闰秒等原因,可能与日历日不完全对应。
// 默认情况下不会根据年龄删除旧日志文件。
MaxAge int `json:"maxage" yaml:"maxage"`
// MaxBackups 是要保留的旧日志文件的最大数量。
// 默认是保留所有旧的日志文件(尽管 MaxAge 可能仍会导致它们被删除。)
MaxBackups int `json:"maxbackups" yaml:"maxbackups"`
// LocalTime 确定用于格式化备份文件中时间戳的时间是否是计算机的本地时间。默认是使用 UTC 时间。
LocalTime bool `json:"localtime" yaml:"localtime"`
// Compress 确定是否应使用 gzip 压缩旋转的日志文件。默认是不执行压缩。
Compress bool `json:"compress" yaml:"compress"`
...
}