Golang 14_Golang日志库zap

一、uber-go 的日志操作库 zap 简介

1.1 zap库简介

zap 是 uber 开源的一个高性能、结构化、分级记录的日志记录包。

zap 的特性**:

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

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

Zap 使用一个无反射、零分配的 JOSN 编码器,基础 Logger 尽可能避免序列化开销和内存分配开销。在此基础上,zap 还构建了更高级的 SuggaredLogger。

1.2 快速开始使用 zap

zap 安装:

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

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),
)

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

  • Example 一般用在测试代码中;
  • Development 用在开发环境中;
  • Production 用在生成环境中;

这三种方法都预先设置好了配置信息。

二、zap 的使用

2.1 NewExample()使用

NewExample() 构建一个 logger,专门为在 zap 的测试示例使用。它将 DebugLevel 及以上日志用 JSON 格式标准输出,但它省略了时间戳和调用函数,以保持示例输出的简短和确定性。 这个方法里,zap 已经定义好了日志配置项部分默认值,来看它的代码:

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

// NewExample builds a Logger that's designed for use in zap's testable
// examples. It writes DebugLevel and above logs to standard out as JSON, but
// omits the timestamp and calling function to keep example output
// short and deterministic.
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...)
}

NewExample() 使用示例

 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"}

2.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
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
//    cfg := zap.NewDevelopmentEncoderConfig()
//    cfg.EncodeTime = zapcore.ISO8601TimeEncoder
func NewDevelopmentEncoderConfig() zapcore.EncoderConfig {
    return zapcore.EncoderConfig{
        // Keys can be anything except the empty string.
        TimeKey:        "T",
        LevelKey:       "L",
        NameKey:        "N",
        CallerKey:      "C",
        FunctionKey:    zapcore.OmitKey,
        MessageKey:     "M",
        StacktraceKey:  "S",
        LineEnding:     zapcore.DefaultLineEnding,
        EncodeLevel:    zapcore.CapitalLevelEncoder,
        EncodeTime:     zapcore.ISO8601TimeEncoder,
        EncodeDuration: zapcore.StringDurationEncoder,
        EncodeCaller:   zapcore.ShortCallerEncoder,
    }
}

// NewDevelopmentConfig builds a reasonable default development logging
// configuration.
// Logging is enabled at DebugLevel and above, and uses a console encoder.
// Logs are written to standard error.
// Stacktraces are included on logs of WarnLevel and above.
// DPanicLevel logs will panic.
//
// See [NewDevelopmentEncoderConfig] for information
// on the default encoder configuration.
func NewDevelopmentConfig() Config {
    return Config{
        Level:            NewAtomicLevelAt(DebugLevel),
        Development:      true,
        Encoding:         "console",
        EncoderConfig:    NewDevelopmentEncoderConfig(),
        OutputPaths:      []string{"stderr"},
        ErrorOutputPaths: []string{"stderr"},
    }
}


// Build constructs a logger from the Config and Options.
func (cfg Config) Build(opts ...Option) (*Logger, error) {
    enc, err := cfg.buildEncoder()
    if err != nil {
        return nil, err
    }

    sink, errSink, err := cfg.openSinks()
    if err != nil {
        return nil, err
    }

    if cfg.Level == (AtomicLevel{}) {
        return nil, errors.New("missing Level")
    }

    log := New(
        zapcore.NewCore(enc, sink, cfg.Level),
        cfg.buildOptions(errSink)...,
    )
    if len(opts) > 0 {
        log = log.WithOptions(opts...)
    }
    return log, nil
}

// NewDevelopment builds a development Logger that writes DebugLevel and above
// logs to standard error in a human-friendly format.
//
// It's a shortcut for NewDevelopmentConfig().Build(...Option).
func NewDevelopment(options ...Option) (*Logger, error) {
    return NewDevelopmentConfig().Build(options...)
}

NewDevelopment()使用示例:

 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")
}

输出:

1
2
2024-01-16T21:53:54.502+0800    INFO    SourceCodeTest/zap1.go:13    failed to fetch url    {"url": "http://example.com", "attempt": 3, "duration": "1s"}
2024-01-16T21:53:54.502+0800    INFO    SourceCodeTest/zap1.go:25    [With] failed to fetch url    {"url": "http://development.com", "attempt": 4, "duration": "5s"}

2.3 NewProduction()使用

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

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

1
2
3
4
5
6
7
// NewDevelopment builds a development Logger that writes DebugLevel and above
// logs to standard error in a human-friendly format.
//
// It's a shortcut for NewDevelopmentConfig().Build(...Option).
func NewDevelopment(options ...Option) (*Logger, error) {
    return NewDevelopmentConfig().Build(options...)
}

NewProduction()使用示例:

 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":1705413428.8185952,"caller":"SourceCodeTest/zap2.go:15","msg":"failed to fetch URL","url":"http://zap.uber.io","attempt":3,"time":1}
{"level":"info","ts":1705413428.8186848,"caller":"SourceCodeTest/zap2.go:21","msg":"Failed to fetch URL: http://zap.uber.io"}

2.4 修改 logger 配置

从源代码实现上可以看出,在调用 NewExample / NewDevelopment / NewProduction 这 3 个函数 创建 logger 实例对象时,可以传入一些配置项。这些函数传参有一个 …Option 选项,是一个 interface 类型,它关联的是 Logger struct。只要返回 Option 就可以传进 这类New 函数 里。

在 zap/options.go 文件中可以看到很多返回 Option 的函数,也就是说这些函数都可以传入 New 函数里。这里用到了 Go 里面的一个编码技巧,函数选项模式。

2.5 给 logger 添加全局字段

使用 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":1705414301.849495,"caller":"SourceCodeTest/zap3.go:14","msg":"test fields output","log_name":"testlog","log_author":"prometheus"}
{"level":"warn","ts":1705414301.849549,"caller":"SourceCodeTest/zap3.go:16","msg":"warn info","log_name":"testlog","log_author":"prometheus"}

2.6 zap 的 Hook (钩子函数) 添加回调函数

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

 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

2.7 zap.Namespace() 创建日志命名空间

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"),
    )
}

三、logger 和 sugaredlogger 的区别

3.1 logger 和 sugaredlogger 的区别

前面介绍到,zap 有 2 种格式化日志方式:logger 和 sugared logger。

  • sugared logger:

    • 它有很好的性能,比一般日志包快 4-10 倍;
    • 支持结构化的日志;
    • 支持 printf 风格的日志;
    • 日志字段不需要定义类型;
  • logger(没有sugar):

    • 它的性能比 sugared logger 还要快
    • 它只支持强类型的结构化日志

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

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 每次分配内存更少,性能更高。但它仅支持强类型的结构化日志记录。

四、zap 自定义配置

4.1 zap 自定义配置结构说明

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

但是有的项目需要更丰富、个性化的配置定制,zap 的 Config 结构和 zapcore 的 EncoderConfig 结构提供自定义配置的能力。

Config 配置项源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
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 zap.Config 基本配置示例

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
}
*/

输出:

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

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

4.3 zap.EncoderConfig 高级配置

上面的配置只是基本的自定义配置,如果有一些复杂的需求,比如在多个文件之间分割日志,或者输出到不是 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")
}

4.4 日志写入文件

 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 根据日志级别写入不同文件

 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 中。

五、日志切割归档

5.1 lumberjack 切割归档日志

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

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

zap 日志 切割归档 示例:

 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,M 为单位,达到这个设置数后就进行日志切割
        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 ")
    }

}

六、zap 其它方法使用

6.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
19
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()

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

6.2 zap 与标准日志库搭配

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")
}

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

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

6.3 一段代码中使用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")
}

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

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

6.4 输出调用堆栈

主要是调用函数 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
{"level":"warn","ts":1705417225.963784,"caller":"SourceCodeTest/zap6.go:13","msg":"hello","h":"world","c":1,"stacktrace":"main.Warn\n\t/Users/mac/MubeiSpaces/NoteOnLearning/SourceCodeTest/zap6.go:13\nmain.Hello\n\t/Users/mac/MubeiSpaces/NoteOnLearning/SourceCodeTest/zap6.go:9\nmain.main\n\t/Users/mac/MubeiSpaces/NoteOnLearning/SourceCodeTest/zap6.go:22\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:250"}

6.5 输出文件名和行号

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

 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")
}

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

 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":1705417428.385977,"caller":"SourceCodeTest/zap7.go:17","msg":"hello","h":"world","c":1}

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

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

七、zap使用总结

7.1 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 就像一个文件夹,后面文件都放在这个文件夹里。