日誌是程式開發中必不可少的模組,同時也是日常運維定位故障的最重要環節之一。一般日誌類的操作包括日誌採集,日誌查詢,日誌監控、日誌統計等等。本文,我們將介紹日誌模組在Gin中的使用。
Golang如何列印日誌
- 日誌列印需要滿足幾個條件
- 重定向到日誌檔案
- 區分日誌級別,一般有
DEBUG
,INFO
,WARNING
,ERROR
,CRITICAL
- 日誌分割,按照日期分割或者按照大小分割
- Golang中使用
logrus
列印日誌
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中介軟體包括日誌、鑑權、鏈路跟蹤、異常捕捉等等
- 預設中介軟體
router := gin.Default()
檢視原始碼可以看到
// 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
func Middleware1(c *gin.Context) {
... // do something first
c.Next() // 繼續執行後續的中介軟體
// c.Abort() 不再執行後面的中介軟體
... // do something last
}
- 自定義中介軟體方式2
func Middleware2() gin.HandlerFunc {
return func(c *gin.Context) {
... // do something first
c.Next() // 繼續執行後續的中介軟體
// c.Abort() 不再執行後面的中介軟體
... // do something last
}
}
- 全域性使用中介軟體
route := gin.Default()
route.Use(Middleware1)
route.Use(Middleware2())
- 指定路由使用中介軟體
route := gin.Default()
route.Get("/test", Middleware1)
route.POST("/test", Middleware2())
- 多箇中介軟體執行順序
Gin裡面多箇中介軟體的執行順序是按照呼叫次序來執行的。
無論在全域性使用還是指定路由使用,Gin都支援多箇中介軟體順序執行
Gin中介軟體之日誌模組
- 模組程式碼
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)
}
}
}
- 啟用全域性日誌中介軟體
route := gin.New() // 不用預設的日誌中介軟體
route.Use(route_middleware.Logger())
非同步列印日誌
由於我們的日誌中介軟體使用的是全域性中介軟體,在高併發處理請求時日誌落地會導致大量的IO操作,這些操作會拖慢整個伺服器,所以我們需要使用非同步列印日誌
- 非同步函式
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)
}
}
}
- 呼叫方法
go logHandlerFunc()
... // 省略
logChannel <- logField
至此,我們完成了Gin中介軟體的介紹和日誌模組的設計,接下來,我們將使用更多的中介軟體,完善我們的Api服務。
Github 程式碼
請訪問 Gin-IPs 或者搜尋 Gin-IPs