Go日志门面的设计与实现-自动注入追踪ID标记代码位置、简化日志操作

科技   2024-10-21 08:51   江苏  

上篇文章《三个实用细节,让Zap在Go项目中变得更好用》我们为项目使用的基础Logger -- Zap 做了初始化,完成了应用日志的多环境配置和文件自动切割,这节我们会自定义项目自己的Logger 门面,让整个日志组件变得更好用。

文章大纲如下:








日志组件好用的标准

好用有两个标准:

  1. 简化程序打日志的操作, 如果打一个日志要写好几行代码,那也有点太过分了。
  2. 生成的日志要好查,怎么叫好查呢
    1. 日志内容好检索
    2. 提供有价值的代码问题定位信息

简化使用:通过咱们的封装要能简化程序打日志这个操作,Zap 原生的打日志方式为

通过我们的自定义Logger 门面会把打日志的操作简化为

logger.Info("loggerFlag""name""张三""age"15)

我们会把它在自定义Logge门面中转换为

zap.Info("loggerFlag", zap.Any("name""张三"), zap.Any("age"15))

从而达到简化程序日志操作的目的。

好查: 一次请求往往会生成多条日志,那么怎么才能快点检索到一个请求中的所有日志呢?这就需要我们在日志中增加追踪参数。

而日志要包含有价值的定位信息,就是通过看日志我能知道这条日志是在程序哪部分打印的,这样才能快速定位到程序中的具体位置,所以我们需要增加一些日志调用者的程序方法名、代码文件和代码行号这些信息。

比如下面这个日志,我们在其中加入了 traceid spanid 这些用于链路追踪的参数,已经日志调用者的信息 func、file、line 这些信息。

{
  "level":"info",
  "ts":"2024-06-01T18:14:17.727+0800",
  "msg":"loggerFlag",
  "name":"张三",
  "age":2,
  "traceid":"e404ec287e430e1a",
  "spanid":"e404ec287e430e1a",
  "pspanid":"",
  "func":"main.main.func3",
  "file":"main.go",
  "line":31
}

我们能通过traceid 检索出一个请求的所有日志,同时通过调用者信息定位到每条日志都是在程序的哪个位置打进去的,这样我们无论是观测应用还是排查应用的问题都会很方便。

下面就带大家自定义项目的Logger ,去一步步实现上面这些功能。

Logger 门面的结构定义和初始化

首先我们在项目的 common/logger 目录中新建logger.go

在logger.go中先定义出我们项目自己Logger门面的结构。

type logger struct {
 ctx     context.Context
 traceId string
 spanId  string
 pSpanId string
 _logger *zap.Logger
}

可以看到我们的Logger门面持有项目全局的 Zap Logger 作为其底层的Logger。而Logger门面中的Ctx 是Gin 框架的gin.Ctx ,其中会保存当前请求的上下文。

这里把Logger 的ctx 字段声明称context.Context类型是为了让它更通用些,未来即使是换到其他Web框架或者是自己写单元测试都能用,不局限于gin.Contxt 这种类型。

剩下的三个字段 traceId、spanId、pSpanId 就是我们要介绍的日志中的追踪信息。

这些追踪参数,未来我们会在请求一进来的时候就把它存放到 gin.Context 中,供我们全局使用。我们在使用Logger门面打日志前,需要先把这几个追踪信息从Context中读取出来存放到Logger的这三个字段中。

func New(ctx context.Context) *logger {
 var traceId, spanId, pSpanId string
 if ctx.Value("traceid") != nil {
  traceId = ctx.Value("traceid").(string)
 }
 if ctx.Value("spanid") != nil {
  spanId = ctx.Value("spanid").(string)
 }
 if ctx.Value("psapnid") != nil {
  pSpanId = ctx.Value("pspanid").(string)
 }

 return &logger{
  ctx:     ctx,
  traceId: traceId,
  spanId:  spanId,
  pSpanId: pSpanId,
  _logger: _logger,
 }
}

这样未来使用它打日志的时候在日志信息里都会带上追踪信息 -- 当然这一步得自己实现,稍后我们就会细讲这部分, 我们先来讲怎么简化程序打印日志的操作。

简化程序日志操作

我们想用

logger.Info("loggerFlag""name""张三""age"15)

替代Zap 原来的打日志方式

zap.Info("loggerFlag", zap.Any("name""张三"), zap.Any("age"15))

那么就得在Logger中封装自己的 Info 方法,因为日志参数第一个固定时 msg 信息,后面的日志信息我们打算以键值对形式打到日志中去,我们就需要键值对在参数中成对提供。

func (l *logger) Info(msg string, kv ...interface{}) {
 
}

那使用起来,肯定有人会疏忽,没有提供成对的参数,所以我们需要在这里做一下兜底然后再去调用Zap进行写日志。

上面我们可以看到,如果kv 参数没有成对,我们自动给他补一个值,这样最起码保证日志写入没问题,键值会错位,但是查日志的时候还是能查到你想在日志中输出的信息的。

封装完Logger 的 Info 方法后,新的问题出现了, 难不成 Debug、Warn、Error 都要把这个逻辑重复实现一遍?我们先看看Zap的Info方法是怎么实现的。

func (log *Logger) Info(msg string, fields ...Field) {
 if ce := log.check(InfoLevel, msg); ce != nil {
  ce.Write(fields...)
 }
}

Zap 写日志前先调用了check方法,判断这个日志级别是否能写入再去写日志的,而且这个check方法我看了一下是有提供外部调用的版本的。

那么我们就按照这个模式,封装一个通用的写日志方法。

再让各日志级别的日志函数去调它就行啦。

func (l *logger) Debug(msg string, kv ...interface{}) {
 l.log(zapcore.DebugLevel, msg, kv...)
}

func (l *logger) Info(msg string, kv ...interface{}) {
 l.log(zapcore.InfoLevel, msg, kv...)
}

func (l *logger) Warn(msg string, kv ...interface{}) {
 l.log(zapcore.WarnLevel, msg, kv...)
}

func (l *logger) Error(msg string, kv ...interface{}) {
 l.log(zapcore.ErrorLevel, msg, kv...)
}

简化日志操作到这里就实现了,我们等把追踪信息和程序定位信息都放到日志里后再去统一测试成果。

给日志增加追踪信息

让日志门面支持链路追踪、代码位置标记以及如何为自己的项目封装日志门面,这三部分的内容在专栏中已经更新,并有的配套代码版本为大家详细记录代码实现过程

扫下方海报二维码订阅专栏,即可阅读完整版,也有专属的读者群,欢迎加入一起学习专栏分为五大部分,主要内容架构如下:

  • 第一部分介绍让框架变得好用的诸多实战技巧,比如通过自定义日志门面让项目日志更简单易用、支持自动记录请求的追踪信息和程序位置信息、通过自定义Error在实现Go error接口的同时支持给给错误添加错误链,方便追溯错误源头。
  • 第二部分:讲解项目分层架构的设计和划分业务模块的方法和标准,让你以后无论遇到什么项目都能按这套标准自己划分出模块和逻辑分层。后面几个部分均是该部分所讲内容的实践。
  • 第三部分:设计实现一个套支持多平台登录,Token泄露检测、同平台多设备登录互踢功能的用户认证体系,这套用户认证体系既可以在你未来开发产品时直接应用
  • 第四部分:商城app C端接口功能的实现,强化分层架构实现的讲解,这里还会讲解用责任链、策略和模版等设计模式去解决订单结算促销、支付方式支付场景等多种多样的实际问题。
  • 第五部分:单元测试、项目Docker镜像、K8s部署和服务保障相关的一些基础内容和注意事项

扫描上方的海报二维码或者访问 https://xiaobot.net/p/golang,

点击阅读原文可跳转。

网管叨bi叨
分享软件开发和系统架构设计基础、Go 语言和Kubernetes。
 最新文章