ZhouChuang's Blog ZhouChuang's Blog
首页
  • 常用命令
  • 日常总结
  • Prometheus
  • 基础
  • 常用库
专题
  • 「编码」
更多
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Zhou Chuang

为学日益 为道日损
首页
  • 常用命令
  • 日常总结
  • Prometheus
  • 基础
  • 常用库
专题
  • 「编码」
更多
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Go语言基础

  • 常用库

    • 命令行工具 flag
    • 日志库 Zap
      • 日志
      • Uber-go Zap 介绍
        • 为什么选择 zap
        • 性能 && 基准测试
      • 基本使用
        • 安装
        • 入门
      • 日志级别
      • 时间格式化
      • Sugar 日志
      • 输出美化
      • 加日志前缀
      • 全局日志
      • 定制 Logger​ ⭐️
        • 写入文件而不是终端
        • 添加调用者详细信息
        • AddCallerSkip
        • 日志输出到多个位置
        • 将err日志单独输出到文件
      • 用Lumberjack日志切割归档
  • 数据库相关内容

  • 技巧

  • golang
  • 常用库
ZhouChuang
2025-03-30
目录

日志库 Zap

摘要:本文介绍 Go 语言日志库 Zap,突出其高性能、结构化日志及灵活性。通过与其他日志库比较及基准测试,展示 Zap 优势。涵盖安装、使用、配置日志级别、时间格式化、输出美化及日志切割等,附代码示例,适合开发者快速上手与定制。

# 日志

一个好的日志收集器需要有哪些功能?

  • 多级别日志记录:支持不同级别的日志记录(如 DEBUG、INFO、WARN、ERROR、FATAL)
  • 灵活的输出目标:控制台、文件、远程、标准输出流等。
  • 日志格式化,如文本格式、JSON、结构化日志等。
  • 日志轮转、日志切割
  • 上下文支持,如调用文件/函数名和行号,时间等。
  • ...

Go 目前主流的日志库:

  • Zap: 高性能、结构化、分流、上下文支持,但配置稍复杂。
  • slog: 标准库、高性能、结构化、易用,但分流需自定义。
  • Zerolog: 零分配、结构化、链式 API,扩展性稍弱。
  • Logrus: 易用、结构化、钩子支持,但性能较低

Go 标准库提供的 log 包是一个基本的日志记录工具,具有一些明显的优势和劣势。

优势:

简单易用、轻量级、线程安全。我们可以设置任何io.Writer作为日志记录输出并向其发送要写入的日志。

劣势:

  • 功能有限:相比于许多第三方日志库,Go 标准 logger 的功能较为基本,例如不支持多级别日志、不同的日志格式、日志轮转等。

  • 配置不灵活:缺乏对输出目标的灵活配置,用户只能选择输出到标准输出或文件,缺少对复杂需求的支持。

  • 格式化能力不足:默认的输出格式比较简单,不支持如 JSON、结构化日志等格式,限制了日志的可读性和可解析性。

  • 缺少高级功能:不支持如异步日志、日志过滤、上下文支持等高级特性,可能影响在复杂应用中的使用。

虽然 Go 的标准 logger 提供了基本的日志记录功能,但在需要高级日志管理的应用场景中,可能会显得不足。因此,很多开发者在更复杂的环境中倾向于使用第三方日志库,比如 logrus、zap 和 zerolog,它们提供了更强大的功能和灵活性。

# Uber-go Zap 介绍

# 为什么选择 zap

选择 Zap 作为日志库主要是因为它在性能、灵活性和易用性上表现得很出色。

首先,Zap 是 Go 语言生态中最快的日志库之一,它采用了零分配的设计,能大幅减少内存分配和 GC 压力,这在高并发场景下特别重要。

其次,Zap 提供了结构化日志的功能,可以方便地输出 JSON 或其他格式,便于日志分析和集成到现代日志系统中,比如 ELK 或其他 observability 工具。

此外,它还支持日志级别的动态调整和自定义配置,能很好地满足不同场景的需求,比如开发时用详细的调试日志,生产环境切换到高效的错误日志。

最后,Zap 的社区活跃,文档完善,用起来上手快,维护成本也低。综合这些因素,Zap 就成了一个很理想的选择。

# 性能 && 基准测试

Zap 采用包含一个无反射、零分配的 JSON 编码器,并且基础的Logger尽力避免序列化开销并在任何可能的地方避免内存分配。通过在此基础上构建高级的SugaredLogger,Zap 让用户选择何时需要计算每一次内存分配,以及何时他们更喜欢更熟悉的、松散类型的 API。

根据Uber-go Zap的文档,它的性能比类似的结构化日志包更好——也比标准库更快。 以下是Zap发布的基准测试信息:

记录一条消息和10个字段:

Package Time Time % to zap Objects Allocated
⚡ zap 656 ns/op +0% 5 allocs/op
⚡ zap (sugared) 935 ns/op +43% 10 allocs/op
zerolog 380 ns/op -42% 1 allocs/op
go-kit 2249 ns/op +243% 57 allocs/op
slog (LogAttrs) 2479 ns/op +278% 40 allocs/op
slog 2481 ns/op +278% 42 allocs/op
apex/log 9591 ns/op +1362% 63 allocs/op
log15 11393 ns/op +1637% 75 allocs/op
logrus 11654 ns/op +1677% 79 allocs/op

使用已经具有 10 个上下文字段的记录器记录一条消息:

Package Time Time % to zap Objects Allocated
⚡ zap 67 ns/op +0% 0 allocs/op
⚡ zap (sugared) 84 ns/op +25% 1 allocs/op
zerolog 35 ns/op -48% 0 allocs/op
slog 193 ns/op +188% 0 allocs/op
slog (LogAttrs) 200 ns/op +199% 0 allocs/op
go-kit 2460 ns/op +3572% 56 allocs/op
log15 9038 ns/op +13390% 70 allocs/op
apex/log 9068 ns/op +13434% 53 allocs/op
logrus 10521 ns/op +15603% 68 allocs/op

记录一个静态字符串,无需任何上下文或printf风格的模板。

Package Time Time % to zap Objects Allocated
⚡ zap 63 ns/op +0% 0 allocs/op
⚡ zap (sugared) 81 ns/op +29% 1 allocs/op
zerolog 32 ns/op -49% 0 allocs/op
standard library 124 ns/op +97% 1 allocs/op
slog 196 ns/op +211% 0 allocs/op
slog (LogAttrs) 200 ns/op +217% 0 allocs/op
go-kit 213 ns/op +238% 9 allocs/op
apex/log 771 ns/op +1124% 5 allocs/op
logrus 1439 ns/op +2184% 23 allocs/op
log15 2069 ns/op +3184% 20 allocs/op

# 基本使用

# 安装

go get -u go.uber.org/zap
1

# 入门

  1. 通过调用zap.NewProduction()/zap.NewDevelopment()或者zap.Example()创建一个Logger。
  2. 上面的每一个函数都将创建一个logger。唯一的区别在于它将记录的信息不同。例如production logger默认记录调用函数信息、日期和时间等。
  3. 通过Logger调用Info/Error等方法。
  4. 默认情况下日志都会打印到应用程序的console界面。
func dev() {
  logger, _ := zap.NewDevelopment()
  logger.Info("dev this is info")
  logger.Warn("dev this is warn")
  logger.Error("dev this is error")
}

func example() {
  logger := zap.NewExample()
  logger.Info("exam this is info")
  logger.Warn("exam this is warn")
  logger.Error("exam this is error")
}

func prod() {
  logger, _ := zap.NewProduction()
  logger.Info("prod this is info")
  logger.Warn("prod this is warn")
  logger.Error("prod this is error")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

在 Development 模式下,日志的格式是 test 格式,Warn 和 Error 会打印栈信息。

在 Example 模式下,格式为 json ,并且字段只有 level 和 msg。

在 Production 模式下,格式为json,字段有level、时间、调用位置、msg。这就方便了生产环境中排查问题。

zap-1

日志记录器的方法的签名是这样的?

func (log *Logger) MethodXXX(msg string, fields ...Field)
1

其中MethodXXX是一个可变参数函数,可以是Info / Error/ Debug / Panic等。每个方法都接受一个消息字符串和任意数量的zapcore.Field场参数。

每个zapcore.Field 是一个结构体,我们可以用zap提供的方法,传入键值对的方式转化。

logger.Info("It is a message", zap.String("key", "value"), zap.Int("key", 123), zap.Any("key", "hello"))
1

{"level":"info","ts":1743354106.234351,"caller":"learn_zap/main.go:21","msg":"It is a message","key":"value","key":123,"key":"hello"}

# 日志级别

const (
	// DebugLevel logs are typically voluminous, and are usually disabled in
	// production.
	DebugLevel = zapcore.DebugLevel
	// InfoLevel is the default logging priority.
	InfoLevel = zapcore.InfoLevel
	// WarnLevel logs are more important than Info, but don't need individual
	// human review.
	WarnLevel = zapcore.WarnLevel
	// ErrorLevel logs are high-priority. If an application is running smoothly,
	// it shouldn't generate any error-level logs.
	ErrorLevel = zapcore.ErrorLevel
	// DPanicLevel logs are particularly important errors. In development the
	// logger panics after writing the message.
	DPanicLevel = zapcore.DPanicLevel
	// PanicLevel logs a message, then panics.
	PanicLevel = zapcore.PanicLevel
	// FatalLevel logs a message, then calls os.Exit(1).
	FatalLevel = zapcore.FatalLevel
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使用 zap 的 NewDevelopmentConfig 快速配置
cfg := zap.NewDevelopmentConfig()
cfg.Level = zap.NewAtomicLevelAt(zap.WarnLevel) //只有Warn级别或以上的日志会被打印
// 创建 logger
logger, _ := cfg.Build()
logger.Debug("this is dev debug log")
logger.Info("this is dev info log")
logger.Warn("this is dev warn log")
logger.Error("this is dev error log")
logger.Fatal("this is dev fatal log")
1
2
3
4
5
6
7
8
9
10

# 时间格式化

Zap 默认输出的时间戳是以 Unix 时间戳(通常是纳秒级别)或 ISO8601 格式(如 2025-03-30T12:00:00Z)显示的,虽然精确,但对人类可读性不够友好。如果想要改成更直观的格式,比如 2025-03-30 12:00:00,可以通过自定义 Zap 的配置来实现。

// 使用 zap 的 NewDevelopmentConfig 快速配置
cfg := zap.NewDevelopmentConfig()
cfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05") // 替换时间格式化方式

// 创建 logger
logger, _ := cfg.Build()

logger.Info("这是一个测试日志", zap.String("key", "value"))
1
2
3
4
5
6
7
8

说明:

  1. 运行后,日志的时间戳会变成类似 "2025-03-30 12:00:00" 的格式。
  2. 如果你想要更详细或不同的风格,可以调整格式字符串,比如:
    • "2006-01-02 15:04:05.000":带毫秒的格式(如 2025-03-30 12:00:00.123)。
    • "Jan 02 15:04:05":简洁的英文风格(如 Mar 30 12:00:00)。

# Sugar 日志

Zap提供了两种类型的日志记录器—Sugared Logger和Logger。

在性能很好但不是很关键的上下文中,使用SugaredLogger。它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录。

在每一微秒和每一次内存分配都很重要的上下文中,使用Logger。它比SugaredLogger更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。

使用的差别:

  • 大部分的实现基本都相同。
  • 惟一的区别是,我们通过调用主logger的Sugar()方法来获取一个SugaredLogger。
  • 然后使用SugaredLogger以printf格式记录语句
func production() {
	logger, _ := zap.NewProduction()
	sugarLogger := logger.Sugar()
	defer sugarLogger.Sync()

	sugarLogger.Debug("debug message %s", "for sugar logger")
	sugarLogger.Infof("info message %s", "for sugar logger")
}
1
2
3
4
5
6
7
8
➜  go run main.go
{"level":"info","ts":1743355770.759996,"caller":"learn_zap/main.go:11","msg":"info message for sugar logger"}
1
2

# 输出美化

我们让 info,warn,error显示不同的颜色,看起来好看些。

变色的关键 颜色控制字符

// 定义颜色
const (
	colorRed    = "\033[31m"
	colorGreen  = "\033[32m"
	colorYellow = "\033[33m"
	colorBlue   = "\033[34m"
	colorReset  = "\033[0m"
)

// 自定义 EncodeLevel
func coloredLevelEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
	switch level {
	case zapcore.DebugLevel:
		enc.AppendString(colorBlue + "DEBUG" + colorReset)
	case zapcore.InfoLevel:
		enc.AppendString(colorGreen + "INFO" + colorReset)
	case zapcore.WarnLevel:
		enc.AppendString(colorYellow + "WARN" + colorReset)
	case zapcore.ErrorLevel, zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel:
		enc.AppendString(colorRed + "ERROR" + colorReset)
	default:
		enc.AppendString(level.String()) // 默认行为
	}
}
func dev() {
	// 使用 zap 的 NewDevelopmentConfig 快速配置
	cfg := zap.NewDevelopmentConfig()
	cfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05") // 替换时间格式化方式
	cfg.EncoderConfig.EncodeLevel = coloredLevelEncoder
	// 创建 logger
	logger, _ := cfg.Build()
	defer logger.Sync() // 确保日志刷新

	logger.Info("dev this is info")
	logger.Warn("dev this is warn")
	logger.Error("dev this is error")
}
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
zap-2

# 加日志前缀

const logPrefix = "[MyAPP]"

type prefixedEncoder struct {
	zapcore.Encoder
}

func (e *prefixedEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
	// 先调用原始的 EncodeEntry 方法生成日志行
	buf, err := e.Encoder.EncodeEntry(entry, fields)
	if err != nil {
		return nil, err
	}
	// 在日志行的最前面添加前缀
	logLine := buf.String()
	buf.Reset()
	buf.AppendString(logPrefix + logLine)

	return buf, nil
}

func dev() {
	cfg := zap.NewDevelopmentConfig()
	cfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05") // 替换时间格式化方式
	// 创建自定义的 Encoder
	encoder := &prefixedEncoder{
		Encoder: zapcore.NewConsoleEncoder(cfg.EncoderConfig), // 使用 Console 编码器
	}
	// 创建 Core
	core := zapcore.NewCore(
		encoder,                    // 使用自定义的 Encoder
		zapcore.AddSync(os.Stdout), // 输出到控制台
		zapcore.DebugLevel,         // 设置日志级别
	)

	// 创建 Logger
	logger := zap.New(core)
	defer logger.Sync() // 确保日志缓冲区中的所有日志都被刷新到输出

	logger.Info("Hello, World!")
	logger.Warn("development, this is  a warning message")
}
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

结果:

[MyAPP]2025-03-31 09:35:28      INFO    Hello, World!
[MyAPP]2025-03-31 09:35:28      WARN    development, this is  a warning message
1
2

# 全局日志

zap推崇的还是以对象的形式使用日志,但是有些时候想要在应用程序的任何地方都可以直接使用的日志实例,那么可以用到全局日志

// 初始化全局日志
func initLogger() {
	// 使用 zap 的 NewDevelopmentConfig 快速配置
	cfg := zap.NewDevelopmentConfig()
	cfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05") // 替换时间格式化方式
	// 创建 Logger
	logger, _ := cfg.Build()
	zap.ReplaceGlobals(logger)
}

func dev() {
	zap.L().Info("dev this is info")
	zap.L().Warn("dev this is warn")
	zap.L().Error("dev this is error")
	zap.S().Infof("dev this is info %s", "xxx")
	zap.S().Warnf("dev this is warn %s", "xxx")
	zap.S().Errorf("dev this is error %s", "xxx")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

L方法返回的是标准zap实例,S方法返回的是superZap的实例,superZap主要多了模板字符串方法

# 定制 Logger​ ⭐️

# 写入文件而不是终端

我们使用zap.New(…)方法来手动传递所有配置,而不是使用像zap.NewProduction()这样的预置方法来创建logger。

zapcore.Core需要三个配置——Encoder,WriteSyncer,LogLevel。

  1. Encoder:编码器(如何写入日志)。我们将使用开箱即用的NewJSONEncoder(),并使用预先设置的ProductionEncoderConfig()。

    zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
    
    1
  2. WriterSyncer :指定日志将写到哪里去。我们使用zapcore.AddSync()函数并且将打开的文件句柄传进去。

    file, _ := os.Create("./test.log")
    writeSyncer := zapcore.AddSync(file)
    
    1
    2
  3. Log Level:哪种级别的日志将被写入。

示例:

func InitLogger() {
	writeSyncer := getLogWriter()
	encoder := getEncoder()
	core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)

	logger := zap.New(core)
	sugarLogger = logger.Sugar()
}

func getEncoder() zapcore.Encoder {
    // return zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()) // 普通的log encoder
	return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}

func getLogWriter() zapcore.WriteSyncer {
	file, _ := os.Create("./test.log")
	return zapcore.AddSync(file)
}

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

# 添加调用者详细信息

我们要做的第一件事是覆盖默认的ProductionConfig(),并进行以下更改:

  • 修改时间编码器
  • 在日志文件中使用大写字母记录日志级别
func getEncoder() zapcore.Encoder {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
	return zapcore.NewConsoleEncoder(encoderConfig)
}
1
2
3
4
5
6

接下来,我们将修改zap logger代码,添加将调用函数信息记录到日志中的功能。为此,我们将在zap.New(..)函数中添加一个Option。

logger := zap.New(core, zap.AddCaller())
1

# AddCallerSkip

当我们不是直接使用初始化好的logger实例记录日志,而是将其包装成一个函数等,此时日录日志的函数调用链会增加,想要获得准确的调用信息就需要通过AddCallerSkip函数来跳过。

logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
1

# 日志输出到多个位置

func getLogWriter() zapcore.WriteSyncer {
	file, _ := os.Create("./test.log")
	// 利用io.MultiWriter支持文件和终端两个输出目标
	ws := io.MultiWriter(file, os.Stdout)
	return zapcore.AddSync(ws)
}
1
2
3
4
5
6

# 将err日志单独输出到文件

有时候我们除了将全量日志输出到xx.log文件中之外,还希望将ERROR级别的日志单独输出到一个名为xx.err.log的日志文件中。我们可以通过以下方式实现。

func InitLogger() {
	encoder := getEncoder()
	// test.log记录全量日志
	logF, _ := os.Create("./test.log")
	c1 := zapcore.NewCore(encoder, zapcore.AddSync(logF), zapcore.DebugLevel)
	// test.err.log记录ERROR级别的日志
	errF, _ := os.Create("./test.err.log")
	c2 := zapcore.NewCore(encoder, zapcore.AddSync(errF), zap.ErrorLevel)
	// 使用NewTee将c1和c2合并到core
	core := zapcore.NewTee(c1, c2)
	logger = zap.New(core, zap.AddCaller())
}
1
2
3
4
5
6
7
8
9
10
11
12

# 用Lumberjack日志切割归档

为了添加日志切割归档功能,我们将使用第三方库Lumberjack (opens new window)来实现。

目前只支持按文件大小切割,原因是按时间切割效率低且不能保证日志数据不被破坏。

安装:

go get gopkg.in/natefinch/lumberjack.v2
1

zap logger中加入Lumberjack

func getLogWriter() zapcore.WriteSyncer {
	lumberJackLogger := &lumberjack.Logger{
		Filename:   "./test.log",
		MaxSize:    10,
		MaxBackups: 5,
		MaxAge:     30,
		Compress:   false,
	}
	return zapcore.AddSync(lumberJackLogger)
}
1
2
3
4
5
6
7
8
9
10

Lumberjack Logger采用以下属性作为输入:

  • Filename: 日志文件的位置
  • MaxSize:在进行切割之前,日志文件的最大大小(以MB为单位)
  • MaxBackups:保留旧文件的最大个数
  • MaxAges:保留旧文件的最大天数
  • Compress:是否压缩/归档旧文件

完整示例:

var sugarLogger *zap.SugaredLogger

func main() {
	InitLogger()
	defer sugarLogger.Sync()
	simpleHttpGet("www.sogo.com")
	simpleHttpGet("http://www.sogo.com")
}

func InitLogger() {
	writeSyncer := getLogWriter()
	encoder := getEncoder()
	core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)

	logger := zap.New(core, zap.AddCaller())
	sugarLogger = logger.Sugar()
}

func getEncoder() zapcore.Encoder {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
	return zapcore.NewConsoleEncoder(encoderConfig)
}

func getLogWriter() zapcore.WriteSyncer {
	lumberJackLogger := &lumberjack.Logger{
		Filename:   "./test.log",
		MaxSize:    1,
		MaxBackups: 5,
		MaxAge:     30,
		Compress:   false,
	}
	return zapcore.AddSync(lumberJackLogger)
}

func simpleHttpGet(url string) {
	sugarLogger.Debugf("Trying to hit GET request for %s", url)
	resp, err := http.Get(url)
	if err != nil {
		sugarLogger.Errorf("Error fetching URL %s : Error = %s", url, err)
	} else {
		sugarLogger.Infof("Success! statusCode = %s for URL %s", resp.Status, url)
		resp.Body.Close()
	}
}
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
#zap#log
上次更新: 2025/05/06, 13:52:56
命令行工具 flag
database/sql 库

← 命令行工具 flag database/sql 库→

最近更新
01
find 命令介绍
05-06
02
交换分区的配置
04-09
03
top 命令介绍
04-08
更多文章>
Theme by Vdoing | Copyright © 2019-2025 Zhou Chuang 版权所有| 鲁ICP备2021031629号-2
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式