golang依赖库Zerolog 详细使用指南:配置、压缩与最佳实践

定是柠檬吃多了,所以才会心酸,一定是眼里飞进了沙子,所以才会流泪

Posted by yishuifengxiao on 2024-04-22

Zerolog 基础使用

安装与基本配置

go get -u github.com/rs/zerolog
package main

import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"os"
"time"
)

func main() {
// 基本配置
zerolog.TimeFieldFormat = time.RFC3339Nano
zerolog.SetGlobalLevel(zerolog.InfoLevel)

// 基本日志输出
log.Info().Msg("这是一条信息日志")
log.Debug().Msg("这条日志不会显示,因为级别是Info")

// 结构化日志
log.Info().
Str("service", "auth").
Int("attempt", 3).
Float64("latency", 123.45).
Msg("用户认证成功")

// 错误日志
err := errors.New("数据库连接失败")
log.Error().
Err(err).
Str("db_host", "localhost").
Int("db_port", 5432).
Msg("数据库连接错误")
}

日志级别

func logLevels() {
// 设置全局日志级别
zerolog.SetGlobalLevel(zerolog.DebugLevel)

// 不同级别日志
log.Trace().Msg("跟踪信息") // 最低级别,用于详细调试
log.Debug().Msg("调试信息") // 调试信息
log.Info().Msg("信息日志") // 常规信息
log.Warn().Msg("警告信息") // 警告信息
log.Error().Msg("错误信息") // 错误信息
log.Fatal().Msg("致命错误") // 致命错误,会调用 os.Exit(1)
log.Panic().Msg("恐慌错误") // 恐慌错误,会调用 panic()
}

Zerolog 高级配置

输出配置

func outputConfiguration() {
// 1. 控制台输出(美化格式)
consoleWriter := zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: time.RFC3339,
FormatLevel: func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("[%s]", i))
},
FormatMessage: func(i interface{}) string {
return fmt.Sprintf("**%s**", i)
},
}
consoleLogger := zerolog.New(consoleWriter).With().Timestamp().Logger()
consoleLogger.Info().Msg("控制台美化输出")

// 2. 文件输出
file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatal().Err(err).Msg("无法打开日志文件")
}
fileLogger := zerolog.New(file).With().Timestamp().Logger()
fileLogger.Info().Msg("文件日志输出")

// 3. 多输出目标
multiWriter := zerolog.MultiLevelWriter(consoleWriter, file)
multiLogger := zerolog.New(multiWriter).With().Timestamp().Logger()
multiLogger.Info().Msg("同时输出到控制台和文件")

// 4. 不同级别输出到不同位置
infoFile, _ := os.OpenFile("info.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
errorFile, _ := os.OpenFile("error.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)

levelWriter := zerolog.LevelWriter{
Info: infoFile,
Error: errorFile,
// 可以设置其他级别
}
levelLogger := zerolog.New(levelWriter).With().Timestamp().Logger()
levelLogger.Info().Msg("信息日志到info.log")
levelLogger.Error().Msg("错误日志到error.log")
}

上下文与子日志器

func contextAndSubloggers() {
// 1. 添加上下文字段
logger := log.With().
Str("service", "user-api").
Str("version", "1.0.0").
Str("environment", os.Getenv("ENV")).
Logger()

logger.Info().Msg("服务启动")

// 2. 请求上下文日志器
requestLogger := logger.With().
Str("request_id", "req-12345").
Str("user_id", "user-67890").
Logger()

requestLogger.Info().Msg("处理用户请求")
requestLogger.Info().Str("endpoint", "/api/users").Msg("API调用")

// 3. 函数特定日志器
funcLogger := logger.With().Str("function", "processUser").Logger()
funcLogger.Info().Msg("开始处理用户")

// 4. 动态添加上下文
dynamicLogger := logger.With().Func(func(e *zerolog.Event) {
e.Str("dynamic_field", "dynamic_value")
e.Int("random_value", rand.Intn(100))
}).Logger()

dynamicLogger.Info().Msg("带动态字段的日志")
}

采样与速率限制

func samplingAndRateLimiting() {
// 1. 基本采样(每N条记录1条)
sampledLogger := log.Sample(&zerolog.BasicSampler{N: 10})
for i := 0; i < 100; i++ {
sampledLogger.Info().Int("iteration", i).Msg("采样日志")
}

// 2. 按时间窗口采样
burstSampler := &zerolog.BurstSampler{
Burst: 5, // 突发允许5条
Period: time.Second, // 每秒
}
burstLogger := log.Sample(burstSampler)

for i := 0; i < 20; i++ {
burstLogger.Info().Int("count", i).Msg("突发采样日志")
time.Sleep(100 * time.Millisecond)
}

// 3. 自定义采样逻辑
customSampler := &zerolog.LevelSampler{
Info: &zerolog.BasicSampler{N: 5}, // 每5条Info记录1条
Warn: &zerolog.BasicSampler{N: 2}, // 每2条Warn记录1条
Error: &zerolog.BasicSampler{N: 1}, // 所有Error都记录
}
customSampledLogger := zerolog.New(os.Stdout).Sample(customSampler)
customSampledLogger.Info().Msg("信息采样")
customSampledLogger.Warn().Msg("警告采样")
customSampledLogger.Error().Msg("错误采样")
}

配置文件详解

配置文件结构

# config/logging.yaml
logging:
# 日志级别: trace, debug, info, warn, error, fatal, panic
level: "info"

# 输出格式: json, console
format: "json"

# 输出目标: stdout, stderr, 或文件路径
output: "stdout"

# 文件输出配置
file:
path: "/var/log/app/app.log"
max_size: 100 # MB
max_backups: 10 # 保留的旧日志文件数量
max_age: 30 # 保留天数
compress: true # 是否压缩旧日志

# 采样配置
sampling:
enabled: true
initial: 100 # 初始采样率
thereafter: 100 # 后续采样率

# 上下文字段
context_fields:
service: "my-app"
environment: "production"
version: "1.0.0"

# 错误堆栈跟踪
stack_trace: true

配置加载与初始化

package config

import (
"io"
"os"
"time"

"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
)

type LogConfig struct {
Level string `yaml:"level"`
Format string `yaml:"format"`
Output string `yaml:"output"`
File FileConfig `yaml:"file"`
Sampling SamplingConfig `yaml:"sampling"`
ContextFields map[string]string `yaml:"context_fields"`
StackTrace bool `yaml:"stack_trace"`
}

type FileConfig struct {
Path string `yaml:"path"`
MaxSize int `yaml:"max_size"`
MaxBackups int `yaml:"max_backups"`
MaxAge int `yaml:"max_age"`
Compress bool `yaml:"compress"`
}

type SamplingConfig struct {
Enabled bool `yaml:"enabled"`
Initial int `yaml:"initial"`
Thereafter int `yaml:"thereafter"`
}

func LoadLogConfig(path string) (*LogConfig, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}

var config LogConfig
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, err
}

return &config, nil
}

func SetupLogger(config *LogConfig) (zerolog.Logger, error) {
// 设置日志级别
level, err := zerolog.ParseLevel(config.Level)
if err != nil {
level = zerolog.InfoLevel
}
zerolog.SetGlobalLevel(level)

// 创建输出写入器
var output io.Writer
switch config.Output {
case "stdout":
output = os.Stdout
case "stderr":
output = os.Stderr
default:
// 文件输出
fileOutput := &lumberjack.Logger{
Filename: config.File.Path,
MaxSize: config.File.MaxSize,
MaxBackups: config.File.MaxBackups,
MaxAge: config.File.MaxAge,
Compress: config.File.Compress,
}
output = fileOutput
}

// 设置输出格式
if config.Format == "console" {
output = zerolog.ConsoleWriter{
Out: output,
TimeFormat: time.RFC3339,
}
}

// 创建基础日志器
logger := zerolog.New(output).With().Timestamp()

// 添加上下文字段
for key, value := range config.ContextFields {
logger = logger.Str(key, value)
}

// 配置采样
if config.Sampling.Enabled {
sampler := &zerolog.BasicSampler{
Initial: config.Sampling.Initial,
Thereafter: config.Sampling.Thereafter,
}
logger = logger.Sample(sampler)
}

// 配置堆栈跟踪
if config.StackTrace {
logger = logger.With().Caller().Logger()
}

return logger.Logger(), nil
}

使用配置初始化日志器

package main

import (
"your-app/config"
)

func main() {
// 加载配置
logConfig, err := config.LoadLogConfig("config/logging.yaml")
if err != nil {
log.Fatal().Err(err).Msg("无法加载日志配置")
}

// 初始化日志器
logger, err := config.SetupLogger(logConfig)
if err != nil {
log.Fatal().Err(err).Msg("无法初始化日志器")
}

// 替换全局日志器
log.Logger = logger

// 使用日志器
log.Info().Msg("应用程序启动")
log.Info().Str("component", "main").Msg("主组件初始化")
}

历史日志压缩与管理

使用 Lumberjack 进行日志轮转和压缩

Zerolog 本身不提供日志轮转功能,但可以与 Lumberjack 等库配合使用。

go get gopkg.in/natefinch/lumberjack.v2
package main

import (
"github.com/rs/zerolog"
"gopkg.in/natefinch/lumberjack.v2"
)

func setupLogRotation() zerolog.Logger {
// 配置 Lumberjack 进行日志轮转和压缩
logRotator := &lumberjack.Logger{
Filename: "/var/log/app/app.log", // 日志文件路径
MaxSize: 100, // 每个日志文件的最大大小(MB)
MaxBackups: 10, // 保留的旧日志文件最大数量
MaxAge: 30, // 保留旧日志文件的最大天数
Compress: true, // 是否压缩/归档旧日志文件
}

// 创建 Zerolog 日志器
logger := zerolog.New(logRotator).With().Timestamp().Logger()

return logger
}

func main() {
logger := setupLogRotation()

// 模拟大量日志写入
for i := 0; i < 10000; i++ {
logger.Info().
Int("iteration", i).
Msg("测试日志轮转和压缩")
}
}

自定义日志压缩策略

package logmanager

import (
"compress/gzip"
"io"
"os"
"path/filepath"
"time"

"github.com/rs/zerolog"
)

// LogManager 管理日志轮转和压缩
type LogManager struct {
currentFile *os.File
basePath string
maxSize int64
maxBackups int
maxAge time.Duration
}

func NewLogManager(basePath string, maxSize int64, maxBackups int, maxAge time.Duration) *LogManager {
return &LogManager{
basePath: basePath,
maxSize: maxSize,
maxBackups: maxBackups,
maxAge: maxAge,
}
}

func (lm *LogManager) Write(p []byte) (n int, err error) {
// 检查是否需要轮转
if lm.currentFile == nil || lm.needRotate() {
if err := lm.rotate(); err != nil {
return 0, err
}
}

return lm.currentFile.Write(p)
}

func (lm *LogManager) needRotate() bool {
if lm.currentFile == nil {
return true
}

info, err := lm.currentFile.Stat()
if err != nil {
return true
}

return info.Size() >= lm.maxSize
}

func (lm *LogManager) rotate() error {
// 关闭当前文件
if lm.currentFile != nil {
if err := lm.currentFile.Close(); err != nil {
return err
}

// 压缩刚关闭的文件
go lm.compressFile(lm.currentFile.Name())
}

// 创建新日志文件
timestamp := time.Now().Format("2006-01-02_15-04-05")
newPath := filepath.Join(lm.basePath, "app_"+timestamp+".log")

file, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return err
}

lm.currentFile = file

// 清理旧日志
go lm.cleanupOldLogs()

return nil
}

func (lm *LogManager) compressFile(path string) {
// 打开原始日志文件
src, err := os.Open(path)
if err != nil {
return
}
defer src.Close()

// 创建压缩文件
dstPath := path + ".gz"
dst, err := os.Create(dstPath)
if err != nil {
return
}
defer dst.Close()

// 使用 gzip 压缩
gz := gzip.NewWriter(dst)
defer gz.Close()

// 复制数据
if _, err := io.Copy(gz, src); err != nil {
return
}

// 删除原始文件
os.Remove(path)
}

func (lm *LogManager) cleanupOldLogs() {
files, err := filepath.Glob(filepath.Join(lm.basePath, "app_*.log.gz"))
if err != nil {
return
}

// 按修改时间排序
sort.Slice(files, func(i, j int) bool {
infoI, _ := os.Stat(files[i])
infoJ, _ := os.Stat(files[j])
return infoI.ModTime().After(infoJ.ModTime())
})

// 删除超数量的旧文件
if len(files) > lm.maxBackups {
for _, file := range files[lm.maxBackups:] {
os.Remove(file)
}
}

// 删除超时的文件
cutoff := time.Now().Add(-lm.maxAge)
for _, file := range files {
info, err := os.Stat(file)
if err != nil {
continue
}

if info.ModTime().Before(cutoff) {
os.Remove(file)
}
}
}

// 使用自定义日志管理器
func setupCustomLogManager() zerolog.Logger {
logManager := NewLogManager("/var/log/app", 100*1024*1024, 10, 30*24*time.Hour)
logger := zerolog.New(logManager).With().Timestamp().Logger()
return logger
}

基于时间的日志轮转

func timeBasedRotation() {
// 创建基于时间的轮转器
timeRotator := &TimeBasedRotator{
BasePath: "/var/log/app",
TimeFormat: "2006-01-02", // 按天轮转
MaxAge: 7 * 24 * time.Hour, // 保留7天
Compress: true,
}

logger := zerolog.New(timeRotator).With().Timestamp().Logger()

// 启动轮转检查
go timeRotator.StartRotationCheck(1 * time.Hour)

logger.Info().Msg("基于时间的日志轮转示例")
}

type TimeBasedRotator struct {
currentFile *os.File
currentDate string
BasePath string
TimeFormat string
MaxAge time.Duration
Compress bool
}

func (t *TimeBasedRotator) Write(p []byte) (n int, err error) {
currentDate := time.Now().Format(t.TimeFormat)

if t.currentFile == nil || currentDate != t.currentDate {
if err := t.rotate(currentDate); err != nil {
return 0, err
}
}

return t.currentFile.Write(p)
}

func (t *TimeBasedRotator) rotate(currentDate string) error {
if t.currentFile != nil {
t.currentFile.Close()

if t.Compress {
go t.compressFile(t.currentFile.Name())
}
}

filename := filepath.Join(t.BasePath, "app_"+currentDate+".log")
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return err
}

t.currentFile = file
t.currentDate = currentDate

// 清理旧日志
go t.cleanupOldLogs()

return nil
}

func (t *TimeBasedRotator) StartRotationCheck(interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()

for range ticker.C {
currentDate := time.Now().Format(t.TimeFormat)
if currentDate != t.currentDate {
t.rotate(currentDate)
}
}
}

func (t *TimeBasedRotator) compressFile(path string) {
// 压缩实现(同上)
}

func (t *TimeBasedRotator) cleanupOldLogs() {
// 清理实现(同上)
}

高级功能与最佳实践

错误堆栈跟踪

func stackTraceExample() {
// 启用调用者信息
logger := zerolog.New(os.Stdout).With().Caller().Logger()

logger.Info().Msg("这条日志会包含调用者信息")

// 添加堆栈跟踪
err := errors.New("示例错误")
logger.Error().
Err(err).
Stack().
Msg("错误详情")
}

// 自定义堆栈跟踪深度
func deepCall() {
logger := zerolog.New(os.Stdout).With().CallerWithSkipFrameCount(3).Logger()
logger.Info().Msg("跳过3帧的调用者信息")
}

性能优化技巧

func performanceOptimization() {
// 1. 避免不必要的日志计算
if log.Debug().Enabled() {
// 只在需要时计算昂贵操作
expensiveData := calculateExpensiveData()
log.Debug().Interface("data", expensiveData).Msg("调试数据")
}

// 2. 使用池化减少内存分配
var eventPool = sync.Pool{
New: func() interface{} {
return log.Info()
},
}

// 从池中获取事件
event := eventPool.Get().(*zerolog.Event)
event.Msg("使用池化事件")
// 重置并放回池中
eventPool.Put(event)

// 3. 批量日志处理
batchLogger := log.With().Batch()
for i := 0; i < 100; i++ {
batchLogger = batchLogger.Str(fmt.Sprintf("key_%d", i), fmt.Sprintf("value_%d", i))
}
batchLogger.Msg("批量日志")
}

集成监控和告警

func monitoringIntegration() {
// 1. 错误率监控
errorCount := 0
totalRequests := 0
var mu sync.Mutex

http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
totalRequests++
mu.Unlock()

err := processRequest(r)
if err != nil {
mu.Lock()
errorCount++
errorRate := float64(errorCount) / float64(totalRequests)
mu.Unlock()

log.Error().
Err(err).
Float64("error_rate", errorRate).
Msg("请求处理失败")

// 触发告警
if errorRate > 0.1 {
sendAlert("错误率超过10%")
}
}
})

// 2. 性能指标日志
start := time.Now()
result, err := expensiveOperation()
duration := time.Since(start)

log.Info().
Dur("duration_ms", duration).
Bool("success", err == nil).
Msg("操作完成")
}