【Go】Go zap 日志模块

1. 日志通用理论

1.1 引入日志的原因

在项目开发过程中,日志的重要性不言而喻,如果没有日志的存在,就会出现以下问题:

  • 无法确认当前系统的状态,出了问题也不知道
  • 出现了问题以后没有办法及时定位排查
  • 难以对日志进行归纳、整理、总结等操作

1.2 日志级别

在大多数的公司规范当中,日志是有不同级别的,常见级别如下:

  • DEBUG 级别:记录一些辅助排查问题的信息,一般在线上不会打印
  • INFO 级别:中性的描述了发生了什么事情。线上一般从这个级别开始打印
  • WARN 级别:系统当中发生了一些不好的事情(偶尔触发可以接收,频繁触发需要关注)
  • ERROR 级别:系统当中出现了一些不应该出现的错误,需要及时处理

除此以外还有一些额外的日志级别:

  • DATA:有些公司用这个级别来处理接收请求与响应的日志
  • FATAL:一般用来表示及其严重的错误,需要立刻手工介入

1.3 日志参考规范

什么时候该打日志?需要打什么级别的日志?这些都没有统一的规范,这时可以参考一个宁滥勿缺原则:如果纠结是否需要打日志,那就打上;如果纠结是否要打更高级别的日志,那就打上更高级别

有些大佬的做法:

  • 统一利用 AOP 机制,记录任何跟第三方交互的请求与响应,包括数据库、缓存、RPC调用,使用 DEBUG 级别
  • 统一利用 AOP 机制,当系统接收请求以及返回响应,使用 INFO 级别日志
  • 怀疑系统出现了某些问题,偶尔触发没有问题但是频繁触发需要关注,使用 WARN 级别日志
  • 当某些条件不应该触发却触发了或者有人攻击系统的时候,需要记录 ERROR 日志

2. zap 快速入门

2.1 Go 接入日志模块

在 Go 项目当中,可以使用 zap 作为日志框架

⭐ zap 项目地址:https://github.com/uber-go/zap

Go 接入 zap 非常简单,只需要按照以下代码替换全局 Logger 对象即可:

go 复制代码
import "go.uber.org/zap"

func InitLogger() {
	logger, err := zap.NewDevelopment()
	if err != nil {
		panic(err)
	}
	// 替换全局Logger对象
	zap.ReplaceGlobals(logger)
}

2.2 webook 日志使用案例

在我们当前这个 webook 项目中,还没有接入日志模块,现在我们演示在该系统当中如何打印 DEBUG、INFO、WARN、ERROR 各种级别的日志

2.2.1 ERROR 级别演示

上图所示代码便是 ERROR 级别日志的案例,因为正常情况下调用验证码服务验证短信是不会出现错误的,因此这里可以需要使用 ERROR 级别,并且我们在这里也可以看出两个细节:

  1. 后端系统是不会也不应该把 err 等错误信息返回给前端的
  2. 对于手机号这类敏感字段,哪怕是在日志中也不应该打印出来
2.2.2 WARN 级别演示

上图所示代码便是 WARN 级别日志的案例,因为正常情况是不可能会触发一分钟内连续发送多个验证码请求的(意味着有人可能会攻击你的系统),因此偶尔出现不需要关注,频繁出现则需要关注

💡 温馨提示:同样这里不应该打印手机号等敏感字段

2.2.3 INFO 级别演示

上图所示代码便是 INFO 级别日志的案例,这次中性的记录了用户首次进行微信扫码登录,开始用户注册的流程

2.2.4 DEBUG 级别演示

上图所示代码便是 DEBUG 级别日志的案例,记录了和第三方短信服务平台交互的日志

2.3 zap 优雅实践

在上述使用案例中,我们都是通过直接使用 zap 内部全局 Logger 对象完成的操作,但是包变量无法实现不同模块日志的相互隔离,因此我们还是希望保持依赖注入的写法:

3. zap 实战

3.1 封装统一日志接口

现在的情况是业务代码与 zap 日志紧密耦合在一起,如果后续需要更换其他的日志框架,就需要同步修改业务代码,因此我们可以抽象出自己的一套统一日志接口

step1:定义统一日志接口 LoggerX

go 复制代码
package logx

type LoggerX interface {
	Debug(msg string, args ...Field)
	Info(msg string, args ...Field)
	Warn(msg string, args ...Field)
	Error(msg string, args ...Field)
}

type Field struct {
	Key   string
	Value any
}

这里我们模拟 zap 的日志定义,参数使用 Field 结构体封装,我们还可以借鉴 zap 提供如 zap.Error、zap.String类似快速创建 Field 的方法

step2:定义 ZapLogger 的结构体实现

go 复制代码
package logx

import "go.uber.org/zap"

type ZapLogger struct {
	logger *zap.Logger
}

func NewZapLogger(logger *zap.Logger) *ZapLogger {
	return &ZapLogger{
		logger: logger,
	}
}

func (z *ZapLogger) toArgs(args []Field) []zap.Field {
	var result = make([]zap.Field, len(args))
	for i, arg := range args {
		result[i] = zap.Any(arg.Key, arg.Value)
	}
	return result
}

func (z *ZapLogger) Debug(msg string, args ...Field) {
	z.logger.Debug(msg, z.toArgs(args)...)
}

func (z *ZapLogger) Info(msg string, args ...Field) {
	z.logger.Info(msg, z.toArgs(args)...)
}

func (z *ZapLogger) Warn(msg string, args ...Field) {
	z.logger.Warn(msg, z.toArgs(args)...)
}

func (z *ZapLogger) Error(msg string, args ...Field) {
	z.logger.Error(msg, z.toArgs(args)...)
}

step3:业务代码改造成使用自定义接口

go 复制代码
type UserHandler struct {
	EmailCompile    *regexp.Regexp
	PasswordCompile *regexp.Regexp
	NicknameCompile *regexp.Regexp
	BirthdayCompile *regexp.Regexp
	AboutMeCompile  *regexp.Regexp
	svc             service.IUserService
	codeSvc         service.CodeService
	jwtHdl          IJwtHandler
	logger          logx.LoggerX
}

3.2 封装请求响应日志中间件

目前我们有一个通用功能需要解决:那就是使用 AOP 机制在系统的入口接收请求以及出口返回响应的地方记录日志,当然 Gin 框架提供的 middleware 机制本质上就是 AOP 的实现方式,即我们可以借助 Gin 的 middleware 实现这个通用日志处理功能:

go 复制代码
package middleware

import (
	"bytes"
	"context"
	"github.com/gin-gonic/gin"
	"io"
	"time"
)

type LogMiddlewareBuilder struct {
	allowReqBody  bool                                        // 是否打印请求体
	allowRespBody bool                                        // 是否打印响应体
	logFunc       func(context context.Context, al AccessLog) // 打印逻辑
}

func NewLogMiddlewareBuilder(logFunc func(context context.Context, al AccessLog)) *LogMiddlewareBuilder {
	return &LogMiddlewareBuilder{
		logFunc: logFunc,
	}
}

func (l *LogMiddlewareBuilder) AllowReqBody() *LogMiddlewareBuilder {
	l.allowReqBody = true
	return l
}

func (l *LogMiddlewareBuilder) AllowRespBody() *LogMiddlewareBuilder {
	l.allowRespBody = true
	return l
}

func (l *LogMiddlewareBuilder) Build() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		// 1. 打印请求
		var accessLog AccessLog
		accessLog.url = ctx.Request.URL.String()
		accessLog.method = ctx.Request.Method
		// 究竟要不要打印请求体(可能很大)
		// 究竟是用info呢还是debug呢?
		if l.allowReqBody && ctx.Request.Body != nil {
			data, _ := ctx.GetRawData()
			// 需要将数据放回去
			ctx.Request.Body = io.NopCloser(bytes.NewBuffer(data))
			accessLog.reqBody = string(data[:1024])
		}
		// 2. 打印响应
		if l.allowRespBody {
			// 替换ctx中的responseWrite
			ctx.Writer = &responseWriter{
				ResponseWriter: ctx.Writer,
				al:             &accessLog,
			}
		}
		start := time.Now()
		defer func() {
			accessLog.duration = time.Since(start)
			l.logFunc(ctx, accessLog)
		}()
		// 放行
		ctx.Next()
	}
}

// AccessLog 允许打印的日志对象
type AccessLog struct {
	statusCode int
	url        string
	method     string
	reqBody    string
	respBody   string
	duration   time.Duration
}

type responseWriter struct {
	gin.ResponseWriter
	al *AccessLog
}

func (w *responseWriter) Write(data []byte) (n int, err error) {
	w.al.respBody = string(data)
	return w.ResponseWriter.Write(data)
}

func (w *responseWriter) WriteHeader(code int) {
	w.al.statusCode = code
	w.ResponseWriter.WriteHeader(code)
}
相关推荐
欢乐少年19041 小时前
SpringBoot集成Sentry日志收集-3 (Spring Boot集成)
spring boot·后端·sentry
夏天的味道٥2 小时前
使用 Java 执行 SQL 语句和存储过程
java·开发语言·sql
IT、木易3 小时前
大白话JavaScript实现一个函数,将字符串中的每个单词首字母大写。
开发语言·前端·javascript·ecmascript
Mr.NickJJ4 小时前
JavaScript系列06-深入理解 JavaScript 事件系统:从原生事件到 React 合成事件
开发语言·javascript·react.js
浪九天5 小时前
Java直通车系列13【Spring MVC】(Spring MVC常用注解)
java·后端·spring
Archer1945 小时前
C语言——链表
c语言·开发语言·链表
My Li.5 小时前
c++的介绍
开发语言·c++
功德+n6 小时前
Maven 使用指南:基础 + 进阶 + 高级用法
java·开发语言·maven
达斯维达的大眼睛6 小时前
qt小项目,简单的音乐播放器
开发语言·qt
uhakadotcom6 小时前
Apache CXF 中的拒绝服务漏洞 CVE-2025-23184 详解
后端·面试·github