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

news2024/11/27 8:47:46

Go web框架——Gin日志与第三方工具Logrus

  • Gin日志功能
    • 1 基础介绍
      • 1.1 快速入门
      • 1.2 日志基础使用
      • 1.3 将日志输出到文件
    • 2 定义路由格式
    • 3 修改日志级别
    • 4 修改日志格式
  • 第三方日志工具logrus
    • 1 快速入门
      • 1.1 安装
      • 1.2 使用
    • 2 基本功能使用
      • 2.1 设置日志输出格式
      • 2.2 设置日志输出位置
      • 2.3 设置日志级别
      • 2.4 添加字段到日志信息中
      • 2.5 错误处理
      • 2.6 格式化参数
      • 2.7 显示行号
    • 3 自定义日志格式
    • 4 钩子Hook
      • 4.1 快速入门
      • 4.2 场景案例
    • 5 日志分割
      • 5.1 时间分割日志
        • 自定义Write方法
        • 自定义hook钩子
      • 5.2 按日志等级分割
  • 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!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1146549.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TYWZOJ 种树苗 待定题解

文章目录 题目描述输入格式输出格式样例样例输入样例输出 数据范围与提示思路与部分实现完整代码 题目描述 在游戏 Minecraft 中,玩家可以通过种树来使木材再生。玩家需要将树苗种在泥土上,然后等待它长成大树,期间可以利用骨粉来催熟树苗。…

[NSSCTF 2nd] web刷题记录

文章目录 php签到MyBox非预期解预期解 php签到 源代码 <?phpfunction waf($filename){$black_list array("ph", "htaccess", "ini");$ext pathinfo($filename, PATHINFO_EXTENSION);foreach ($black_list as $value) {if (stristr($ext, …

[python 刷题] 974 Subarray Sums Divisible by K

[python 刷题] 974 Subarray Sums Divisible by K 题目如下&#xff1a; Given an integer array nums and an integer k, return the number of non-empty subarrays that have a sum divisible by k. A subarray is a contiguous part of an array. 依旧是 prefix sum 的变种…

EASYX绘制卡通头像

#include <stdio.h> #include <easyx.h> #include <iostream> #include <math.h> #define PI 3.14 // 1PI 180度 2PI 360度int main() {// 创建1024*1024的窗体initgraph(1024, 1024);// 将背景颜色设施为白色setbkcolor(WHITE);cleardevice();// to…

怀旧,20款曾经辉煌至极的PC软件,用过5个你是老网民

博主是1999年接触电脑的&#xff0c;2000年家里有了台式机&#xff0c;然后和众多孩子一样&#xff0c;迷上了这玩意&#xff0c;虽然博主也毫无意外地沉迷游戏&#xff0c;但同时也对早期的电脑硬件、软件技术有过深入研究&#xff0c;比如BIOS、注册表、黑客技术这种东西。今…

Chatgpt网页版根据关键词自动批量写原创文章软件【可多开自动登录切换gpt账号】

Chatgpt网页版根据关键词自动批量写原创文章软件介绍&#xff1a; 1、需要放入GPT账号和密码放入在账号库.txt里&#xff0c;可以放入多组账号密码&#xff0c;账号切换轮流使用。 2、可以自定义回答指令&#xff0c;也可多个回答指令随机切换。 3、可以给关键词加双标题&…

队列(Queue)概念+通过单、双链表来模拟队列+环形队列+OJ面试题(用队列实现栈、用栈实现队列、设计环形队列)

文章目录 队列(Queue)一、 概念1.尾进头出 二、模拟队列1.单链表实现队列1.1 设置结点1.2 入队offer1.3出队 poll1.4 empty方法&#xff0c;peek方法&#xff0c;getUsedSize方法 2.双链表实现队列2.1 创建结点2.2 入队列2.3 出队列2.4 peek、size、isEmpty方法 三、环形队列1.…

基于Java的婚纱摄影网站系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09; 代码参考数据库参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&am…

基于Java的民航售票管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09; 代码参考数据库参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&am…

C++深度优化(DFS)算法的应用:收集所有金币可获得的最大积分

涉及知识点 深度优化(DFS) 记忆化 题目 节点 0 处现有一棵由 n 个节点组成的无向树&#xff0c;节点编号从 0 到 n - 1 。给你一个长度为 n - 1 的二维 整数 数组 edges &#xff0c;其中 edges[i] [ai, bi] 表示在树上的节点 ai 和 bi 之间存在一条边。另给你一个下标从 0…

利用二叉树对表达式求值(只能处理个位的操作数)

题目&#xff1a; 题目要求: 用二叉树来表示表达式&#xff0c;树的每一个节点包括一个运算符和运算数。代数表达式中只包含&#xff0c;-&#xff0c;*&#xff0c;/&#xff0c;&#xff08;&#xff0c;&#xff09;和一位整数且没有错误。按照先括号&#xff0c;再乘除&am…

批量爬取指定多个网址的爱站权重关键词词库(爱站拓词自动去重)

批量爬取指定多个网址的爱站权重关键词词库软件介绍&#xff1a; 1、软件可以设置权重词的长度范围。 2、可设置权重词必须包含词。 3、可以设置爬取的页数。 4、可以设置爬取PC权重词、移动权重词。 5、可以放入多个网站&#xff0c;批量爬取多个网站的权重词。 6、爬取完…

Ubuntu更新中文包

设置 重启Ubuntu系统

灯光布置和场景模拟软件:Set A Light 3D Studio

Set A Light 3D Studio是一款专业的灯光模拟软件&#xff0c;旨在帮助摄影师和电影制片人在电脑上进行虚拟灯光布置和场景模拟&#xff0c;以实现更加精准和高质量的拍摄效果。该软件提供了丰富的灯光和场景模型&#xff0c;支持灵活调整光源位置、强度、颜色和效果等参数&…

IOC课程整理-20 Spring 应用上下文生命周期

0.目录 1. Spring 应用上下文启动准备阶段 2. BeanFactory 创建阶段 3. BeanFactory 准备阶段 4. BeanFactory 后置处理阶段 5. BeanFactory 注册 BeanPostProcessor 阶段 6. 初始化內建 Bean&#xff1a;MessageSource 7. 初始化內建 Bean&#xff1a;Spring 事件广播器…

Megatron-LM GPT 源码分析(四) Virtual Pipeline Parallel分析

引言 本文接着上一篇【Megatron-LM GPT 源码分析&#xff08;三&#xff09; Pipeline Parallel分析】&#xff0c;基于开源代码 GitHub - NVIDIA/Megatron-LM: Ongoing research training transformer models at scale &#xff0c;通过GPT的模型运行示例&#xff0c;从三个维…

sql-50练习题6-10

sql练习题6-10题 前言数据库表结构介绍学生表课程表成绩表教师表 0-6 查询"李"姓老师的数量0-7 查询学过"李四"老师授课的同学的信息0-8 查询没学过"李四"老师授课的同学的信息0-9 查询学过编号为"01"并且也学过编号为"02"的…

37回溯算法-理论基础

目录 什么是回溯算法 基本思想 问题场景 回溯算法的理解 回溯算法模板 LeetCode之路——257. 二叉树的所有路径 分析 什么是回溯算法 回溯算法是一种解决组合优化问题、搜索问题以及决策问题的算法。它通常用于尝试在一组可能的解决方案中搜索并找到满足特定条件的解。…

C++入门05—指针

1. 指针的基本概念 指针的作用&#xff1a; 可以通过指针间接访问内存 内存编号是从0开始记录的&#xff0c;一般用十六进制数字表示 可以利用指针变量保存地址 2. 指针变量的定义和使用 指针变量定义语法&#xff1a; 数据类型 * 变量名&#xff1b; 示例&#xff1a; …

如何防“AI换脸”诈骗

风险提示 “AI换脸”诈骗利用仿真技术冒充亲人、同事或公职人员声音相貌行骗&#xff0c;此类新型网络诈骗手段多样、门槛降低、辨别难度加大&#xff0c;常令公众放松警惕&#xff0c;短时间内造成较大损失。国家金融监督管理总局北京监管局近日发布风险提示&#xff1a;眼见…