为Gin设计中间件(一): 在系统的入口打印日志

388 阅读1分钟

需求:在web后端开发中,想输出http请求和响应的日志

思路:利用Gin提供的middlewareAOP 机制

实现

(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)
}

注意:Ginctx 没有暴露响应,所以我们无法直接输出响应,但是 ctx 暴露了 ResponsWriter,所以我们换了一个实现,这个实现会帮我们记录响应。

(3)使用该中间件

middleware.NewLogMiddlewareBuilder(func(ctx context.Context, al middleware.AccessLog) {
    // 打印 debug 级别的
    l.Debug("", logger.Field{Key: "req", Val: al})
}).AllowReqBody().AllowRespBody().Build(),