Go如何优雅的记录操作日志

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 这篇文章比较硬核,会涉及到这几个知识点:协程、协程池、钩子函数、中间件以及异步方法的使用,文章最后会带大家去阅读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:


微信图片_20221113152856.jpg


step2:


微信图片_20221113152859.jpg


step3:


微信图片_20221113152903.jpg


step4:


微信图片_20221113152906.jpg


我们最终发现Async()的底层实现是基于GoFrame的asyncPool实现的。

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

关注我,下一篇带大家更进一步分析Go的源码。


总结回顾


我们再来回顾一下这篇文章提到的知识点:


  1. group.Hook()HookAfterOutput 在全局添加日志记录的入口
  2. 使用中间件实现字段的统一管理、统一入参
  3. 使用Go的协程机制提高记录日志的效率,并发执行,存入DB,供管理后台查看使用(在这里我们使用GoFrame的grpool更好的管理协程)
  4. 使用g.Log()Async()特性异步保存请求日志到文件,方便我们技术人员查错使用。
  5. 通过查看源码的方式了解了Async的底层实现是基于gf框架的asyncPool
相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
相关文章
|
1月前
|
存储 监控 算法
防止员工泄密软件中文件访问日志管理的 Go 语言 B + 树算法
B+树凭借高效范围查询与稳定插入删除性能,为防止员工泄密软件提供高响应、可追溯的日志管理方案,显著提升海量文件操作日志的存储与检索效率。
89 2
|
安全 Go
用 Zap 轻松搞定 Go 语言中的结构化日志
在现代应用程序开发中,日志记录至关重要。Go 语言中有许多日志库,而 Zap 因其高性能和灵活性脱颖而出。本文详细介绍如何在 Go 项目中使用 Zap 进行结构化日志记录,并展示如何定制日志输出,满足生产环境需求。通过基础示例、SugaredLogger 的便捷使用以及自定义日志配置,帮助你在实际开发中高效管理日志。
391 1
|
8月前
|
存储 JSON Go
PHP 日志系统的最佳搭档:一个 Go 写的远程日志收集服务
为了不再 SSH 上去翻日志,我写了个 Go 小脚本,用来接收远程日志。PHP 负责记录日志,Go 负责存储和展示,按天存储、支持 API 访问、可远程管理,终于能第一时间知道项目炸了。
176 10
|
SQL Java Serverless
实时计算 Flink版操作报错合集之在写入SLS(Serverless Log Service)时出现报错,该如何排查
在使用实时计算Flink版过程中,可能会遇到各种错误,了解这些错误的原因及解决方法对于高效排错至关重要。针对具体问题,查看Flink的日志是关键,它们通常会提供更详细的错误信息和堆栈跟踪,有助于定位问题。此外,Flink社区文档和官方论坛也是寻求帮助的好去处。以下是一些常见的操作报错及其可能的原因与解决策略。
|
JSON 中间件 Go
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
本文详细介绍了如何在Go项目中集成并配置Zap日志库。首先通过`go get -u go.uber.org/zap`命令安装Zap,接着展示了`Logger`与`Sugared Logger`两种日志记录器的基本用法。随后深入探讨了Zap的高级配置,包括如何将日志输出至文件、调整时间格式、记录调用者信息以及日志分割等。最后,文章演示了如何在gin框架中集成Zap,通过自定义中间件实现了日志记录和异常恢复功能。通过这些步骤,读者可以掌握Zap在实际项目中的应用与定制方法
637 1
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
|
中间件 Go 数据库
slog 简介:用于 Go 的结构化日志
slog 简介:用于 Go 的结构化日志
|
存储 JSON 前端开发
一文搞懂 Go 1.21 的日志标准库 - slog
一文搞懂 Go 1.21 的日志标准库 - slog
609 2
|
Java Serverless 应用服务中间件
函数计算操作报错合集之JVM启动时找不到指定的日志目录,该如何解决
Serverless 应用引擎(SAE)是阿里云提供的Serverless PaaS平台,支持Spring Cloud、Dubbo、HSF等主流微服务框架,简化应用的部署、运维和弹性伸缩。在使用SAE过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
188 1
|
监控 数据管理 关系型数据库
数据管理DMS使用问题之是否支持将操作日志导出至阿里云日志服务(SLS)
阿里云数据管理DMS提供了全面的数据管理、数据库运维、数据安全、数据迁移与同步等功能,助力企业高效、安全地进行数据库管理和运维工作。以下是DMS产品使用合集的详细介绍。
|
监控 Go 开发者
掌握Go语言中的日志管理
【8月更文挑战第31天】
182 0