后面两篇曾经为大家介绍了golang中的日志如何应用,并在诸多日志框架库中抉择了zap作为咱们的日志框架,本篇将会解说:
- 如何联合当下支流的Web框架gin进行申请日志的打印
- 对zap进行二次封装,注入trace信息,一遍咱们能够在业务中查问一次申请的所有残缺日志
这里是前两篇的链接:
- https://www.yuque.com/jinsesi...
- https://www.yuque.com/jinsesi...
1、gin 默认的中间件
首先咱们来看一个最简略的 gin 我的项目:
func main() { r := gin.Default() r.GET("/hello", func(c *gin.Context) { c.String("hello jianfan.com!") }) r.Run(}
接下来咱们看一下gin.Default()
的源码:
func Default() *Engine { debugPrintWARNINGDefault() engine := New() engine.Use(Logger(), Recovery()) return engine}
也就是咱们在应用gin.Default()
的同时是用到了 gin 框架内的两个默认中间件Logger()
和Recovery()
。
其中Logger()
是把 gin 框架自身的日志输入到规范输入(咱们本地开发调试时在终端输入的那些日志就是它的功绩),而Recovery()
是在程序呈现 panic 的时候复原现场并写入 500 响应的。
2、基于 zap 的中间件
gin框架反对用户自定义的middleware,咱们能够模拟Logger()
和Recovery()
的实现,应用咱们的日志库来接管 gin 框架默认输入的日志。
这里以 zap 为例,咱们实现两个中间件如下:
// GinLogger 接管gin框架默认的日志func GinLogger(logger *zap.Logger) 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) logger.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掉我的项目可能呈现的panicfunc GinRecovery(logger *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 { logger.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 { logger.Error("[Recovery from panic]", zap.Any("error", err), zap.String("request", string(httpRequest)), zap.String("stack", string(debug.Stack())), ) } else { logger.Error("[Recovery from panic]", zap.Any("error", err), zap.String("request", string(httpRequest)), ) } c.AbortWithStatus(http.StatusInternalServerError) } }() c.Next() }}
如果不想本人实现,能够应用 github 上有他人封装好的 https://github.com/gin-contrib/zap。
这样咱们就能够在 gin 框架中应用咱们下面定义好的两个中间件来代替 gin 框架默认的Logger()
和Recovery()
了。
r := gin.New()r.Use(GinLogger(), GinRecovery())
3、减少Trace信息
3.1 定义trace信息
// 定义trace构造体type Trace struct { TraceId string `json:"trace_id"` SpanId string `json:"span_id"` Caller string `json:"caller"` SrcMethod *string `json:"srcMethod,omitempty"` UserId int `json:"user_id"`}// 依据gin的context获取context,使log trace更加通用func GetTraceCtx(c *gin.Context) context.Context { return c.MustGet(consts.TraceCtx).(context.Context)}
3.2 包装基于zap的日志工具
- 包装办法将context传入
解析trace信息,打印到logger中
package logimport ( "best-practics/common" "best-practics/common/consts" "context" "go.uber.org/zap")type LogWrapper struct { logger *zap.Logger}var Log LogWrapperfunc Debug(tag string, fields ...zap.Field) { Log.logger.Debug(tag, fields...)}func DebugF(ctx context.Context, tag string, fields ...zap.Field) { trace := ctx.Value(consts.TraceKey).(*common.Trace) Log.logger.Debug(tag, append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))..., )}func Info(tag string, fields ...zap.Field) { Log.logger.Info(tag, fields...)}func InfoF(ctx context.Context, tag string, fields ...zap.Field) { trace := ctx.Value(consts.TraceKey).(*common.Trace) Log.logger.Info(tag, append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))..., )}func Warn(tag string, fields ...zap.Field) { Log.logger.Warn(tag, fields...)}func WarnF(ctx context.Context, tag string, fields ...zap.Field) { trace := ctx.Value(consts.TraceKey).(*common.Trace) Log.logger.Warn(tag, append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))..., )}func Error(tag string, fields ...zap.Field) { Log.logger.Error(tag, fields...)}func ErrorF(ctx context.Context, tag string, fields ...zap.Field) { trace := ctx.Value(consts.TraceKey).(*common.Trace) Log.logger.Error(tag, append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))..., )}func Fatal(tag string, fields ...zap.Field) { Log.logger.Fatal(tag, fields...)}func FatalF(ctx context.Context, tag string, fields ...zap.Field) { trace := ctx.Value(consts.TraceKey).(*common.Trace) Log.logger.Fatal(tag, append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))..., )}
3.3 解决Caller问题
此形式存在个问题,就是打印的caller信息全都是对应日志工具类的代码行,而不是调用处。看了下zap的源码,zap打印时将整个调用的stack链路会寄存到内存中,默认打印调用处的caller信息。所以为了解决解决这个问题,须要再初始化zap时额定减少AddCallerSkip跳过指定层级的caller,外围代码如下:
logger = logger.WithOptions(zap.AddCaller(),zap.AddCallerSkip(1))
残缺的zap工具类如下:
/* * Copyright (C) 2021 Baidu, Inc. All Rights Reserved. */package logimport ( "best-practics/common" "best-practics/utils" "fmt" "os" "time" rotatelogs "github.com/lestrrat-go/file-rotatelogs" "go.uber.org/zap" "go.uber.org/zap/zapcore")func InitZap() { if ok, _ := utils.PathExists(common.GlobalConfig.Zap.Director); !ok { // 判断是否有Director文件夹 fmt.Printf("create %v directory\n", common.GlobalConfig.Zap.Director) _ = os.Mkdir(common.GlobalConfig.Zap.Director, os.ModePerm) } // 调试级别 debugPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool { return lev == zap.DebugLevel }) // 日志级别 infoPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool { return lev == zap.InfoLevel }) // 正告级别 warnPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool { return lev == zap.WarnLevel }) // 谬误级别 errorPriority := zap.LevelEnablerFunc(func(lev zapcore.Level) bool { return lev >= zap.ErrorLevel }) cores := [...]zapcore.Core{ getEncoderCore(fmt.Sprintf("./%s/server_debug.log", common.GlobalConfig.Zap.Director), debugPriority), getEncoderCore(fmt.Sprintf("./%s/server_info.log", common.GlobalConfig.Zap.Director), infoPriority), getEncoderCore(fmt.Sprintf("./%s/server_warn.log", common.GlobalConfig.Zap.Director), warnPriority), getEncoderCore(fmt.Sprintf("./%s/server_error.log", common.GlobalConfig.Zap.Director), errorPriority), } logger := zap.New(zapcore.NewTee(cores[:]...), zap.AddCaller(), zap.AddCallerSkip(1)) common.Logger = logger}// getEncoderConfig 获取zapcore.EncoderConfigfunc getEncoderConfig() (config zapcore.EncoderConfig) { config = zapcore.EncoderConfig{ MessageKey: "message", LevelKey: "level", TimeKey: "time", NameKey: "logger", CallerKey: "caller", StacktraceKey: common.GlobalConfig.Zap.StacktraceKey, LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.LowercaseLevelEncoder, EncodeTime: CustomTimeEncoder, EncodeDuration: zapcore.SecondsDurationEncoder, EncodeCaller: zapcore.FullCallerEncoder, } switch { case common.GlobalConfig.Zap.EncodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认) config.EncodeLevel = zapcore.LowercaseLevelEncoder case common.GlobalConfig.Zap.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带色彩 config.EncodeLevel = zapcore.LowercaseColorLevelEncoder case common.GlobalConfig.Zap.EncodeLevel == "CapitalLevelEncoder": // 大写编码器 config.EncodeLevel = zapcore.CapitalLevelEncoder case common.GlobalConfig.Zap.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带色彩 config.EncodeLevel = zapcore.CapitalColorLevelEncoder default: config.EncodeLevel = zapcore.LowercaseLevelEncoder } return config}// getEncoder 获取zapcore.Encoderfunc getEncoder() zapcore.Encoder { if common.GlobalConfig.Zap.Format == "json" { return zapcore.NewJSONEncoder(getEncoderConfig()) } return zapcore.NewConsoleEncoder(getEncoderConfig())}// getEncoderCore 获取Encoder的zapcore.Corefunc getEncoderCore(fileName string, level zapcore.LevelEnabler) (core zapcore.Core) { writer := GetWriteSyncer(fileName) // 应用file-rotatelogs进行日志宰割 return zapcore.NewCore(getEncoder(), writer, level)}// 自定义日志输入工夫格局func CustomTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { enc.AppendString(t.Format("2006/01/02 15:04:05"))}//@function: GetWriteSyncer//@description: zap logger中退出file-rotatelogs//@return: zapcore.WriteSyncer, errorfunc GetWriteSyncer(file string) zapcore.WriteSyncer { // 每小时一个文件 logf, _ := rotatelogs.New(file +".%Y%m%d%H", rotatelogs.WithLinkName(file), rotatelogs.WithMaxAge(7*24*time.Hour), rotatelogs.WithRotationTime(time.Minute), ) if common.GlobalConfig.Zap.LogInConsole { return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(logf)) } return zapcore.AddSync(logf)}
3.4 Gin的middleware中减少trace信息
func SetLoggerMiddleware() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() uuidStr := strings.ReplaceAll(uuid.New().String(), "-", "") path := c.Request.URL.Path userId := c.GetInt("user_id") ctx := context.WithValue(context.Background(), consts.TraceKey, &common.Trace{TraceId: uuidStr, Caller: path, UserId: userId}) c.Set(consts.TraceCtx,ctx) c.Next() cost := time.Since(start) common.Logger.Info("_com_request_info", zap.Int("Status", c.Writer.Status()), zap.String("Method", c.Request.Method), zap.String("IP",c.ClientIP()), zap.String("Path",path), zap.String("TraceId", uuidStr), zap.Int("UserId", userId), zap.String("query", c.Request.URL.RawQuery), zap.String("UserAgent",c.Request.UserAgent()), zap.Duration("Cost",cost), ) }}
3.5 Controller层生成trace_context
func main() { r := gin.Default() r.GET("/hello", func(c *gin.Context) { // 3.1中封装的转换方法 ctx := common.GetTraceCtx(c) log.WarnF(ctx,"BookNotFoundError",zap.String("test","666")) c.String("hello jianfan.com!") }) r.Run(}
至此咱们就实现了在一个gin我的项目中接入zap作为日志框架,并可能打印残缺的链路日志信息。联合前两节咱们曾经满足了对常见日志框架的需要:
- 良好日志写入性能
- 反对不同的日志级别。并且可拆散成多个日志文件
- 多输入 - 同时反对规范输入,文件等
- 可能打印根本信息,如调用文件 / 函数名和行号,日志工夫等
- 可读性与结构化,Json格局或有分隔符,不便后续的日志采集、监控等
- 文件切割,可按小时、天进行日志拆分,或者按文件大小
- 日志书写敌对,反对通过context主动log trace等
- 文件定时删除
- 开源性,与其余开源框架反对较好
参考资料:
- https://liwenzhou.com/posts/Go/use_zap_in_gin/
咱们下期见,Peace
我是简凡,一个励志用最简略的语言,形容最简单问题的新时代农民工。求点赞,求关注,如果你对此篇文章有什么纳闷,欢送在我的微信公众号中留言,我还能够为你提供以下帮忙:
- 帮忙建设本人的常识体系
- 互联网实在高并发场景实战解说
- 不定期分享Golang、Java相干业内的经典场景实际
我的博客:https://besthpt.github.io/
我的微信:bestbear666
微信公众号: