[Go Package] 主流 Go 日志方案 logrus + rotatelogs + lfshook

3,033 阅读6分钟

一、综述

Gin自带的日志功能较弱,

func (logger *Logger) WithFields(fields Fields) *Entry
//Fields 的实际类型是 map[string]interface{} ,包含了自定义日志的各个字段名和字段值
  • 使用rotatelogs完成日志分割、日志定期清理、生成软链文件指向最新日志。核心函数:
func New(p string, options ...Option) (*RotateLogs, error)
  • 使用lfshook决定哪些级别的日志可以使用rotatelogs的切割设置,并决定输出格式(TEXT / JSON)。
    核心数据结构是一个 WriteMap,记录可用于输出的日志级别;核心函数是NewHook(),加载WriteMap和输出格式(TEXT /JSON),并返回一个logrus可挂载的hook
type WriterMap map[logrus.Level]io.Writer
func NewHook(output interface{}, formatter logrus.Formatter) *LfsHook

二、rotatelogslfshook的配合

loglogrus的一个实例

// 设置日志切割 rotatelogs
writer, _ := rotatelogs.New(
   filePath+"%Y%m%d.log",
   //在项目根目录下生成软链文件 latest_log.log 指向最新的日志文件。注意!!!必须在管理员权限下开终端启动。
   rotatelogs.WithLinkName(linkName),
   //日志最大保存时间
   rotatelogs.WithMaxAge(7*24*time.Hour),
   ////设置日志切割时间间隔(1天)(隔多久分割一次)
   rotatelogs.WithRotationTime(24*time.Hour),
)

// lfshook 决定哪些日志级别可用日志分割
writeMap := lfshook.WriterMap{
   logrus.PanicLevel: writer,
   logrus.FatalLevel: writer,
   logrus.ErrorLevel: writer,
   logrus.WarnLevel:  writer,
   logrus.InfoLevel:  writer,
   logrus.DebugLevel: writer,
}

// 配置 lfshook
hook := lfshook.NewHook(writeMap, &logrus.TextFormatter{
   // 设置日期格式
   TimestampFormat: "2006.01.02 - 15:04:05",
})

//为 logrus 实例添加自定义 hook
log.AddHook(hook)

三、logrus的使用

1.基本用法

logrus 可以取代自带的log,局部地使用

package main

import (
  log "github.com/sirupsen/logrus"
)

func main() {
  log.WithFields(log.Fields{
    "animal": "walrus",
  }).Info("A walrus appears")
}

上面代码执行后,标准输出上输出如下:

time="2022-08-22T15:42:22+08:00" level=info msg="A walrus appears" animal=walrus

 logrus可以通过简单的配置,来定义输出格式或者日志级别。

package main

import (
    "os"
    log "github.com/sirupsen/logrus"
)

func init() {
    // 设置日志格式为json格式
    log.SetFormatter(&log.JSONFormatter{})

    // 设置将日志输出到标准输出(默认的输出为stderr,标准错误)
    // 日志消息输出可以是任意的io.writer类型
    log.SetOutput(os.Stdout)

    // 设置日志级别为warn以上
    log.SetLevel(log.WarnLevel)
}

func main() {
    log.WithFields(log.Fields{
        "animal": "walrus",
        "size":   10,
    }).Info("A group of walrus emerges from the ocean")

    log.WithFields(log.Fields{
        "omg":    true,
        "number": 122,
    }).Warn("The group's number increased tremendously!")

    log.WithFields(log.Fields{
        "omg":    true,
        "number": 100,
    }).Fatal("The ice breaks!")
}

2.自定义Logger

如果想在一个应用里面向多个地方log,可以创建Logger实例。 logger是一种相对高级的用法,,对于一个大型项目, 往往需要一个全局的logrus实例,即logger对象来记录项目所有的日志。如:

package main

import (
    "github.com/sirupsen/logrus"
    "os"
)

// logrus提供了New()函数来创建一个logrus的实例.
// 项目中,可以创建任意数量的logrus实例.
var log = logrus.New()

func main() {
    // 为当前logrus实例设置消息的输出,同样地,
    // 可以设置logrus实例的输出到任意io.writer
    log.Out = os.Stdout

    // 为当前logrus实例设置消息输出格式为json格式.
    // 同样地,也可以单独为某个logrus实例设置日志级别和hook,这里不详细叙述.
    log.Formatter = &logrus.JSONFormatter{}

    log.WithFields(logrus.Fields{
        "animal": "walrus",
        "size":   10,
    }).Info("A group of walrus emerges from the ocean")
}

3.Fields用法

logrus推荐使用Fields来进行精细化的,结构化的信息记录,如下:

log.WithFields(log.Fields{
  "event": event,
  "topic": topic,
  "key": key,
}).Fatal("Failed to send event")

WithFields()可以规范使用者按照其提倡的方式记录日志。但是WithFields()依然是可选的,因为某些场景下只需记录一条简单的消息。

通常在一个应用中,都有一些固定的Field。比如在处理用户http请求时,上下文中,所有的日志都会有request_iduser_ip。为了避免每次记录日志都要使用

log.WithFields(log.Fields{“request_id”: request_id, “user_ip”: user_ip})

我们可以创建一个logrus.Entry实例,为这个实例设置默认Fields,在上下文中使用这个logrus.Entry实例记录日志即可。

entry := log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})
//↓ will log request_id and user_ip
entryr.Info("something happened on that request") 
entry.Warn("something not great happened")

4.Hook接口用法

logrus最令人心动的功能就是其可扩展的HOOK机制了,通过在初始化时为logrus添加hook,logrus可以实现各种扩展功能.

logrus的hook接口定义如下,其原理是每此写入日志时拦截,修改logrus.Entry.

// logrus在记录Levels()返回的日志级别的消息时会触发HOOK,
// 按照Fire方法定义的内容修改logrus.Entry.
type Hook interface {
    Levels() []Level
    Fire(*Entry) error
}

一个简单自定义hook如下,DefaultFieldHook定义会在所有级别的日志消息中加入默认字段appName=”myAppName”.

type DefaultFieldHook struct {
}

func (hook *DefaultFieldHook) Fire(entry *log.Entry) error {
    entry.Data["appName"] = "MyAppName"
    return nil
}

func (hook *DefaultFieldHook) Levels() []log.Level {
    return log.AllLevels
}

hook的使用也很简单,在初始化前调用log.AddHook(hook)添加相应的hook即可.

logrus官方仅仅内置了sysloghook. 此外,但Github也有很多第三方的hook可供使用,文末将提供一些第三方HOOK的连接.

4.1 Logrus-Hook-Email

email这里只需用NewMailAuthHook方法得到hook,再添加即可

func Email(){
    logger:= logrus.New()
    //parameter"APPLICATION_NAME", "HOST", PORT, "FROM", "TO"
    //首先开启smtp服务,最后两个参数是smtp的用户名和密码
    hook, err := logrus_mail.NewMailAuthHook("testapp", "smtp.163.com",25,"username@163.com","username@163.com","smtp_name","smtp_password")
    if err == nil {
        logger.Hooks.Add(hook)
    }
    //生成*Entry
    var filename="123.txt"
    contextLogger :=logger.WithFields(logrus.Fields{
        "file":filename,
        "content":  "GG",
    })
    //设置时间戳和message
    contextLogger.Time=time.Now()
    contextLogger.Message="这是一个hook发来的邮件"
    //只能发送Error,Fatal,Panic级别的log
    contextLogger.Level=logrus.FatalLevel

    //使用Fire发送,包含时间戳,message
    hook.Fire(contextLogger)
}

4.2 Logrus-Hook-Slack

安装slackrus github.com/johntdyer/slackrus

package main

import (
	logrus "github.com/sirupsen/logrus"
	"github.com/johntdyer/slackrus"
	"os"
)

func main() {

	logrus.SetFormatter(&logrus.JSONFormatter{})

	logrus.SetOutput(os.Stderr)

	logrus.SetLevel(logrus.DebugLevel)
	
	logrus.AddHook(&slackrus.SlackrusHook{
		HookURL:        "https://hooks.slack.com/services/abc123/defghijklmnopqrstuvwxyz",
		AcceptedLevels: slackrus.LevelThreshold(logrus.DebugLevel),
		Channel:        "#slack-testing",
		IconEmoji:      ":ghost:",
		Username:       "foobot",
	})

	logrus.Warn("warn")
	logrus.Info("info")
	logrus.Debug("debug")
}
  • HookURL: 填写slack web-hook地址
  • AcceptedLevels: 设置日志输出级别
  • Channel: 设置日志频道
  • Username: 设置需要@的用户名

四、Go Blog 项目中的一个完整的log中间件

package middleware

import (
   "fmt"
   "github.com/gin-gonic/gin"
   rotatelogs "github.com/lestrrat-go/file-rotatelogs"
   "github.com/rifflock/lfshook"
   "github.com/sirupsen/logrus"
   "os"
   "time"
)

func Logger() gin.HandlerFunc {
   log := logrus.New()

   // 设置输出文件
   filePath := "log/"
   linkName := "latest_log.log"
   //        打开指定处的文件,并指定权限为:可读可写,可创建
   src, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0755) //0755-> rwx r-x r-x linux知识
   if err != nil {
      fmt.Println("err:", err)
   }
   log.Out = src

   // 设置日志级别。低于 Debug 级别的 Trace 将不会被打印
   log.SetLevel(logrus.DebugLevel)

   // 设置日志切割 rotatelogs
   writer, _ := rotatelogs.New(
      filePath+"%Y%m%d.log",
      //在项目根目录下生成软链文件 latest_log.log 指向最新的日志文件。注意!!!必须在管理员权限下开终端启动。
      rotatelogs.WithLinkName(linkName),
      //日志最大保存时间
      rotatelogs.WithMaxAge(7*24*time.Hour),
      ////设置日志切割时间间隔(1天)(隔多久分割一次)
      rotatelogs.WithRotationTime(24*time.Hour),
   )

   // lfshook 决定哪些日志级别可用日志分割
   writeMap := lfshook.WriterMap{
      logrus.PanicLevel: writer,
      logrus.FatalLevel: writer,
      logrus.ErrorLevel: writer,
      logrus.WarnLevel:  writer,
      logrus.InfoLevel:  writer,
      logrus.DebugLevel: writer,
   }

   // 配置 lfshook
   hook := lfshook.NewHook(writeMap, &logrus.TextFormatter{
      // 设置日期格式
      TimestampFormat: "2006.01.02 - 15:04:05",
   })

   //为 logrus 实例添加自定义 hook
   log.AddHook(hook)
   return func(c *gin.Context) {
      // 一.配置所需的 Fields
      startTime := time.Now()
      c.Next()
      spendTime := time.Since(startTime).Milliseconds()
      ST := fmt.Sprintf("%d ms", spendTime) // 1.API 调用耗时
      hostName, err := os.Hostname()        // 2.主机名
      if err != nil {
         hostName = "unknown"
      }
      statusCode := c.Writer.Status()    // 3.状态码
      clientIP := c.ClientIP()           // 4.请求客户端的 IP
      userAgent := c.Request.UserAgent() // 5.用户代理,通常是某个浏览器。dev环境下是apipost
      dataSize := c.Writer.Size()        // 6.响应报文 body 的字节长度
      if dataSize < 0 {
         dataSize = 0
      }
      method := c.Request.Method   // 7.请求方法
      path := c.Request.RequestURI // 8.请求 URL

      // 二.从标准记录器创建一个条目,并向其中添加多个字段(隐式添加 log 本身的时间戳,信息等 fields )
      entry := log.WithFields(logrus.Fields{
         "HostName":  hostName,
         "Status":    statusCode,
         "SpendTime": ST,
         "IP":        clientIP,
         "UserAgent": userAgent,
         "Method":    method,
         "DataSize":  dataSize,
         "Path":      path,
      })

      // Errors 保存了使用当前context的所有中间件/handler 所产生的全部错误信息。
      // 源码注释: Errors is a list of errors attached to all the handlers/middlewares who used this context.
      // 三.将系统内部的错误 log 出去
      if len(c.Errors) > 0 {
         log.Error(c.Errors.ByType(gin.ErrorTypePrivate).String())
      }

      // 四.根据状态码决定打印 log 的等级
      if statusCode >= 500 {
         entry.Error()
      } else if statusCode >= 400 {
         entry.Warn()
      } else {
         entry.Info()
      }
   }
}

PS: 一些有用的blog