Gin框架学习笔记(六)——gin中的日志使用

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Gin框架学习笔记(六)——gin中的日志使用

gin内置日志组件的使用

前言

在之前我们要使用Gin框架定义路由的时候我们一般会使用Default方法来实现,我们来看一下他的实现:

func Default(opts ...OptionFunc) *Engine {
  debugPrintWARNINGDefault()
  engine := New()
  engine.Use(Logger(), Recovery())
  return engine.With(opts...)
}

我们可以看到它注册了两个中间件Logger()Recovery(),而Logger就是我们今天的主角:gin框架自带的日志组件。

输出日志到文件中

package main
import (
  "fmt"
  "github.com/gin-gonic/gin"
  "io"
  "os"
)
func main() {
  file, err := os.Create("ginlog")
  if err != nil {
    fmt.Println("Create file error! err:", err)
  }
  gin.DefaultWriter = io.MultiWriter(file)
  r := gin.Default()
  r.GET("/", func(c *gin.Context) {
    c.JSON(200, gin.H{
      "message": "Hello World!",
    })
  })
  r.Run()
}

运行上面代码我们会发现,控制台不再会有相关日志的输出,而是打印到了ginlog文件中:

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env: export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

当然我们也可以选择既在控制台输出也在文件内输出:

package main
import (
  "fmt"
  "github.com/gin-gonic/gin"
  "io"
  "os"
)
func main() {
  file, err := os.Create("ginlog")
  if err != nil {
    fmt.Println("Create file error! err:", err)
  }
  gin.DefaultWriter = io.MultiWriter(file, os.Stdout)
  r := gin.Default()
  r.GET("/", func(c *gin.Context) {
    c.JSON(200, gin.H{
      "message": "Hello World!",
    })
  })
  r.Run()
}

我们可以看到无论是日志文件ginlog和控制台,都实现了对日志的打印

定义日志中的路由格式

当我们运行Gin框架的时候,它会自动打印当前所有被定义的路由,比如下面这样的格式:

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

而在Gin框架中它允许我们去自己定义路由的输出格式,我们可以自己去定义我们的路由格式:

func _Router_print_init() {
  gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string,
    nuHandlers int) {
    fmt.Printf("[三玖]: %v  %v   %v   %v  \n",
      httpMethod, absolutePath, handlerName, nuHandlers)
  }
}

输出的路由格式是这样的:

[三玖]: GET  /   main.main.func1   3

生产模式与开发模式

在我们程序其实是有两种模式的:

  • debug:开发模式
  • release:生产模式

如果我们希望控制台不在显示日志,可以将模式切换到release模式:

gin.SetMode(gin.ReleaseMode)
  r := gin.Default()

我们可以看到控制台已不再输出日志信息了。

第三方包logrus日志包的使用

logrus包的安装与基本使用

logrus包的安装

logrus的安装很简单,只需要终端输入以下命令即可:

go get github.com/sirupsen/logrus

logrus包的基本使用

logrus常用方法:
logrus.Debug("debug")
  logrus.Info("info")
  logrus.Warn("warn")
  logrus.Error("error")
  logrus.Println("println")

当我们运行该代码的时候会发现打印结果只有四行:

这主要是因为logrus默认的打印等级是info,在这个等级之下的不会打印,在我们生产环境下一般会要求不打印Warn以下的日志,我们可以对打印等级进行调整:

logrus.SetLevel(logrus.WarnLevel)

再次运行上面的代码,运行结果就会有所不同:

我们还可以查看当前的打印等级:

fmt.Println(logrus.GetLevel())

设置特定字段

如果我们希望某条日志记录的打印中添加某一条特定的字段,我们可以使用WithField方法:

log1 := logrus.WithField("key1", "value1")
log1.Info("hello world")

通常,在一个应用中、或者应用的一部分中,都有一些固定的Field。比如我们在处理用户http请求时,上下文中,所有的日志都会有request_id和user_ip为了避免每次记录日志都要使用log.WithFields(log.Fields{“request_id”: request_id, “user_ip”: user_ip}),我们可以创建一个logrus.Entry实例,为这个实例设置默认Fields,在上下文中使用这个logrus.Entry实例记录日志即可,这里我写了一个demo,仅做参考:

package main
import (
  "github.com/sirupsen/logrus"
)
type DefaultLogger struct {
  *logrus.Entry
  defaultFields logrus.Fields
}
func NewDefaultLogger() *DefaultLogger {
  logger := logrus.New()
  entry := logrus.NewEntry(logger)
  return &DefaultLogger{
    Entry:         entry,
    defaultFields: logrus.Fields{},
  }
}
func (l *DefaultLogger) WithFields(fields logrus.Fields) *logrus.Entry {
  allFields := make(logrus.Fields, len(fields))
  for k, v := range fields {
    allFields[k] = v
  }
  return l.Entry.WithFields(allFields)
}
func (l *DefaultLogger) WithDefaultField() {
  l.Entry = l.Entry.WithFields(l.defaultFields)
}
func (l *DefaultLogger) Info(msg string) {
  l.WithDefaultField()
  l.Entry.Info(msg)
}
func (l *DefaultLogger) AddDefaultField(key string, value interface{}) {
  l.defaultFields[key] = value
}
func main() {
  defaultLogger := NewDefaultLogger()
  defaultLogger.AddDefaultField("request_id", "123")
  defaultLogger.AddDefaultField("user_ip", "127.0.0.1")
  // 使用默认字段记录日志
  defaultLogger.Info("This is a log message with default fields")
  // 添加额外字段记录日志
  defaultLogger.WithFields(logrus.Fields{
    "additional_field": "abc",
  }).Info("This is a log message with additional field")
}

输出结果为:

设置显示样式

虽然日志的打印默认是txt格式的,但是我们也可以将格式修改为json格式的:

logrus.SetFormatter(&logrus.TextFormatter{})
• 1

将日志输入到文件

package main
import (
  "github.com/sirupsen/logrus"
  "os"
)
func main() {
  file, err := os.OpenFile("./logrus.log", os.O_CREATE|os.O_WRONLY, 0666)
  if err != nil {
    panic(err)
  }
  logrus.SetOutput(file)
  logrus.Error("error")
}

我们还可以让控制台和日志文件一起输出:

package main
import (
  "github.com/sirupsen/logrus"
  "golang.org/x/sys/windows"
  "io"
  "os"
)
func main() {
  file, err := os.OpenFile("./logrus.log", os.O_CREATE|os.O_WRONLY|windows.O_APPEND, 0666)
  if err != nil {
    panic(err)
  }
  writers := []io.Writer{
    file,
    os.Stdout,
  }
  lod := io.MultiWriter(writers...)
  logrus.SetOutput(lod)
  logrus.Error("error")
  logrus.Info("info")
}

显示行号

logrus.SetReportCaller(true)

logus的Hook机制

在使用logrus这一第三方包的时候,我们可以基于Hook机制来为logrus添加一些拓展功能。

首先我们先定义Hook结构体:

type Hook struct {
  Levels() []logrus.Level  // 返回日志级别
  Fire(entry *logrus.Entry) error  // 日志处理
}

我们Hook结构体中一般会有两个成员:

  • Levels:Hook机制起作用的日志级别
  • Fire:对应的日志处理方式

这里我们举一个例子,如果我们希望将所有Error级别的日志单独拎出来,我们可以基于Hook机制来实现:

package main
import (
  "fmt"
  "github.com/sirupsen/logrus"
  "os"
)
type Hook struct {
  Writer *os.File
}
func (MyHook *Hook) Fire(entry *logrus.Entry) error {
  line, err := entry.String()
  if err != nil {
    fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
  }
  MyHook.Writer.Write([]byte(line))
  return nil
}
func (MyHook *Hook) 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("./error.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
  hook := &Hook{Writer: file}
  logrus.AddHook(hook)
  logrus.Error("error")
}

日志分割

按时间分割

  • Write写法
package main
import (
  "fmt"
  "github.com/sirupsen/logrus"
  "io"
  "os"
  "path/filepath"
  "strings"
  "time"
)
type LogFormatter struct{}
// Format 格式详情
func (s *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
  timestamp := time.Now().Local().Format("2006-01-02 15:04:05")
  var file string
  var len int
  if entry.Caller != nil {
    file = filepath.Base(entry.Caller.File)
    len = entry.Caller.Line
  }
  //fmt.Println(entry.Data)
  msg := fmt.Sprintf("[%s] %s [%s:%d] %s\n", strings.ToUpper(entry.Level.String()), timestamp, file, len, entry.Message)
  return []byte(msg), nil
}
type LogWriter struct {
  Writer   *os.File
  logPath  string
  fileDate string //判断是否需要切换日志文件
  fileName string //日志文件名
}
func (writer *LogWriter) Write(p []byte) (n int, err error) {
  if writer == nil {
    logrus.Error("writer is nil")
    return 0, nil
  }
  if writer.Writer == nil {
    logrus.Error("writer.Writer is nil")
    return 0, nil
  }
  timer := time.Now().Format("2006-01-02 04:12")
  //需要切换日志文件
  if writer.fileDate != timer {
    writer.fileDate = timer
    writer.Writer.Close()
    err = os.MkdirAll(writer.logPath, os.ModePerm)
    if err != nil {
      logrus.Error(err)
      return 0, nil
    }
    filename := fmt.Sprintf("%s/%s.log", writer.logPath, writer.fileDate)
    writer.Writer, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
    if err != nil {
      logrus.Error(err)
      return 0, nil
    }
  }
  return writer.Writer.Write(p)
}
func Initing(logPath string, fileName string) {
  fileDate := time.Now().Format("20060102")
  filepath := fmt.Sprintf("%s/%s", logPath, fileDate)
  err := os.MkdirAll(filepath, os.ModePerm)
  if err != nil {
    logrus.Error(err)
    return
  }
  filename := fmt.Sprintf("%s/%s.log", filepath, fileName)
  writer, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
  if err != nil {
    logrus.Error(err)
    return
  }
  Logwriter := LogWriter{logPath: logPath, fileDate: fileDate, fileName: fileName, Writer: writer}
  logrus.SetOutput(os.Stdout)
  writers := []io.Writer{
    Logwriter.Writer,
    os.Stdout,
  }
  multiWriter := io.MultiWriter(writers...)
  logrus.SetOutput(multiWriter)
  logrus.SetReportCaller(true)
  logrus.SetFormatter(new(LogFormatter))
}
func main() {
  Initing("./", "fengxu")
  logrus.Warn("fengxu")
  logrus.Error("fengxu")
  logrus.Info("fengxu")
}
  • Hook写法
package main
import (
  "fmt"
  "github.com/sirupsen/logrus"
  "os"
  "time"
)
type Hook struct {
  writer   *os.File
  logPath  string
  fileName string
  fileDate string
}
func (MyHook *Hook) Levels() []logrus.Level {
  return logrus.AllLevels
}
func (MyHook *Hook) Fire(entry *logrus.Entry) error {
  timer := time.Now().Format("2006-01-02")
  line, _ := entry.String()
  //需要切换日志文件
  if MyHook.fileDate != timer {
    MyHook.fileDate = timer
    MyHook.writer.Close()
    filepath := fmt.Sprintf("%s/%s", MyHook.logPath, MyHook.fileDate)
    err := os.MkdirAll(filepath, os.ModePerm)
    if err != nil {
      logrus.Error(err)
      return err
    }
    filename := fmt.Sprintf("%s/%s.log", filepath, MyHook.fileName)
    MyHook.writer, _ = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
  }
  MyHook.writer.Write([]byte(line))
  return nil
}
func InitFile(logPath string, fileName string) {
  timer := time.Now().Format("2006-01-02")
  filepath := fmt.Sprintf("%s/%s", logPath, timer)
  err := os.MkdirAll(filepath, os.ModePerm)
  if err != nil {
    logrus.Error(err)
    return
  }
  filename := fmt.Sprintf("%s/%s.log", filepath, fileName)
  writer, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
  if err != nil {
    logrus.Error(err)
    return
  }
  logrus.AddHook(&Hook{
    writer:   writer,
    logPath:  logPath,
    fileName: fileName,
    fileDate: timer,
  })
}
func main() {
  InitFile("./log", "fengxu")
  logrus.Error("test")
}

按日志等级分割

package main
import (
  "fmt"
  "github.com/sirupsen/logrus"
  "os"
)
const (
  alllog   = "all"
  errorlog = "error"
  warnlog  = "warn"
)
type Hook struct {
  allLevel   *os.File
  errorLevel *os.File
  warnLevel  *os.File
}
func (MyHook *Hook) Levels() []logrus.Level {
  return logrus.AllLevels
}
func (MyHook *Hook) Fire(entry *logrus.Entry) error {
  line, _ := entry.String()
  switch entry.Level {
  case logrus.ErrorLevel:
    MyHook.errorLevel.Write([]byte(line))
  case logrus.WarnLevel:
    MyHook.warnLevel.Write([]byte(line))
  }
  MyHook.allLevel.Write([]byte(line))
  return nil
}
func InitLevel(logPath string) {
  err := os.MkdirAll(logPath, os.ModePerm)
  if err != nil {
    logrus.Error("创建目录失败")
    return
  }
  allFile, err := os.OpenFile((fmt.Sprintf("%s/%s", logPath, alllog)), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)
  errFile, err := os.OpenFile((fmt.Sprintf("%s/%s", logPath, errorlog)), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)
  warnFile, err := os.OpenFile((fmt.Sprintf("%s/%s", logPath, warnlog)), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)
  logrus.AddHook(&Hook{allLevel: allFile, errorLevel: errFile, warnLevel: warnFile})
}
func main() {
  InitLevel("./log")
  logrus.SetReportCaller(true)
  logrus.Errorln("你好")
  logrus.Errorln("err")
  logrus.Warnln("warn")
  logrus.Infof("info")
  logrus.Println("print")
}

gin集成logrus

  • main函数(main.go)
package main
import (
  "gin/Logger/gin/gin_logrus/log"
  "gin/Logger/gin/gin_logrus/middleware"
  "github.com/gin-gonic/gin"
)
func main() {
  log.InitFile("./log", "fengxu")
  r := gin.New()
  r.Use(middleware.Logmiddleware())
  r.GET("/", func(c *gin.Context) {
    c.JSON(200, gin.H{
      "message": "pong",
    })
  })
  r.Run(":8080")
}
  • log.go
package log
import (
  "bytes"
  "fmt"
  "github.com/sirupsen/logrus"
  "os"
  "time"
)
type Hook struct {
  writer   *os.File
  logPath  string
  fileName string
  fileDate string
}
func (MyHook *Hook) Levels() []logrus.Level {
  return logrus.AllLevels
}
func (MyHook *Hook) Fire(entry *logrus.Entry) error {
  timer := time.Now().Format("2006-01-02")
  line, _ := entry.String()
  //需要切换日志文件
  if MyHook.fileDate != timer {
    MyHook.fileDate = timer
    MyHook.writer.Close()
    filepath := fmt.Sprintf("%s/%s", MyHook.logPath, MyHook.fileDate)
    err := os.MkdirAll(filepath, os.ModePerm)
    if err != nil {
      logrus.Error(err)
      return err
    }
    filename := fmt.Sprintf("%s/%s.log", filepath, MyHook.fileName)
    MyHook.writer, _ = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
  }
  MyHook.writer.Write([]byte(line))
  return nil
}
type LogFormat struct {
}
func (l *LogFormat) Format(entry *logrus.Entry) ([]byte, error) {
  var buff *bytes.Buffer
  if entry.Buffer != nil {
    buff = entry.Buffer
  } else {
    buff = &bytes.Buffer{}
  }
  _, _ = fmt.Fprintf(buff, "%s\n", entry.Message) //这里可以自己去设置输出格式
  return buff.Bytes(), nil
}
func InitFile(logPath string, fileName string) {
  logrus.SetFormatter(&LogFormat{})
  timer := time.Now().Format("2006-01-02")
  filepath := fmt.Sprintf("%s/%s", logPath, timer)
  err := os.MkdirAll(filepath, os.ModePerm)
  if err != nil {
    logrus.Error(err)
    return
  }
  filename := fmt.Sprintf("%s/%s.log", filepath, fileName)
  writer, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
  if err != nil {
    logrus.Error(err)
    return
  }
  logrus.AddHook(&Hook{
    writer:   writer,
    logPath:  logPath,
    fileName: fileName,
    fileDate: timer,
  })
}
  • 中间件(lmiddleware.go)
package middleware
import (
  "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
    if raw != "" {
      path = path + "?" + raw
    }
    c.Next() //执行其他中间件
    //end := time.Now()
    //timesub := end.Sub(start)  //响应所需时间
    //ClientIp := c.ClientIP()  //客户端ip
    statuscode := c.Writer.Status()
    //var statusColor string  
    //switch c.Writer.Status() {
    //case 200:
    //  statusColor = fmt.Sprintf("\033[%dm%d\033[0m", status200, statuscode)
    //case 404:
    //  statusColor = fmt.Sprintf("\033[%dm%d\033[0m", status404, statuscode)
    //default:
    //  statusColor = fmt.Sprintf("\033[%dm%d\033[0m", status500, statuscode)
    //}
    //
    //var methodColor string
    //switch c.Request.Method {
    //case "GET":
    //  methodColor = fmt.Sprintf("\033[%dm%s\033[0m", methodGET, c.Request.Method)
    //}
    logrus.Infof("[GIN] %s  |%d  |%s  |%s",
      start.Format("2006-01-02 15:04:06"),
      statuscode,
      c.Request.Method,
      path,
    )
  }
}

项目结构:

结语

至此我们对Gin框架的简单学习就到此为止了,更多的学习大家可以前去查看Gin框架官方文档:

Gin框架官方文档

后面就要开始对Gorm的学习了,下篇见!

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
11天前
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
116 30
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
|
1月前
|
XML JSON Java
Logback 与 log4j2 性能对比:谁才是日志框架的性能王者?
【10月更文挑战第5天】在Java开发中,日志框架是不可或缺的工具,它们帮助我们记录系统运行时的信息、警告和错误,对于开发人员来说至关重要。在众多日志框架中,Logback和log4j2以其卓越的性能和丰富的功能脱颖而出,成为开发者们的首选。本文将深入探讨Logback与log4j2在性能方面的对比,通过详细的分析和实例,帮助大家理解两者之间的性能差异,以便在实际项目中做出更明智的选择。
216 3
|
1月前
|
PyTorch 算法框架/工具
Pytorch学习笔记(七):F.softmax()和F.log_softmax函数详解
本文介绍了PyTorch中的F.softmax()和F.log_softmax()函数的语法、参数和使用示例,解释了它们在进行归一化处理时的作用和区别。
377 1
Pytorch学习笔记(七):F.softmax()和F.log_softmax函数详解
|
21天前
|
Java 程序员 API
Android|集成 slf4j + logback 作为日志框架
做个简单改造,统一 Android APP 和 Java 后端项目打印日志的体验。
83 1
|
2月前
|
设计模式 SQL 安全
PHP中的设计模式:单例模式的深入探索与实践在PHP的编程实践中,设计模式是解决常见软件设计问题的最佳实践。单例模式作为设计模式中的一种,确保一个类只有一个实例,并提供全局访问点,广泛应用于配置管理、日志记录和测试框架等场景。本文将深入探讨单例模式的原理、实现方式及其在PHP中的应用,帮助开发者更好地理解和运用这一设计模式。
在PHP开发中,单例模式通过确保类仅有一个实例并提供一个全局访问点,有效管理和访问共享资源。本文详细介绍了单例模式的概念、PHP实现方式及应用场景,并通过具体代码示例展示如何在PHP中实现单例模式以及如何在实际项目中正确使用它来优化代码结构和性能。
45 2
|
1月前
|
数据可视化
Tensorboard可视化学习笔记(一):如何可视化通过网页查看log日志
关于如何使用TensorBoard进行数据可视化的教程,包括TensorBoard的安装、配置环境变量、将数据写入TensorBoard、启动TensorBoard以及如何通过网页查看日志文件。
193 0
|
1月前
|
SQL XML 监控
SpringBoot框架日志详解
本文详细介绍了日志系统的重要性及其在不同环境下的配置方法。日志用于记录系统运行时的问题,确保服务的可靠性。文章解释了各种日志级别(如 info、warn、error 等)的作用,并介绍了常用的日志框架如 SLF4J 和 Logback。此外,还说明了如何在 SpringBoot 中配置日志输出路径及日志级别,包括控制台输出与文件输出的具体设置方法。通过这些配置,开发者能够更好地管理和调试应用程序。
|
3月前
|
jenkins 持续交付
jenkins学习笔记之三:使用jenkins共享库实现日志格式化输出
jenkins学习笔记之三:使用jenkins共享库实现日志格式化输出
jenkins学习笔记之三:使用jenkins共享库实现日志格式化输出
|
2月前
|
Java
日志框架log4j打印异常堆栈信息携带traceId,方便接口异常排查
日常项目运行日志,异常栈打印是不带traceId,导致排查问题查找异常栈很麻烦。
|
2月前
|
运维 NoSQL Java
SpringBoot接入轻量级分布式日志框架GrayLog技术分享
在当今的软件开发环境中,日志管理扮演着至关重要的角色,尤其是在微服务架构下,分布式日志的统一收集、分析和展示成为了开发者和运维人员必须面对的问题。GrayLog作为一个轻量级的分布式日志框架,以其简洁、高效和易部署的特性,逐渐受到广大开发者的青睐。本文将详细介绍如何在SpringBoot项目中接入GrayLog,以实现日志的集中管理和分析。
225 1