6-日志使用与配置

思考并回答以下问题:

实验介绍

本次实验主要讲解Gin框架中日志的使用和配置,包括Gin框架内置中间件、输出格式、错误日志处理,等等。了解如何在Gin框架使用日志非常重要,因为在实战项目中特别是生产环境通常会有需要及时定位问题的情况,这就需要深入了解日志模块。

知识点

  • Gin内置中间件
  • 日志过滤
  • 错误日志处理
  • 写入日志文件

Gin内置中间件

Gin框架中已经内置了日志中间件,可以通过简单的调用来配置Gin框架的日志输出。

下表是Gin框架中内置的日志中间件方法。

方法
描述
Logger 写日志到gin.DefaultWriter,默认写到标准输出
LoggerWithFormatter 跟Logger一样,不过可以通过传入LogFormatter参数来配置日志格式
LoggerWithWriter 跟LoggerWithConfig类似,不过定义了输出写入目标,只要是实现了io.Writer接口的都可以传入
LoggerWithConfig 更精细化配置的日志中间件,通过传入LoggerConfig来控制日志行为

上表的描述或许有些简略,有能力的同学可以查看一下Gin日志模块源码,也就是gin/logger.go文件,在其中可以看到详细的方法参数和配置。

下面我们将通过实际实验来体验如何使用Gin框架内置的中间件。

在middlewares文件夹下创建文件log.go,输入以下内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package middlewares

import (
"fmt"
"github.com/gin-gonic/gin"
)

func LogMiddleware() gin.HandlerFunc {
return gin.LoggerWithFormatter(func(params gin.LogFormatterParams) string {
return fmt.Sprintf(
"%s - [%d] %s %s %s - %s - %s - %s %s\n",
params.TimeStamp.Format("2006/01/02 15:04:05"),
params.StatusCode,
params.Method,
params.Path,
params.Request.Proto,
params.Latency,
params.Request.UserAgent(),
params.ClientIP,
params.ErrorMessage,
)
})
}

可以看到,上面的代码就是将Gin上下文中的一些参数,例如时间戳、IP地址、请求方法、请求路径等,传递出来,并打印在标准输出中。

接下来,我们需要在main.go中注册该路由,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"gin-course/middlewares"
"gin-course/routes"

"github.com/gin-gonic/gin"
)

func main() {
// 创建 Gin 引擎
app := gin.New()

// 注册日志中间件
app.Use(middlewares.LogMiddleware())

// 注册 JWT 鉴权中间件
app.Use(middlewares.JwtAuth())

// 注册路由 users
routes.RegisterUsers(app)

// 运行 Gin 引擎
app.Run(":8080")
}

完成后,打开终端,运行go run main.go启动程序。

成功启动后,我们打开Web服务,路径输入为/login/jwt?username=admin&password=admin,即通过上个实验编写的JWT方式登陆。

回到终端,我们可以看到如下类似输出结果。

可以看到,这个HTTP请求的信息被打印到了标准输出。

感兴趣的朋友还可以试试其他的接口请求,同时尝试更改一下log.go日志中间件,看打印的日志是否符合预期。

日志过滤

在真正的企业级应用中,我们并不需要将每一条HTTP请求日志都打印并记录下来,因为这样会导致占用大量的资源。而很多时候,我们只需要将自己关注的接口日志数据记录下来。因此,我们需要对日志进行过滤,只保留重要的、对我们排查问题和监控系统有用的日志。

下面我们将利用LoggerWithConfig这个方法来实现日志过滤。

打开middleware/log.go,更改代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package middlewares

import (
"fmt"
"github.com/gin-gonic/gin"
)

func logFormatter(params gin.LogFormatterParams) string {
return fmt.Sprintf(
"%s - [%d] %s %s %s - %s - %s - %s %s\n",
params.TimeStamp.Format("2006/01/02 15:04:05"),
params.StatusCode,
params.Method,
params.Path,
params.Request.Proto,
params.Latency,
params.Request.UserAgent(),
params.ClientIP,
params.ErrorMessage,
)
}

func LogMiddleware() gin.HandlerFunc {
return gin.LoggerWithFormatter(logFormatter)
}

func NonLoginLogMiddleware() gin.HandlerFunc {
return gin.LoggerWithConfig(gin.LoggerConfig{
Formatter: logFormatter,
SkipPaths: []string{
"/login",
"/login/jwt",
},
})
}

在这里,我们加入了NonLoginLogMiddleware,表示只记录非登陆接口的HTTP请求日志,格式与LogMiddleware一样。这里我们稍微重构了一下之前的代码,用logFormatter封装了日志格式。

然后我们在main.go中将之前的中间件注释掉,注册NonLoginLogMiddleware,代码如下。

1
2
3
4
5
...
// 注册日志中间件
//app.Use(middlewares.LogMiddleware())
app.Use(middlewares.NonLoginLogMiddleware())
...

在终端里运行go run main.go重新启动应用,成功启动后在Web服务路径中输入/login/jwt?username=admin&password=admin,回到终端,可以看到不会出现日志。

拷贝下token内容,并通过ThunderClient请求接口/me,再回到终端,会看到如下结果。

这样,我们就过滤掉了所有登陆接口的HTTP请求日志。

错误日志处理

错误日志是生产环境中非常重要的部分,因为生产环境中有很多不确定因素都可能导致故障出现,以至于造成业务流失、用户抱怨等后果。因此,及时排查故障原因就非常重要,而记录下来的错误日志能帮助我们有效定位故障原因,好第一时间修复问题。

如果你细心的话,可能会发现,目前咱们带错误状态码(4XX、5XX)的HTTP请求都没有被记录下来。我们需要单独处理这样的错误日志。

打开VS Code,在middlewares/log.go中加入如下中间件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func ErrorLogMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()

// 返回状态码
status := c.Writer.Status()

// 如果状态码大于等于400,说明有错误发生
if status >= 400 {
// 记录日志
gin.DefaultErrorWriter.Write([]byte(fmt.Sprintf(
"%s - [%d] %s %s %s - %s - %s \n",
time.Now().Format("2006/01/02 15:04:05"),
status,
c.Request.Method,
c.Request.URL.Path,
c.Request.Proto,
c.Request.UserAgent(),
c.ClientIP(),
)))

// 打印堆栈信息
debug.PrintStack()
}
}
}

ErrorLogMiddleware定义了一个处理错误日志的中间件,然后我们将其注册到Gin应用中,并注释掉其他日志中间件,在main.go中代码如下。
1
2
3
4
5
6
...
// 注册日志中间件
//app.Use(middlewares.LogMiddleware())
//app.Use(middlewares.NonLoginLogMiddleware())
app.Use(middlewares.ErrorLogMiddleware())
...

在VS Code终端中运行go run main.go,再在Web服务中URL路径中输入/me,回到终端,可以看到如下结果。

从上面的日志可以看到,除了有一行HTTP请求的信息以外,下面还有一些堆栈信息,而这个堆栈信息对于调试代码和定位问题来说很有帮助,可以帮助开发者快速找到问题代码。

写入日志文件

在生产环境中,日志通常是通过文件形式保存的,上面终端的输出方式叫标准输出(Standard Output),可以让开发者很方便的看到实时调试信息。但标准输出有个重要问题,那就是无法长时间保存,而且会随着终端缓存刷新而丢失掉。因此,我们需要将日志持久化的保存下来,而最简单的方法就是写入日志文件。

下面,我们将实现一个文件写入的中间件。

在VS Code中打开middlewares/log.go,添加一个新方法,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...

// getLogWriter 获取日志文件写入器
func getLogWriter() io.Writer {
logWriter, err := os.OpenFile("gin.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(err)
}
return logWriter
}

// FileLogMiddleware 日志写入文件中间件
func FileLogMiddleware() gin.HandlerFunc {
return gin.LoggerWithConfig(gin.LoggerConfig{
Formatter: logFormatter,
Output: getLogWriter(),
})
}

这里,我们再次调用了gin.LoggerWithConfig这个方法,不过我们传入了Output这个参数,即要写入的日志文件写入器。

在main.go中,注册FileLogMiddleware,注释掉其他日志中间件。

启动应用,并在Web服务中多请求几次/me或其他接口,可以看到左侧文件导航栏会出现gin.log文件,点击打开后可以看到日志已经写入了。

实验总结

通过本次实验,我们掌握了如何在Gin框架中配置日志记录、错误日志处理、写入日志文件等内容。这些都可以帮助我们在生产环境中配置好必要的日志记录逻辑,以允许我们在部署上线之后能够第一时间排查问题。

0%