【翻】Go 中 Zap 日志的综合指南

Zap 是 Uber 开发的结构化日志包,专为 Go 应用程序设计。根据他们的 GitHub README 文档,它以最少的分配提供“极快”、结构化、分级的日志记录。这一说法得到了他们的基准测试结果的支持,该结果表明 Zap 优于几乎所有其他可比较的 Go 结构化日志记录库,但 Zerolog 除外。【翻】Go 中 Zap 日志的综合指南在这份综合指南中,我们将深入研究 Zap 包并讨论它的许多最有用的功能。我们将从 Go 程序中 Zap 的基本设置开始,然后转到详细示例,说明如何编写和管理各种级别和格式的日志。最后,我们将通过触及更高级的主题(例如自定义编码器、多输出日志记录以及使用 Zap 作为 Slog 后端)来结束本文。让我们开始吧!

快速开始

在开始使用 Zap 之前,您需要通过以下命令将其安装到您的项目中:

go get -u go.uber.org/zap

一旦你安装了 Zap,你就可以像这样在你的程序中开始使用它了:

package main

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

func main() {
    logger := zap.Must(zap.NewProduction())

    defer logger.Sync()   // 因为存在缓存,所以需要强制刷新

    logger.Info("Hello from Zap logger!")
}


// output:{"level":"info","ts":1684092708.7246346,"caller":"zap/main.go:12","msg":"Hello from Zap logger!"}

与 Go 的大多数其他日志记录包不同,Zap 不提供可供使用的预配置全局记录器。因此,您必须先创建一个 zap.Logger 实例,然后才能开始写入日志。NewProduction() 方法返回一个配置为以 JSON 格式记录标准错误的 Logger,其最低日志级别设置为 INFO。 生成的输出相对简单,除了其默认时间戳格式 (ts) 之外没有任何意外,它表示为自 1970 年 1 月 1 日 UTC 以来经过的纳秒数,而不是典型的 ISO-8601 格式。 您还可以利用 NewDevelopment() 预设来创建更优化的 Logger,以便在开发环境中使用。这意味着在 DEBUG 级别记录日志并使用更人性化的格式:

2023-05-14T20:42:39.137+0100    INFO    zap/main.go:12  Hello from Zap logger!

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

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

设置全局变量

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

package main

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

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

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

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

测试Zap 的日志记录 API

Zap 为日志记录提供了两个主要的 API。第一个是低级别的 Logger 类型,它提供了一种结构化的方式来记录消息。它设计用于对性能敏感的上下文,其中每个分配都很重要,并且它仅支持强类型上下文字段:

func main() {
    logger := zap.Must(zap.NewProduction())

    defer logger.Sync()

    logger.Info("User logged in",
                zap.String("username""johndoe"),
                zap.Int("userid"123456),
                zap.String("provider""google"),
               )
}


// {"level":"info","ts":1684094903.7353888,"caller":"zap/main.go:17","msg":"User logged in","username":"johndoe","userid":123456,"provider":"google"}

Logger 类型为每个支持的日志级别(Info()、Warn()、Error() 等)公开一个方法,并且每个级别接受一条消息和零个或多个强类型键/值对字段,如上例所示。 第二个更高级别的 API 是 SugaredLogger 类型,它代表了一种更悠闲的日志记录方法。它具有比 Logger 类型更简洁的 API,但性能成本较低。在幕后,它依赖于 Logger 类型来进行实际的日志记录操作:

func main() {
    logger := zap.Must(zap.NewProduction())

    defer logger.Sync()

    sugar := logger.Sugar()

    sugar.Info("Hello from Zap logger!")
    sugar.Infoln(
        "Hello from Zap logger!",
    )
    sugar.Infof(
        "Hello from Zap logger! The time is %s",
        time.Now().Format("03:04 AM"),
    )

    sugar.Infow("User logged in",
        "username""johndoe",
        "userid"123456,
        zap.String("provider""google"),
    )
}

可以通过调用 Sugar() 方法将 Logger 转换为 SugaredLogger 类型。相反,Desugar() 方法将 SugaredLogger 转换为 Logger,您可以根据需要经常执行这些转换,因为性能开销可以忽略不计。

sugar := zap.Must(zap.NewProduction()).Sugar()

defer sugar.Sync()
sugar.Infow("Hello from SugaredLogger!")

logger := sugar.Desugar()

logger.Info("Hello from Logger!")

此功能意味着您不必在代码库中采用一个或另一个。例如,您可以在常见情况下默认使用 SugaredLogger,因为它具有灵活性,然后在性能敏感代码的边界处转换为 Logger 类型。SugaredLogger 类型为每个支持的级别提供四种方法:

  1. 第一个(Info()、Error() 等)在名称上与 Logger 上的级别方法相同,但它接受一个或多个任何类型的参数。在幕后,他们使用 fmt.Sprint() 方法将参数连接到输出中的 msg 属性中。
  2. 以 ln 结尾的方法(例如 Infoln() 和 Errorln() 与第一个方法相同,只是 fmt.Sprintln() 用于构造和记录消息。
  3. 以 f 结尾的方法使用 fmt.Sprintf() 方法来构造和记录模板化消息。
  4. 最后,以 w 结尾的方法允许您将强类型和松散类型的键/值对混合添加到您的日志记录中。日志消息是第一个参数;如上例所示,后续参数应为键/值对。

关于使用松散类型的键/值对需要注意的一件事是键总是期望为字符串,而值可以是任何类型。如果您使用非字符串键,您的程序将在开发中出现 panic。

Zap中日志等级

Zap 按照严重性的递增顺序提供以下日志级别。每个都与相应的整数相关联:

  • DEBUG (-1):用于记录对调试有用的消息。
  • INFO (0):用于描述正常应用程序操作的消息。
  • WARN (1):用于记录指示发生异常情况的消息,在升级为更严重的问题之前可能需要引起注意。
  • ERROR (2):用于记录程序中意外的错误情况。
  • DPANIC (3):用于记录开发中的严重错误情况。它在开发中表现得像 PANIC,在生产中表现得像 ERROR。
  • PANIC (4):在记录错误情况后调用 panic()。
  • FATAL (5):在记录错误情况后调用 os.Exit(1)。

这些级别在 zapcore 包中定义,该包定义并实现了构建 Zap 的低级接口。值得注意的是,没有 TRACE 级别,也没有办法向 Logger 添加自定义级别,这对某些人来说可能是一个交易破坏者。 如前所述,生产记录器默认设置的日志级别为 INFO。如果你想修改这个设置,你必须创建一个自定义记录器,我们将在下一节中详细说明。

创建自定义记录器

到目前为止,我们已经展示了如何使用 Zap 通过其生产和开发预设提供的默认配置。现在让我们研究如何使用自定义配置选项创建 Logger 实例。 使用 Zap 创建自定义 Logger 有两种主要方法。第一个涉及使用其 Config 类型来构建自定义记录器,如下所示:

package main

import (
    "os"

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

func createLogger() *zap.Logger {
    encoderCfg := zap.NewProductionEncoderConfig()
    encoderCfg.TimeKey = "timestamp"
    encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder

    config := zap.Config{
        Level:             zap.NewAtomicLevelAt(zap.InfoLevel),
        Development:       false,
        DisableCaller:     false,
        DisableStacktrace: false,
        Sampling:          nil,
        Encoding:          "json",
        EncoderConfig:     encoderCfg,
        OutputPaths: []string{
            "stderr",
        },
        ErrorOutputPaths: []string{
            "stderr",
        },
        InitialFields: map[string]interface{}{
            "pid": os.Getpid(),
        },
    }

    return zap.Must(config.Build())
}

func main() {
    logger := createLogger()

    defer logger.Sync()

    logger.Info("Hello from Zap!")
}

上面的 createLogger() 函数返回一个新的 zap.Logger,它的行为类似于 NewProduction() Logger,但有一些不同。我们通过调用 NewProductionEncoderConfig() 并通过将 ts 字段更改为时间戳并将时间格式更改为 ISO-8601 来稍微修改它,从而使用 Zap 的生产配置作为我们自定义记录器的基础。zapcore 包公开了构建 Zap 的接口,以便您可以自定义和扩展其功能。 Config 对象包含创建新 Logger 时所需的许多最常见的配置选项。每个字段代表什么的详细描述都在项目的文档中,所以我们不会在这里重复它们,除了几个:

  • OutputPaths 为日志输出指定一个或多个目标(有关详细信息,请参阅打开)。
  • ErrorOutputPaths 类似于 OutputPaths 但仅用于 Zap 的内部错误,而不是那些由您的应用程序生成或记录的错误(例如来自不匹配的松散类型键/值对的错误)。
  • InitialFields 指定全局上下文字段,这些字段应包含在每个从 Config 对象创建的记录器生成的每个日志条目中。我们在这里只包含程序的进程 ID,但您可以添加其他有用的全局元数据,例如运行程序的 Go 版本、git 提交哈希或应用程序版本、环境或部署信息等。

一旦您设置了您的首选配置设置,您必须调用 Build() 方法来生成一个记录器。确保查看 Config 和 zapcore.EncoderConfig 的文档以了解所有可用选项。 创建自定义记录器的第二种更高级的方法涉及使用 zap.New() 方法。它接受一个 zapcore.Core 接口和零个或多个选项来配置记录器。下面是一个将彩色输出记录到控制台并将 JSON 格式同时记录到文件的示例:

func createLogger() *zap.Logger {
    stdout := zapcore.AddSync(os.Stdout)

    file := zapcore.AddSync(&lumberjack.Logger{
        Filename:   "logs/app.log",
        MaxSize:    10// megabytes
        MaxBackups: 3,
        MaxAge:     7// days
    })

    level := zap.NewAtomicLevelAt(zap.InfoLevel)

    productionCfg := zap.NewProductionEncoderConfig()
    productionCfg.TimeKey = "timestamp"
    productionCfg.EncodeTime = zapcore.ISO8601TimeEncoder

    developmentCfg := zap.NewDevelopmentEncoderConfig()
    developmentCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder

    consoleEncoder := zapcore.NewConsoleEncoder(developmentCfg)
    fileEncoder := zapcore.NewJSONEncoder(productionCfg)

    core := zapcore.NewTee(
        zapcore.NewCore(consoleEncoder, stdout, level),
        zapcore.NewCore(fileEncoder, file, level),
    )

    return zap.New(core)
}

func main() {
    logger := createLogger()

    defer logger.Sync()

    logger.Info("Hello from Zap!")
}

此示例使用 Lumberjack 包来自动轮换日志文件,以免它们变得太大。NewTee() 方法将日志条目复制到两个或多个目的地。在这种情况下,日志将使用彩色明文格式发送到标准输出,而等效的 JSON 格式将发送到 logs/app.log 文件。顺便说一句,我们通常建议使用 Logrotate 等外部工具来管理和轮换日志文件,而不是在应用程序本身中进行。

将上下文添加到您的日志

如前所述,Zap 的上下文日志记录是通过在日志消息之后传递强类型键/值对来完成的,如下所示:

logger.Warn("User account is nearing the storage limit",
    zap.String("username""john.doe"),
    zap.Float64("storageUsed"4.5),
    zap.Float64("storageLimit"5.0),
)

使用子记录器,您还可以将上下文属性添加到在特定范围内生成的所有日志。这有助于您避免在日志点进行不必要的重复。子记录器是使用 Logge 上的 With() 方法创建的:

func main() {
    logger := zap.Must(zap.NewProduction())

    defer logger.Sync()

    childLogger := logger.With(
        zap.String("service""userService"),
        zap.String("requestID""abc123"),
    )

    childLogger.Info("user registration successful",
        zap.String("username""john.doe"),
        zap.String("email""john@example.com"),
    )

    childLogger.Info("redirecting user to admin dashboard")
}

注意 service 和 requestID 如何出现在两个日志中:

{"level":"info","ts":1684164941.7644951,"caller":"zap/main.go:52","msg":"user registration successful","service":"userService","requestID":"abc123","username":"john.doe","email":"john@example.com"}
{"level":"info","ts":1684164941.764551,"caller":"zap/main.go:57","msg":"redirecting user to admin dashboard","service":"userService","requestID":"abc123"}

您可以使用相同的方法将全局元数据添加到所有日志中。例如,你可以像这样在你的所有记录中包含用于编译程序的进程 ID 和 Go 版本:

func createLogger() *zap.Logger {
    . . .

    buildInfo, _ := debug.ReadBuildInfo()

    return zap.New(samplingCore.With([]zapcore.Field{
        zap.String("go_version", buildInfo.GoVersion),
        zap.Int("pid", os.Getpid()),
    },
    ))
}

使用 Zap 记录错误

错误是最重要的日志记录目标之一,因此在采用框架之前了解框架如何处理错误至关重要。在 Zap 中,您可以使用 Error() 方法记录错误。如果使用 zap.Error() 方法,则输出中将包含堆栈跟踪以及错误属性。 对于更严重的错误,可以使用 Fatal() 方法。它在写入和刷新日志消息后调用 os.Exit(1)。 如果错误是可恢复的,您可以改用 Panic() 方法。它记录在 PANIC 级别并调用 panic() 而不是 os.Exit(1)。还有一个 DPanic() 级别,只有在 DPANIC 级别登录后才会在开发中出现恐慌。在生产环境中,它在 DPANIC 级别登录,实际上并没有恐慌。如果您不想使用 PANIC 和 DPANIC 等非标准级别,您可以将这两种方法配置为在 ERROR 级别记录,而不是使用以下代码:

func lowerCaseLevelEncoder(
    level zapcore.Level,
    enc zapcore.PrimitiveArrayEncoder,
)
 {
    if level == zap.PanicLevel || level == zap.DPanicLevel {
        enc.AppendString("error")
        return
    }

    zapcore.LowercaseLevelEncoder(level, enc)
}

func createLogger() *zap.Logger {
    stdout := zapcore.AddSync(os.Stdout)

    level := zap.NewAtomicLevelAt(zap.InfoLevel)

    productionCfg := zap.NewProductionEncoderConfig()
    productionCfg.TimeKey = "timestamp"
    productionCfg.EncodeTime = zapcore.ISO8601TimeEncoder
    productionCfg.EncodeLevel = lowerCaseLevelEncoder

    jsonEncoder := zapcore.NewJSONEncoder(productionCfg)

    core := zapcore.NewCore(jsonEncoder, stdout, level)

    return zap.New(core)
}

func main() {
    logger := createLogger()

    defer logger.Sync()

    logger.DPanic(
        "this was never supposed to happen",
    )
}

使用 Zap 进行日志采样

日志采样是一种用于通过有选择地仅捕获和记录日志事件的子集来减少应用程序日志量的技术。其目的是在全面日志记录的需求与记录过多数据的潜在性能影响之间取得平衡。日志抽样不是捕获每个日志事件,而是允许您根据特定条件或规则选择具有代表性的日志消息子集。这种方式大大减少了生成的日志数据量,这在高吞吐量系统中尤其有益。在 Zap 中,可以使用 zapcore.NewSamplerWithOptions() 方法在 Logger 上配置采样,如下所示:

func createLogger() *zap.Logger {
    stdout := zapcore.AddSync(os.Stdout)

    level := zap.NewAtomicLevelAt(zap.InfoLevel)

    productionCfg := zap.NewProductionEncoderConfig()
    productionCfg.TimeKey = "timestamp"
    productionCfg.EncodeTime = zapcore.ISO8601TimeEncoder
    productionCfg.EncodeLevel = lowerCaseLevelEncoder
    productionCfg.StacktraceKey = "stack"

    jsonEncoder := zapcore.NewJSONEncoder(productionCfg)

    jsonOutCore := zapcore.NewCore(jsonEncoder, stdout, level)

    samplingCore := zapcore.NewSamplerWithOptions(
        jsonOutCore,
        time.Second, // interval
        3// log first 3 entries
        0// thereafter log zero entires within the interval
    )

    return zap.New(samplingCore)
}

Zap 通过在指定时间间隔内记录具有给定级别和消息的前 N 个条目来进行采样。在上面的例子中,每1秒只记录前3条具有相同级别和消息的日志条目。由于此处指定了 0,因此间隔内的所有其他日志条目都将被删除。您可以通过登录 for 循环来对此进行测试:

func main() {
    logger := createLogger()

    defer logger.Sync()

    for i := 1; i <= 10; i++ {
        logger.Info("an info message")
        logger.Warn("a warning")
    }
}

因此,您应该只看到 6 个,而不是观察 20 个日志条目:

{"level":"info","timestamp":"2023-05-17T16:00:17.611+0100","msg":"an info message"}
{"level":"warn","timestamp":"2023-05-17T16:00:17.611+0100","msg":"a warning"}
{"level":"info","timestamp":"2023-05-17T16:00:17.611+0100","msg":"an info message"}
{"level":"warn","timestamp":"2023-05-17T16:00:17.611+0100","msg":"a warning"}
{"level":"info","timestamp":"2023-05-17T16:00:17.611+0100","msg":"an info message"}
{"level":"warn","timestamp":"2023-05-17T16:00:17.611+0100","msg":"a warning"}

在这里,只有循环的前三个迭代产生了一些输出。这是因为在其他七次迭代中产生的日志由于采样配置而被丢弃。同样,当由于负载过重或应用程序遇到一系列错误而每秒记录多次类似条目时,Zap 将删除重复项。虽然日志采样可用于减少日志量和日志记录对性能的影响,但它也可能导致某些日志事件被遗漏,这可能会影响故障排除和调试工作。因此,只有在仔细考虑相关应用的具体要求后才能进行抽样。

在日志中隐藏敏感细节

早在 2018 年,由于用户不小心将数百万明文形式的密码记录到内部日志中,Twitter 不得不敦促用户更改密码。虽然没有发现滥用的证据,但这一事件尖锐地提醒人们,如果不尽职处理应用程序日志,可能会损害用户的安全和隐私。一种防止无意中记录具有敏感字段的类型的技术是在记录点编辑或屏蔽数据。 在 Zap 中,这可以通过实现 Stringer 接口然后定义在记录类型时应返回的确切字符串来完成。这是一个简短的演示:

type User struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func (u User) String() string {
    return u.ID
}

func main() {
    logger := createLogger()

    defer logger.Sync()

    user := User{
        ID:    "USR-12345",
        Name:  "John Doe",
        Email: "john.doe@example.com",
    }

    logger.Info("user login", zap.Any("user", user))
}

如果您需要更多控制,您可以创建自己的 zapcore.Encoder,它使用 JSON 编码器作为基础,同时过滤掉敏感字段:

type SensitiveFieldEncoder struct {
    zapcore.Encoder
    cfg zapcore.EncoderConfig
}

// EncodeEntry is called for every log line to be emitted so it needs to be
// as efficient as possible so that you don't negate the speed/memory advantages
// of Zap
func (e *SensitiveFieldEncoder) EncodeEntry(
    entry zapcore.Entry,
    fields []zapcore.Field,
)
 (*buffer.Buffer, error)
 {
    filtered := make([]zapcore.Field, 0len(fields))

    for _, field := range fields {
        user, ok := field.Interface.(User)
        if ok {
            user.Email = "[REDACTED]"
            field.Interface = user
        }

        filtered = append(filtered, field)
    }

    return e.Encoder.EncodeEntry(entry, filtered)
}

func NewSensitiveFieldsEncoder(config zapcore.EncoderConfig) zapcore.Encoder {
    encoder := zapcore.NewJSONEncoder(config)
    return &SensitiveFieldEncoder{encoder, config}
}

func createLogger() *zap.Logger {
    . . .

    jsonEncoder := NewSensitiveFieldsEncoder(productionCfg)

    . . .

    return zap.New(samplingCore)
}

上面的代码片段确保电子邮件属性被编辑,而其他字段保持原样:

{"level":"info","timestamp":"2023-05-17T17:38:11.749+0100","msg":"user login","user":{"id":"USR-12345","name":"John Doe","email":"[REDACTED]"}

当然,如果 User 类型记录在不同的键下,例如 user_details,这将无济于事。您可以删除 if field.Key == “user” 条件以确保无论提供的密钥如何都执行编辑。

自定义编码器的一些注意事项

当使用 Zap 的自定义编码时,就像在上一节中一样,您可能还需要在 zapcore.Encoder 接口上实现 Clone() 方法,以便它也适用于使用 With() 方法创建的子记录器:

child := logger.With(zap.String("name""main"))
child.Info("an info log", zap.Any("user", u))

在实施 Clone() 之前,您会观察到自定义 EncodeEntry() 未针对子记录器执行,导致电子邮件字段显示为未编辑:

{"level":"info","timestamp":"2023-05-20T09:14:46.043+0100","msg":"an info log","name":"main","user":{"id":"USR-12345","name":"John Doe","email":"john.doe@example.com"}}

当使用 With() 创建子记录器时,将执行配置的编码器上的 Clone() 方法来复制它,并确保添加的字段不会影响原始记录器。如果不在您的自定义编码器类型上实现此方法,则会调用在嵌入式 zapcore.Encoder(本例中为 JSON 编码器)上声明的 Clone() 方法,这意味着子记录器将不会使用您的自定义编码。 您可以通过实施 Clone() 方法来纠正这种情况,如下所示:

func (e *SensitiveFieldEncoder) Clone() zapcore.Encoder {
    return &SensitiveFieldEncoder{
        Encoder: e.Encoder.Clone(),
    }
}

您现在将观察到正确的编辑输出:

{"level":"info","timestamp":"2023-05-20T09:28:31.231+0100","msg":"an info log","name":"main","user":{"id":"USR-12345","name":"John Doe","email":"[REDACTED]"}}

但是,请注意自定义编码器不会影响使用 With() 方法附加的字段,因此如果您执行以下操作:

child := logger.With(zap.String("name""main"), zap.Any("user", u))
child.Info("an info log")

无论是否实现了 Clone(),您都会得到之前未编辑的输出,因为只有在日志点添加的字段才会出现在 EncodeEntry() 的字段 []zapcore.Field 参数中:

{"level":"info","timestamp":"2023-05-20T09:31:11.919+0100","msg":"an info log","name":"main","user":{"id":"USR-12345","name":"John Doe","email":"john.doe@example.com"}

在 Go 应用程序中使用 Zap 进行日志记录

现在我们已经探索了 Zap 的日志记录 API 及其一些最有用的功能,现在是时候检查一个实际示例,展示如何使用它来将日志记录合并到 Go web 应用程序中。您可以在此处找到此实现的具体示例。

git clone https://github.com/betterstack-community/go-logging
git checkout zap

在文本编辑器中打开 logger/logger.go 文件并检查其内容:

package logger

. . .

func Get() *zap.Logger {

    . . .

    return logger
}

func FromCtx(ctx context.Context) *zap.Logger {
    if l, ok := ctx.Value(ctxKey{}).(*zap.Logger); ok {
        return l
    } else if l := logger; l != nil {
        return l
    }

    return zap.NewNop()
}

func WithCtx(ctx context.Context, l *zap.Logger) context.Context {
    if lp, ok := ctx.Value(ctxKey{}).(*zap.Logger); ok {
        if lp == l {
            return ctx
        }
    }

    return context.WithValue(ctx, ctxKey{}, l)
}

Get() 函数用于初始化 zap.Logger 实例(如果尚未初始化),并为后续调用返回相同的实例。记录器配置为同时记录到标准输出和 logs/app.log 文件:

func Get() *zap.Logger {
    once.Do(func() {
        . . .

        // log to multiple destinations (console and file)
        // extra fields are added to the JSON output alone
        core := zapcore.NewTee(
            zapcore.NewCore(consoleEncoder, stdout, logLevel),
            zapcore.NewCore(fileEncoder, file, logLevel).
                With(
                    []zapcore.Field{
                        zap.String("git_revision", gitRevision),
                        zap.String("go_version", buildInfo.GoVersion),
                    },
                ),
        )

        logger = zap.New(core)
    })

    return logger
}

它在 main 函数中使用如下:

func main() {
    l := logger.Get()
    . . .
}

另一方面,WithCtx() 方法将 zap.Logger 实例与 context.Context 关联并返回它,而 FromCtx() 获取 context.Context 并返回与其关联的 zap.Logger(如果有)。这使得在 HTTP 请求的上下文中存储和检索相同的 Logger 实例变得容易。 例如,requestLogger() 是一个中间件函数,它检索记录器实例并使用请求的相关 ID 创建子记录器。然后它继续将子记录器与请求上下文相关联,以便您可以在后续处理程序中检索它:

func requestLogger(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // retrieve the standard logger instance
        l := logger.Get()

        // create a correlation ID for the request
        correlationID := xid.New().String()

        ctx := context.WithValue(
            r.Context(),
            correlationIDCtxKey,
            correlationID,
        )

        r = r.WithContext(ctx)

        // create a child logger containing the correlation ID
        // so that it appears in all subsequent logs
        l = l.With(zap.String(string(correlationIDCtxKey), correlationID))

        w.Header().Add("X-Correlation-ID", correlationID)

        lrw := newLoggingResponseWriter(w)

        // the logger is associated with the request context here
        // so that it may be retrieved in subsequent `http.Handlers`
        r = r.WithContext(logger.WithCtx(ctx, l))

        . . .

        next.ServeHTTP(lrw, r)
    })
}

随后可以从请求上下文中检索记录器,如下所示:

func searchHandler(w http.ResponseWriter, r *http.Request) error {
    ctx := r.Context()

    l := logger.FromCtx(ctx)

    l.Debug("entered searchHandler()")

     . . .
}

注意输出中存在 correlation_id:

2023-05-20T15:32:50.821+0100    DEBUG   entered searchHandler() {"correlation_id""chkdk4koo2ej1bpr4l90"}

使用 Zap 作为 Slog 的后端

在为 Go 引入新的结构化日志记录包(称为 Slog)之后,在 Zap 中实现了 slog.Handler 接口,允许使用带有 Zap 后端的 Slog API。这种集成确保了日志记录 API 在各种依赖项中的一致性,并有助于在对代码进行最少更改的情况下无缝交换日志记录包。截至目前,Slog 尚未包含在正式的 Go 版本中。因此,官方已经在单独的模块中提供了Zap与Slog的集成,可以使用以下命令安装:

go get go.uber.org/zap/exp/zapslog

之后,您可以像这样在您的程序中使用它:

func main() {
    logger := zap.Must(zap.NewProduction())

    defer logger.Sync()

    sl := slog.New(zapslog.NewHandler(logger.Core()))

    sl.Info(
        "incoming request",
        slog.String("method""GET"),
        slog.String("path""/api/user"),
        slog.Int("status"200),
    )
}

如果您决定切换到不同的后端,唯一需要更改的是 slog.New() 方法的参数。例如,您可以通过进行以下更改从 Zap 切换到 Slog 的 JSONHandler 后端:

func main() {
    sl := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    sl.Info(
        "incoming request",
        slog.String("method""GET"),
        slog.String("path""/api/user"),
        slog.Int("status"200),
    )
}

其他一切都应该继续工作,除了日志输出可能会根据您的配置略有不同。


原文始发于微信公众号(小唐云原生):【翻】Go 中 Zap 日志的综合指南

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/247600.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!