seopc流量排名网站网站排名前十
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 对象即可:
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 级别,并且我们在这里也可以看出两个细节:
- 后端系统是不会也不应该把 err 等错误信息返回给前端的
- 对于手机号这类敏感字段,哪怕是在日志中也不应该打印出来
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
package logxtype 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 stringValue any
}
这里我们模拟 zap 的日志定义,参数使用 Field 结构体封装,我们还可以借鉴 zap 提供如 zap.Error、zap.String类似快速创建 Field 的方法
step2:定义 ZapLogger 的结构体实现
package logximport "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:业务代码改造成使用自定义接口
type UserHandler struct {EmailCompile *regexp.RegexpPasswordCompile *regexp.RegexpNicknameCompile *regexp.RegexpBirthdayCompile *regexp.RegexpAboutMeCompile *regexp.Regexpsvc service.IUserServicecodeSvc service.CodeServicejwtHdl IJwtHandlerlogger logx.LoggerX
}
3.2 封装请求响应日志中间件
目前我们有一个通用功能需要解决:那就是使用 AOP 机制在系统的入口接收请求以及出口返回响应的地方记录日志,当然 Gin 框架提供的 middleware 机制本质上就是 AOP 的实现方式,即我们可以借助 Gin 的 middleware 实现这个通用日志处理功能:
package middlewareimport ("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 = truereturn l
}func (l *LogMiddlewareBuilder) AllowRespBody() *LogMiddlewareBuilder {l.allowRespBody = truereturn l
}func (l *LogMiddlewareBuilder) Build() gin.HandlerFunc {return func(ctx *gin.Context) {// 1. 打印请求var accessLog AccessLogaccessLog.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中的responseWritectx.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 inturl stringmethod stringreqBody stringrespBody stringduration time.Duration
}type responseWriter struct {gin.ResponseWriteral *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 = codew.ResponseWriter.WriteHeader(code)
}