立个flag,冲刺一周

这篇文章比较硬核,会涉及到这几个知识点:协程、协程池、钩子函数、中间件以及异步办法的运用,文章终究会带咱们去阅读Async的源码,了解其底层完成。

应用场景

办理后台需求记载登录用户的各种操作日志,便于出现问题时追溯办理员做了哪些操作。

一起咱们也需求记载每次的恳求日志到log文件中,供开发人员定位问题。

咱们需求考虑哪些问题呢?

留意问题

  1. 首要需求大局增加操作日志的记载,即用户登录后的各种操作都要记载
  2. 日志记载的操作应该尽量避免对主程序的影响,不能由于记载日志而糟蹋功能
  3. 要记载的数据应该界说好规矩,一致办理、一致入参。

先说定论

咱们会用到以下知识点,来完成上面的场景,处理上面说到的留意问题:

  1. group.Hook()HookAfterOutput 在大局增加日志记载的进口
  2. 运用中间件完成字段的一致办理、一致入参
  3. 运用Go的协程机制进步记载日志的效率,并发履行,存入DB,供办理后台检查运用(在这儿咱们运用GoFrame的grpool更好的办理协程
  4. 运用g.Log()Async()特性异步保存恳求日志到文件,便利咱们技术人员查错运用。

完成流程

前言

下面的示例代码根据GoFrame 1.16版别完成,在这儿不再赘述项目初始化等基础问题,感兴趣的同学可以检查这篇文章:GoFrame入门实践,里边介绍了GoFrame的特点,以及我依照官方文档学习踩得一些坑。

为了便利咱们看懂逻辑,不重要的代码用运用三个竖着的.省略;GoFrame结构在下文简称gf结构。

路由文件

  1. 咱们界说了ApiLog的中间件,在业务路由办法之前调用
  2. 咱们界说了log()函数,其中应用到了gf结构路由的钩子函数group.Hook,用于在接口回来业务数据之后记载操作日志。
  3. log()函数的位置在登录路由之后,业务逻辑路由之前。
package app
func Run() {
   s := g.Server()
   s.Use(middleware.Cors.CORS)
   s.Use(middleware.Logs.ApiLog)
   //AKSK交换token
   account.Token()
   //账号登录
   account.Login()
   //操作日志
   log(s)
   admin.Init(s)
   .
   .
   .
   s.Run()
}
func log(s *ghttp.Server) {
   s.Group("/", func(group *ghttp.RouterGroup) {
      group.Hook("/*", ghttp.HookAfterOutput, oper_log.OperLog.OperationLog)
   })
}

操作日志记载

下面咱们来要点看一下上文中说到的钩子函数中的OperationLog()办法都完成了哪些功能,是怎么完成的?

  1. 首要经过路由上下文函数获取了登录中间件中设置的UID,对登录态进行判断,只有UID不为0时才记载
  2. 根据UID去账号服务中台获取账号相关信息,用于后续的信息登记
  3. subUserIduserNickname等都经过账号服务中台取得
  4. 本次恳求的url经过r.Request对象获取
  5. 经过一系列逻辑处理,咱们拼装好了需求登记的数据data
  6. 终究咱们将data数据传入Invoke办法,经过协程池写入操作日志
// OperationLog 操作日志记载
func (s *operLog) OperationLog(r *ghttp.Request) {
   userId := gconv.Int(r.GetCtxVar(middleware.CtxUID))
   if userId == 0 {
      return
   }
   account, err := service2.Account.GetAccountInfo(userId)
   if nil != err {
      response.FailureCode(r, 0)
   }
   deptId := uint64(account.DeptId)
   userNickname := account.Name
   if "" == userNickname {
      userNickname = account.Phone
   }
    .
    .
    .
   userInfo := &dao.CtxUser{
      Id:           uint64(account.Id),
      UserName:     userNickname,
      SubId:        uint64(subUserId),
      DeptId:       deptId,
      UserNickname: userNickname,
      UserStatus:   account.Status,
      IsAdmin:      account.IsAdmin,
      Avatar:       "",
   }
   url := r.Request.URL //恳求地址
   //获取菜单
   //获取地址对应的菜单id
    .
    .
    .
   data := &SysOperLogAdd{
      User:         userInfo,
      Menu:         menu,
      Url:          url,
      Params:       r.GetMap(),
      Method:       r.Method,
      ClientIp:     library.GetClientIp(r),
      OperatorType: 1,
   }
   s.Invoke(data)
}

运用grpool并发插入

办法十分简略,调用s.Pool.Add()办法即可。

func (s *operLog) Invoke(data *SysOperLogAdd) {
   s.Pool.Add(func() {
      //写入日志数据
      s.OperationLogAdd(data)
   })
}

是不是猎奇OperationLogAdd做了什么事情,别着急,咱们接着往下看:

增加操作日志

咱们终究的意图就是把上文传入的data数据存入DB

在存入DB之前咱们会在做一些逻辑判断,校验传入的数据是否合规,合规矩入库。

(PS:这儿还有优化空间,比方我最近的一个心得体会是要充分化耦,明确各自的职责,比方可以优化成:入库前的函数做数据校验和数据拼装,比方依照入库函数的要求供给数据;而入库函数不考虑校验的问题,只考虑如果以最高效的方法入库的问题。)

//OperationLogAdd 增加操作日志
func (s operLog) OperationLogAdd(data *SysOperLogAdd) {
    //省略参数校验
    .
    .
    .
   insertData := g.Map{
      dao.SysOperLog.C.Title:         menuTitle,
      dao.SysOperLog.C.Method:        data.Url.Path,
      dao.SysOperLog.C.RequestMethod: data.Method,
      dao.SysOperLog.C.OperatorType:  data.OperatorType,
      dao.SysOperLog.C.OperName:      data.User.UserName,
      dao.SysOperLog.C.Uid:           data.User.Id,
      dao.SysOperLog.C.SubUid:        data.User.SubId,
      dao.SysOperLog.C.DeptName:      deptName,
      dao.SysOperLog.C.OperIp:        data.ClientIp,
      dao.SysOperLog.C.OperLocation:  library.GetCityByIp(data.ClientIp),
      dao.SysOperLog.C.OperTime:      gtime.Now(),
   }    
    .
    .
    .
   _, err = dao.SysOperLog.Insert(insertData)
   if err != nil {
      g.Log().Error(err)
   }
}

到这儿咱们就现已知道如果利用协程池和钩子函数怎么坚持操作日志了。

是不是猎奇上面说到的中间件,下面再来剖析一下日志中间件是怎么完成的。

咱们再来着重一下差异:

上文说到的协程池保存操作记载到DB中,是供办理员在办理后台检查的。

而日志中间件的作用是把每次网络恳求的信息存入到log日志中,供开发人员检查。

恳求日志中间件

咱们经过下面的日志中间件可以记载:恳求的链接、恳求参数、呼应数据、恳求时刻。

package middleware
import (
   "context"
   "github.com/gogf/gf/frame/g"
   "github.com/gogf/gf/net/ghttp"
   "github.com/gogf/gf/os/gtime"
   "github.com/gogf/gf/text/gregex"
   "github.com/gogf/gf/util/grand"
)
const (
   CtxAppKey      = "AK"
   CtxAppID       = "app_id"       //token|签名获取
   CtxAccountName = "account_name" //token获取
   CtxSubID       = "sub_id"       //token获取
    .
    .
    .
)
var Logs = logsMiddleware{}
type logsMiddleware struct{}
// Log 日志
func (s *logsMiddleware) Log(r *ghttp.Request) {
   r.SetCtxVar(RequestId, grand.S(20))
   r.SetCtxVar(CtxAppKey, r.GetHeader("Api-App-Key"))
   r.SetCtxVar(CtxIP, r.GetClientIp())
   r.SetCtxVar(CtxURI, r.RequestURI)
   r.Middleware.Next()
   errStr := ""
   if err := r.GetError(); err != nil {
      errStr = err.Error()
   }
   responseTime := gtime.TimestampMilli() - r.EnterTime
   g.Log().Ctx(r.Context()).Async(true).
      Cat("admin").
      Infof("恳求参数: 【%v】 呼应参数: 【%v】 呼应时刻:【%v ms】error:【%v】", r.GetBodyString(),
         r.Response.BufferString(), responseTime, errStr)
}
// ApiLog 日志
func (s *logsMiddleware) ApiLog(r *ghttp.Request) {
   r.SetCtxVar(RequestId, grand.S(20))
   r.SetCtxVar(CtxAppKey, r.GetHeader("Api-App-Key"))
   r.SetCtxVar(CtxIP, r.GetClientIp())
   r.SetCtxVar(CtxURI, r.RequestURI)
   var body, _ = gregex.ReplaceString(`\s`, "", r.GetBodyString())
   g.Log().Ctx(r.Context()).
      Cat("request").
      Infof("恳求参数:【%v】", body)
   r.Middleware.Next()
   responseTime := gtime.TimestampMilli() - r.EnterTime
   g.Log().Ctx(r.Context()).Async(true).
      Cat("request").
      Infof("恳求参数:【%v】呼应参数:【%v】呼应时刻:【%v ms】", body, r.Response.BufferString(), responseTime)
}
func Ctx(req context.Context) (res context.Context) {
   res = context.Background()
   res = context.WithValue(res, RequestId, req.Value(RequestId))
   return
}

仔细的同学会留意到这个办法:g.Log().Ctx(r.Context()).Async(true),没错,咱们是经过Async(true)异步的方法来记载日志的。

带你看源码

咱们经过追踪源码,来研究一下Async(true)是怎么完成异步的?

step1:

Go如何优雅的记录操作日志 | 冲刺掘金一周

step2:

Go如何优雅的记录操作日志 | 冲刺掘金一周

step3:

Go如何优雅的记录操作日志 | 冲刺掘金一周

step4:

Go如何优雅的记录操作日志 | 冲刺掘金一周

咱们终究发现Async()的底层完成是根据GoFrame的asyncPool完成的。

上面这个追踪定位源码的进程是不是很有意思?

重视我,下一篇带咱们更进一步剖析Go的源码。

总结回忆

咱们再来回忆一下这篇文章说到的知识点:

  1. group.Hook()HookAfterOutput在大局增加日志记载的进口
  2. 运用中间件完成字段的一致办理、一致入参
  3. 运用Go的协程机制进步记载日志的效率,并发履行,存入DB,供办理后台检查运用(在这儿咱们运用GoFrame的grpool更好的办理协程)
  4. 运用g.Log()Async()特性异步保存恳求日志到文件,便利咱们技术人员查错运用。
  5. 经过检查源码的方法了解了Async的底层完成是根据gf结构的asyncPool

一起学习

这是收藏破万的:# 《Go学习路线图》让你少走弯路,升职加薪。

Go如何优雅的记录操作日志 | 冲刺掘金一周

大众号:程序员升职加薪之旅 微信号:wangzhongyang1993 B站视频:王中阳Go