审计日志管理是我们在web系统开发中的常见的模块,虽然它有时并不属于业务模块的范畴,但对于系统整体来说却十分关键,用户的操作(尤其是关键操作)、用户的登录,我们的系统都应加以记录,以便后续溯源。
日志管理的方案可以看到很多,本文介绍的是一种基于Golang Gin框架的自定义中间件的实现方案,为大家抛砖引玉了。
个人认为有以下几个优势:
(1)中间件的方式可灵活地匹配路由组,从而灵活地指定需要记录日志的路由组;
(2)同一个路由组中通过context value 来区分接口是否需要记录操作日志;
(3)业务处理函数中可灵活配置需记录内容,不需集中处理。
本文转载自微信公众号「小小平头哥」,作者小小平头哥。转载本文请联系小小平头哥公众号。
01整体流程
1) 中间件函数整体的流程
图片
2) 业务函数流程
图片
02代码实现
1) 中间件函数实现
type Response struct {
Code int `json:"code" bson:"code"`
}
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)
}
const (
HttpRespSuccessCode = 0
)
// Logger 日志记录
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
//备份请求体
blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
c.Writer = blw
//继续执行请求
c.Next()
//判断记录标志
needToLog, ok := c.Get("need_to_log")
if !ok {
log.Warn("获取是否需要记录日志失败")
return
}
if !needToLog.(bool) {
return
}
//也可以在这儿加入白名单 判断是否是不需记录的URL
// 获取请求的HTTP状态码
statusCode := c.Writer.Status()
// 获取请求IP
clientIP := common.GetClientIP(c)
isSuccess := false
//若HTTP状态码为200
if c.Writer.Status() == http.StatusOK {
var resp Response
// 获取返回的数据
err := json.Unmarshal(blw.body.Bytes(), &resp)
if err != nil {
log.Warn("Logs Operation Unmarshal Error: %s", err.Error())
return
}
//判断操作是否成功 需结合业务函数的返回值结构
if resp.Code == HttpRespSuccessCode {
isSuccess = true
}
}
if statusCode != http.StatusNotFound {
SetDBLog(c, clientIP, isSuccess)
}
}
}
// SetDBLog 写入日志表
func SetDBLog(c *gin.Context, clientIP string, status bool) {
user, ok := c.Get("user")
if !ok {
log.Warn("审计日志-获取用户名失败")
}
//日志格式化 然后入库
logInfo := table.Logs{}
//构造日志ID 可使用其他方式替代
logInfo.LogID = NewNanoid()
if user != nil {
logInfo.Username = user.(string)
}
operatorType, exist := c.Get("operation_type")
if exist {
logInfo.OperationType = operatorType.(string)
}
logInfo.IP = clientIP
operation, exist := c.Get("operation")
if exist {
logInfo.Description = operation.(string)
}
if status == true {
logInfo.Description = logInfo.Description + "成功"
} else {
logInfo.Description = logInfo.Description + "失败"
}
//日志入库
err := InsertLog(logInfo)
if err != nil {
log.Warn("InsertLog %s error, %s", logInfo.LogID, err.Error())
}
}
// InsertLog 插入log
func InsertLog(logs table.Logs) error {
}
2) 业务函数实现
func (User) UserLoginOut(c *ctx.Context) {
//设定记录日志标志
c.Set("need_to_log", true)
//设定操作类型
c.Set("operation_type", "用户退出登录")
//设定具体操作
c.Set("operation", "用户退出登录")
c.Success()
}
3) 路由组应用
//设定路由组
UserRouter := apiV1Group.Group("users")
//为路由组应用中间件
UserRouter.Use(middleware.Logger())
03注意事项
1) 中间件处理函数中的备份原始请求体很重要,否则可能会出现业务代码无法获取请求参数的情况;
- 中间件的报错不应影响原有业务逻辑。