golang-gin 日志库&Logrus介绍

3,819 阅读9分钟

日志库

Gin自带日志系统

gin自带的日志系统使用起来不是特别方便,现在流行使用logrus和zap,这里对gin自带日志系统作简要介绍

写日志文件

 func main() {
     // 禁用控制台颜色
     gin.DisableConsoleColor()
 ​
     // 创建记录日志的文件
     f, _ := os.Create("gin.log")
     gin.DefaultWriter = io.MultiWriter(f)
 ​
     // 如果需要将日志同时写入文件和控制台,请使用以下代码
     // gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
 ​
     router := gin.Default()
     router.GET("/ping", func(c *gin.Context) {
         c.String(200, "pong")
     })
 ​
     router.Run(":8080")
 }

自定义日志格式

 func main() {
     router := gin.New()
 ​
     // LoggerWithFormatter 中间件会将日志写入 gin.DefaultWriter
     // By default gin.DefaultWriter = os.Stdout
     router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
 ​
         // 你的自定义格式
         return fmt.Sprintf("[wan] %s - [%s] "  %s %s %s | %s | %s | %s %d %s | %s " || %s || " %s"\n",
             param.ClientIP,
             param.TimeStamp.Format(time.RFC1123),
             param.MethodColor(), param.Method, param.ResetColor(),
             param.Path,
             param.Request.Proto,
             param.StatusCodeColor(), param.StatusCode, param.ResetColor(), // 设置颜色
             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")
 }

输出示例:

 [wan] ::1 - [Fri, 07 Apr 2023 19:37:01 CST] "  GET | /ping | HTTP/1.1 | 200 | 0s " || PostmanRuntime/7.31.3 || " "

logrus

下载

 go get github.com/sirupsen/logrus

logrus常用方法

 logrus.Debug("Debugln")     logrus.Debugf("Debugln:%s", "any..")    logrus.Debugln("Debugln")
 logrus.Info("Infoln")       logrus.Infof("Infoln:%s", "any..")      logrus.Infoln("Infoln")
 logrus.Warn("Warnln")       logrus.Warnf("Warnln:%s", "any..")      logrus.Warnln("Warnln")
 logrus.Error("Errorln")     logrus.Errorf("Errorln:%s", "any..")    logrus.Errorln("Errorln")       
 logrus.Print("Println")     logrus.Printf("Println:%s", "any..")    logrus.Println("Println")
 ​
 // 输出如下
 time="2022-12-17T14:02:01+08:00" level=info msg=Infoln   
 time="2022-12-17T14:02:01+08:00" level=warning msg=Warnln
 time="2022-12-17T14:02:01+08:00" level=error msg=Errorln 
 time="2022-12-17T14:02:01+08:00" level=info msg=Println

debug的没有输出,是因为logrus默认的日志输出等级是 info

 fmt.Println(logrus.GetLevel())  // info

日志等级

 PanicLevel  // 会抛一个异常
 FatalLevel  // 打印日志之后就会退出
 ErrorLevel
 WarnLevel
 InfoLevel
 DebugLevel
 TraceLevel  // 低级别

如果你想显示Debug的日志,那么你可以更改日志显示等级

 logrus.SetLevel(logrus.DebugLevel)

日志级别一般是和系统环境挂钩,例如开发环境,肯定就要显示debug信息,测试环境也是需要的

线上环境就不需要这些日志,可能只显示warnning的日志

设置特定字段

 log1 := logrus.WithField("project", "study")
 log1.Errorln("hello")
 // 输出:time="2022-12-17T15:02:28+08:00" level=error msg=hello project=study
 ​
 log1 := logrus.WithField("project", "study").WithField("project2", "study2")    // 链式引用
 log1.Errorln("hello")
 // 输出:time="2022-12-17T15:02:28+08:00" level=error msg=hello project=study project2=study2
 ​
 ​
 log2 := logrus.WithFields(logrus.Fields{
     "func": "main",
     "auth": "枫枫",
 })
 log2.Warningf("你好")
 // 输出:time="2022-12-17T15:02:28+08:00" level=warning msg="你好" func=main auth="枫枫"
 ​
 log3 := log2.WithFields(logrus.Fields{  // 链式引用
     "auth2": "枫枫2",
 })
 log3.Warnln("你好")
 // 输出:time="2022-12-17T15:02:28+08:00" level=warning msg="你好" func=main auth="枫枫" auth2="枫枫2"

通常,在一个应用中、或者应用的一部分中,都有一些固定的Field。比如在处理用户http请求时,上下文中,所有的日志都会有request_id和user_ip。

为了避免每次记录日志都要使用log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip}),我们可以创建一个logrus.Entry实例,为这个实例设置默认Fields,在上下文中使用这个logrus.Entry实例记录日志即可。

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

日志条目

除了使用WithFieldWithFields添加的字段外,一些字段会自动添加到所有日志记录事中:

  • time:记录日志时的时间戳
  • msg:记录的日志信息
  • level:记录的日志级别

显示样式 Text和Json

默认的是以text的形式展示,也可以设置为json形式展示,如下:

 logrus.SetFormatter(&logrus.JSONFormatter{})
 log1 := logrus.WithField("project", "study")
 log1.Errorln("hello")
 // {"level":"error","msg":"hello","project":"study","time":"2022-12-17T15:08:24+08:00"}

在设置显示样式的时候还可以进行其他的一些配置:

 ForceColors:是否强制使用颜色输出。
 DisableColors:是否禁用颜色输出。
 ForceQuote:是否强制引用所有值。
 DisableQuote:是否禁用引用所有值。
 DisableTimestamp:是否禁用时间戳记录。
 FullTimestamp:是否在连接到 TTY 时输出完整的时间戳。
 TimestampFormat:用于输出完整时间戳的时间戳格式。
 func main() {
     logrus.SetFormatter(&logrus.TextFormatter{
         ForceColors:     true,
         TimestampFormat: "2006-01-02 15:04:05",
         FullTimestamp:   true,
     })
     logrus.Debugln("Debugln")
     logrus.Infoln("Infoln")
     logrus.Warnln("Warnln")
     logrus.Errorln("Errorln")
     logrus.Println("Println")
 }

输出结果:

image-20230407215027501.png

如果以上这两个格式不满足需求,可以自己动手实现接口 Formatter 接口来定义自己的日志格式。

Tip:关于颜色设置的一些知识

 // 前景色
 fmt.Println("\033[30m 前景颜色展示:黑色 \033[0m")
 fmt.Println("\033[31m 前景颜色展示:红色 \033[0m")
 fmt.Println("\033[32m 前景颜色展示:绿色 \033[0m")
 fmt.Println("\033[33m 前景颜色展示:黄色 \033[0m")
 fmt.Println("\033[34m 前景颜色展示:蓝色 \033[0m")
 fmt.Println("\033[35m 前景颜色展示:紫色 \033[0m")
 fmt.Println("\033[36m 前景颜色展示:青色 \033[0m")
 fmt.Println("\033[37m 前景颜色展示:灰色 \033[0m")
 // 背景色
 fmt.Println("\033[40m 背景颜色展示:黑色 \033[0m")
 fmt.Println("\033[41m 背景颜色展示:红色 \033[0m")
 fmt.Println("\033[42m 背景颜色展示:绿色 \033[0m")
 fmt.Println("\033[43m 背景颜色展示:黄色 \033[0m")
 fmt.Println("\033[44m 背景颜色展示:蓝色 \033[0m")
 fmt.Println("\033[45m 背景颜色展示:紫色 \033[0m")
 fmt.Println("\033[46m 背景颜色展示:青色 \033[0m")
 fmt.Println("\033[47m 背景颜色展示:灰色 \033[0m")

输出样式:

image-20230407215455889.png

自定义颜色输出函数:

 const (
     CBlack = iota
     CRed
     CGreen
     CYellow
     CBlue
     CPurple
     CCyan
     CGray
 )
 ​
 func PrintColor(colorCode int, text string, isBackGround bool) {
     if isBackGround {
         fmt.Printf("\033[4%dm %s \033[0m", colorCode, text)
         return
     }
     fmt.Printf("\033[3%dm %s \033[0m", colorCode, text)
 }
 ​
 func main() {
     PrintColor(CCyan, "随便写的", true)
     PrintColor(CRed, "随便写的", false)
 }

输出到日志文件

默认的输出是在控制台上

使用SetOutput(out io.Writer)函数即可将日志输出到文件上:

 file, _ := os.OpenFile("info.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
 logrus.SetOutput(file)

同时输出屏幕和文件

利用io.MultiWriter()实现写入复制

 func MultiWriter(writers ...Writer) Writer
 // 将多个writers对象合成一个Writer输出,其可以实现将写入复制到所有提供的writers中
 file, err := os.OpenFile("checkemstools.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
 //  同时写文件和屏幕
 fileAndStdoutWriter := io.MultiWriter(file, os.Stdout)
 logrus.SetOutput(fileAndStdoutWriter)

显示行号

 logrus.SetReportCaller(true)
 // 设置为true后可以显示行号和函数名,但开启这个模式会增加性能开销。

Hook接口用法

logrus最令人心动的功能就是其可扩展的HOOK机制了,通过在初始化时为logrus添加hook,logrus可以实现各种扩展功能。比如可以通过 Hooks 实现:Error 以上级别日志发送邮件通知、重要日志告警、日志切割、程序优雅退出等,非常实用。

hook的使用也很简单,在初始化前调用log.AddHook(hook)添加相应的hook即可。logrus官方仅仅内置了sysloghook。此外,但Github也有很多第三方的hook可供使用,文末将提供一些第三方HOOK链接.

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

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

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

 package main
 ​
 import "github.com/sirupsen/logrus"
 ​
 type DefaultFieldHook struct {
 }
 ​
 func (hook *DefaultFieldHook) Fire(entry *logrus.Entry) error {
     entry.Data["appName"] = "MyAppName"
     return nil
 }
 ​
 func (hook *DefaultFieldHook) Levels() []logrus.Level {
     return logrus.AllLevels
 }
 func main() {
     logrus.SetFormatter(&logrus.TextFormatter{
         ForceColors:     true,
         TimestampFormat: "2006-01-02 15:04:05",
         FullTimestamp:   true,
     })
     logrus.AddHook(&DefaultFieldHook{})
     logrus.Errorf("hello world")
     logrus.Infoln("hello world")
     logrus.Warnln("hello world")
 }

输出结果:

image-20230408141627498.png

示例2:将error级别的日志独立输出到error.log文件里,其他都放在一起。

 package main
 ​
 import (
     "fmt"
     "github.com/sirupsen/logrus"
     "os"
 )
 ​
 type MyHook struct {
     Writer *os.File
 }
 ​
 func (hook *MyHook) Fire(entry *logrus.Entry) error {
     line, err := entry.String()
     if err != nil {
         fmt.Fprintf(os.Stderr, "Unable to read entry, %v", err)
         return err
     }
     hook.Writer.Write([]byte(line))
     return nil
 }
 ​
 func (hook *MyHook) Levels() []logrus.Level {
     return []logrus.Level{logrus.ErrorLevel}
 }
 ​
 func main() {
     logrus.SetFormatter(&logrus.TextFormatter{
         ForceColors:     true,
         TimestampFormat: "2006-01-02 15:04:05",
         FullTimestamp:   true,
     })
     logrus.SetReportCaller(true) // 显示行号
     // 日志的打开格式是追加,所以不能用os.Create,需要自定义文件打开模式
     file, _ := os.OpenFile("err.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
     hook := &MyHook{Writer: file}
     logrus.AddHook(hook)
     logrus.Errorf("hello world")
     logrus.Infoln("hello world")
     logrus.Warnln("hello world")
 }

控制台输出:

image-20230408141503228.png

文件输出:

image-20230408141542354.png

日志分割

按照时间分割

利用file-rotatelogs 包实现日志切割

logrus 本身不支持日志轮转切割功能,需要配合 file-rotatelogs 包来实现,防止日志打满磁盘。file-rotatelogs 实现了 io.Writer 接口,并且提供了文件的切割功能,其实例可以作为 logrus 的目标输出,两者能无缝集成。

日志轮转相关函数: WithLinkName 为最新的日志建立软连接 WithRotationTime 设置日志分割的时间,隔多久分割一次 WithMaxAge 和 WithRotationCount二者只能设置一个

  • WithMaxAge 设置文件清理前的最长保存时间
  • WithRotationCount 设置文件清理前最多保存的个数

示例代码:

 package main
 ​
 import (
     "github.com/lestrrat-go/file-rotatelogs"
     "github.com/rifflock/lfshook"
     log "github.com/sirupsen/logrus"
     "time"
 )
 ​
 var logLevels = map[string]log.Level{
     "panic": log.PanicLevel,
     "fatal": log.FatalLevel,
     "error": log.ErrorLevel,
     "warn":  log.WarnLevel,
     "info":  log.InfoLevel,
     "debug": log.DebugLevel,
     "trace": log.TraceLevel,
 }
 ​
 func newLfsHook(logName string, logLevel string, maxRemainCnt uint) log.Hook {
     writer, err := rotatelogs.New(
         logName+"-%Y%m%d%H.log",
         // WithLinkName为最新的日志建立软连接,以方便随着找到当前日志文件
         rotatelogs.WithLinkName(logName),
 ​
         // WithRotationTime设置日志分割的时间,这里设置为一小时分割一次
         rotatelogs.WithRotationTime(time.Hour),
 ​
         // WithMaxAge和WithRotationCount二者只能设置一个,
         // WithMaxAge设置文件清理前的最长保存时间,
         // WithRotationCount设置文件清理前最多保存的个数.
         // rotatelogs.WithMaxAge(time.Hour*24),
         rotatelogs.WithRotationCount(maxRemainCnt),
     )
 ​
     if err != nil {
         log.Errorf("config local file system for logger error: %v", err)
     }
 ​
     // 判断输入的日志等级是否存在,不存在则给一个默认值
     if level, ok := logLevels[logLevel]; ok {
         log.SetLevel(level)
     } else {
         log.SetLevel(log.WarnLevel)
     }
     
     // 使用了lfshook软件包创建了一个新的日志钩子,该钩子将日志记录到指定的日志文件中。
     // lfshook.WriterMap指定了每个日志级别所使用的写入器(writer)。
     // 在这个函数中,所有的日志级别都使用同一个写入器writer。
     lfsHook := lfshook.NewHook(lfshook.WriterMap{
         log.DebugLevel: writer,
         log.InfoLevel:  writer,
         log.WarnLevel:  writer,
         log.ErrorLevel: writer,
         log.FatalLevel: writer,
         log.PanicLevel: writer,
     }, &log.TextFormatter{
         ForceColors:     true,
         TimestampFormat: "2006-01-02 15:04:05",
         FullTimestamp:   true,
     })
 ​
     return lfsHook
 }
 ​
 func main() {
     log.SetReportCaller(true) // 显示行号
     rotateHook := newLfsHook("./rotate", "debug", 3)
     log.AddHook(rotateHook)
     log.Errorf("hello world")
     log.Debugln("hello world")
     log.Infoln("hello world")
     log.Warnln("hello world")
 }

按日志等级分割

与前面hook接口用法的示例2类似。

 package main
 ​
 import (
 "fmt"
 "github.com/sirupsen/logrus"
 "os"
 )
 ​
 const (
     allLog  = "all"
     errLog  = "err"
     warnLog = "warn"
     infoLog = "info"
 )
 ​
 type FileLevelHook struct {
     file     *os.File
     errFile  *os.File
     warnFile *os.File
     infoFile *os.File
     logPath  string
 }
 ​
 func (hook FileLevelHook) Levels() []logrus.Level {
     return logrus.AllLevels
 }
 func (hook FileLevelHook) Fire(entry *logrus.Entry) error {
     line, _ := entry.String()
     switch entry.Level {
     case logrus.ErrorLevel:
         hook.errFile.Write([]byte(line))
     case logrus.WarnLevel:
         hook.warnFile.Write([]byte(line))
     case logrus.InfoLevel:
         hook.infoFile.Write([]byte(line))
     }
     hook.file.Write([]byte(line))
     return nil
 }
 ​
 func InitLevel(logPath string) {
     err := os.MkdirAll(fmt.Sprintf("%s", logPath), os.ModePerm)
     if err != nil {
         logrus.Error(err)
         return
     }
     allFile, err := os.OpenFile(fmt.Sprintf("%s/%s.log", logPath, allLog), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
     errFile, err := os.OpenFile(fmt.Sprintf("%s/%s.log", logPath, errLog), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
     warnFile, err := os.OpenFile(fmt.Sprintf("%s/%s.log", logPath, warnLog), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
     infoFile, err := os.OpenFile(fmt.Sprintf("%s/%s.log", logPath, infoLog), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
     fileHook := FileLevelHook{allFile, errFile, warnFile, infoFile, logPath}
     logrus.AddHook(&fileHook)
 }
 ​
 func main() {
     InitLevel("logrus_study/log_level")
     logrus.Errorln("你好")
     logrus.Errorln("err")
     logrus.Warnln("warn")
     logrus.Infof("info")
     logrus.Println("print")
 }

线程安全

默认的logger在并发写的时候是被mutex保护的,比如当同时调用hook和写log时mutex就会被请求,有另外一种情况,文件是以appending mode打开的, 此时的并发操作就是安全的,可以用logger.SetNoLock()来关闭它。

gin框架使用logrus

 // a gin with logrus demo
 ​
 var log = logrus.New()
 ​
 func init() {
     // Log as JSON instead of the default ASCII formatter.
     log.Formatter = &logrus.JSONFormatter{}
     // Output to stdout instead of the default stderr
     // Can be any io.Writer, see below for File example
     f, _ := os.Create("./gin.log")
     log.Out = f
     gin.SetMode(gin.ReleaseMode)
     gin.DefaultWriter = log.Out
     // Only log the warning severity or above.
     log.Level = logrus.InfoLevel
 }
 ​
 func main() {
     // 创建一个默认的路由引擎
     r := gin.Default()
     // GET:请求方式;/hello:请求的路径
     // 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数
     r.GET("/hello", func(c *gin.Context) {
         log.WithFields(logrus.Fields{
             "animal": "walrus",
             "size":   10,
         }).Warn("A group of walrus emerges from the ocean")
         // c.JSON:返回JSON格式的数据
         c.JSON(200, gin.H{
             "message": "Hello world!",
         })
     })
     // 启动HTTP服务,默认在0.0.0.0:8080启动服务
     r.Run()
 }