日志对于项目重要性不言而喻,如果用过Gin框架大家都知道,Gin框架中自带日志logger;可以文件和控制台输出,但性能和功能远不如Zap。
下面简单写个例子
func main() {
r := gin.New() // 创建一个不包含中间件的路由器
r.Use(gin.Logger(), gin.Recovery()) // 使用 Logger 中间件、Recovery 中间件
r.GET("/hello", func(c *gin.Context) { // 路由添加
c.String(200, "hello!")
})
r.Run(":9090")
}
浏览器访问http://127.0.0.1:9090/hello,控制台会输出一下日志内容:
我在Gin框架下创建了一个pkg目录,创建了一个glog包,包含三个文件zap.go、sugar.go、logger.go。里面涉及的global的配置大家可以下载项目看看(https://gitee.com/tfc2016/gino)
zap.go
package glog
import (
"gino/global"
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"time"
)
var (
logger *zap.Logger
sugar *zap.SugaredLogger
Logger *zap.Logger
)
func NewZapLogger() {
writeSyncer := getLogWriter()
encoder := getEncoder()
core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)
// zap打印时将整个调用的stack链路会存放到内存中,默认打印调用处的caller信息。所以需要再初始化zap时额外增加AddCallerSkip跳过指定层级的caller
logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
defer logger.Sync()
Logger = logger
sugar = Logger.Sugar()
}
// getEncoder zapcore.Encoder
func getEncoder() zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = CustomTimeEncoder
if global.Config.Log.StacktraceKey != "" {
encoderConfig.StacktraceKey = global.Config.Log.StacktraceKey
}
if global.Config.Log.Format == "json" {
return zapcore.NewJSONEncoder(encoderConfig)
}
return zapcore.NewConsoleEncoder(encoderConfig)
}
func getLogWriter() zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: global.Config.Log.Path + "/" + global.Config.Log.Filename, // 日志文件位置
MaxSize: global.Config.Log.MaxSize, // 进行切割之前,日志文件的最大大小(MB为单位)
MaxBackups: global.Config.Log.MaxBackups, // 保留旧文件的最大个数
MaxAge: global.Config.Log.MaxAge, // 保留旧文件的最大天数
Compress: global.Config.Log.Compress, // 是否压缩/归档旧文件
}
return zapcore.AddSync(lumberJackLogger)
}
// CustomTimeEncoder 自定义日志输出时间格式
func CustomTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05.000"))
}
sugar.go
package glog
// Debug uses fmt.Sprint to construct and log a message.
func Debug(args ...interface{}) {
sugar.Debug(args)
}
// Info uses fmt.Sprint to construct and log a message.
func Info(args ...interface{}) {
sugar.Info(args)
}
// Warn uses fmt.Sprint to construct and log a message.
func Warn(args ...interface{}) {
sugar.Warn(args)
}
// Error uses fmt.Sprint to construct and log a message.
func Error(args ...interface{}) {
sugar.Error(args)
}
// DPanic uses fmt.Sprint to construct and log a message. In development, the
// logger then panics. (See DPanicLevel for details.)
func DPanic(args ...interface{}) {
sugar.DPanic(args)
}
// Panic uses fmt.Sprint to construct and log a message, then panics.
func Panic(args ...interface{}) {
sugar.Panic(args)
}
// Fatal uses fmt.Sprint to construct and log a message, then calls os.Exit.
func Fatal(args ...interface{}) {
sugar.Fatal(args)
}
// Debugf uses fmt.Sprintf to log a templated message.
func Debugf(template string, args ...interface{}) {
sugar.Debugf(template, args)
}
// Infof uses fmt.Sprintf to log a templated message.
func Infof(template string, args ...interface{}) {
sugar.Infof(template, args, nil)
}
// Warnf uses fmt.Sprintf to log a templated message.
func Warnf(template string, args ...interface{}) {
sugar.Warnf(template, args, nil)
}
// Errorf uses fmt.Sprintf to log a templated message.
func Errorf(template string, args ...interface{}) {
sugar.Errorf(template, args, nil)
}
// DPanicf uses fmt.Sprintf to log a templated message. In development, the
// logger then panics. (See DPanicLevel for details.)
func DPanicf(template string, args ...interface{}) {
sugar.DPanicf(template, args, nil)
}
// Panicf uses fmt.Sprintf to log a templated message, then panics.
func Panicf(template string, args ...interface{}) {
sugar.Panicf(template, args, nil)
}
// Fatalf uses fmt.Sprintf to log a templated message, then calls os.Exit.
func Fatalf(template string, args ...interface{}) {
sugar.Fatalf(template, args, nil)
}
logger.go(此文件主要是Gorm输出日志采用Zap)
package glog
import (
"go.uber.org/zap"
)
func ZapDebug(msg string, fields ...zap.Field) {
logger.Debug(msg, fields...)
}
func ZapInfo(msg string, fields ...zap.Field) {
logger.Info(msg, fields...)
}
func ZapWarn(msg string, fields ...zap.Field) {
logger.Warn(msg, fields...)
}
func ZapError(msg string, fields ...zap.Field) {
logger.Error(msg, fields...)
}
func ZapDPanic(msg string, fields ...zap.Field) {
logger.DPanic(msg, fields...)
}
func ZapPanic(msg string, fields ...zap.Field) {
logger.Panic(msg, fields...)
}
func ZapFatal(msg string, fields ...zap.Field) {
logger.Fatal(msg, fields...)
}
package middleware
import (
"bytes"
"github.com/gin-gonic/gin"
jsoniter "github.com/json-iterator/go"
"go.uber.org/zap"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"time"
)
// ZapLogger 接收gin框架默认的日志
func ZapLogger(lg *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
post := ""
if c.Request.Method == "POST" {
// 把request的内容读取出来
bodyBytes, _ := ioutil.ReadAll(c.Request.Body)
c.Request.Body.Close()
// 把刚刚读出来的再写进去
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
switch c.ContentType() {
case "application/json":
var result map[string]interface{}
d := jsoniter.NewDecoder(bytes.NewReader(bodyBytes))
d.UseNumber()
if err := d.Decode(&result); err == nil {
bt, _ := jsoniter.Marshal(result)
post = string(bt)
}
default:
post = string(bodyBytes)
}
}
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("post", post),
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),
)
}
}
// ZapRecovery recover项目可能出现的panic,并使用zap记录相关日志
func ZapRecovery(lg *zap.Logger, 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: err check
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()
}
}
使用 Use(middleware.ZapLogger(glog.Logger), middleware.ZapRecovery(glog.Logger, true)) 替换默认的Logger()、Recovery()的中间件,运行项目中注册接口,会看到,日志文件中输出记录
18:21:24.044","caller":"[email protected]/context.go:168","msg":"/api/v1/register","status":400,"method":"POST","path":"/api/v1/register","query":"","post":"{\"username\":\"ceshi\",\"password\":\"123456\"}","ip":"127.0.0.1","user-agent":"PostmanRuntime/7.29.0","errors":"","cost":0.0129476}