Go中日志库

自定义日志(推荐)

这里实现了控制台和文件日志

/utils/zlog/zlog.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package zlog

import (
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
)

type LogType int

const (
Console LogType = iota
File
ConsoleFile
)

var mLogType = Console
var mProjectPath = ""

// Init 在main.go中调用
func Init() {
_, file, _, _ := runtime.Caller(1)
dirPath := filepath.Dir(file)
dirPath = strings.ReplaceAll(dirPath, "\\", "/")
mProjectPath = dirPath + "/"
}

func SetLogType(mType LogType) {
mLogType = mType
}

func LogDebug(msg string, a ...any) {
logRecord(fmt.Sprintf(msg, a...), "DEBUG", mLogType)
}

func LogError(msg string, a ...any) {
logRecord(fmt.Sprintf(msg, a...), "ERROR", mLogType)
}

func logRecord(msg string, level string, logType LogType) {
now := time.Now().Format("2006-01-02 15:04:05")
_, mFilepath, line, _ := runtime.Caller(2)
if mProjectPath != "" {
mFilepath = strings.Replace(mFilepath, mProjectPath, "", 1)
}
logStr := "[" + now + "]\t" + level + "\t" + mFilepath + ":" + strconv.Itoa(line) + "\n\t" + msg + "\n"
switch logType {
case Console:
writeConsole(logStr)
case File:
writeFile(logStr, level)
case ConsoleFile:
writeConsole(logStr)
writeFile(logStr, level)
}
}

func logConsoleDebug(msg string) {
logRecord(msg, "DEBUG", Console)
}

func logConsoleError(msg string) {
logRecord(msg, "ERROR", Console)
}

func writeConsole(logStr string) {
fmt.Print(logStr)
}

var fileName = ""
var saveFile *os.File

func writeFile(logStr string, level string) {
ymd := time.Now().Format("2006-01-02")
err := os.MkdirAll("logs", os.ModePerm)
if err != nil {
logConsoleError(err.Error())
return
}
separator := string(filepath.Separator)
tempFileName := "logs" + separator + level + "-" + ymd + ".log"
if tempFileName != fileName || saveFile == nil {
logConsoleDebug("日志文件路径: " + tempFileName)
if saveFile != nil {
err := saveFile.Close()
if err != nil {
logConsoleError(err.Error())
}
}
saveFile, err = os.OpenFile(tempFileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
logConsoleError(err.Error())
return
}
fileName = tempFileName
}
_, err = saveFile.WriteString(logStr)
if err != nil {
logConsoleError(err.Error())
return
}
return
}

调用

1
2
3
4
5
6
7
8
9
//初始化 必须调用 用来确定源代码的根路径
zlog.Init()

//设置日志的输出类型 可以不调用
zlog.SetLogType(zlog.Console)
//打印日志的地方这样调用
zlog.LogDebug("测试日志")

zlog.LogDebug("insert success, last id:%d\n", 123)

和Gin结合

基本封装

当我们使用Gin框架的时候,我们可以简单的封装一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package zlog

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

func Debug(format string, values ...any) {
now := time.Now().Format("2006/01/02 15:04:05")
f := fmt.Sprintf("[Debug] %s %s\n", now, format)
_, _ = fmt.Fprintf(gin.DefaultWriter, f, values...)
}

func Error(format string, values ...any) {
now := time.Now().Format("2006/01/02 15:04:05")
f := fmt.Sprintf("[Error] %s %s\n", now, format)
_, _ = fmt.Fprintf(gin.DefaultWriter, f, values...)
}

调用

1
zlog.Debug(utils.ObjToJson(list))

打印行号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package zlog

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

func mCaller(skip int) string {
pc, file, line, ok := runtime.Caller(skip)
if !ok {
return ""
}
// 获取方法名
funcName := runtime.FuncForPC(pc).Name()
return fmt.Sprintf("%s:%d(%s)", file, line, funcName)
}

func Info(format string, values ...any) {
fileStrAll := mCaller(2)
now := time.Now().Format("2006-01-02 15:04:05")
f := fmt.Sprintf("%s INFO %s ↓\n%s\n", now, fileStrAll, format)
_, _ = fmt.Fprintf(gin.DefaultWriter, f, values...)
}

func Error(format string, values ...any) {
fileStrAll := mCaller(2)
now := time.Now().Format("2006-01-02 15:04:05")
f := fmt.Sprintf("%s ERROR %s ↓\n%s\n", now, fileStrAll, format)
_, _ = fmt.Fprintf(gin.DefaultWriter, f, values...)
}

调用

1
zlog.Info("Hello World")

打印日志

2024-02-26 21:53:15 INFO D:/Project/go/z-wiki/main.go:33(main.main) ↓
Hello World

这样我们点击日志就直接跳转到对应的代码位置了。

在文件输出日志

Gin框架的请求日志默认在控制台输出,但更多的时候,尤其上线运行时,我们希望将用户的请求日志保存到日志文件中,以便更好的分析与备份。

在Gin框架中,通过gin.DefaultWriter变量可能控制日志的保存方式,gin.DefaultWriter在Gin框架中的定义如下:

1
var DefaultWriter io.Writer = os.Stdout

从上面的定义我们可以看出,gin.DefaultWriter的类型为io.Writer,默认值为os.Stdout,即控制台输出,

因此我们可以通过修改gin.DefaultWriter值来将请求日志保存到日志文件或其他地方(比如数据库)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import (
"github.com/gin-gonic/gin"
"io"
"os"
)
func main() {
gin.DisableConsoleColor()//保存到文件不需要颜色
file, _ := os.Create("access.log")
gin.DefaultWriter = file
//gin.DefaultWriter = io.MultiWriter(file) 效果是一样的
router := gin.Default()
router.GET("/test", func(c *gin.Context) {
c.String(200, "test")
})
_ = router.Run(":8080")
}

运行后上面的程序,会在程序所在目录创建access.log文件,当我们发起Web请求后,请求的日志会保存到access.log文件,而不会在控制台输出。

也可以让请求日志同行保存到文件和在控制台输出:

1
2
file, _ := os.Create("access.log")
gin.DefaultWriter = io.MultiWriter(file,os.Stdout) //同时保存到文件和在控制台中输出

zap(推荐)

需要go1.19版本及以上

安装

1
go get go.uber.org/zap

设置全局变量

如果您想在不先创建 Logger 实例的情况下写入日志,您可以在 init() 函数中使用 ReplaceGlobals() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"go.uber.org/zap"
)

func init() {
zap.ReplaceGlobals(zap.Must(zap.NewDevelopment()))
}

func main() {
zap.L().Info("Hello from Zap!")
}

此方法将可通过 zap.L() 访问的全局记录器替换为功能性 Logger 实例,以便您只需将 zap 包导入文件即可直接使用它。

不同环境切换

您可以使用环境变量轻松地在开发和生产 Logger 之间切换:

1
2
3
4
logger := zap.Must(zap.NewProduction())
if os.Getenv("APP_ENV") == "development" {
logger = zap.Must(zap.NewDevelopment())
}

在Gin中

1
2
3
4
5
6
7
func init() {
if gin.Mode() == gin.DebugMode {
zap.ReplaceGlobals(zap.Must(zap.NewDevelopment()))
} else {
zap.ReplaceGlobals(zap.Must(zap.NewProduction()))
}
}

不同模式

示例

1
2
3
4
5
6
7
8
9
10
11
logger := zap.NewExample(zap.AddCaller())
defer logger.Sync()
logger.Info("Hello World", zap.String("name", "psvmc"))

logger2, _ := zap.NewProduction(zap.AddCaller())
defer logger2.Sync()
logger2.Info("Hello World", zap.String("name", "psvmc"))

logger3, _ := zap.NewDevelopment(zap.AddCaller())
defer logger3.Sync()
logger3.Info("Hello World", zap.String("name", "psvmc"))

打印输出结果:

{“level”:”info”,”msg”:”Hello World”,”name”:”psvmc”}
{“level”:”info”,”ts”:1708956240.9281151,”caller”:”z-wiki/main.go:38”,”msg”:”Hello World”,”name”:”psvmc”}
2024-02-26T22:04:00.928+0800 INFO z-wiki/main.go:42 Hello World {“name”: “psvmc”}

log/slog

需要go1.21版本及以上

log/slog是Go 1.21中引入的一个新的结构化日志库,它与标准库的log包兼容,但提供了更多的功能和灵活性。

log/slog定义了一个类型,Logger,用于记录不同级别和格式的日志信息。每个Logger都关联一个Handler,用于处理日志记录。

log/slog还提供了一个默认的Logger,可以通过顶级函数(如Info和Error)来使用,它们会调用相应的Logger方法。

该默认Logger将日志信息写入标准错误,并在每条日志信息前添加日期和时间。

log/slog的日志记录由以下几个部分组成:

  • 时间:日志记录发生的时间,可以是本地时间或UTC时间。
  • 级别:日志记录的严重程度,可以是预定义的四个级别之一(Debug、Info、Warn、Error),也可以是自定义的整数值。
  • 消息:日志记录的主要内容,通常是一个简短的描述性字符串。
  • 属性:日志记录的额外信息,以键值对的形式表示,键是字符串,值可以是任意类型。

基本示例

例如,以下代码:

1
2
3
4
5
6
7
8
9
10
package main

import (
"log/slog"
"os"
)

func main() {
slog.Info("helloworld", "user", os.Getenv("USER"))
}

会产生以下输出:

1
2024/02/26 18:58:17 INFO hello-world user=""

其中,2024/02/26 18:58:17是时间,INFO是级别,hello-world是消息,user=""是属性。

log/slog还提供了一些有用的特性,如:

  • 可以自定义日志级别、输出目标、格式器(JSON或文本)、时间戳等。
  • 可以使用字段(Fields)来添加结构化的上下文信息,如键值对。
  • 可以使用处理器(Handler)来处理不同级别或条件的日志信息,如过滤、分割、彩色等。
  • 可以使用条目(Entry)来记录带有字段的日志信息,或者使用WithFields、WithTime、WithError等方法来创建带有字段的条目。
  • 可以使用日志级别函数(如Info、Warn、Error等)来记录不同级别的日志信息,或者使用Log或Print等方法来记录默认级别的日志信息。

标准错误和标准输出

以下是一个使用log/slog的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"log/slog"
"os"
)

func main() {
// 创建一个文本处理器
textHandler := slog.NewTextHandler(os.Stderr, nil)
// 创建一个文本 Logger
textLogger := slog.New(textHandler)
// 使用Logger记录结构化的日志信息
textLogger.Info("hello-world", "user", os.Getenv("USER"))

// 创建一个JSON处理器
jsonHandler := slog.NewJSONHandler(os.Stdout, nil)
// 创建一个 JSON Logger
jsonLogger := slog.New(jsonHandler)
// 使用Logger记录结构化的日志信息
jsonLogger.Info("hello-world", "user", os.Getenv("USER"))
}

该程序会在标准错误上输出文本格式的日志信息:

1
time=2024-02-26T22:35:53.533+08:00 level=INFO msg=hello-world user=""

然后在标准输出上输出JSON格式的日志信息:

1
{"time":"2024-02-26T22:35:53.5512541+08:00","level":"INFO","msg":"hello-world","user":""}

logrus

1
go get github.com/sirupsen/logrus

引用

1
import "github.com/sirupsen/logrus"

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"os"

"github.com/sirupsen/logrus"
)

func main() {
// 创建一个Logrus实例
log := logrus.New()

// 设置日志级别为Debug
log.SetLevel(logrus.DebugLevel)

// 设置输出目标为标准输出
log.SetOutput(os.Stdout)

// 设置格式器为JSON
log.SetFormatter(&logrus.JSONFormatter{})

// 使用Fields添加结构化的上下文信息
log.WithFields(logrus.Fields{
"key1": "value1",
"key2": "value2",
}).Info("This is a structured log")

// 使用Entry记录带有字段的日志信息
entry := log.WithFields(logrus.Fields{
"key3": "value3",
"key4": "value4",
})
entry.Warn("This is another structured log")

// 使用日志级别函数记录不同级别的日志信息
log.Debug("This is a debug log")
log.Info("This is an info log")
log.Warn("This is a warn log")
log.Error("This is an error log")
log.Fatal("This is a fatal log")
log.Panic("This is a panic log")

// 使用Log或Print等方法记录默认级别的日志信息
log.Log(logrus.InfoLevel, "This is a log with level")
log.Print("This is a print log")
}

IDEA日志高亮

image-20240226222446166

File => Settings => Plugins 搜索 Grep Console安装即可,安装后会根据日志中日志级别显示不同颜色。