Go学习第十八章——Gin日志与第三方工具Logrus

Gin日志功能

1 基础介绍

1.1 快速入门

在使用Gin框架的过程中,日志是一个重要的组成部分,它可以记录框架和应用程序的运行情况,帮助开发者排查问题和监控应用程序的性能。Gin框架提供了方便的方法来设置和使用日志。

  1. 默认日志 Gin框架默认使用的是标准库的log包,将日志输出到控制台。可以通过gin.Default()方法来创建一个带有默认中间件的路由引擎。
// 使用Gin.Default自带一个日志中间件
router := gin.Default()

// Logger()就是
func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}

以上代码创建的路由引擎将会使用默认的日志中间件,该中间件会将请求的信息以及处理时间记录到控制台。

  1. 自定义日志输出 如果想要自定义日志的输出方式,可以通过gin.New()方法来创建一个不带默认中间件的路由引擎,并使用gin.Logger()方法设置自定义的日志中间件。
router := gin.New()
router.Use(gin.Logger())

以上代码创建了一个不带默认中间件的路由引擎,并设置了自定义的日志中间件。

不过上面这里,直接又调用了gin自带的日志中间件,后面会讲解如何自定义日志。

1.2 日志基础使用

  1. 使用日志 在实际项目中,可以在处理请求的函数中使用日志记录相关信息。
func handler(c *gin.Context) {
	log.Println("Handling request...")
	c.JSON(http.StatusOK, gin.H{"message": "Hello, world!"})
}

以上代码在处理请求的函数中使用了log包的Println函数记录了一条信息。

  1. 日志格式化 Gin框架中提供了方便的方法来格式化日志的输出。可以使用log包的Printf函数来格式化日志信息。
func handler(c *gin.Context) {
	log.Printf("Handling request: %s", c.Request.URL.Path)
	c.JSON(http.StatusOK, gin.H{"message": "Hello, world!"})
}

以上代码使用Printf函数格式化了日志输出,打印了请求的URL路径。

1.3 将日志输出到文件

日志文件 除了输出到控制台,还可以将日志输出到文件中。

  1. gin.DefaultWriter = io.MultiWriter(f) 将日志写入文件,但是控制台不显示
  2. gin.DefaultWriter = io.MultiWriter(f, os.Stdout)同时将日志写入文件和控制台
func main() {
  // 输出到文件
  f, _ := os.Create("gin.log")
  //gin.DefaultWriter = io.MultiWriter(f)
  // 如果需要同时将日志写入文件和控制台,请使用以下代码。
  gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
  router := gin.Default()
  router.GET("/", func(c *gin.Context) {
    c.JSON(200, gin.H{"msg": "/"})
  })
  router.Run()
}

以上代码将日志输出到名为"logfile.log"的文件中。

还有一种方式,可以做一个中间件,用来将日志写入文件,并且控制台也显示:

使用log包的SetOutput函数将日志输出到指定的文件。

func main() {
    router := gin.Default()
    router.GET("/", handler)
    router.Run(":8000")
}

func handler(c *gin.Context) {
    file, _ := os.OpenFile("logfile.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    log.SetOutput(file)
    log.Printf("Handling request: %s", c.Request.URL.Path)
    c.JSON(http.StatusOK, gin.H{"message": "Hello, world!"})
}

2 定义路由格式

启动gin,它会显示所有的路由,默认格式如下

[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] GET    /hello                    --> main.main.func2 (3 handlers)

image-20231028231600355

我们也可以进行修改,自定义这个输出格式:

gin.DebugPrintRouteFunc是Gin框架提供的一个全局变量,用于自定义路由信息的调试输出格式和行为。该变量是一个函数类型,声明如下:

type DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int)

该函数类型接收以下参数:

  • httpMethod:HTTP方法,表示请求使用了哪种HTTP方法(GET、POST、PUT、DELETE等)。
  • absolutePath:请求路径,包括了路由组前缀和被路由匹配的路径。
  • handlerName:处理函数的名称,用于标识该路由绑定的处理函数。
  • nuHandlers:处理函数的数量,即路由绑定的处理函数个数。

用户可以通过定义一个自定义的DebugPrintRouteFunc函数,并将其赋值给gin.DebugPrintRouteFunc变量来定制网站路由信息的输出。

func main() {
	gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
		fmt.Printf("[小智] %v - url:%v --> handlerName:%v (%v handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
	}

	router := gin.Default()

	router.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{"msg": "/"})
	})

	router.GET("/hello", func(c *gin.Context) {
		c.JSON(200, gin.H{"msg": "/"})
	})

	router.Run()
}

输出结果:

[小智] GET - url:/ --> handlerName:main.main.func2 (3 handlers)
[小智] GET - url:/hello --> handlerName:main.main.func3 (3 handlers)

3 修改日志级别

Gin框架提供了四种日志级别:

  • gin.DebugMode:启用DEBUG级别日志,显示所有日志信息。
  • gin.ReleaseMode:启用INFO级别日志,仅显示INFO、WARN和ERROR级别的日志信息。
  • gin.TestMode:禁用日志,不显示任何日志信息。

可以通过gin.DebugModegin.ReleaseModegin.TestMode方法设置不同的日志级别。

// 设置为DEBUG级别日志
gin.SetMode(gin.DebugMode)

// 设置为INFO级别日志
gin.SetMode(gin.ReleaseMode)

// 禁用日志
gin.SetMode(gin.TestMode)

我这里选择了一个设置,再次运行,下面的内容就少了很多,特别是设置INFO之后,完美~~

4 修改日志格式

默认的是这样的

[GIN] 2023/10/28 - 23:21:00 | 200 |  0s |  127.0.0.1 | GET  "/"

如果觉得不好看,我们可以自定义

package main

import (
  "fmt"
  "github.com/gin-gonic/gin"
)

func LoggerWithFormatter(params gin.LogFormatterParams) string {
  return fmt.Sprintf(
    "[ 小智 ] %s  | %d | \t %s | %s | %s \t  %s\n",
    params.TimeStamp.Format("2006/01/02 - 15:04:05"),
    params.StatusCode,  // 状态码
    params.ClientIP,  // 客户端ip
    params.Latency,  // 请求耗时
    params.Method,  // 请求方法
    params.Path,  // 路径
  )
}

func main() {
  router := gin.New()
  router.Use(gin.LoggerWithFormatter(LoggerWithFormatter))
  router.Run()

}

也可以这样

func LoggerWithFormatter(params gin.LogFormatterParams) string {
  return fmt.Sprintf(
    "[ 小智 ] %s  | %d | \t %s | %s | %s \t  %s\n",
    params.TimeStamp.Format("2006/01/02 - 15:04:05"),
    params.StatusCode,
    params.ClientIP,
    params.Latency,
    params.Method,
    params.Path,
  )
}

func main() {
  router := gin.New()
  router.Use(
    gin.LoggerWithConfig(
      gin.LoggerConfig{Formatter: LoggerWithFormatter},
    ),
  )
  router.Run()

}

但是你会发现自己这样输出之后,没有颜色了,不太好看,我们可以输出有颜色的log

func LoggerWithFormatter(params gin.LogFormatterParams) string {
  var statusColor, methodColor, resetColor string
  statusColor = params.StatusCodeColor()
  methodColor = params.MethodColor()
  resetColor = params.ResetColor()
  return fmt.Sprintf(
    "[ 小智 ] %s  | %s %d  %s | \t %s | %s | %s %-7s %s \t  %s\n",
    params.TimeStamp.Format("2006/01/02 - 15:04:05"),
    statusColor, params.StatusCode, resetColor,
    params.ClientIP,
    params.Latency,
    methodColor, params.Method, resetColor,
    params.Path,
  )
}

我们进行运行,然后看一下结果~~

image-20231028233512662

第三方日志工具logrus

1 快速入门

1.1 安装

可以使用Go的包管理工具go get来安装Logrus:

go get github.com/sirupsen/logrus

安装完成后,可以在项目的代码中引入Logrus的包:

import log "github.com/sirupsen/logrus"

1.2 使用

下面是一个简单的入门案例,展示了如何使用Logrus进行基本的日志输出:

package main

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

func main() {
	// 设置日志输出格式为JSON格式
	log.SetFormatter(&log.JSONFormatter{})

	// 设置日志输出到标准输出
	log.SetOutput(os.Stdout)
    
    // 得到当前的日志级别
    fmt.Println("修改前",log.GetLevel()) 

	// 设置日志级别为debug
	log.SetLevel(log.DebugLevel)
    
    // 得到当前的日志级别
    fmt.Println("修后",log.GetLevel())

	// 输出不同级别的日志信息
	log.Debug("This is a debug message")
	log.Info("This is an info message")
	log.Warn("This is a warning message")
	log.Error("This is an error message")
}

输出结果:

{"level":"debug","msg":"This is a debug message","time":"2023-10-29T09:51:48+08:00"}
{"level":"info","msg":"This is an info message","time":"2023-10-29T09:51:48+08:00"}
{"level":"warning","msg":"This is a warning message","time":"2023-10-29T09:51:48+08:00"}
{"level":"error","msg":"This is an error message","time":"2023-10-29T09:51:48+08:00"}

在这个例子中,我们首先设置了日志输出的格式为JSON格式,然后将日志输出到标准输出。接着,我们设置了日志级别为debug,这意味着只有debug级别及以上的日志信息才会被输出。

最后,我们分别输出了debug、info、warning和error级别的日志信息。可以在控制台中看到对应级别的日志信息以及它们的格式。

2 基本功能使用

2.1 设置日志输出格式

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

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

Logrus支持多种日志输出格式,如JSON、文本(默认)、自定义格式等。可以使用logrus提供的SetFormatter方法来设置日志输出格式。以下是一些常用的日志输出格式:

  • JSON格式:
log.SetFormatter(&log.JSONFormatter{})
  • 文本格式(默认格式):
log.SetFormatter(&log.TextFormatter{})
  • 自定义格式:

自定义能够选择的类型:

ForceColors:是否强制使用颜色输出。
DisableColors:是否禁用颜色输出。
ForceQuote:是否强制引用所有值。
DisableQuote:是否禁用引用所有值。
DisableTimestamp:是否禁用时间戳记录。
FullTimestamp:是否在连接到 TTY 时输出完整的时间戳。
TimestampFormat:用于输出完整时间戳的时间戳格式。

使用方式:

log.SetFormatter(&log.TextFormatter{
	DisableColors: true, 
	FullTimestamp: true,
})

完整代码:

func main() {
    // JSON格式
    log.SetFormatter(&log.JSONFormatter{})
    log.Errorf("JSON格式")

    // TEXT格式
    log.SetFormatter(&log.TextFormatter{})
    log.Errorf("TEXT格式")

    // 自定义格式
    log.SetFormatter(&log.TextFormatter{
       DisableColors: false,
       FullTimestamp: true,
       ForceColors:   true,
    })
    log.Errorf("自定义格式")

    log.Error("你好")
    log.Info("你好")
    log.Warnln("你好")
    log.Debug("你好")
    log.Println("你好")
}

输出结果:

{"level":"error","msg":"JSON格式","time":"2023-10-29T10:23:25+08:00"}             
time="2023-10-29T10:23:25+08:00" level=error msg="TEXT格式"                       
ERRO[2023-10-29T10:23:25+08:00] 自定义格式                                        
ERRO[2023-10-29T10:23:25+08:00] 你好                                              
INFO[2023-10-29T10:23:25+08:00] 你好                                              
WARN[2023-10-29T10:23:25+08:00] 你好                                              
INFO[2023-10-29T10:23:25+08:00] 你好

image-20231029102407564

2.2 设置日志输出位置

Logrus可以将日志输出到标准输出、文件、网络等多个位置。默认情况下,日志输出到标准输出。

以下是一些常用的日志输出位置:

  • 标准输出:
log.SetOutput(os.Stdout)
  • 文件:
  1. 输出在文件,并且控制台不输出
func main() {
	log.SetFormatter(&log.JSONFormatter{})

	file, err := os.OpenFile("logfile.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
	if err == nil {
		log.SetOutput(file)
	} else {
		log.Info("Failed to log to file, using default stderr")
	}

	log.Error("你好")
}
  1. 输出在文件,并且控制台也输出
func main() {
    // 创建一个文件,用于写入日志
    file, err := os.OpenFile("logfile.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
    if err == nil {
       // 设置日志输出到文件和标准输出
       mw := io.MultiWriter(file, os.Stdout)

       // 设置日志输出格式为文本格式
       log.SetFormatter(&log.TextFormatter{})
       // 设置日志输出位置为MultiWriter
       log.SetOutput(mw)
       // 设置日志级别为debug
       log.SetLevel(log.DebugLevel)

       // 输出日志信息
       log.Debug("This is a debug message")
       log.Info("This is an info message")
       log.Warn("This is a warning message")
       log.Error("This is an error message")

       // 关闭日志文件
       file.Close()
    } else {
       fmt.Println("Failed to log to file, using default stderr")
    }
}

在这个例子中,我们首先创建了一个文件,用于写入日志信息。然后,我们创建了一个MultiWriter,并将文件和标准输出作为参数传入MultiWriter的构造函数中。

接着,我们设置了日志的输出格式为TextFormatter,并将输出位置设置为MultiWriter。最后,我们设置了日志的级别为debug,并输出不同级别的日志信息。

通过使用MultiWriter,日志信息将同时输出到文件和控制台,控制台会显示日志信息,而文件中也会写入相同的日志内容。

  • 网络:
conn, err := net.Dial("tcp", "localhost:12345")
if err == nil {
    log.SetOutput(conn)
} else {
    log.Info("Failed to connect to log server, using default stderr")
}

2.3 设置日志级别

Logrus支持多个日志级别,包括debug、info、warning、error、fatal和panic。可以使用SetLevel方法来设置日志级别:

log.SetLevel(log.DebugLevel)

可以根据项目需要设置不同的日志级别。当设置为不同的日志级别时,只有大于等于该级别的日志信息才会被输出。

2.4 添加字段到日志信息中

Logrus提供了多种方法来添加自定义字段到日志信息中。可以使用WithFieldWithFields方法来添加字段。

func main() {
	// 给日志添加一个字段
	log1 := log.WithField("user1", "alice")
	log1.Errorf("你好")

	log2 := log.WithFields(log.Fields{
		"user2": "alice",
		"ip":    "127.0.0.1",
	})
	log2.Errorf("你好")

	// 嵌套使用
	log3 := log.WithFields(log.Fields{
		"user3": "alice",
		"ip":    "127.0.0.1",
	}).WithField("user4", "alice")
	log3.Errorf("你好")
}

输出结果:

time="2023-10-29T10:16:22+08:00" level=error msg="你好" user1=alice                         
time="2023-10-29T10:16:22+08:00" level=error msg="你好" ip=127.0.0.1 user2=alice            
time="2023-10-29T10:16:22+08:00" level=error msg="你好" ip=127.0.0.1 user3=alice user4=alice

2.5 错误处理

Logrus可以记录和处理错误信息。可以使用WithError方法来添加错误信息到日志中。

err := someFunc()
if err != nil {
    log.WithError(err).Error("Error occurred")
}

2.6 格式化参数

Logrus支持使用格式化字符串和参数。

log.Infof("The answer is %d", 42)

2.7 显示行号

没有行号,无法定位具体的日志位置

logrus.SetReportCaller(true)

输出结果:看后面多了一个file,并且最后有个29,就是报错的那一行

time="2023-10-29T10:35:56+08:00" level=debug msg="This is a debug message" func=main.main file="F:/goproject/studyGin/GinStudy03_Logrus/main.go:26"
time="2023-10-29T10:35:56+08:00" level=info msg="This is an info message" func=main.main file="F:/goproject/studyGin/GinStudy03_Logrus/main.go:27" 
time="2023-10-29T10:35:56+08:00" level=warning msg="This is a warning message" func=main.main file="F:/goproject/studyGin/GinStudy03_Logrus/main.go
:28"
time="2023-10-29T10:35:56+08:00" level=error msg="This is an error message" func=main.main file="F:/goproject/studyGin/GinStudy03_Logrus/main.go:29
"

3 自定义日志格式

logrus默认的样式我不太喜欢,没有颜色输出

需要实现Formatter(entry *logrus.Entry) ([]byte, error) 接口

package main

import (
  "bytes"
  "fmt"
  "github.com/sirupsen/logrus"
  "os"
  "path"
)

// 颜色
const (
  red    = 31
  yellow = 33
  blue   = 36
  gray   = 37
)

type LogFormatter struct{}

// Format 实现Formatter(entry *logrus.Entry) ([]byte, error)接口
func (t *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
  //根据不同的level去展示颜色
  var levelColor int
  switch entry.Level {
  case logrus.DebugLevel, logrus.TraceLevel:
    levelColor = gray
  case logrus.WarnLevel:
    levelColor = yellow
  case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:
    levelColor = red
  default:
    levelColor = blue
  }
  var b *bytes.Buffer
  if entry.Buffer != nil {
    b = entry.Buffer
  } else {
    b = &bytes.Buffer{}
  }
  //自定义日期格式
  timestamp := entry.Time.Format("2006-01-02 15:04:05")
  if entry.HasCaller() {
    //自定义文件路径
    funcVal := entry.Caller.Function
    fileVal := fmt.Sprintf("%s:%d", path.Base(entry.Caller.File), entry.Caller.Line)
    //自定义输出格式
    fmt.Fprintf(b, "[%s] \x1b[%dm[%s]\x1b[0m %s %s %s\n", timestamp, levelColor, entry.Level, fileVal, funcVal, entry.Message)
  } else {
    fmt.Fprintf(b, "[%s] \x1b[%dm[%s]\x1b[0m %s\n", timestamp, levelColor, entry.Level, entry.Message)
  }
  return b.Bytes(), nil
}

var log *logrus.Logger

func init() {
  log = NewLog()
}

func NewLog() *logrus.Logger {
  mLog := logrus.New()               //新建一个实例
  mLog.SetOutput(os.Stdout)          //设置输出类型
  mLog.SetReportCaller(true)         //开启返回函数名和行号
  mLog.SetFormatter(&LogFormatter{}) //设置自己定义的Formatter
  mLog.SetLevel(logrus.DebugLevel)   //设置最低的Level
  return mLog
}
func main() {
  log.Errorln("你好")
  log.Infof("你好")
  log.Warnln("你好")
  log.Debugf("你好")
}

输出结果:

[2023-10-29 10:37:46] [error] main.go:20 main.main 你好  
[2023-10-29 10:37:46] [info] main.go:21 main.main 你好   
[2023-10-29 10:37:46] [warning] main.go:22 main.main 你好
[2023-10-29 10:37:46] [debug] main.go:23 main.main 你好

4 钩子Hook

4.1 快速入门

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

type Hook interface {
    Levels() []Level
    Fire(*Entry) error
}

logrus支持钩子函数,使得在特定的条件下自动触发操作。可以使用AddHook方法来添加钩子函数。

log.AddHook(&MyHook{})

完整代码:

// 这个 CustomHook 名字是可以改动的
type CustomHook struct {
}

// 设置一个field
func (hook *CustomHook) Fire(entry *logrus.Entry) error {
    // 在日志输出之前执行自定义操作,比如发送到消息队列、保存到数据库等
    // 这里只是一个示例,实际操作可以根据需求进行自定义
    entry.Data["custom_field"] = "custom_value"
    return nil
}

// 哪些等级的日志才会生效
func (hook *CustomHook) Levels() []logrus.Level {
    // 指定要被触发的日志级别,这里设置为所有级别
    return logrus.AllLevels
}

func main() {
    // 日志的打开格式是追加,所以不能用os.Create
    logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true, TimestampFormat: "2006-01-02 15:04:05", FullTimestamp: true})
    logrus.AddHook(&CustomHook{})
    logrus.Errorf("hello")
}

输出结果:

ERRO[2023-10-29 10:49:33] hello                                         custom_field=custom_value

4.2 场景案例

logrus hook 是一个值得深入学习的设计,你可以轻易适用hook来实现多文件写入。

比如,error级别的日志独立输出到error.log文件里,其他都放在一起。

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)
  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")
}

5 日志分割

5.1 时间分割日志

自定义Write方法

自定义Write方法: logrus允许我们自定义写日志的方法,可以通过实现io.Writer接口来自定义写日志的行为。这是在网上找的案例,简单看看它是咋实现的,后面有这段代码的实现详细解析。

import (
	"errors"
	"fmt"
	log "github.com/sirupsen/logrus"
	"io"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// LogFormatter 日志自定义格式
type LogFormatter struct{}

// Format 格式详情
func (s *LogFormatter) Format(entry *log.Entry) ([]byte, error) {
	// 获取当前时间
	timestamp := time.Now().Local().Format("2006-01-02 15:04:05")
	var file string
	var line int
	if entry.Caller != nil {
		// 获取调用者的文件名和行号
		file = filepath.Base(entry.Caller.File)
		line = entry.Caller.Line
	}
	// 格式化日志信息
	msg := fmt.Sprintf("[%s] %s [%s:%d] %s\n", strings.ToUpper(entry.Level.String()), timestamp, file, line, entry.Message)
	return []byte(msg), nil
}

// logFileWriter 自定义日志写入器
type logFileWriter struct {
	file     *os.File
	logPath  string
	fileDate string // 判断日期切换目录
	appName  string
}

// Write 将日志内容写入文件
func (p *logFileWriter) Write(data []byte) (n int, err error) {
	if p == nil {
		return 0, errors.New("logFileWriter is nil")
	}
	if p.file == nil {
		return 0, errors.New("file not opened")
	}

	// 判断是否需要切换日期
	fileDate := time.Now().Format("2006-01-02")
	if p.fileDate != fileDate {
		p.file.Close()
		err = os.MkdirAll(fmt.Sprintf("%s/%s", p.logPath, fileDate), os.ModePerm)
		if err != nil {
			return 0, err
		}
		filename := fmt.Sprintf("%s/%s/%s-%s.log", p.logPath, fileDate, p.appName, fileDate)

		p.file, err = os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
		if err != nil {
			return 0, err
		}
	}

	n, e := p.file.Write(data)
	return n, e
}

// 初始化日志配置
func InitLog(logPath string, appName string) {
	fileDate := time.Now().Format("20060102")
	// 创建目录
	err := os.MkdirAll(fmt.Sprintf("%s/%s", logPath, fileDate), os.ModePerm)
	if err != nil {
		log.Fatal(err)
	}

	filename := fmt.Sprintf("%s/%s/%s-%s.log", logPath, fileDate, appName, fileDate)
	file, err := os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
	if err != nil {
		log.Fatal(err)
	}

	fileWriter := logFileWriter{file, logPath, fileDate, appName}
	log.SetOutput(os.Stdout)
	writers := []io.Writer{
		&fileWriter,
		os.Stdout,
	}
	// 同时写入文件和终端
	fileAndStdoutWriter := io.MultiWriter(writers...)

	log.SetOutput(fileAndStdoutWriter)
	log.SetReportCaller(true)
	log.SetFormatter(new(LogFormatter))
}

func main() {
	InitLog("./logs", "myApp")

	log.Println("This is a log message.")
	log.Printf("This is another log message. Current time: %s\n", time.Now().Format("15:04:05"))

	log.Fatal("This is a fatal error.")
}

输出结果:

image-20231029111740058

  1. 首先定义了一个 LogFormatter 结构体,用于自定义日志的格式。

    它实现了 Format 方法,接收一个 log.Entry 参数,该结构体包含了日志的级别、时间戳、调用者的文件名和行号,以及日志信息。在 Format 方法中,通过 time.Now().Local().Format("2006-01-02 15:04:05") 获取当前时间,并使用 filepath.Base(entry.Caller.File) 获取调用者的文件名,entry.Caller.Line 获取行号。最后使用 fmt.Sprintf 将这些信息格式化为特定的字符串。

  2. 然后定义了一个 logFileWriter 结构体,实现了 io.Writer 接口的 Write 方法。

    该方法用于将日志写入文件。在 Write 方法中,首先判断是否需要切换日期。如果日期发生变化,需要关闭之前的日志文件,并创建新的日期目录和文件。然后将日志写入文件,并返回写入的字节数和错误。

  3. 最后定义了一个 InitLog 函数,用于初始化日志。该函数接收两个参数:日志路径 logPath 和应用名称 appName

    在函数内部,首先根据当前日期创建日志目录。然后根据日志路径、应用名称和当前日期创建日志文件。接着创建一个 logFileWriter 实例,并将其作为输出写入器。同时,还将标准输出作为另一个写入器。然后使用 io.MultiWriter 将这两个写入器组合起来,实现同时将日志写入文件和标准输出。最后,使用 log.SetOutput 将写入器设置为日志输出,并设置 log.SetReportCaller(true) 启用调用者报告功能。最后,将 LogFormatter 设置为日志的格式化器。

这样,通过调用 InitLog 函数可以初始化日志记录器,并将日志输出到文件和标准输出。日志的格式可以通过修改 LogFormatter 结构体的 Format 方法来自定义。

自定义hook钩子

自定义 hook: logrus还允许我们自定义hook函数,在日志输出前或输出后执行额外的操作。

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

// 定义了一个类型FileDateHook,它包含了需要的字段,如日志文件的文件指针、日志保存路径、当前的日期和应用名称。
type FileDateHook struct {
	file     *os.File
	logPath  string
	fileDate string //判断日期切换目录
	appName  string
}

// 定义了FileDateHook类型的方法Levels。
// 该方法返回了一个包含所有日志级别的切片,表示该钩子对所有日志级别都生效。
func (hook FileDateHook) Levels() []logrus.Level {
	return logrus.AllLevels
}


/* 定义了FileDateHook类型的方法Fire,它是钩子触发时所执行的行为。该方法首先获取当前时间,并格式化为"2006-01-02_15-04"的字符串。然后通过entry.String()方法获取日志的字符串表示。接着判断当前日期与上一次的日期是否相同,如果相同,则向日志文件写入日志内容;如果不同,关闭上一个日志文件,创建新的目录并打开一个新的日志文件,并将当前日期和日志内容写入该文件。*/
func (hook FileDateHook) Fire(entry *logrus.Entry) error {
	timer := entry.Time.Format("2006-01-02_15-04")
	line, _ := entry.String()
	if hook.fileDate == timer {
		hook.file.Write([]byte(line))
		return nil
	}
	// 时间不等
	hook.file.Close()
	os.MkdirAll(fmt.Sprintf("%s/%s", hook.logPath, timer), os.ModePerm)
	filename := fmt.Sprintf("%s/%s/%s.log", hook.logPath, timer, hook.appName)

	hook.file, _ = os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
	hook.fileDate = timer
	hook.file.Write([]byte(line))
	return nil
}

/* 定义了InitFile函数,用于初始化日志文件配置。首先获取当前日期并创建对应的目录。然后构建日志文件的路径,创		建并打开日志文件,并将文件指针和其他参数传递给FileDateHook,最后通过logrus的AddHook方法将该钩子添加到日	志记录器中。*/
func InitFile(logPath, appName string) {
	fileDate := time.Now().Format("2006-01-02_15-04")
	//创建目录
	err := os.MkdirAll(fmt.Sprintf("%s/%s", logPath, fileDate), os.ModePerm)
	if err != nil {
		logrus.Error(err)
		return
	}

	filename := fmt.Sprintf("%s/%s/%s.log", logPath, fileDate, appName)
	file, err := os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
	if err != nil {
		logrus.Error(err)
		return
	}
	fileHook := FileDateHook{file, logPath, fileDate, appName}
	logrus.AddHook(&fileHook)
}

func main() {
   
	InitFile("logrus_study/log", "XiaoZhi")

	for {
		logrus.Errorf("error")
		time.Sleep(20 * time.Second)
		logrus.Warnln("warnning")
	}
}

在main函数中,首先调用InitFile函数初始化日志文件配置。然后进入一个无限循环,每隔20秒记录一条带有"error"级别的日志,并记录一条带有"warning"级别的日志。

这段代码实现了一个简单的日志记录功能,根据日期创建目录和文件,并将不同日期的日志分别保存到对应的文件中。通过FileDateHook和logrus库的配合使用,可以灵活地对日志进行处理和管理。

5.2 按日志等级分割

package main

import (
  "fmt"
  "github.com/sirupsen/logrus"
  "os"
)
// 定义了4个常量,分别用于表示所有日志、错误日志、警告日志和信息日志。
const (
  allLog  = "all"
  errLog  = "err"
  warnLog = "warn"
  infoLog = "info"
)

// 定义了一个类型FileLevelHook,它包含了需要的字段,
// 如所有日志文件、错误日志文件、警告日志文件、信息日志文件和保存日志的路径。
type FileLevelHook struct {
  file     *os.File
  errFile  *os.File
  warnFile *os.File
  infoFile *os.File
  logPath  string
}

// 定义了FileLevelHook类型的方法Levels。
// 该方法返回一个包含所有日志级别的切片,表示该钩子对所有日志级别都生效。
func (hook FileLevelHook) Levels() []logrus.Level {
  return logrus.AllLevels
}

// 定义了FileLevelHook类型的方法Fire,它是钩子触发时所执行的行为。
/* 该方法将entry转换为字符串,然后根据entry的级别分别向错误日志文件、警告日志文件和信息日志文件中写入日志内容。另外,该方法还向所有日志文件中写入日志内容。*/
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
}
// 定义了InitLevel函数,用于初始化日志文件配置。
/* 该函数首先创建保存日志的目录,然后打开所有日志文件、错误日志文件、警告日志文件和信息日志文件,并将它们添加到FileLevelHook中。最后通过logrus的AddHook方法将该钩子添加到日志记录器中。*/
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")
}

在main函数中,首先调用InitLevel函数初始化日志文件配置。然后使用logrus记录了一些不同级别的日志,这些日志将根据级别分别保存到不同的文件中。

这段代码实现了一个可以将不同级别的日志分别保存到不同文件的日志记录器,可以根据实际需求对不同级别的日志进行细分管理。

Gin 集成 logrus

视频学习:Gin 集成 logrus

先创建两个文件夹,log,middleware,并且两个文件,log.go,log_middleware.go

image-20231029113609743

log_middleware.go文件的代码:

package middleware

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
	"time"
)

const (
	status200 = 42
	status404 = 43
	status500 = 41

	methodGET = 44
)

func LogMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path
		raw := c.Request.URL.RawQuery

		// Process request
		c.Next()

		// Log only when path is not being skipped

		// Stop timer
		end := time.Now()
		timeSub := end.Sub(start)
		clientIP := c.ClientIP()
		method := c.Request.Method
		statusCode := c.Writer.Status()
		//bodySize := c.Writer.Size()
		if raw != "" {
			path = path + "?" + raw
		}

		var statusColor string
		switch statusCode {
		case 200:
			statusColor = fmt.Sprintf("\033[%dm %d \033[0m", status200, statusCode)
		case 404:
			statusColor = fmt.Sprintf("\033[%dm %d \033[0m", status404, statusCode)
		}

		var methodColor string
		switch method {
		case "GET":
			methodColor = fmt.Sprintf("\033[%dm %s \033[0m", methodGET, method)

		}

		logrus.Infof("[GIN] %s |%s| %d | %s | %s | %s",
			start.Format("2006-01-02 15:04:06"),
			statusColor,
			timeSub,
			clientIP,
			methodColor,
			path,
		)

	}
}

log.go文件的代码:

package log

import (
	"bytes"
	"fmt"
	"github.com/sirupsen/logrus"
	"os"
	"time"
)

type FileDateHook struct {
	file     *os.File
	logPath  string
	fileDate string //判断日期切换目录
	appName  string
}

func (hook FileDateHook) Levels() []logrus.Level {
	return logrus.AllLevels
}
func (hook FileDateHook) Fire(entry *logrus.Entry) error {
	timer := entry.Time.Format("2006-01-02_15-04")
	line, _ := entry.String()
	if hook.fileDate == timer {
		hook.file.Write([]byte(line))
		return nil
	}
	// 时间不等
	hook.file.Close()
	os.MkdirAll(fmt.Sprintf("%s/%s", hook.logPath, timer), os.ModePerm)
	filename := fmt.Sprintf("%s/%s/%s.log", hook.logPath, timer, hook.appName)

	hook.file, _ = os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
	hook.fileDate = timer
	hook.file.Write([]byte(line))
	return nil
}

type MyFormatter struct {
}

func (f MyFormatter) Format(entry *logrus.Entry) ([]byte, error) {

	// 设置buffer 缓冲区
	var b *bytes.Buffer
	if entry.Buffer == nil {
		b = &bytes.Buffer{}
	} else {
		b = entry.Buffer
	}
	// 设置格式
	fmt.Fprintf(b, "%s\n", entry.Message)

	return b.Bytes(), nil
}

func InitFile(logPath, appName string) {
	logrus.SetFormatter(&MyFormatter{})

	fileDate := time.Now().Format("2006-01-02_15-04")
	//创建目录
	err := os.MkdirAll(fmt.Sprintf("%s/%s", logPath, fileDate), os.ModePerm)
	if err != nil {
		logrus.Error(err)
		return
	}

	filename := fmt.Sprintf("%s/%s/%s.log", logPath, fileDate, appName)
	file, err := os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
	if err != nil {
		logrus.Error(err)
		return
	}
	fileHook := FileDateHook{file, logPath, fileDate, appName}

	logrus.AddHook(&fileHook)
}

最后main代码:

import (
    "github.com/gin-gonic/gin"
    "github.com/sirupsen/logrus"
    "studyGin/GinStudy03_Logrus/log"
    "studyGin/GinStudy03_Logrus/middleware"
)

func main() {

    log.InitFile("logrus_study/gin_logrus/logs", "server")
    router := gin.New()
    router.Use(middleware.LogMiddleware())

    router.GET("/", func(c *gin.Context) {
       logrus.Info("来了")
       c.JSON(200, gin.H{"msg": "你好"})
    })
    router.Run(":8081")

}

访问地址:http://127.0.0.1:8081/

image-20231029113919780

Over!!!

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值