需求:在web后端开发中,想输出http请求和响应的日志
思路:利用Gin提供的middleware的 AOP 机制
实现:
(1)欲打印如下字段
type AccessLog struct {
Path string `json:"path"`
Method string `json:"method"`
ReqBody string `json:"req_body"`
Status int `json:"status"`
RespBody string `json:"resp_body"`
Duration time.Duration `json:"duration"`
}
(2)具体实现
package middleware
import (
"bytes"
"context"
"github.com/gin-gonic/gin"
"io"
"time"
)
type LogMiddlewareBuilder struct {
// 打印什么级别的日志
logFn func(ctx context.Context, l AccessLog)
// 考虑到线上环境:是否允许 req 和 resp 打印(敏感信息)
allowReqBody bool
allowRespBody bool
}
func NewLogMiddlewareBuilder(logFn func(ctx context.Context, l AccessLog)) *LogMiddlewareBuilder {
return &LogMiddlewareBuilder{
logFn: logFn,
}
}
// note 方便链式调用
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) {
path := ctx.Request.URL.Path
// 防止黑客使得 path 过长
if len(path) > 1024 {
path = path[:1024]
}
method := ctx.Request.Method
al := AccessLog{
Path: path,
Method: method,
}
if l.allowReqBody {
// note Request.Body 是一个 Stream 对象,只能读一次
// 不处理这个err
body, _ := ctx.GetRawData()
if len(body) > 2048 {
al.ReqBody = string(body[:2048])
} else {
al.ReqBody = string(body)
}
// note 放回去
ctx.Request.Body = io.NopCloser(bytes.NewReader(body))
//ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body))
}
start := time.Now()
// note 骚操作,装饰器模式
if l.allowRespBody {
ctx.Writer = &responseWriter{
ResponseWriter: ctx.Writer,
al: &al,
}
}
defer func() {
al.Duration = time.Since(start)
//duration := time.Now().Sub(start)
l.logFn(ctx, al)
}()
// 直接执行下一个 middleware...直到业务逻辑
ctx.Next()
// 在这里,你就拿到了响应
}
}
// note 装饰器模式
type responseWriter struct {
gin.ResponseWriter
al *AccessLog
}
func (w *responseWriter) Write(data []byte) (int, error) {
w.al.RespBody = string(data)
return w.ResponseWriter.Write(data)
}
func (w *responseWriter) WriteHeader(statusCode int) {
w.al.Status = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}
注意:Gin 的 ctx 没有暴露响应,所以我们无法直接输出响应,但是 ctx 暴露了 ResponsWriter,所以我们换了一个实现,这个实现会帮我们记录响应。
(3)使用该中间件
middleware.NewLogMiddlewareBuilder(func(ctx context.Context, al middleware.AccessLog) {
// 打印 debug 级别的
l.Debug("", logger.Field{Key: "req", Val: al})
}).AllowReqBody().AllowRespBody().Build(),