Go Web輕量級框架Gin學習系列:HTTP請求日誌

張君鴻發表於2019-05-09

我們知道,使用者向伺服器發起的每一次Web請求,都會通過HTTP協議頭部或Body攜帶許多的請求元資訊給伺服器,如請求的URL地址,請求方法,請求頭部和請求IP地址等等諸多原始資訊,而在Gin框架中,我們可以使用日誌的方式記錄和輸出這些資訊,記錄使用者的每一次請求行為。

下面是一條Gin框架在控制檯中輸出的日誌:

[GIN] 2019/05/04 - 22:08:56 | 200 |      5.9997ms |             ::1 | GET      /test
複製程式碼

好了,下面看看要如何輸出上面的日誌吧!

日誌中件間

在Gin框架中,要輸出使用者的http請求日誌,最直接簡單的方式就是藉助日誌中介軟體,下面Gin框架的中介軟體定義:

func Logger() HandlerFunc
複製程式碼

所以,當我們使用下面的程式碼建立一個gin.Engine時,會在控制檯中使用者的請求日誌:

router := gin.Default()
複製程式碼

而使用下面的程式碼建立gin.Engine時,則不會在控制檯輸出使用者的請求日誌:

router := gin.New()
複製程式碼

這是為什麼呢?這是由於使用Default()函式建立的gin.Engine例項預設使用了日誌中件間gin.Logger(),所以,當我們使用第二種方式建立gin.Engine時,可以呼叫gin.Engine中的Use()方法呼叫gin.Logger(),如下:

router := gin.New()
router.Use(gin.Logger())
複製程式碼

在控制檯輸出日誌

Gin框架請求日誌預設是在我們執行程式的控制檯中輸出,而且輸出的日誌中有些字型有標顏色,如下圖所示:

Go Web輕量級框架Gin學習系列:HTTP請求日誌

當然,我們可以使用DisableConsoleColor()函式禁用控制檯日誌的顏色輸出,程式碼如下所示

gin.DisableConsoleColor()//禁用請求日誌控制檯字型顏色

router := gin.Default()
router.GET("test",func(c *gin.Context){
    c.JSON(200,"test")
})
複製程式碼

執行後發出Web請求,在控制檯輸出日誌字型則沒有顏色:

Go Web輕量級框架Gin學習系列:HTTP請求日誌

雖然Gin框架預設是開始日誌字型顏色的,但可以使用DisableConsoleColor()函式來禁用,但當被禁用後,在程式中執行需要重新開啟控制檯日誌的字型顏色輸出時,可以使用ForceConsoleColor()函式重新開啟,使用如下:

gin.ForceConsoleColor()
複製程式碼

在檔案輸出日誌

Gin框架的請求日誌預設在控制檯輸出,但更多的時候,尤其上線執行時,我們希望將使用者的請求日誌儲存到日誌檔案中,以便更好的分析與備份。

1. DefaultWriter

在Gin框架中,通過gin.DefaultWriter變數可能控制日誌的儲存方式,gin.DefaultWriter在Gin框架中的定義如下:

var DefaultWriter io.Writer = os.Stdout
複製程式碼

從上面的定義我們可以看出,gin.DefaultWriter的型別為io.Writer,預設值為os.Stdout,即控制檯輸出,因此我們可以通過修改gin.DefaultWriter值來將請求日誌儲存到日誌檔案或其他地方(比如資料庫)。

package main
import (
    "github.com/gin-gonic/gin"
    "io"
    "os"
)
func main() {
    gin.DisableConsoleColor()//儲存到檔案不需要顏色
    file, _ := os.Create("access.log")
    gin.DefaultWriter = file
    //gin.DefaultWriter = io.MultiWriter(file) 效果是一樣的
    router := gin.Default()
    router.GET("/test", func(c *gin.Context) {
        c.String(200, "test")
    })
    _ = router.Run(":8080")
}
複製程式碼

執行後上面的程式,會在程式所在目錄建立access.log檔案,當我們發起Web請求後,請求的日誌會儲存到access.log檔案,而不會在控制檯輸出。

通過下面的程式碼,也可能讓請求日誌同行儲存到檔案和在控制檯輸出:

file, _ := os.Create("access.log")
gin.DefaultWriter = io.MultiWriter(file,os.Stdout) //同時儲存到檔案和在控制檯中輸出
複製程式碼

2. LoggerWithWriter

另外,我們可以使用gin.LoggerWithWriter中介軟體,其定義如下:

func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc
複製程式碼

示例程式碼:

package main

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

func main() {
    gin.DisableConsoleColor()
    router := gin.New()
    file, _ := os.Create("access.log")
    router.Use(gin.LoggerWithWriter(file,""))
    router.GET("test", func(c *gin.Context) {
        c.JSON(200,"test")
    })
    _ = router.Run()
}
複製程式碼

gin.LoggerWithWriter中介軟體的第二個引數,可以指定哪個請求路徑不輸出請求日誌,例如下面程式碼,/test請求不會輸出請求日誌,而/ping請求日誌則會輸出請求日誌。

router.Use(gin.LoggerWithWriter(file,"/test"))//指定/test請求不輸出日誌
router.GET("test", func(c *gin.Context) {
    c.JSON(200,"test")
})
router.GET("ping", func(c *gin.Context) {
    c.JSON(200,"pong")
})
複製程式碼

定製日誌格式

1. LogFormatterParams

上面的例子,我們都是採用Gin框架預設的日誌格式,但預設格式可能並不能滿足我們的需求,所以,我們可以使用Gin框架提供的gin.LoggterWithFormatter()中介軟體,定製日誌格式,gin.LoggterWithFormatter()中介軟體的定義如下:

func LoggerWithFormatter(f LogFormatter) HandlerFunc
複製程式碼

gin.LoggterWithFormatter()中介軟體的定義可以看到該中介軟體的接受一個資料型別為LogFormatter的引數,LogFormatter定義如下:

type LogFormatter func(params LogFormatterParams) string
複製程式碼

LogFormatter的定義看到該型別為func(params LogFormatterParams) string的函式,其引數是為LogFormatterParams,其定義如下:

type LogFormatterParams struct {
    Request *http.Request
    TimeStamp time.Time
    StatusCode int
    Latency time.Duration
    ClientIP string
    Method string
    Path string
    ErrorMessage string
    BodySize int
    Keys map[string]interface{}
}
複製程式碼

定製日誌格式示例程式碼:

func main() {
    router := gin.New()
    router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
        //定製日誌格式
        return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
            param.ClientIP,
            param.TimeStamp.Format(time.RFC1123),
            param.Method,
            param.Path,
            param.Request.Proto,
            param.StatusCode,
            param.Latency,
            param.Request.UserAgent(),
            param.ErrorMessage,
        )
    }))
    router.Use(gin.Recovery())
	router.GET("/ping", func(c *gin.Context) {
        c.String(200, "pong")
    })
    _ = router.Run(":8080")
}
複製程式碼

執行上面的程式後,發起Web請求,控制檯會輸出以下格式的請求日誌:

::1 - [Wed, 08 May 2019 21:53:17 CST] "GET /ping HTTP/1.1 200 1.0169ms "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36" "
複製程式碼

2. LoggerWithConfig

在前面的例子中,我們使用gin.Logger()開啟請求日誌、使用gin.LoggerWithWriter將日誌寫到檔案中,使用gin.LoggerWithFormatter定製日誌格式,而實際上,這三個中介軟體,其底層都是呼叫gin.LoggerWithConfig中介軟體,也就說,我們使用gin.LoggerWithConfig中介軟體,便可以完成上述中介軟體所有的功能,gin.LoggerWithConfig的定義如下:

func LoggerWithConfig(conf LoggerConfig) HandlerFunc
複製程式碼

gin.LoggerWithConfig中介軟體的引數為LoggerConfig結構,該結構體定義如下:

type LoggerConfig struct {
    // 設定日誌格式
    // 可選 預設值為:gin.defaultLogFormatter
    Formatter LogFormatter

    // Output用於設定日誌將寫到哪裡去
    // 可選. 預設值為:gin.DefaultWriter.
    Output io.Writer

    // 可選,SkipPaths切片用於定製哪些請求url不在請求日誌中輸出.
    SkipPaths []string
}
複製程式碼

以下例子演示如何使用gin.LoggerConfig達到日誌格式、輸出日誌檔案以及忽略某些路徑的用法:

func main() {
    router := gin.New()
    file, _ := os.Create("access.log")
    c := gin.LoggerConfig{
        Output:file,
        SkipPaths:[]string{"/test"},
        Formatter: func(params gin.LogFormatterParams) string {
            return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
                params.ClientIP,
                params.TimeStamp.Format(time.RFC1123),
                params.Method,
                params.Path,
                params.Request.Proto,
                params.StatusCode,
                params.Latency,
                params.Request.UserAgent(),
                params.ErrorMessage,
            )
        },
    }
    router.Use(gin.LoggerWithConfig(c))
    router.Use(gin.Recovery())
    router.GET("/ping", func(c *gin.Context) {
        c.String(200, "pong")
    }) 
    router.GET("/test", func(c *gin.Context) {
        c.String(200, "test")
    })
    _ = router.Run(":8080")
}
複製程式碼

執行上面的程式後,發起Web請求,控制檯會輸出以下格式的請求日誌:

::1 - [Wed, 08 May 2019 22:39:43 CST] "GET /ping HTTP/1.1 200 0s "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36" "
::1 - [Wed, 08 May 2019 22:39:46 CST] "GET /ping HTTP/1.1 200 0s "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36" "
複製程式碼

小結

每條HTTP請求日誌,都對應一次使用者的請求行為,記錄每一條使用者請求日誌,對於我們追蹤使用者行為,過濾使用者非法請求,排查程式執行產生的各種問題至關重要,因此,開發Web應用時一定要記錄使用者請求行為,並且定時分析過濾。

相關文章