这个句子比较硬核,这些知识点:使用协议、协议池、钩子函数、中间件、异步方法。
应用场景
管理后台应记录登录用户的各种活动日志,以便在出现问题时跟踪管理员执行的操作。
此外,为了帮助开发人员找到问题,必须将每个请求日志记录在日志文件中。
我们应该考虑什么问题?
注意问题
必须首先全局添加操作日志中的记录。也就是说,用户登录后需要记录各种操作的操作对主程序的影响应降至最低。不能因为日志记录而浪费性能。要记录的数据必须定义规则、集成管理和集成参与。先说结论
为了实现上述方案并解决上述注意事项,请使用以下知识点:
集团。Hook()和HookAfterOutput在添加全局日志记录的门户中使用中间件实现字段的统一管理,使用Go的协作机制提高日志记录效率,同时运行,并存储在DB中以供管理后台查看(此处使用GoFrame的grpool改进管理协作流程的实现过程)
前言
以下示例代码基于GoFrame 1.16实现。这里不再详细说明文章初始化等基本问题。感兴趣的学生可以看这个项目。介绍GoFrame入门实践,GOFrame的特点,以及根据官方文件踩的坑。
为了帮助大家理解逻辑,不重要的代码有三个垂直的。省略的GoFrame框架在下面称为gf框架。
路由文件
定义了ApiLog的中间件。group是在业务路由方法之前应用于gf框架路由的挂接函数。呼叫了Hook。界面返回业务数据后,用于记录活动日志。Log()函数的位置在登录路径之后,在业务逻辑路径之前。Package app
Func Run() {
S :=g.Server()
S.Use)
S.Use)
//AKSK令牌交换
帐户。Token()
//帐户登录
帐户。Login()
//操作日志
日志(s)
管理员。Init(s)
.
.
.
S.Run()
}
Func log(s *g) {
S.group ('/'func(组* g) {
集团。Hook('/* 'g,o)
})
}
复制代码操作日志记录
让我们看一下前面提到的挂接函数的OperationLog()方法中实现了哪些功能,以及如何实现。
首先,通过路由上下文函数获取登录中间件中设置的UID,判断登录状态,只有在UID不为零的情况下,才会根据UID在帐户服务台记录与帐户相关的信息,并用于后续信息注册。subUserId、userNickname等通过获取r.request对象通过一系列逻辑处理通过帐户服务中队收到此请求的URL。我们组装了需要注册的数据data。最后,将data数据传递到Invoke方法,通过协作池创建了操作日志//操作日志操作日志
Func (s * operlog)操作日志(r * g) {
UserId :=gconv。In))
If userId==0 {
Return
}
帐户,err:=(userid)
If nil!=err {
Re(r,0)
}
DeptId :=uint64)
UserNickname :=帐户。Name
If ''==userNickname {
UserNickname=account。Phone
}
.
.
.
UserInfo :=dao。CtxUser{
Id: uint64)、
UserName: userNickname、
SubId: uint64(subUs
erId), DeptId: deptId, UserNickname: userNickname, UserStatus: account.Status, IsAdmin: account.IsAdmin, Avatar: "", } url := r.Reque //请求地址 //获取菜单 //获取地址对应的菜单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.Sy: menuTitle,
dao.Sy: da,
dao.Sy: da,
dao.Sy: da,
dao.Sy: da,
dao.Sy: da,
dao.Sy: da,
dao.Sy: deptName,
dao.Sy: da,
dao.Sy: library.GetCityByIp(da),
dao.Sy: g(),
}
.
.
.
_, err = dao.Sy(insertData)
if err != nil {
g.Log().Error(err)
}
}
复制代码
到这里我们就已经知道如果利用协程池和钩子函数如何保持操作日志了。
是不是好奇上面提到的中间件,下面再来剖析一下日志中间件是如何实现的。
我们再来强调一下区别:
上文提到的协程池保存操作记录到DB中,是供管理员在管理后台查看的。
而日志中间件的作用是把每次网络请求的信息存入到log日志中,供开发人员查看。
请求日志中间件
我们通过下面的日志中间件可以记录:请求的链接、请求参数、响应数据、请求时间。
package middleware
import (
"context"
"gi;
"gi;
"gi;
"gi;
"gi;
)
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 *g) {
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 := g() - r.EnterTime
g.Log().Ctx()).Async(true).
Cat("admin").
Infof("请求参数: 【%v】 响应参数: 【%v】 响应时间:【%v ms】error:【%v】", r.GetBodyString(),
r.Re(), responseTime, errStr)
}
// ApiLog 日志
func (s *logsMiddleware) ApiLog(r *g) {
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()).
Cat("request").
Infof("请求参数:【%v】", body)
r.Middleware.Next()
responseTime := g() - r.EnterTime
g.Log().Ctx()).Async(true).
Cat("request").
Infof("请求参数:【%v】响应参数:【%v】响应时间:【%v ms】", body, r.Re(), responseTime)
}
func Ctx(req con) (res con) {
res = con()
res = con(res, RequestId, req.Value(RequestId))
return
}
复制代码
细心的同学会注意到这个方法:g.Log().Ctx()).Async(true),没错,我们是通过Async(true)异步的方式来记录日志的。
带你看源码
咱们通过追踪源码,来研究一下Async(true)是如何实现异步的?
step1:
step2:
step3:
step4:
我们最终发现Async()的底层实现是基于GoFrame的asyncPool实现的。
上面这个追踪定位源码的过程是不是很有意思?
关注我,下一篇带大家更进一步分析Go的源码。
总结回顾
我们再来回顾一下这篇文章提到的知识点:
- group.Hook()和HookAfterOutput 在全局添加日志记录的入口
- 使用中间件实现字段的统一管理、统一入参
- 使用Go的协程机制提高记录日志的效率,并发执行,存入DB,供管理后台查看使用(在这里我们使用GoFrame的grpool更好的管理协程)
- 使用g.Log()的Async()特性异步保存请求日志到文件,方便我们技术人员查错使用。
- 通过查看源码的方式了解了Async的底层实现是基于gf框架的asyncPool
最后
对Go和GoFrame感兴趣的同学查看 # 《Go学习路线图》让你少走弯路,Let's Go !,通过对Go语言进行系统的知识梳理之后,再来读这篇文章一定会有更多的收获。