这是我参与「第四届青训营 」笔记创作活动的第8天。
前言
虽然我本次参加的是青训营前端专场的,但是想要做一个完整的项目或者demo,前端始终需要后端做逻辑、存数据等。这次我也是采用了 go 语言写后端。现在简单介绍一下使用 zap 的使用。
安装
运行下面的命令安装zap
go get -u go.uber.org/zap
实现 logger.go 文件
先在 logger 下新建一个文件
具体内容如下:
package Logger
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"server/Config"
"strings"
"time"
)
var lg *zap.Logger
// InitLogger 初始化Logger
func InitLogger(cfg *Config.LogConfig) (err error) {
writeSyncer := getLogWriter(cfg.Filename, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)
encoder := getEncoder()
var l = new(zapcore.Level)
err = l.UnmarshalText([]byte(cfg.Level))
if err != nil {
return
}
core := zapcore.NewCore(encoder, writeSyncer, l)
lg = zap.New(core, zap.AddCaller())
zap.ReplaceGlobals(lg) // 替换zap包中全局的logger实例,后续在其他包中只需使用zap.L()调用即可
return
}
func getEncoder() zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.TimeKey = "time"
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
return zapcore.NewJSONEncoder(encoderConfig)
}
func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: filename,
MaxSize: maxSize,
MaxBackups: maxBackup,
MaxAge: maxAge,
}
return zapcore.AddSync(lumberJackLogger)
}
// GinLogger 接收gin框架默认的日志
func GinLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
cost := time.Since(start)
lg.Info(path,
zap.Int("status", c.Writer.Status()),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", c.ClientIP()),
zap.String("user-agent", c.Request.UserAgent()),
zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
zap.Duration("cost", cost),
)
}
}
// GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志
func GinRecovery(stack bool) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
lg.Error(c.Request.URL.Path,
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) // nolint: errcheck
c.Abort()
return
}
if stack {
lg.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
zap.String("stack", string(debug.Stack())),
)
} else {
lg.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
}
c.AbortWithStatus(http.StatusInternalServerError)
}
}()
c.Next()
}
}
配置
新建一个 config.go 用来, 定义日志相关的配置, 具体内容如下:
package Config
import (
"encoding/json"
"io/ioutil"
)
// Config 整个项目的配置
type Config struct {
Mode string `json:"mode"`
Port int `json:"port"`
*LogConfig `json:"log"`
}
// LogConfig 日志配置
type LogConfig struct {
Level string `json:"level"`
Filename string `json:"filename"`
MaxSize int `json:"maxsize"`
MaxAge int `json:"max_age"`
MaxBackups int `json:"max_backups"`
}
// Conf 全局配置变量
var Conf = new(Config)
// Init 初始化配置;从指定文件加载配置文件
func Init(filePath string) error {
b, err := ioutil.ReadFile(filePath)
if err != nil {
return err
}
return json.Unmarshal(b, Conf)
}
config.json
{
"mode": "debug",
"port": 8080,
"log": {
"level": "debug",
"filename": "app.log",
"maxsize": 200,
"max_age": 7,
"max_backups": 10
}
}
可以看到 在 config.json 中 的 filename 为 app.log ,是log 输出文件。
main.go
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"log"
"os"
"server/Config"
"server/Logger"
"server/Router"
"server/pkg/snowflake"
)
func main() {
r := gin.Default()
// load config from config.json
if len(os.Args) < 1 {
return
}
if err := Config.Init(os.Args[1]); err != nil {
panic(err)
}
// init logger
if err := Logger.InitLogger(Config.Conf.LogConfig); err != nil {
fmt.Printf("init logger failed, err:%v\n", err)
return
}
gin.SetMode(Config.Conf.Mode)
Router.Start(r)
// 注册zap相关中间件
r.Use(Logger.GinLogger(), Logger.GinRecovery(true))
err := r.Run()
if err != nil {
log.Println("r.Run() Failed!")
}
}
在项目中先从配置文件加载配置信息,再调用logger.InitLogger(config.Conf.LogConfig)即可完成logger实例的初识化。其中,通过r.Use(logger.GinLogger(), logger.GinRecovery(true))注册我们的中间件来使用zap接收gin框架自身的日志,在项目中需要的地方通过使用zap.L().Xxx()方法来记录自定义日志信息。
使用
比如这里:
使用到了 zap.Error() .
查看官方文档,还有其他的形式。 文档:pkg.go.dev/go.uber.org…
常用的:
[Debug]
func (log *Logger) Debug(msg string, fields ...Field)
Debug logs a message at DebugLevel. The message includes any fields passed at the log site, as well as any fields accumulated on the logger.
[Error]
func (log *Logger) Error(msg string, fields ...Field)
Error logs a message at ErrorLevel. The message includes any fields passed at the log site, as well as any fields accumulated on the logger.
[Fatal]
func (log *Logger) Fatal(msg string, fields ...Field)
Fatal logs a message at FatalLevel. The message includes any fields passed at the log site, as well as any fields accumulated on the logger.
The logger then calls os.Exit(1), even if logging at FatalLevel is disabled.
[Info]
func (log *Logger) Info(msg string, fields ...Field)
Info logs a message at InfoLevel. The message includes any fields passed at the log site, as well as any fields accumulated on the logger.
总结
日志是用来记录,用户操作,系统状态,错误信息等等内容的文件,是一个软件系统的重要组成部分。一个良好的日志规范,对于系统运行状态的分析,以及线上问题的解决具有重大的意义。
- 重要功能日志尽可能的完善。
- 不要随意打印无用的日志,过多无用的日志会增加分析日志的难度。
- 日志要区分等级 如 debug,warn,info,error 等。
- 捕获到未处理错误时最好打印错误堆栈信息