学会开发这几个中间件,Go项目就有保障啦

科技   2024-10-23 08:51   北京  

为了让Go项目的日志组件更好用以及产出的日志能够帮助我们快速定位问题,我在《Go日志门面的设计与实现-自动注入追踪ID标记代码位置、简化日志操作》中给项目单独定制了一个日志门面来简化项目中对日志的操作。

组件开发好了,我们还得把它用起来才行,这节课介绍三个Gin框架的全局中间件,应用上它们后能让我们的项目更稳健,一些关键时间点的关键信息不需要我们手动记录,框架会帮助我们自动把这些信息记录下来。

本节我们会为Gin开发三个中间件,我会说明其中的实现细节,即使你未来使用其他框架也能按照思路轻松实现类似的功能。

全局路由中间件,意味着我们在项目启动后,会把他们应用到所有的路由上,进入项目的任何请求都会让这些全局路有撸一遍。

本节开发的所有中间件的源码和测试接口都单独封存了Git版本, 大家加入项目后可直接访问 
https://github.com/go-study-lab/go-mall/compare/c3.2...c3.3 查看本章节对应的代码更新。

项目加入方式:扫码或者复制链接在浏览器中打开:https://xiaobot.net/p/golang  订阅后即可加入项目。


链路追踪中间件

个中间件的作用上节课已经介绍过,其功能逻辑是尝试从请求Header中获取链路追踪相关的请求头 traceId、spanId,如果能获取到证明自身不是直接对C端的服务,需要把调用者的spanId作为自己的parent spanId, traceId整个链路保持一致,然后再生成自身内部链路的spanId,把这些都记录到Gin的context中去。

func main() {
 g := gin.New()
    ...
 g.Use(middleware.StartTrace(), middleware.LogAccess())
}

如果请求Header里没有那些字段,则判定自己是请求的发端,自己生成traceId 和 spanId 再记录到Gin的Context中去。

具体的实现代码,还有traceId、spanId这些怎么用我们上节课已经详细说过一遍这里就不再贴代码了,并且跟今天项目新增的代码在同一项目文件中,大家练习时可以直接看项目代码再复习一遍。

请求和响应日志中间件

项目的请求和响应信息对于问题排查和用户行为分析都很重要。我在这里设计了项目会针对每个API访问分别记录一条请求日志和一条响应日志,它们的样例分别如下

请求日志的query、body等字段会详细记录请求的数据信息

{
  "level":"info",
  "msg":"AccessLog",
  "type":"access_start",
  "query""",
  "body""",
  "method""GET",
  "path""/building/config-read",
  ...
  "traceid":"b2355ee830fb5a3d",
  "spanid":"b2355ee830fb5a3d"
}

响应日志的output则会记录返回给客户端的响应信息,整个请求的处理时长等。

{
    "level""info",
    "ts""2024-09-20T16:06:17.470+0800",
    "msg""AccessLog",
    "type""access_end",
    "method""GET",
    "path""/building/config-read",
    "output""{\"max_life\":300000000,\"type\":\"mysql\"}",
    "traceid""b2355ee830fb5a3d",
    "spanid""b2355ee830fb5a3d",
}

日志信息中有些无关紧要的字段我删掉了,没在这里展示,请求和响应日志的msg关键字都是 AccessLog ,type字段分别用access_start 和 access_end 来标记。 日志中会把请求体,请求参数,响应结果在两条日志中分别记录。

设计用两条日志分别记录请求和响应的思路是,假设程序在执行中崩溃了,除了能拿到Panic信息,还是拿到请求进来时的信息,方便我们自己Debug时分析和复现问题

聊明白了为什么这么设计,那让人每次写接口时都手动写着两个日志肯定是不现实的,接下来我们来写代码实现访问日志中间件为我们自动记录所有访问的请求和响应日志。

Gin 框架有一个问题,我们程序拿不到响应结果,所以需要自己做一个包装 gin.ResponseWriter 的 Wrapper 然后实现 Write 方法,在这个Write 方法里我们可以先把响应结果暂存一份再去调用 gin.ResponseWriter 的 Write 方法让Gin写响应。

type bodyLogWriter struct {
 gin.ResponseWriter
 body *bytes.Buffer
}

func (w bodyLogWriter) Write(b []byte) (int, error) {
 w.body.Write(b)
 return w.ResponseWriter.Write(b)
}

这样我们在程序中就能通过 bodyLogWriter 拿到响应信息了。

在中间件里能拿到响应结果后,其他部分实现起来就比较简单了,我们直接看一下下面这个中间件 LogAccess 的代码。

func LogAccess() gin.HandlerFunc {
 return func(c *gin.Context) {
  //保存body
  reqBody, _ := ioutil.ReadAll(c.Request.Body)
  c.Request.Body = ioutil.NopCloser(bytes.NewReader(reqBody))
  start := time.Now()
  blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
  c.Writer = blw

  accessLog(c, "access_start", time.Since(start), reqBody, nil)
  defer func() {
   accessLog(c, "access_end", time.Since(start), reqBody, blw.body.String())
  }()
  c.Next()

  return
 }
}

func accessLog(c *gin.Context, accessType string, dur time.Duration, body []byte, dataOut interface{}) {
 req := c.Request
 bodyStr := string(body)
 query := req.URL.RawQuery
 path := req.URL.Path
 // TODO: 实现Token认证后再把访问日志里也加上token记录
 // token := c.Request.Header.Get("token")
 logger.New(c).Info("AccessLog",
  "type", accessType,
  "ip", c.ClientIP(),
  //"token", token,
  "method", req.Method,
  "path", path,
  "query", query,
  "body", bodyStr,
  "output", dataOut,
  "time(ms)"int64(dur/time.Millisecond))
}

针对请求和响应日志的功能测试,以及Panic中间件在崩溃后的恢复大家可以订阅后查看完整版。

加入项目后请使用代码版本c3.3 进行学习, 采用下面的git命令切换项目版本,除了参考和学习代码外还可直接启动项目进行功能测试。

git fetch --tags
git checkout tags/c3.3

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

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

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

点击阅读原文可跳转。


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