gin框架下得logrus使用

3,460 阅读5分钟

logrus 库使用

  1. 简介

    Logrus 是 Go (golang) 的结构化记录器,API 与标准库记录器完全兼容。golang中的标准库中的log库,由于功能太过于简单,在大多数的时候不能满足我们现在的需求,主要还是提供的接口功能太过于简单了。Logrus的出现就是为了解决这个问题,它目前兼容标准的log库,还支持json和text文本的输出。

  2. 安装

    go get github.com/sirupsen/logrus

  3. 快速上手

    1. //创建一个实例
      log := *logrus.New()	
      //设置为json格式
      log.SetFormatter(&logrus.JSONFormatter{
         TimestampFormat: "2006-01-02 15:04:05",
      })
      //设置日志等级
      log.SetLevel(logrus.InfoLevel)
      //写入日志
      log.WithFields(logrus.Fields{
         "name": "一颗蛋蛋",
      }).Info("这里是logrus快速使用")
      
    2. logrus支持的日志等级

      1. Panic: 记录日志,然后panic
      2. Fatal: 有致命性错误,导致程序崩溃,记录日志,然后退出
      3. Error: 错误日志
      4. Warn: 警告日志
      5. Info: 核心流程日志
      6. Debug: debug日志(调试日志)
      7. Trace: 粒度超细的,一般情况下我们使用不上

      Lavel: Panic < Fatal < Error < Warn < Info < Debug < Trace

  4. 日志信息输出到文件中

    package tool
    
    import (
       "bytes"
       "fmt"
       "github.com/gin-gonic/gin"
       "github.com/google/uuid"
       "github.com/sirupsen/logrus"
       "os"
       "path"
       "time"
    )
    //定义一个结构体获取返回体的数据
    type bodyLogWriter struct {
       gin.ResponseWriter
       body *bytes.Buffer
    }
    //定义一个实例
    var loggerInfo logrus.Logger
    //设置用户uid,hook中使用的上
    var uid string
    
    func init() {
       setUid()
       Logger()
    }
    
    func Logger(){
    		//当前时间
       nowTime := time.Now()
       //获取日志文件存储的目录,这里我采用的是自己封装的一个获取配置文件的方法,可以看我上一篇viper获取配置信息的文章
       logFilePath := GetString("logrus.LOG_FILE_PATH")
       // 如果没有获取到配置文件的话那就是直接代码写死一个文件地址
       if len(logFilePath) <= 0 {
       		//获取当前目前的地址,也就是项目的根目录
          if dir, err := os.Getwd(); err == nil {
             logFilePath = dir + "/logs/"
          }
       }
       //创建文件夹
       if err := os.MkdirAll(logFilePath,os.ModePerm); err != nil {
          fmt.Println(err.Error())
       }
       //文件名称
       logFileName := nowTime.Format("2006-01-02") + ".log"
       //日志文件地址拼接
       fileName := path.Join(logFilePath, logFileName)
       //fmt.Println("文件名称:"+fileName)
       if _, err := os.Stat(fileName); err != nil {
          fmt.Println("检测文件:"+err.Error())
          _, err := os.Create(fileName)
          if err != nil {
             fmt.Println(err.Error())
          }
       }
       //打开文件
       src, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_APPEND,os.ModeAppend|os.ModePerm)
       if err != nil {
          fmt.Println("write file log error", err)
       }
       //实例化
       loggerInfo = *logrus.New()
       //设置输出
       loggerInfo.Out = src
       //这里我觉得应该是交给需要封装的方法去确认使用什么等级的日志和什么格式
       //设置日志级别
       //logger.SetLevel(logrus.InfoLevel)
       ////设置日志格式 json格式
       //logger.SetFormatter(&logrus.JSONFormatter{
       // TimestampFormat: "2006-01-02 15:04:05",
       //})
       loggerInfo.AddHook(&LogrusHook{})
       loggerInfo.SetFormatter(&logrus.JSONFormatter{
          TimestampFormat: "2006-01-02 15:04:05",
       })
    }
    //关键操作,核心流程的日志 
    func LogErrorInfoToFile(fields logrus.Fields)  {
       loggerInfo.SetLevel(logrus.InfoLevel)
       loggerInfo.WithFields(fields).Info()
    }
    //把二进制写入缓冲区
    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)
    }
    //获取返回体的中间件
    func GinBodyLogMiddleware() gin.HandlerFunc {
       return func(c *gin.Context) {
          blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
          c.Writer = blw
          c.Next()
          responseStr := blw.body.String()
          //开始时间
          startTime := time.Now()
          //结束时间
          endTime := time.Now()
          //执行时间
          latencyTime := endTime.Sub(startTime)
          //请求方式
          reqMethod := c.Request.Method
          //请求路由
          reqUri := c.Request.RequestURI
          // 状态码
          statusCode := c.Writer.Status()
          //请求ip
          clientIP := c.ClientIP()
          //请求参数
          reqParams := c.Request.Body
          //请求ua
          reqUa := c.Request.UserAgent()
          var resultBody logrus.Fields
          resultBody = make(map[string]interface{})
          resultBody["response"] = responseStr
          resultBody["requestUri"] = reqUri
          resultBody["clientIp"] = clientIP
          resultBody["body"] = reqParams
          resultBody["userAgent"] = reqUa
          resultBody["requestMethod"] = reqMethod
          resultBody["startTime"] = startTime
          resultBody["endTime"] = endTime
          resultBody["latencyTime"] = latencyTime
          resultBody["statusCode"] = statusCode
          LogErrorInfoToFile(resultBody)
    
          setUid()
       }
    }
    
    func setUid()  {
       uid = uuid.New().String()
    }
    
    func GetNewUid() string {
       return uid
    }
    

    由于我这里使用的是gin框架,所以我使用了一个中间件来记录日志,这里会把每次返回的结构体给写入到日志中。但是这里还有一个问题就是我们怎么区分一个请求过来到结束的日志,这个时候我们就要用上hook,请看下小节。

  5. hook

    ”hook是扩展日志的功能,在每次写入日志的时候就拦截,修改其中的entry。”

    基于此功能我们就可以往深处思考一下,这是不是我们可以在其中动动手脚,添加一下我们想要的数据进去。

    要实现这个想法我们必须先实现hook这个接口,logrus中定义了这么一个接口。

    这个是logrus中定义的接口
    
    package logrus
    
    // A hook to be fired when logging on the logging levels returned from
    // `Levels()` on your implementation of the interface. Note that this is not
    // fired in a goroutine or a channel with workers, you should handle such
    // functionality yourself if your call is non-blocking and you don't wish for
    // the logging calls for levels returned from `Levels()` to block.
    type Hook interface {
       Levels() []Level
       Fire(*Entry) error
    }
    
    // Internal type for storing the hooks on a logger instance.
    type LevelHooks map[Level][]Hook
    
    // Add a hook to an instance of logger. This is called with
    // `log.Hooks.Add(new(MyHook))` where `MyHook` implements the `Hook` interface.
    func (hooks LevelHooks) Add(hook Hook) {
       for _, level := range hook.Levels() {
          hooks[level] = append(hooks[level], hook)
       }
    }
    
    // Fire all the hooks for the passed level. Used by `entry.log` to fire
    // appropriate hooks for a log entry.
    func (hooks LevelHooks) Fire(level Level, entry *Entry) error {
       for _, hook := range hooks[level] {
          if err := hook.Fire(entry); err != nil {
             return err
          }
       }
    
       return nil
    }
    

    所以我们就要实现这个接口,我们疯转一下属于我们自己的方法

    package tool
    
    import (
       "github.com/sirupsen/logrus"
    )
    
    type LogrusHook struct {
    }
    
    //设置所有的日志等级都走这个钩子
    func (hook *LogrusHook) Levels() []logrus.Level {
       return logrus.AllLevels
    }
    //修改其中的数据,或者进行其他操作
    func (hook *LogrusHook) Fire(entry *logrus.Entry) error {
       entry.Data["request_id"] = GetNewUid()
       return nil
    }
    

    这里我是修改了data中的数据,往其中增加了一个请求的ID,这个ID在当前请求下是唯一的,这样我们就可以保证请求统一记录,这样也方便我们查找日志。

  6. 扩展 错误日志记录,获取错误信息所在的文件以及行数

    func getErrorFileAndLine(err error) {
       //获取上层运行时的文件以及行数
       for skip := 1; true; skip++ {
          //获取上层运行时的文件以及行数
          _, file, line, ok := runtime.Caller(skip)
          if ok {
             var resultBody logrus.Fields
             resultBody = make(map[string]interface{})
             resultBody["file_path"] = file
             resultBody["error_line"] = line
             resultBody["error_message"] = err.Error()
             LogErrorInfoToFile(resultBody)
          }else {
             break
          }
       }
    }
    

    这里最主要的runtime.Caller这个方法,这里采用的是死循环的方式去获取,这样就可以获取到从请求进来到报错时候记录。

  7. 总的来说这个还是不够深入,logrus和hook还是日志切割或者runtime.Caller这些都是可以优化点,目前我只是做到了可以用,但没去优化。