1. 程式人生 > >【Gin-API系列】Gin中介軟體之日誌模組(四)

【Gin-API系列】Gin中介軟體之日誌模組(四)

日誌是程式開發中必不可少的模組,同時也是日常運維定位故障的最重要環節之一。一般日誌類的操作包括日誌採集,日誌查詢,日誌監控、日誌統計等等。本文,我們將介紹日誌模組在Gin中的使用。 ## Golang如何列印日誌 * 日誌列印需要滿足幾個條件 1. 重定向到日誌檔案 2. 區分日誌級別,一般有`DEBUG`,`INFO`,`WARNING`,`ERROR`,`CRITICAL` 3. 日誌分割,按照日期分割或者按照大小分割 * Golang中使用`logrus`列印日誌 ```golang var LevelMap = map[string]logrus.Level{ "DEBUG": logrus.DebugLevel, "ERROR": logrus.ErrorLevel, "WARN": logrus.WarnLevel, "INFO": logrus.InfoLevel, } // 建立 @filePth: 如果路徑不存在會建立 @fileName: 如果存在會被覆蓋 @std: os.stdout/stderr 標準輸出和錯誤輸出 func New(filePath string, fileName string, level string, std io.Writer, count uint) (*logrus.Logger, error) { if _, err := os.Stat(filePath); os.IsNotExist(err) { if err := os.MkdirAll(filePath, 755); err != nil { return nil, err } } fn := path.Join(filePath, fileName) logger := logrus.New() //timeFormatter := &logrus.TextFormatter{ // FullTimestamp: true, // TimestampFormat: "2006-01-02 15:04:05.999999999", //} logger.SetFormatter(&logrus.JSONFormatter{ TimestampFormat: "2006-01-02 15:04:05.999999999", }) // 設定日誌格式為json格式 if logLevel, ok := LevelMap[level]; !ok { return nil, errors.New("log level not found") } else { logger.SetLevel(logLevel) } //logger.SetFormatter(timeFormatter) /* 根據檔案大小分割日誌 // import "gopkg.in/natefinch/lumberjack.v2" logger := &lumberjack.Logger{ // 日誌輸出檔案路徑 Filename: "D:\\test_go.log", // 日誌檔案最大 size, 單位是 MB MaxSize: 500, // megabytes // 最大過期日誌保留的個數 MaxBackups: 3, // 保留過期檔案的最大時間間隔,單位是天 MaxAge: 28, //days // 是否需要壓縮滾動日誌, 使用的 gzip 壓縮 Compress: true, // disabled by default } */ if 0 == count { count = 90 // 0的話則是預設保留90天 } logFd, err := rotatelogs.New( fn+".%Y-%m-%d", // rotatelogs.WithLinkName(fn), //rotatelogs.WithMaxAge(time.Duration(24*count)*time.Hour), rotatelogs.WithRotationTime(time.Duration(24)*time.Hour), rotatelogs.WithRotationCount(count), ) if err != nil { return nil, err } defer func() { _ = logFd.Close() // don't need handle error }() if nil != std { logger.SetOutput(io.MultiWriter(logFd, std)) // 設定日誌輸出 } else { logger.SetOutput(logFd) // 設定日誌輸出 } // logger.SetReportCaller(true) // 測試環境可以開啟,生產環境不能開,會增加很大開銷 return logger, nil } ``` ## Gin中介軟體介紹 Gin中介軟體的是Gin處理Http請求的一個模組或步驟,也可以理解為Http攔截器。 > 我們將Http請求拆分為四個步驟 1、伺服器接到客戶端的Http請求 2、伺服器解析Http請求進入到路由轉發系統 3、伺服器根據實際路由執行操作並得到結果 4、伺服器返回結果給客戶端 Gin中介軟體的執行包括2個部分(first和last),分佈對應的就是在步驟`1-2`之間(first)和`3-4`之間(last)的操作。常見的Gin中介軟體包括日誌、鑑權、鏈路跟蹤、異常捕捉等等 * 預設中介軟體 ```golang router := gin.Default() ``` 檢視原始碼可以看到 ```golang // Default returns an Engine instance with the Logger and Recovery middleware already attached. func Default() *Engine { debugPrintWARNINGDefault() engine := New() engine.Use(Logger(), Recovery()) // 包含 Logger、Recovery 中介軟體 return engine } ``` * 自定義中介軟體方式1 ```golang func Middleware1(c *gin.Context) { ... // do something first c.Next() // 繼續執行後續的中介軟體 // c.Abort() 不再執行後面的中介軟體 ... // do something last } ``` * 自定義中介軟體方式2 ```golang func Middleware2() gin.HandlerFunc { return func(c *gin.Context) { ... // do something first c.Next() // 繼續執行後續的中介軟體 // c.Abort() 不再執行後面的中介軟體 ... // do something last } } ``` * 全域性使用中介軟體 ```golang route := gin.Default() route.Use(Middleware1) route.Use(Middleware2()) ``` * 指定路由使用中介軟體 ```golang route := gin.Default() route.Get("/test", Middleware1) route.POST("/test", Middleware2()) ``` * 多箇中間件執行順序 > Gin裡面多箇中間件的執行順序是按照呼叫次序來執行的。 無論在全域性使用還是指定路由使用,Gin都支援多箇中間件順序執行 ## Gin中介軟體之日誌模組 * 模組程式碼 ```golang 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) } func (w BodyLogWriter) WriteString(s string) (int, error) { w.body.WriteString(s) return w.ResponseWriter.WriteString(s) } var SnowWorker, _ = uuid.NewSnowWorker(100) // 隨機生成一個uuid,100是節點的值(隨便給一個) // 列印日誌 func Logger() gin.HandlerFunc { accessLog, _ := mylog.New( configure.GinConfigValue.AccessLog.Path, configure.GinConfigValue.AccessLog.Name, configure.GinConfigValue.AccessLog.Level, nil, configure.GinConfigValue.AccessLog.Count) detailLog, _ := mylog.New( configure.GinConfigValue.DetailLog.Path, configure.GinConfigValue.DetailLog.Name, configure.GinConfigValue.DetailLog.Level, nil, configure.GinConfigValue.DetailLog.Count) return func(c *gin.Context) { var buf bytes.Buffer tee := io.TeeReader(c.Request.Body, &buf) requestBody, _ := ioutil.ReadAll(tee) c.Request.Body = ioutil.NopCloser(&buf) user := c.Writer.Header().Get("X-Request-User") bodyLogWriter := &BodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer} c.Writer = bodyLogWriter start := time.Now() c.Next() responseBody := bodyLogWriter.body.Bytes() response := route_response.Response{} if len(responseBody) > 0 { _ = json.Unmarshal(responseBody, &response) } end := time.Now() responseTime := float64(end.Sub(start).Nanoseconds()) / 1000000.0 // 納秒轉毫秒才能保留小數 logField := map[string]interface{}{ "user": user, "uri": c.Request.URL.Path, "start_timestamp": start.Format("2006-01-02 15:04:05"), "end_timestamp": end.Format("2006-01-02 15:04:05"), "server_name": c.Request.Host, "server_addr": fmt.Sprintf("%s:%d", configure.GinConfigValue.ApiServer.Host, configure.GinConfigValue.ApiServer.Port), // 無法動態讀取 "remote_addr": c.ClientIP(), "proto": c.Request.Proto, "referer": c.Request.Referer(), "request_method": c.Request.Method, "response_time": fmt.Sprintf("%.3f", responseTime), // 毫秒 "content_type": c.Request.Header.Get("Content-Type"), "status": c.Writer.Status(), "user_agent": c.Request.UserAgent(), "trace_id": SnowWorker.GetId(), } accessLog.WithFields(logField).Info("Request Finished") detailLog.WithFields(logField).Info(c.Request.URL) detailLog.WithFields(logField).Info(string(requestBody)) // 不能列印GET請求引數 if response.Code != configure.RequestSuccess { detailLog.WithFields(logField).Errorf("code=%d, message=%s", response.Code, response.Message) } else { detailLog.WithFields(logField).Infof("total=%d, page_size=%d, page=%d, size=%d", response.Data.Total, response.Data.PageSize, response.Data.Page, response.Data.Size) } } } ``` * 啟用全域性日誌中介軟體 ```golang route := gin.New() // 不用預設的日誌中介軟體 route.Use(route_middleware.Logger()) ``` ## 非同步列印日誌 > 由於我們的日誌中介軟體使用的是全域性中介軟體,在高併發處理請求時日誌落地會導致大量的IO操作,這些操作會拖慢整個伺服器,所以我們需要使用非同步列印日誌 * 非同步函式 ```golang var logChannel = make(chan map[string]interface{}, 300) func logHandlerFunc() { accessLog, _ := mylog.New( configure.GinConfigValue.AccessLog.Path, configure.GinConfigValue.AccessLog.Name, configure.GinConfigValue.AccessLog.Level, nil, configure.GinConfigValue.AccessLog.Count) detailLog, _ := mylog.New( configure.GinConfigValue.DetailLog.Path, configure.GinConfigValue.DetailLog.Name, configure.GinConfigValue.DetailLog.Level, nil, configure.GinConfigValue.DetailLog.Count) for logField := range logChannel { var ( msgStr string levelStr string detailStr string ) if msg, ok := logField["msg"]; ok { msgStr = msg.(string) delete(logField, "msg") } if level, ok := logField["level"]; ok { levelStr = level.(string) delete(logField, "level") } if detail, ok := logField["detail"]; ok { detailStr = detail.(string) delete(logField, "detail") } accessLog.WithFields(logField).Info("Request Finished") if "info" == levelStr { detailLog.WithFields(logField).Info(detailStr) detailLog.WithFields(logField).Info(msgStr) } else { detailLog.WithFields(logField).Error(detailStr) detailLog.WithFields(logField).Error(msgStr) } } } ``` * 呼叫方法 ```golang go logHandlerFunc() ... // 省略 logChannel <- logField ``` 至此,我們完成了Gin中介軟體的介紹和日誌模組的設計,接下來,我們將使用更多的中介軟體,完善我們的Api服務。 ## Github 程式碼 > 請訪問 [Gin-IPs](https://github.com/AutoBingo/Gin-IPs.git) 或者搜尋