摘抄自:https://www.cnblogs.com/jiujuan/p/17304844.html

一、介绍

zap的特性

  • 高性能:zap 对日志输出进行了多项优化以提高它的性能
  • 日志分级:有 Debug,Info,Warn,Error,DPanic,Panic,Fatal 等
  • 日志记录结构化:日志内容记录是结构化的,比如 json 格式输出
  • 自定义格式:用户可以自定义输出的日志格式
  • 自定义公共字段:用户可以自定义公共字段,大家输出的日志内容就共同拥有了这些字段
  • 调试:可以打印文件名、函数名、行号、日志时间等,便于调试程序
  • 自定义调用栈级别:可以根据日志级别输出它的调用栈信息
  • Namespace:日志命名空间。定义命名空间后,所有日志内容就在这个命名空间下。命名空间相当于一个文件夹
  • 支持 hook 操作

做了哪些优化

基于反射的序列化和字符串格式化,它们都是 CPU 密集型计算且分配很多小的内存。具体到 Go 语言中,使用 encoding/json 和 fmt.Fprintf 格式化 interface{} 会使程序性能降低。

Zap 咋解决呢?Zap 使用一个无反射、零分配的 JSON 编码器,基础 Logger(完全无反射) 尽可能避免序列化开销和内存分配开销。在此基础上,zap 还构建了更高级的 SuggaredLogger(有反射)。


安装

1
go get go.uber.org/zap




二、日志记录器logger使用

2.1 两种日志记录器

zap 提供了 2 种日志记录器:SugaredLoggerLogger

在需要性能但不是很重要的情况下,使用 SugaredLogger 较合适。它比其它结构化日志包快 4-10 倍,包括 结构化日志和 printf 风格的 API。看下面使用 SugaredLogger 例子:

1
2
3
4
5
6
7
8
9
10
logger, _ := zap.NewProduction()
defer logger.Sync() // zap底层有缓冲。在任何情况下执行 defer logger.Sync() 是一个很好的习惯
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
// 字段是松散类型,不是强类型
"url", url,
"attempt", 3,
"backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)

当性能和类型安全很重要时,请使用 Logger。它比 SugaredLogger 更快,分配的资源更少,但它只支持结构化日志和强类型字段。

1
2
3
4
5
6
7
8
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("failed to fetch URL",
// 字段是强类型,不是松散类型
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)

sugared logger:

  1. 它有很好的性能,比一般日志包快 4-10 倍。
  2. 支持结构化的日志。
  3. 支持 printf 风格的日志。
  4. 日志字段不需要定义类型

logger(没有sugar)

  1. 它的性能比 sugared logger 还要快

  2. 它只支持强类型的结构化日志。

  3. 它应用在对性能更加敏感日志记录中,它的内存分配次数更少。比如如果每一次内存分配都很重要的话可以使用这个。对类型安全有严格要求也可以使用这个。


logger 和 sugaredlogger 相互转换:

1
2
3
4
5
6
7
8
9
// 创建 logger
logger := zap.NewExample()
defer logger.Sync()

// 转换 SugaredLogger
sugar := logger.Sugar()

// 转换 logger
plain := sugar.Desugar()

怎么选择:

  • 需要不错的性能但不是很重要的情况下,可以选择 sugaredlogger。它支持结构化日志和 printf 风格的日志记录。sugaredlogger 的日志记录是松散类型的,不是强类型,能接受可变数量的键值对。如果你要用强类型字段记录,可以使用 SugaredLogger.With 方法。
  • 如果是每次或每微秒记录日志都很重要情况下,可以使用 logger,它比 sugaredlogger 每次分配内存更少,性能更高。但它仅支持强类型的结构化日志记录。




2.2 怎么打印结构体

在 Zap 中打印结构体有多种方式,可根据性能需求和开发场景选择合适的方案。以下是详细的实现方法及示例:

一、使用 zap.Reflect(简单但有反射开销)

zap.Reflect 会通过反射递归序列化结构体,包含所有可导出字段名和值,类似 fmt.Printf("%+v", struct) 的效果。

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
package main

import (
"log"
"time"

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

type User struct {
ID string `json:"user_id"`
Name string `json:"user_name"`
Age int `json:"age"`
CreatedAt time.Time `json:"created_at"`
}

func main() {
logger, err := zap.NewProduction()
if err != nil {
log.Fatalf("failed to create logger: %v", err)
}
defer logger.Sync()

user := User{
ID: "u123",
Name: "Bob",
Age: 25,
CreatedAt: time.Now(),
}

// 使用 zap.Reflect 打印结构体
logger.Info("用户信息", zap.Reflect("user", user))
}

输出:

1
{"level":"info","msg":"用户信息","user":{"user_id":"u123","user_name":"Bob","age":25,"created_at":"2025-06-08T12:00:00+08:00"}}

注意事项

  • 反射开销:每次调用 zap.Reflect 都会触发反射,频繁使用会影响性能(尤其在高并发场景)。
  • 字段名:使用结构体字段的 json 标签(若无则用字段名)。
  • 适用场景:开发调试、非性能敏感的日志记录。

二、实现 zapcore.ObjectMarshaler 接口(高性能方案)

通过自定义结构体的序列化逻辑,避免反射开销,性能与直接调用 zap.String 等方法相当。

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
package main

import (
"log"
"time"

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

type User struct {
ID string `json:"user_id"`
Name string `json:"user_name"`
Age int `json:"age"`
CreatedAt time.Time `json:"created_at"`
}

// 实现 zapcore.ObjectMarshaler 接口
func (u User) MarshalLogObject(enc zapcore.ObjectEncoder) error {
enc.AddString("ID", u.ID)
enc.AddString("Name", u.Name)
enc.AddInt("Age", u.Age)
enc.AddString("CreatedAt", u.CreatedAt.Format("2006-01-02 15:04:05"))
return nil
}

func main() {
logger, err := zap.NewProduction()
if err != nil {
log.Fatalf("failed to create logger: %v", err)
}
defer logger.Sync()

user := User{
ID: "u123",
Name: "Bob",
Age: 25,
CreatedAt: time.Now(),
}

// 使用 zap.Object 打印结构体(自动调用 MarshalLogObject)
logger.Info("用户信息", zap.Object("user", user))
}

输出结果

1
{"level":"info","msg":"用户信息","user":{"ID":"u123","Name":"Bob","Age":25,"CreatedAt":"2025-06-08 12:00:00"}}

优点

  • 无反射开销:手动定义序列化逻辑,性能最优。
  • 灵活控制:可自定义字段名、格式(如时间格式化)。
  • 支持嵌套:结构体中的嵌套类型也可通过实现接口优化。

适用场景:生产环境、高性能要求的日志记录。


三、使用 zap.Any(折中方案)

zap.Any 会根据类型自动选择序列化方式:

  • 若类型实现了 zapcore.ObjectMarshalerjson.Marshaler,则使用接口序列化(无反射)。
  • 否则,退化为 zap.Reflect(反射序列化)。
1
logger.Info("用户信息", zap.Any("user", user))

使用场景

  • 当结构体可能实现接口时,优先使用 zap.Any,避免重复代码。
  • 开发中不确定类型是否需要优化时,作为过渡方案。

四、自定义编码器(高级场景)

若需完全控制序列化(如包含未导出字段、特殊格式),可自定义编码器:

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 (
"log"
"time"

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

type User struct {
id string // 未导出字段
name string // 未导出字段
age int // 未导出字段
createdAt time.Time // 未导出字段
}

// 自定义编码器类型
type UserEncoder User

// 实现序列化逻辑(可访问未导出字段)
func (u UserEncoder) MarshalLogObject(enc zapcore.ObjectEncoder) error {
enc.AddString("id", u.id)
enc.AddString("name", u.name)
enc.AddInt("age", u.age)
enc.AddString("created_at", u.createdAt.Format("2006-01-02"))
return nil
}

func main() {
logger, err := zap.NewProduction()
if err != nil {
log.Fatalf("failed to create logger: %v", err)
}
defer logger.Sync()

user := User{
id: "u123",
name: "Bob",
age: 25,
createdAt: time.Now(),
}

// 转换为自定义编码器类型后打印
logger.Info("用户信息", zap.Object("user", UserEncoder(user)))
}

输出结果

1
{"level":"info","msg":"用户信息","user":{"id":"u123","name":"Bob","age":25,"created_at":"2025-06-08"}}

五、SugaredLogger 中的结构体打印

SugaredLogger(logger.Sugar())不直接支持结构体格式化,但可通过 With 方法结合上述方案:

1
2
3
4
sugar := logger.Sugar()
sugar.With("user", zap.Reflect("user", user)).Info("用户信息")
// 或
sugar.With("user", user).Info("用户信息") // 等价于 zap.Any

性能与场景总结

方法 反射开销 性能(高→低) 适用场景
zap.Object + 接口 最高 生产环境、高频日志记录
zap.Any 部分有 代码简洁性与性能的平衡
zap.Reflect 最低 开发调试、低频日志记录

建议

  • 生产环境中,对高频日志的结构体实现 zapcore.ObjectMarshaler 接口。
  • 开发调试时,使用 zap.Reflect 快速验证日志格式。
  • 对偶尔记录的结构体,使用 zap.Any 简化代码。




三、logger创建

zap 为我们提供了三种快速创建 logger 的方法: zap.NewProduction()zap.NewDevelopment()zap.NewExample()

见名思义,Example 一般用在测试代码中,Development 用在开发环境中,Production 用在生成环境中。这三种方法都预先设置好了配置信息。

3.1 NewExample()使用

NewExample 构建一个 logger,专门为在 zap 的测试示例使用。它将 DebugLevel 及以上日志用 JSON 格式标准输出,但它省略了时间戳和调用函数,以保持示例输出的简短和确定性。

为什么说 zap.NewExample() 是 zap 为我们提供快速创建 logger 的方法呢?

因为在这个方法里,zap 已经定义好了日志配置项部分默认值。来看它的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// https://github.com/uber-go/zap/blob/v1.24.0/logger.go#L127
func NewExample(options ...Option) *Logger {
encoderCfg := zapcore.EncoderConfig{
MessageKey: "msg", // 日志内容key:val, 前面的key设为msg
LevelKey: "level", // 日志级别的key设为level
NameKey: "logger", // 日志名
EncodeLevel: zapcore.LowercaseLevelEncoder, //日志级别,默认小写
EncodeTime: zapcore.ISO8601TimeEncoder, // 日志时间
EncodeDuration: zapcore.StringDurationEncoder,
}
core := zapcore.NewCore(zapcore.NewJSONEncoder(encoderCfg), os.Stdout, DebugLevel)
return New(core).WithOptions(options...)
}

使用例子:

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

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

func main() {
logger := zap.NewExample()
logger.Debug("this is debug message")
logger.Info("this is info message")
logger.Info("this is info message with fileds",
zap.Int("age", 37),
zap.String("agender", "man"),
)
logger.Warn("this is warn message")
logger.Error("this is error message")
}

输出:

1
2
3
4
5
{"level":"debug","msg":"this is debug message"}
{"level":"info","msg":"this is info message"}
{"level":"info","msg":"this is info message with fileds","age":37,"agender":"man"}
{"level":"warn","msg":"this is warn message"}
{"level":"error","msg":"this is error message"}




3.2 NewDevelopment()使用

NewDevelopment() 构建一个开发使用的 Logger,它以人性化的格式将 DebugLevel 及以上日志信息输出。它的底层使用

NewDevelopmentConfig().Build(...Option) 构建。它的日志格式各种设置在函数 NewDevelopmentEncoderConfig() 里,想查看详情设置,请点进去查看。

使用例子:

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
package main

import (
"time"

"go.uber.org/zap"
)

func main() {
logger, _ := zap.NewDevelopment()
defer logger.Sync()

logger.Info("failed to fetch url",
// 强类型字段
zap.String("url", "http://example.com"),
zap.Int("attempt", 3),
zap.Duration("duration", time.Second),
)

logger.With(
// 强类型字段
zap.String("url", "http://development.com"),
zap.Int("attempt", 4),
zap.Duration("duration", time.Second*5),
).Info("[With] failed to fetch url")
}

输出(下面日志输出了文件名和行号,NewExample() 没有)

1
2
2023-03-22T16:02:45.760+0800    INFO    zapdemos/newdevelopment1.go:13  failed to fetch url     {"url": "http://example.com", "attempt": 3, "duration": "1s"}
2023-03-22T16:02:45.786+0800 INFO zapdemos/newdevelopment1.go:25 [With] failed to fetch url {"url": "http://development.com", "attempt": 4, "duration": "5s"}

Example 和 Production 是 json 格式输出,Development 是普通一行格式输出,如果后面带有字段输出话用json格式。





3.3 NewProduction()使用

NewProduction() 构建了一个合理的 Prouction 日志记录器,它将 info 及以上的日志内容以 JSON 格式记写入标准错误里。

它的底层使用 NewProductionConfig().Build(...Option) 构建。它的日志格式设置在函数 NewProductionEncoderConfig 里。

使用例子

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
package main

import (
"time"

"go.uber.org/zap"
)

func main() {
logger, _ := zap.NewProduction()
defer logger.Sync()

url := "http://zap.uber.io"
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
"url", url,
"attempt", 3,
"time", time.Second,
)

sugar.Infof("Failed to fetch URL: %s", url)

// 或更简洁 Sugar() 使用
// sugar := zap.NewProduction().Sugar()
// defer sugar.Sync()
}

输出:

1
2
{"level":"info","ts":1679472893.2944522,"caller":"zapdemos/newproduction1.go:16","msg":"failed to fetch URL","url":"http://zap.uber.io","attempt":3,"time":1}
{"level":"info","ts":1679472893.294975,"caller":"zapdemos/newproduction1.go:22","msg":"Failed to fetch URL: http://zap.uber.io"}

上面日志输出了文件名和行号,NewExample() 没有

直接使用 NewProduction() 就好了,反正都是要打印到文件中的





3.4 使用配置

在这 3 个函数中,可以传入一些配置项。为什么能传入配置项?我们来看看 NewExample() 函数定义:

1
func NewExample(options ...Option) *Logger

它的函数传参有一个 ...Option 选项,是一个 interface 类型,它关联的是 Logger struct。只要返回 Option 就可以传进 NewExample() 里。在 zap/options.go 文件中可以看到很多返回 Option 的函数,也就是说这些函数都可以传入 NewExample 函数里。这里用到了 Go 里面的一个编码技巧,函数选项模式。


zap.Fields() 添加字段到 Logger 中:

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

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

func main() {
logger, _ := zap.NewProduction(zap.Fields(
zap.String("log_name", "testlog"),
zap.String("log_author", "prometheus"),
))
defer logger.Sync()

logger.Info("test fields output")

logger.Warn("warn info")
}

输出:

1
2
{"level":"info","ts":1679477929.842166,"caller":"zapdemos/fields.go:14","msg":"test fields output","log_name":"testlog","log_author":"prometheus"}
{"level":"warn","ts":1679477929.842166,"caller":"zapdemos/fields.go:16","msg":"warn info","log_name":"testlog","log_author":"prometheus"}

zap.Hook() 添加回调函数:

Hook (钩子函数)回调函数为用户提供一种简单方法,在每次日志内容记录后运行这个回调函数,执行用户需要的操作。也就是说记录完日志后你还想做其它事情就可以调用这个函数。

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

import (
"fmt"

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

func main() {
logger := zap.NewExample(zap.Hooks(func(entry zapcore.Entry) error {
fmt.Println("[zap.Hooks]test Hooks")
return nil
}))
defer logger.Sync()

logger.Info("test output")

logger.Warn("warn info")
}

输出:

1
2
3
4
{"level":"info","msg":"test output"}
[zap.Hooks]test Hooks
{"level":"warn","msg":"warn info"}
[zap.Hooks]test Hooks

zap.Namespace():

创建一个命名空间,后面的字段都在这名字空间中。Namespace 就像一个文件夹,后面文件都放在这个文件夹里。

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 (
"go.uber.org/zap"
)

func main() {
logger := zap.NewExample()
defer logger.Sync()

logger.Info("some message",
zap.Namespace("shop"),
zap.String("name", "LiLei"),
zap.String("grade", "No2"),
)

logger.Error("some error message",
zap.Namespace("shop"),
zap.String("name", "LiLei"),
zap.String("grade", "No3"),
)
}

输出:

1
2
{"level":"info","msg":"some message","shop":{"name":"LiLei","grade":"No2"}}
{"level":"error","msg":"some error message","shop":{"name":"LiLei","grade":"No3"}}




四、自定义配置

快速构建 logger 日志记录器最简单的方法就是用 zap 预定义了配置的方法:NewExample(), NewProduction()NewDevelopment(),这 3 个方法通过单个函数调用就可以构建一个日志计记录器,也可以简单配置。

但是有的项目需要更多的定制,怎么办?zap 的 Config 结构和 zapcore 的 EncoderConfig 结构可以帮助你,让你能够进行自定义配置。

4.1 配置结构说明

Config 配置项源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// zap v1.24.0
type Config struct {
// 动态改变日志级别,在运行时你可以安全改变日志级别
Level AtomicLevel `json:"level" yaml:"level"`
// 将日志记录器设置为开发模式,在 WarnLevel 及以上级别日志会包含堆栈跟踪信息
Development bool `json:"development" yaml:"development"`
// 在日志中停止调用函数所在文件名、行数
DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
// 完全禁止自动堆栈跟踪。默认情况下,在 development 中,warnlevel及以上日志级别会自动捕获堆栈跟踪信息
// 在 production 中,ErrorLevel 及以上也会自动捕获堆栈信息
DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
// 设置采样策略。没有 SamplingConfing 将禁止采样
Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
// 设置日志编码。可以设置为 console 和 json。也可以通过 RegisterEncoder 设置第三方编码格式
Encoding string `json:"encoding" yaml:"encoding"`
// 为encoder编码器设置选项。详细设置信息在 zapcore.zapcore.EncoderConfig
EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
// 日志输出地址可以是一个 URLs 地址或文件路径,可以设置多个
OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
// 错误日志输出地址。默认输出标准错误信息
ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
// 可以添加自定义的字段信息到 root logger 中。也就是每条日志都会携带这些字段信息,公共字段
InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}

EncoderConfig 结构源码,它里面也有很多配置选项,具体请看 这里:

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
// zapcore@v1.24.0
type EncoderConfig struct {
// 为log entry设置key。如果 key 为空,那么在日志中的这部分信息也会省略
MessageKey string `json:"messageKey" yaml:"messageKey"`//日志信息的健名,默认为msg
LevelKey string `json:"levelKey" yaml:"levelKey"`//日志级别的健名,默认为level
TimeKey string `json:"timeKey" yaml:"timeKey"`//记录日志时间的健名,默认为time
NameKey string `json:"nameKey" yaml:"nameKey"`
CallerKey string `json:"callerKey" yaml:"callerKey"`
FunctionKey string `json:"functionKey" yaml:"functionKey"`
StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
SkipLineEnding bool `json:"skipLineEnding" yaml:"skipLineEnding"`
LineEnding string `json:"lineEnding" yaml:"lineEnding"`
// 日志编码的一些设置项
EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"`
EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"`
EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"`
// 与其它编码器不同, 这个编码器可选
EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
// 配置 interface{} 类型编码器。如果没设置,将用 json.Encoder 进行编码
NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
// 配置 console 中字段分隔符。默认使用 tab
ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}

type Entry struct {
Level Level
Time time.Time
LoggerName string
Message string
Caller EntryCaller
Stack string
}




4.2 例子1:基本配置

zap.Config 自定义配置,看官方的一个基本例子:

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
package main

import (
"encoding/json"

"go.uber.org/zap"
)

// https://pkg.go.dev/go.uber.org/zap@v1.24.0#hdr-Configuring_Zap
func main() {
// 表示 zap.Config 的 json 原始编码
// outputPath: 设置日志输出路径,日志内容输出到标准输出和文件 logs.log
// errorOutputPaths:设置错误日志输出路径
rawJSON := []byte(`{
"level": "debug",
"encoding": "json",
"outputPaths": ["stdout", "./logs.log"],
"errorOutputPaths": ["stderr"],
"initialFields": {"foo": "bar"},
"encoderConfig": {
"messageKey": "message-customer",
"levelKey": "level",
"levelEncoder": "lowercase"
}
}`)

// 把 json 格式数据解析到 zap.Config struct
var cfg zap.Config
if err := json.Unmarshal(rawJSON, &cfg); err != nil {
panic(err)
}
// cfg.Build() 为配置对象创建一个 Logger
// zap.Must() 封装了 Logger,Must()函数如果返回值不是 nil,就会报 panic。也就是检查Build是否错误
logger := zap.Must(cfg.Build())
defer logger.Sync()

logger.Info("logger construction succeeded")
}

/*
Must() 函数
// var logger = zap.Must(zap.NewProduction())
func Must(logger *Logger, err error) *Logger {
if err != nil {
panic(err)
}

return logger
}
*/

consol 输出如下:

1
{"level":"info","message-customer":"logger construction succeeded","foo":"bar"}

并且在程序目录下生成了一个文件 logs.log,里面记录的日志内容也是上面consol输出内容。每运行一次就在日志文件末尾append一次内容。





4.3 例子2:高级配置

上面的配置只是基本的自定义配置,如果有一些复杂的需求,比如在多个文件之间分割日志。

或者输出到不是 file 的文件中,比如输出到 kafka 中,那么就需要使用 zapcore 包。

在下面的例子中,我们将把日志输出到 kafka 中,并且也输出到 console 里。并且我们对 kafka 不同主题进行编码设置,对输出到 console 编码进行设置,也希望处理高优先级的日志。

官方例子:

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 (
"io"
"os"

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

func main() {
// 首先,定义不同级别日志处理逻辑
highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl >= zapcore.ErrorLevel
})
lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl < zapcore.ErrorLevel
})

// 假设有2个kafka 的 topic,一个 debugging,一个 errors

// zapcore.AddSync 添加一个文件句柄。
topicDebugging := zapcore.AddSync(io.Discard)
topicErrors := zapcore.AddSync(io.Discard)

// 如果他们对并发使用不安全,我们可以用 zapcore.Lock 添加一个 mutex 互斥锁。
consoleDebugging := zapcore.Lock(os.Stdout)
consoleErrors := zapcore.Lock(os.Stderr)

// 设置 kafka 和 console 输出配置
kafkaEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())

// 把上面的设置加入到 zapcore.NewCore() 函数里,然后再把他们加入到 zapcore.NewTee() 函数里
core := zapcore.NewTee(
zapcore.NewCore(kafkaEncoder, topicErrors, highPriority),
zapcore.NewCore(consoleEncoder, consoleErrors, highPriority),
zapcore.NewCore(kafkaEncoder, topicDebugging, lowPriority),
zapcore.NewCore(consoleEncoder, consoleDebugging, lowPriority),
)

// 最后调用 zap.New() 函数
logger := zap.New(core)
defer logger.Sync()
logger.Info("constructed a logger")
}
AI解析

这段代码主要实现了一个使用 Uber 的 zap 日志库的多目的地、多级别日志系统。它将不同级别的日志同时输出到控制台和 Kafka 主题,确保错误日志得到更妥善的处理。


核心功能和实现步骤

  1. 日志级别分离

    • 定义了两个优先级过滤器:
      • highPriority:处理 Error 及以上级别的日志
      • lowPriority:处理低于 Error 级别的日志(Info、Debug 等)
  2. 输出目的地配置

    • Kafka 主题

      :配置了两个 Kafka 主题(示例中使用

      1
      io.Discard

      作为占位符):

      • topicDebugging:用于低优先级日志
      • topicErrors:用于高优先级日志
    • 控制台输出

      • consoleDebugging:标准输出(stdout)
      • consoleErrors:标准错误输出(stderr)
  3. 编码器设置

    • JSON 编码器:用于 Kafka 输出,便于机器解析
    • 控制台编码器:用于终端输出,采用人类可读的格式
  4. 核心配置组合

    • 使用

      1
      zapcore.NewTee

      组合多个日志核心,实现:

      • 错误级别的日志同时输出到 Kafka 的 errors 主题和控制台错误输出
      • 非错误级别的日志同时输出到 Kafka 的 debugging 主题和控制台标准输出
  5. 日志使用

    • 创建完成的日志实例支持所有标准 zap 方法(如 Info、Error 等)
    • 使用defer logger.Sync()确保程序退出前所有日志都被刷新

代码关键点解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 优先级过滤器:决定哪些级别日志被处理
highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl >= zapcore.ErrorLevel
})

// 实际使用时需要替换为真实的Kafka写入器
topicDebugging := zapcore.AddSync(io.Discard)
topicErrors := zapcore.AddSync(io.Discard)

// 构建多输出的核心
core := zapcore.NewTee(
zapcore.NewCore(kafkaEncoder, topicErrors, highPriority),
zapcore.NewCore(consoleEncoder, consoleErrors, highPriority),
zapcore.NewCore(kafkaEncoder, topicDebugging, lowPriority),
zapcore.NewCore(consoleEncoder, consoleDebugging, lowPriority),
)

实际应用注意事项

  1. 代码中使用io.Discard作为 Kafka 输出的占位符,实际使用时需要替换为真正的 Kafka 写入器
  2. 对于高并发场景,zapcore.Lock确保了控制台输出的线程安全
  3. 不同环境(开发 / 生产)可以通过调整编码器配置优化输出格式
  4. defer logger.Sync()是关键的,确保程序退出前所有缓冲的日志都被写入

这种多目的地、多级别日志系统特别适合需要同时进行实时监控和事后分析的生产环境。





4.4 例子3:日志写入文件

与上面例子2相似,但是比它简单

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
package main

import (
"os"

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

func main() {
writetofile()
}

func writetofile() {
// 设置一些配置参数
config := zap.NewProductionEncoderConfig()
config.EncodeTime = zapcore.ISO8601TimeEncoder
fileEncoder := zapcore.NewJSONEncoder(config)
defaultLogLevel := zapcore.DebugLevel // 设置 loglevel

logFile, _ := os.OpenFile("./log-test-zap.json", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 06666)
// or os.Create()
writer := zapcore.AddSync(logFile)

logger := zap.New(
zapcore.NewCore(fileEncoder, writer, defaultLogLevel),
zap.AddCaller(),
zap.AddStacktrace(zapcore.ErrorLevel),
)
defer logger.Sync()

url := "http://www.test.com"
logger.Info("write log to file",
zap.String("url", url),
zap.Int("attemp", 3),
)
}




4.5 例子4:根据日志级别写入不同文件

这个与上面例子2相似

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
package main

import (
"os"

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

func main() {
writeToFileWithLogLevel()
}

func writeToFileWithLogLevel() {
// 设置配置
config := zap.NewProductionEncoderConfig()
config.EncodeTime = zapcore.ISO8601TimeEncoder
fileEncoder := zapcore.NewJSONEncoder(config)

logFile, _ := os.OpenFile("./log-debug-zap.json", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) //日志记录debug信息

errFile, _ := os.OpenFile("./log-err-zap.json", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) //日志记录error信息

teecore := zapcore.NewTee(
zapcore.NewCore(fileEncoder, zapcore.AddSync(logFile), zap.DebugLevel),
zapcore.NewCore(fileEncoder, zapcore.AddSync(errFile), zap.ErrorLevel),
)

logger := zap.New(teecore, zap.AddCaller())
defer logger.Sync()

url := "http://www.diff-log-level.com"
logger.Info("write log to file",
zap.String("url", url),
zap.Int("time", 3),
)

logger.With(
zap.String("url", url),
zap.String("name", "jimmmyr"),
).Error("test error ")
}

主要是设置日志级别,和把 2 个设置的 NewCore 放入到方法 NewTee 中。





4.6 日志切割文档

lumberjack 这个库是按照日志大小切割日志文件。

安装 v2 版本:(推荐)

1
go get github.com/natefinch/lumberjack@v2
1
2
3
4
5
6
7
log.SetOutput(&lumberjack.Logger{
Filename: "/var/log/myapp/foo.log", // 文件位置
MaxSize: 500, // megabytes,M 为单位,达到这个设置数后就进行日志切割
MaxBackups: 3, // 保留旧文件最大份数
MaxAge: 28, //days , 旧文件最大保存天数
Compress: true, // disabled by default,是否压缩日志归档,默认不压缩
})

参照它的文档和结合上面自定义配置的例子,写一个例子:

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
package main

import (
"fmt"

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)

func main() {
lumberjacklogger := &lumberjack.Logger{
Filename: "./log-rotate-test.json",
MaxSize: 1, // megabytes
MaxBackups: 3,
MaxAge: 28, //days
Compress: true, // disabled by default
}
defer lumberjacklogger.Close()

config := zap.NewProductionEncoderConfig()

config.EncodeTime = zapcore.ISO8601TimeEncoder // 设置时间格式
fileEncoder := zapcore.NewJSONEncoder(config)

core := zapcore.NewCore(
fileEncoder, //编码设置
zapcore.AddSync(lumberjacklogger), //输出到文件
zap.InfoLevel, //日志等级
)

logger := zap.New(core)
defer logger.Sync()

// 测试分割日志
for i := 0; i < 8000; i++ {
logger.With(
zap.String("url", fmt.Sprintf("www.test%d.com", i)),
zap.String("name", "jimmmyr"),
zap.Int("age", 23),
zap.String("agradege", "no111-000222"),
).Info("test info ")
}

}




五、日志级别和实践

5.1 日志级别

Zap 定义了 7 个日志级别,按严重程度从低到高排列:

1
DebugLevel → InfoLevel → WarnLevel → ErrorLevel → DPanicLevel → PanicLevel → FatalLevel

各级别适用场景:

  • Debug:开发调试细节,生产环境通常禁用
  • Info:常规运行状态信息(如启动、配置加载)
  • Warn:潜在问题(如配置过时、资源接近耗尽)
  • Error:可恢复错误(如网络临时中断)
  • DPanic:开发环境会触发 panic,生产环境仅记录
  • Panic:记录后触发 panic(调用panic()
  • Fatal:记录后终止程序(调用os.Exit(1)




5.2 核心打印逻辑规则

  1. 级别启用规则
    启用某个级别的日志时,高于它的所有级别日志也会被包含
    例如:

    • 设置为InfoLevel → 实际输出Info/Warn/Error/DPanic/Panic/Fatal
    • 设置为ErrorLevel → 实际输出Error/DPanic/Panic/Fatal
  2. 过滤器优先级
    当多个LevelEnabler存在时,日志会被每个匹配的输出目标接收。例如:

    1
    2
    3
    4
    core := zapcore.NewTee(
    zapcore.NewCore(fileEncoder, errorFile, ErrorLevel), // 接收Error及以上
    zapcore.NewCore(fileEncoder, allFile, InfoLevel), // 接收Info及以上
    )
    • Error日志会同时写入errorFileallFile
    • Info日志只会写入allFile
  3. 输出目标隔离
    通过zapcore.NewTee可以将不同级别的日志定向到不同目的地:

    1
    2
    3
    4
    5
    6
    7
    // Error及以上级别 → 错误文件 + 告警系统
    // Info/Warn → 普通日志文件
    core := zapcore.NewTee(
    zapcore.NewCore(kafkaEncoder, alertTopic, ErrorLevel),
    zapcore.NewCore(fileEncoder, errorFile, ErrorLevel),
    zapcore.NewCore(fileEncoder, infoFile, InfoLevel),
    )




5.3 常见配置模式

5.3.1 按级别拆分文件

1
2
3
4
5
core := zapcore.NewTee(
zapcore.NewCore(fileEncoder, debugFile, DebugLevel),
zapcore.NewCore(fileEncoder, infoFile, InfoLevel),
zapcore.NewCore(fileEncoder, errorFile, ErrorLevel),
)
  • debug.log:仅包含 Debug 日志
  • info.log:包含 Info/Warn/Error/…
  • error.log:包含 Error/DPanic/…




5.3.2 生产环境典型配置

1
2
3
4
5
6
7
8
core := zapcore.NewTee(
// 错误日志 → 单独文件 + 告警系统
zapcore.NewCore(jsonEncoder, errorFile, ErrorLevel),
zapcore.NewCore(kafkaEncoder, alertTopic, ErrorLevel),

// 普通日志 → 控制台(标准输出)
zapcore.NewCore(consoleEncoder, os.Stdout, InfoLevel),
)




5.3.3 严格分离级别

1
2
3
4
5
6
7
8
9
core := zapcore.NewTee(
zapcore.NewCore(fileEncoder, debugFile, zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl == zapcore.DebugLevel
})),
zapcore.NewCore(fileEncoder, infoFile, zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl == zapcore.InfoLevel
})),
// ... 以此类推
)




5.4 最佳实践建议

  1. 合理设置默认级别

    • 开发环境:DebugLevel
    • 测试环境:InfoLevel
    • 生产环境:WarnLevelErrorLevel
  2. 关键错误增加上下文

    1
    2
    3
    4
    5
    logger.Error("数据库连接失败", 
    zap.String("host", dbHost),
    zap.Int("port", dbPort),
    zap.Error(err),
    )
  3. 动态调整级别
    使用zap.AtomicLevel允许运行时调整日志级别:

    1
    2
    3
    4
    5
    6
    7
    level := zap.NewAtomicLevelAt(zap.InfoLevel)
    logger := zap.New(core, zap.WrapCore(func(core zapcore.Core) zapcore.Core {
    return level.LevelEnabler(core)
    }))

    // 动态调整
    level.SetLevel(zap.DebugLevel)
  4. 避免性能损耗(很有用)
    对于高开销操作,使用条件日志:

    1
    2
    3
    if logger.Core().Enabled(zap.DebugLevel) {
    logger.Debug("计算结果", zap.Int("result", expensiveCalculation()))
    }




5.5 常见误区

  1. 误认为日志会重复输出
    • 只有当多个输出目标匹配同一级别时才会重复
    • 通过合理设计LevelEnabler可避免重复
  2. 过度使用高级别日志
    • 仅在真正需要立即关注的情况下使用Fatal/Panic
    • 频繁的Panic/Fatal会导致系统不稳定
  3. 忽略 DPanic 的特殊作用
    • 在生产环境中,DPanic表现得像Error
    • 在测试 / 开发环境中,DPanic会触发崩溃,帮助发现潜在问题




六、生产/开发环境控制

6.1 环境配置核心差异

Zap 通过 显式配置选项 区分生产与开发环境,而非自动检测。核心差异体现在:

特性 生产环境(默认) 开发环境
创建方式 zap.NewProduction() zap.NewDevelopment()
DPanic 行为 仅记录日志,不触发 panic 记录日志后触发 panic
日志格式 JSON 格式(便于机器解析) 控制台格式(更易读)
堆栈跟踪 仅 Error 及以上级别包含堆栈 自动为 Warn 及以上级别添加堆栈
默认日志级别 InfoLevel DebugLevel




6.2 环境配置实现方式

  1. 基础环境创建
1
2
3
4
5
6
7
// 生产环境 logger(默认配置)
prodLogger, _ := zap.NewProduction()
defer prodLogger.Sync()

// 开发环境 logger(启用开发模式)
devLogger, _ := zap.NewDevelopment()
defer devLogger.Sync()

  1. 通过配置对象手动控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 通用配置方式(可通过参数动态调整)
func newLogger(isDevelopment bool) (*zap.Logger, error) {
var config zap.Config
if isDevelopment {
config = zap.NewDevelopmentConfig()
config.Level.SetLevel(zap.DebugLevel) // 开发环境设为 Debug
} else {
config = zap.NewProductionConfig()
config.Level.SetLevel(zap.InfoLevel) // 生产环境设为 Info
}

// 自定义输出路径(生产环境写入文件,开发环境输出控制台)
if !isDevelopment {
config.OutputPaths = []string{"logs/app.log"}
}

logger, err := config.Build()
if err != nil {
return nil, err
}
return logger, nil
}

  1. 环境变量控制示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
env := os.Getenv("APP_ENV")
var logger *zap.Logger
var err error

if env == "development" {
logger, err = zap.NewDevelopment()
} else {
logger, err = zap.NewProduction()
}
if err != nil {
panic(err)
}
defer logger.Sync()

// 使用 logger...
}




6.3 DPanic 在不同环境的行为

  • 生产环境:仅记录日志,不触发 panic,适合标记 “理论上不应发生但可容忍” 的错误。
  • 开发环境:记录日志后触发 panic,强制暴露潜在问题,帮助开发阶段快速定位错误。
1
2
// 示例:开发环境中 DPanic 会触发 panic,生产环境仅记录  
logger.DPanic("数据校验失败,这在正常流程中不应发生")




6.4 环境相关的最佳实践

  1. 日志级别动态调整
    使用 zap.AtomicLevel 允许运行时调整日志级别,无需重启服务:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    level := zap.NewAtomicLevelAt(zap.InfoLevel)
    logger := zap.New(
    zapcore.NewCore(
    encoder, writer, level,
    ),
    )

    // 动态调整(如通过 HTTP 接口或配置中心)
    level.SetLevel(zap.DebugLevel)
  2. 生产环境的性能优化

    • 禁用不必要的堆栈跟踪(仅保留 Error 及以上级别):

      1
      2
      3
      prodLogger, _ := zap.NewProduction(
      zap.AddStacktrace(zapcore.ErrorLevel), // 仅 Error 级别添加堆栈
      )
    • 使用 JSON 格式压缩日志体积,便于 ELK 等系统解析。

  3. 开发环境的调试增强

    • 启用控制台格式和完整堆栈跟踪:

      1
      2
      3
      4
      devLogger, _ := zap.NewDevelopment(
      zap.AddStacktrace(zapcore.DebugLevel), // 所有级别添加堆栈
      zap.AddCaller(), // 显示调用文件和行号
      )




6.5 注意事项

  1. 避免硬编码环境判断:通过配置文件或环境变量管理环境,而非代码内直接判断。
  2. 生产环境慎用 DPanic:除非明确需要在生产环境记录但不触发 panic 的场景。
  3. 日志同步保障:使用 defer logger.Sync() 确保程序退出前日志刷新,避免丢失。




6.6 典型配置对比

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
// 生产环境典型配置  
func initProdLogger() *zap.Logger {
logger, _ := zap.NewProduction(
zap.AddCaller(), // 显示调用位置
zap.AddStacktrace(zapcore.ErrorLevel), // 仅错误级添加堆栈
zap.WriteSyncer(&lumberjack.Logger{ // 日志切割
Filename: "logs/app.log",
MaxSize: 100, // MB
MaxBackups: 30,
MaxAge: 28, // 天
}),
)
return logger
}

// 开发环境典型配置
func initDevLogger() *zap.Logger {
logger, _ := zap.NewDevelopment(
zap.AddCaller(), // 显示调用位置
zap.AddStacktrace(zapcore.DebugLevel), // 所有级别添加堆栈
zap.AddConsoleEncoder(func(cfg *zapcore.EncoderConfig) {
cfg.EncodeTime = zapcore.ISO8601TimeEncoder // 易读时间格式
}),
)
return logger
}

通过合理配置环境差异,Zap 可在开发阶段快速暴露问题,同时在生产环境保证性能与稳定性。





七、其它方法使用

7.1 全局 Logger

zap提供了 2 种全局 Logger,一个是 zap.Logger,调用 zap.L() 获取;
另外一个是 zap.SugaredLogger ,调用 zap.S() 获取。

注意:直接调用 zap.L() 或 zap.S() 记录日志的话,它是不会记录任何日志信息。需要调用 ReplaceGlobals() 函数将它设置为全局 Logger。
ReplaceGlobals 替换全局 Logger 和 SugaredLogger,并返回一个函数来恢复原始值。
并发使用它是安全的。

看看 zap/global.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
// https://github.com/uber-go/zap/blob/v1.24.0/global.go

var (
_globalMu sync.RWMutex
_globalL = NewNop()
_globalS = _globalL.Sugar()
)

func L() *Logger {
_globalMu.RLock() // 加了读锁,所以并发使用是安全的
l := _globalL
_globalMu.RUnlock()
return l
}

func S() *SugaredLogger {
_globalMu.RLock() // 加了读锁,所以并发使用是安全的
s := _globalS
_globalMu.RUnlock()
return s
}

func ReplaceGlobals(logger *Logger) func() {
_globalMu.Lock()
prev := _globalL
_globalL = logger
_globalS = logger.Sugar()
_globalMu.Unlock()
return func() { ReplaceGlobals(prev) } // 返回一个函数类型
}

上面源码中的关键是 _globalL = NewNop() , NewNop 函数源码在 zap/logger.go 中,这个函数返回初始化了的一个 *Logger:

1
2
3
4
5
6
7
8
9
10
// https://github.com/uber-go/zap/blob/v1.24.0/logger.go#L85

func NewNop() *Logger {
return &Logger{
core: zapcore.NewNopCore(),
errorOutput: zapcore.AddSync(io.Discard),
addStack: zapcore.FatalLevel + 1,
clock: zapcore.DefaultClock,
}
}

上面是源码简析,下面给出一个简单使用的例子。

简单使用例子:

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

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

func main() {
// 直接调用是不会记录日志信息的,所以下面日志信息不会输出
zap.L().Info("no log info")
zap.S().Info("no log info [sugared]")

logger := zap.NewExample()
defer logger.Sync()

zap.ReplaceGlobals(logger) // 全局logger,zap.L() 和 zap.S() 需要调用 ReplaceGlobals 函数才会记录日志信息
zap.L().Info("log info")
zap.S().Info("log info [sugared]")
}

运行输出:

1
2
{"level":"info","msg":"log info"}
{"level":"info","msg":"log info [sugared]"}




7.2 与标准日志库搭配

zap 提供了一个函数 NewStdLog,可以把标准日志库 log 转换为 zap 的日志,这为我们从标准日志库转换到 zap 日志库的使用提供了简洁的转换操作。

例子:

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

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

func main() {
logger := zap.NewExample()
defer logger.Sync()

std := zap.NewStdLog(logger)
std.Print("standard logger wrapper")
}

运行输出:

1
{"level":"info","msg":"standard logger wrapper"}

如果你还想设置日志级别,可以使用另外一个函数 NewStdLogAt,它的第二个参数就是日志级别:

1
NewStdLogAt(l *Logger, level zapcore.Level) (*log.Logger, error)

一段代码中使用log另外的使用zap

zap 还提供了另外一个函数 RedirectStdLog,它可以帮助我们在一段代码中使用标准日志库 log,其它地方还是使用 zap.Logger。如下例子:

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

import (
"log"

"go.uber.org/zap"
)

func main() {
logger := zap.NewExample()
defer logger.Sync()

undo := zap.RedirectStdLog(logger)
log.Print("redirected standard library")
undo()

log.Print("this zap logger")
}

输出:

1
2
{"level":"info","msg":"redirected standard library"}
2023/05/06 00:47:11 this zap logger

同样如果想增加日志级别,可以使用函数 RedirectStdLogAt:

1
func RedirectStdLogAt(l *Logger, level zapcore.Level) (func(), error)




7.3 输出调用堆栈

主要是调用函数 zap.AddStacktrace(),见下面例子:

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

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

func Hello() {
Warn("hello", zap.String("h", "world"), zap.Int("c", 1))
}

func Warn(msg string, fields ...zap.Field) {
zap.L().Warn(msg, fields...)
}

func main() {
logger, _ := zap.NewProduction(zap.AddStacktrace(zapcore.WarnLevel))
defer logger.Sync()

zap.ReplaceGlobals(logger)

Hello()
}

运行输出:

1
2
3
4
5
6
{"level":"warn","ts":1683306442.3578277,"caller":"zapdemos/addstacktrace.go:13","msg":"hello","h":"world","c":1,
"stacktrace":
"main.Warn\n\tD:/work/mygo/go-exercises/zapdemos/addstacktrace.go:13\n
main.Hello\n\tD:/work/mygo/go-exercises/zapdemos/addstacktrace.go:9\n
main.main\n\tD:/work/mygo/go-exercises/zapdemos/addstacktrace.go:22\n
runtime.main\n\tE:/programfile/go/src/runtime/proc.go:250"}




7.4 输出文件名和行号

AddCaller 将 Logger 配置为使用 zap 调用者的文件名、行号和函数名称,把这些信息添加到日志记录中。它底层调用的是 WithCaller

addcaller.go:

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

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

func main() {
logger, _ := zap.NewProduction(zap.AddCaller())
defer logger.Sync()

logger.Info("AddCaller:line No and filename")
}

输出:

1
{"level":"info","ts":1683307204.6184027,"caller":"zapdemos/addcaller.go:11","msg":"AddCaller:line No and filename"}

logger.Info() 方法在第11行被调用。

zap 还提供了另外一个函数 zap.AddCallerSkip(skip int) Option,可以设置向上跳几层,然后记录文件名和行号。向上跳几层就是跳过调用者的数量。有时函数调用可能有嵌套,用这个函数可以定位到里面的函数。

addcallerskip.go

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 (
"go.uber.org/zap"
)

func main() {
logger, _ := zap.NewProduction(zap.AddCaller(), zap.AddCallerSkip(1))
defer logger.Sync()

zap.ReplaceGlobals(logger)

Hello()
}

func Hello() {
Warn("hello", zap.String("h", "world"), zap.Int("c", 1))
}

func Warn(msg string, fields ...zap.Field) {
zap.L().Warn(msg, fields...)
}

输出:

1
{"level":"warn","ts":1683308118.1684704,"caller":"zapdemos/addcallerskip.go:17","msg":"hello","h":"world","c":1}

日志中的 17 表示 Hello() 函数里的 Warn() 的行号。

如果 zap.AddCallerSkip(2) ,日志中显示行号为 13,表示 Hello() 的行号。





九、zap使用总结

  • zap 的使用,先创建 logger,再调用各个日志级别方法记录日志信息。比如 logger.Info()。
  • zap 提供了三种快速创建 logger 的方法: zap.Newproduction()zap.NewDevelopment()zap.NewExample()。见名思义,Example 一般用在测试代码中,Development 用在开发环境中,Production 用在生成环境中。这三种方法都预先设置好了配置信息。它们的日志数据类型输出都是强类型。
  • 当然,zap 也提供了给用户自定义的方法 zap.New()。比如用户可以自定义一些配置信息等。
  • 在上面的例子中,几乎都有 defer logger.Sync() 这段代码,为什么?因为 zap 底层 API 允许缓冲日志以提高性能,在默认情况下,日志记录器是没有缓冲的。但是在进程退出之前调用 Sync() 方法是一个好习惯。
  • 如果你在 zap 中使用了 sugaredlogger,把 zap 创建 logger 的三种方法用 logger.Sugar() 包装下,那么 zap 就支持 printf 风格的格式化输出,也支持以 w 结尾的方法。如 Infow,Infof 等。这种就是通用类型日志输出,不是强类型输出,不需要强制指定输出的数据类型。它们的性能区别,通用类型会比强类型下降 50% 左右。

比如 Infow 的输出形式,Infow 不需要 zap.String 这种指定字段的数据类型。如下代码:

1
2
3
4
5
6
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
"url", url,
"attempt", 3,
"backoff", time.Second,
)

强类型输出,比如 Info 方法输出字段和值就需要指定数据类型:

1
2
3
4
5
6
logger.Info("failed to fetch url",
// 强类型字段
zap.String("url", "http://example.com"),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
  • 强类型输出和通用类型输出区别

    通用类型输出,经过 interface{} 转换会有性能损失,标准库的 fmt.Printf 为了通用性就用了 interface{} 这种”万能型“的数据类型,另外它还使用了反射,性能进一步降低。

    zap 强类型输出,zap 为了提供日志输出性能,zap 的强类型输出没有使用 interface{} 和反射。zap 默认输出就是强类型。

    上面介绍,zap 中 3 种创建 logger 方式(zap.Newproduction()zap.NewDevelopment()zap.NewExample())就是强类型日志字段,当然,也可以转化为通用类型,用 logger.Sugar() 方法创建 SugaredLogger。

  • zap.Namespace() 创建一个命名空间,后面的字段都在这名字空间中。Namespace 就像一个文件夹,后面文件都放在这个文件夹里。

1
2
3
4
5
logger.Info("some message",
zap.Namespace("shop"),
zap.String("shopid", "s1234323"),
)
// 输出: {"level":"info","msg":"some message","shop":{"shopid":"s1234323"}}

https://www.doubao.com/thread/w606f7e304a7a23f1