MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Go日志包高效使用的实战技巧

2021-11-123.1k 阅读

1. 理解 Go 日志包基础

在 Go 语言中,标准库提供了 log 包用于日志记录。它提供了简单易用的函数来记录日志信息,如 log.Printlnlog.Printflog.Fatalln 等。

首先来看最基础的使用:

package main

import (
    "log"
)

func main() {
    log.Println("这是一条普通的日志信息")
    log.Printf("格式化日志,数字:%d", 123)
    log.Fatalln("这是一条致命错误日志,程序将终止")
}

在上述代码中,log.Println 输出一行日志信息,log.Printf 支持格式化输出,而 log.Fatalln 会在输出日志后调用 os.Exit(1) 终止程序。

1.1 日志输出目的地

默认情况下,log 包将日志输出到标准错误(os.Stderr)。但我们可以通过 log.SetOutput 函数来改变输出目的地。例如,将日志输出到文件:

package main

import (
    "log"
    "os"
)

func main() {
    file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    log.SetOutput(file)
    log.Println("这条日志将输出到 app.log 文件")
}

在这段代码中,我们首先打开一个名为 app.log 的文件,然后使用 log.SetOutput 将日志输出重定向到该文件。这样,后续的日志信息都会写入到 app.log 中。

1.2 日志前缀与标志

我们可以通过 log.SetPrefix 函数为日志添加前缀,通过 log.SetFlags 函数设置日志标志,以控制日志输出的格式。

package main

import (
    "log"
    "time"
)

func main() {
    log.SetPrefix("[APP] ")
    log.SetFlags(log.LstdFlags | log.Lshortfile)

    log.Println("带有前缀和标志的日志")
}

在上述代码中,log.SetPrefix 设置了日志前缀为 [APP]log.SetFlags 中,LstdFlags 表示输出日期和时间,Lshortfile 表示输出调用日志函数的文件名和行号。运行这段代码,你会看到类似如下的日志输出: [APP] 2024/01/01 12:00:00 main.go:8: 带有前缀和标志的日志

2. 结构化日志

虽然标准库的 log 包简单易用,但在实际生产环境中,结构化日志更为重要。结构化日志可以让日志信息以一种更易于机器解析和分析的格式记录,比如 JSON 格式。

2.1 使用第三方库实现结构化日志 - Zap

Zap 是 Uber 开源的一个高性能的 Go 日志库,它非常适合用于结构化日志记录。

首先,安装 Zap 库:

go get -u go.uber.org/zap

然后,来看一个基本的使用示例:

package main

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

func main() {
    logger, err := zap.NewProduction()
    if err != nil {
        panic(err)
    }
    defer logger.Sync()

    logger.Info("这是一条 Zap 记录的结构化日志",
        zap.String("key1", "value1"),
        zap.Int("key2", 123),
    )
}

在上述代码中,我们首先通过 zap.NewProduction 创建一个生产环境配置的 logger。NewProduction 配置会开启一些默认的优化,比如异步写入日志等。然后,使用 logger.Info 记录日志,并通过 zap.Stringzap.Int 等函数添加结构化字段。

2.2 Zap 的配置定制

Zap 提供了丰富的配置选项,可以根据需求定制日志记录。

package main

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

func main() {
    encoderConfig := zapcore.EncoderConfig{
        TimeKey:        "time",
        LevelKey:       "level",
        NameKey:        "logger",
        CallerKey:      "caller",
        MessageKey:     "msg",
        StacktraceKey:  "stacktrace",
        LineEnding:     zapcore.DefaultLineEnding,
        EncodeLevel:    zapcore.CapitalLevelEncoder,
        EncodeTime:     zapcore.ISO8601TimeEncoder,
        EncodeDuration: zapcore.SecondsDurationEncoder,
        EncodeCaller:   zapcore.ShortCallerEncoder,
    }

    core := zapcore.NewCore(
        zapcore.NewJSONEncoder(encoderConfig),
        zapcore.AddSync(zapcore.Lock(os.Stdout)),
        zapcore.InfoLevel,
    )

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

    logger.Info("定制配置的结构化日志")
}

在这段代码中,我们首先定义了一个 EncoderConfig,用于定制日志的编码格式。例如,TimeKey 定义了时间字段的键名,EncodeTime 定义了时间的编码格式为 ISO8601。然后,通过 zapcore.NewCore 创建一个核心,指定编码器、输出目的地和日志级别。最后,使用这个核心创建一个 logger。

3. 日志级别控制

在实际开发中,根据不同的环境和需求,我们需要控制日志输出的级别,比如在开发环境输出详细的调试信息,而在生产环境只输出错误和重要信息。

3.1 标准库的日志级别模拟

虽然标准库的 log 包没有直接的日志级别控制,但我们可以通过一些简单的技巧来模拟。

package main

import (
    "log"
    "os"
)

const (
    LevelDebug = iota
    LevelInfo
    LevelWarn
    LevelError
)

var currentLevel = LevelDebug

func logWithLevel(level int, format string, v ...interface{}) {
    if level < currentLevel {
        return
    }
    var prefix string
    switch level {
    case LevelDebug:
        prefix = "[DEBUG] "
    case LevelInfo:
        prefix = "[INFO] "
    case LevelWarn:
        prefix = "[WARN] "
    case LevelError:
        prefix = "[ERROR] "
    }
    log.Output(2, prefix+fmt.Sprintf(format, v...))
}

func main() {
    logWithLevel(LevelDebug, "这是一条调试日志")
    logWithLevel(LevelInfo, "这是一条信息日志")
    logWithLevel(LevelWarn, "这是一条警告日志")
    logWithLevel(LevelError, "这是一条错误日志")
}

在上述代码中,我们定义了一个 logWithLevel 函数,通过比较传入的日志级别和当前设置的级别来决定是否输出日志。同时,根据不同的级别添加不同的前缀。

3.2 Zap 的日志级别控制

Zap 库原生支持日志级别控制。

package main

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

func main() {
    config := zap.NewProductionConfig()
    config.Level.SetLevel(zapcore.DebugLevel)

    logger, err := config.Build()
    if err != nil {
        panic(err)
    }
    defer logger.Sync()

    logger.Debug("这是一条调试日志")
    logger.Info("这是一条信息日志")
    logger.Warn("这是一条警告日志")
    logger.Error("这是一条错误日志")
}

在这段代码中,我们通过 zap.NewProductionConfig 获取一个生产环境的配置,然后使用 config.Level.SetLevel 设置日志级别为 DebugLevel。这样,所有级别大于等于 DebugLevel 的日志都会被记录。

4. 日志切割

随着应用程序的运行,日志文件会不断增大,为了便于管理和存储,我们需要进行日志切割。

4.1 使用第三方库实现日志切割 - RotateFile

lumberjack 是一个简单的日志切割库,我们可以结合它与 Zap 实现日志切割。

首先,安装 lumberjack 库:

go get -u gopkg.in/natefinch/lumberjack.v2

然后,来看示例代码:

package main

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

func main() {
    lumberjackLogger := &lumberjack.Logger{
        Filename:   "app.log",
        MaxSize:    10, // megabytes
        MaxBackups: 3,
        MaxAge:     28, // days
        Compress:   true,
    }

    encoderConfig := zapcore.EncoderConfig{
        TimeKey:        "time",
        LevelKey:       "level",
        NameKey:        "logger",
        CallerKey:      "caller",
        MessageKey:     "msg",
        StacktraceKey:  "stacktrace",
        LineEnding:     zapcore.DefaultLineEnding,
        EncodeLevel:    zapcore.CapitalLevelEncoder,
        EncodeTime:     zapcore.ISO8601TimeEncoder,
        EncodeDuration: zapcore.SecondsDurationEncoder,
        EncodeCaller:   zapcore.ShortCallerEncoder,
    }

    core := zapcore.NewCore(
        zapcore.NewJSONEncoder(encoderConfig),
        zapcore.AddSync(lumberjackLogger),
        zapcore.InfoLevel,
    )

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

    for i := 0; i < 100000; i++ {
        logger.Info("模拟日志记录")
    }
}

在上述代码中,我们首先创建了一个 lumberjack.Logger 实例,设置了日志文件名为 app.log,最大文件大小为 10MB,最多保留 3 个备份文件,日志文件最长保留 28 天,并开启压缩。然后,将这个 lumberjack.Logger 作为输出目的地传递给 Zap 的核心,实现日志切割功能。

4.2 基于时间的日志切割

除了基于文件大小的切割,有时我们也需要基于时间进行日志切割。虽然 lumberjack 库没有直接提供基于时间的切割功能,但我们可以结合一些其他库来实现。例如,file-rotatelogs 库可以实现按时间切割日志。

首先,安装 file-rotatelogs 库:

go get -u github.com/lestrrat-go/file-rotatelogs

然后,示例代码如下:

package main

import (
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "github.com/lestrrat-go/file-rotatelogs"
    "time"
)

func main() {
    logWriter, err := file-rotatelogs.New(
        "app-%Y%m%d.log",
        file-rotatelogs.WithLinkName("app.log"),
        file-rotatelogs.WithMaxAge(7*24*time.Hour),
        file-rotatelogs.WithRotationTime(24*time.Hour),
    )
    if err != nil {
        panic(err)
    }

    encoderConfig := zapcore.EncoderConfig{
        TimeKey:        "time",
        LevelKey:       "level",
        NameKey:        "logger",
        CallerKey:      "caller",
        MessageKey:     "msg",
        StacktraceKey:  "stacktrace",
        LineEnding:     zapcore.DefaultLineEnding,
        EncodeLevel:    zapcore.CapitalLevelEncoder,
        EncodeTime:     zapcore.ISO8601TimeEncoder,
        EncodeDuration: zapcore.SecondsDurationEncoder,
        EncodeCaller:   zapcore.ShortCallerEncoder,
    }

    core := zapcore.NewCore(
        zapcore.NewJSONEncoder(encoderConfig),
        zapcore.AddSync(logWriter),
        zapcore.InfoLevel,
    )

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

    for i := 0; i < 100000; i++ {
        logger.Info("模拟日志记录")
    }
}

在这段代码中,我们通过 file-rotatelogs.New 创建了一个日志写入器,设置了日志文件的命名格式为 app-YYYYMMDD.log,设置了软链接名为 app.log,最大保留时间为 7 天,每天进行一次日志切割。然后,将这个日志写入器作为输出目的地传递给 Zap 的核心,实现基于时间的日志切割。

5. 并发安全的日志记录

在并发编程中,确保日志记录的并发安全非常重要,否则可能会导致日志输出混乱或丢失。

5.1 Zap 的并发安全性

Zap 库本身是并发安全的,我们可以在多个 goroutine 中安全地使用同一个 logger 实例。

package main

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

func main() {
    logger, err := zap.NewProduction()
    if err != nil {
        panic(err)
    }
    defer logger.Sync()

    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            logger.Info("并发日志记录", zap.Int("goroutine", id))
        }(i)
    }
    wg.Wait()
}

在上述代码中,我们启动了 10 个 goroutine,每个 goroutine 都使用同一个 logger 实例记录日志。由于 Zap 的并发安全性,日志记录不会出现混乱。

5.2 自定义并发安全日志实现

如果我们想要自己实现一个并发安全的日志记录器,可以使用互斥锁(sync.Mutex)。

package main

import (
    "fmt"
    "log"
    "sync"
)

type SafeLogger struct {
    mu    sync.Mutex
    log   *log.Logger
}

func NewSafeLogger() *SafeLogger {
    return &SafeLogger{
        log: log.New(os.Stdout, "", log.LstdFlags),
    }
}

func (sl *SafeLogger) Println(v ...interface{}) {
    sl.mu.Lock()
    defer sl.mu.Unlock()
    sl.log.Println(v...)
}

func main() {
    safeLogger := NewSafeLogger()
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            safeLogger.Println("并发日志记录", id)
        }(i)
    }
    wg.Wait()
}

在这段代码中,我们定义了一个 SafeLogger 结构体,其中包含一个互斥锁 mu 和一个标准库的 log.Logger 实例。在 Println 方法中,我们通过互斥锁来确保在多 goroutine 环境下日志记录的安全性。

6. 日志集成与监控

在实际的应用开发中,日志不仅仅是记录信息,还需要与监控和告警系统集成,以便及时发现和处理问题。

6.1 与 Prometheus 和 Grafana 集成

我们可以通过将日志中的关键指标提取出来,发送到 Prometheus 进行存储和监控,然后使用 Grafana 进行可视化展示。

例如,我们可以统计不同级别的日志数量,并将其作为指标发送到 Prometheus。

package main

import (
    "context"
    "fmt"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "net/http"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    logLevelCounter = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "log_level_count",
            Help: "Number of logs per level",
        },
        []string{"level"},
    )
)

func init() {
    prometheus.MustRegister(logLevelCounter)
}

func main() {
    encoderConfig := zapcore.EncoderConfig{
        TimeKey:        "time",
        LevelKey:       "level",
        NameKey:        "logger",
        CallerKey:      "caller",
        MessageKey:     "msg",
        StacktraceKey:  "stacktrace",
        LineEnding:     zapcore.DefaultLineEnding,
        EncodeLevel:    zapcore.CapitalLevelEncoder,
        EncodeTime:     zapcore.ISO8601TimeEncoder,
        EncodeDuration: zapcore.SecondsDurationEncoder,
        EncodeCaller:   zapcore.ShortCallerEncoder,
    }

    core := zapcore.NewCore(
        zapcore.NewJSONEncoder(encoderConfig),
        zapcore.AddSync(zapcore.Lock(os.Stdout)),
        zapcore.InfoLevel,
    )

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

    http.Handle("/metrics", promhttp.Handler())
    go func() {
        fmt.Println("Server listening on :8080")
        err := http.ListenAndServe(":8080", nil)
        if err != nil {
            logger.Fatal("HTTP server failed", zap.Error(err))
        }
    }()

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    for {
        select {
        case <-ctx.Done():
            return
        default:
            logger.Debug("模拟调试日志")
            logLevelCounter.WithLabelValues("DEBUG").Inc()
            logger.Info("模拟信息日志")
            logLevelCounter.WithLabelValues("INFO").Inc()
            logger.Warn("模拟警告日志")
            logLevelCounter.WithLabelValues("WARN").Inc()
            logger.Error("模拟错误日志")
            logLevelCounter.WithLabelValues("ERROR").Inc()
            time.Sleep(1 * time.Second)
        }
    }
}

在上述代码中,我们定义了一个 logLevelCounter 指标,用于统计不同级别的日志数量。每次记录日志时,相应级别的计数器就会增加。然后,通过 http.Handle("/metrics", promhttp.Handler()) 暴露 Prometheus 指标端点,这样 Prometheus 就可以抓取这些指标数据。最后,我们可以在 Grafana 中配置数据源为 Prometheus,并创建仪表盘来可视化展示这些日志级别统计信息。

6.2 与告警系统集成

结合日志监控指标,我们可以与告警系统如 Alertmanager 集成。例如,当错误日志数量在短时间内急剧增加时,通过 Alertmanager 发送告警通知。

首先,在 Prometheus 的配置文件(prometheus.yml)中添加告警规则:

rule_files:
  - alert.rules.yml

然后,在 alert.rules.yml 中定义告警规则:

groups:
- name: LoggingAlerts
  rules:
  - alert: HighErrorLogs
    expr: increase(log_level_count{level="ERROR"}[5m]) > 100
    for: 1m
    labels:
      severity: critical
    annotations:
      summary: "高错误日志数量"
      description: "过去5分钟内错误日志数量超过100条"

上述规则表示,如果过去 5 分钟内错误日志数量的增加量超过 100 条,并且这种情况持续 1 分钟,就触发告警。

接着,配置 Alertmanager,将告警信息发送到指定的渠道,如邮件、Slack 等。这样,当错误日志数量达到阈值时,相关人员就能及时收到告警通知,以便快速处理问题。

通过以上对 Go 日志包的高效使用技巧的介绍,从基础使用到结构化日志、日志级别控制、日志切割、并发安全以及日志集成与监控,我们可以更好地在实际项目中利用日志来提升系统的可维护性、可观测性和稳定性。在不同的场景下,选择合适的日志记录方式和工具,能够帮助我们更高效地开发和运维 Go 应用程序。