zerolog
作者: ryan 发布于: 2025/12/3 更新于: 2025/12/3 字数: 0 字 阅读: 0 分钟
zerolog 是 Go 生态中性能最高(几乎零分配)、纯粹为结构化日志设计的日志库。 特点:
- 零内存分配(在热点路径)
- 原生支持 JSON 输出(生产环境最常用)
- 极快的性能(比 logrus、zap 还要快)
- 全局日志 + 上下文日志共存,非常适合现代 Go 应用(结合 context)
GitHub:https://github.com/rs/zerolog
安装 go get -u github.com/rs/zerolog/log
核心类型
zerolog 包的核心类型是 zerolog.Logger
import "github.com/rs/zerolog"
var log zerolog.Logger // 全局日志实例 任意包都可以直接使用内部字段
type Logger struct {
w Writer // 输出目标
level Level // 当前最低输出级别
ctx []context // 预计算好的上下文字段(零分配关键)
hooks []Hook
sampler Sampler
prefix []byte // 预渲染好的固定字段(如时间、caller)
buf []byte // 复用的临时 buffer(避免每次分配)
// ...
}全局缺省Logger
package main
import "github.com/rs/zerolog/log"
func main() {
log.Print("default msg") //使用全局缺省Logger
// log.Print 产生debug级别消息
}
//输出 {"level":"debug","time":"2025-11-26T17:09:09+08:00","message":"default msg"} 日志JSON格式输出源码定义
默认已包含时间戳,输出到 stderr,级别为 debug。
// Logger is the global logger.源码第14行,定义了一个全局导出的缺省Logger
var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger() //链式调用
//缺省Logger 使用标准错误输出log.Print()、log.Printf()方法使用方式和标准库log模块类似
链式调用
我们对 var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger() 链式调用进行解析
zerolog.New(os.Stderr)返回的类型是zerolog.Logger它的作用是创建一个最原始的 Logger,输出到 stderr,没有任何字段。此时还不能直接用,因为连时间戳都没有。.With()返回类型是zerolog.Context功能类似:进入“添加上下文字段”模式.Timestamp()返回类型也是zerolog.Context功能是在With()的上下文中添加一个默认的时间戳字段.Logger()返回类型又成了zerolog.Logger,作用是把配置好的 Context “提交”,生成一个带全局字段的新 Logger
注意:.With() 是 zerolog.Logger 的实例的方法,不是 zerolog.New 的“子方法”。
.With() 是zerolog.Logger 结构体上一个独立的方法,只不过它在链式写法里看起来像是接在 New() 后面的。
func New(w io.Writer) Logger // 返回的是 zerolog.Logger(值类型)
func (l Logger) With() Context // Logger 结构体的方法,返回 Context
func (c Context) Timestamp() Context // Context 的方法
func (c Context) Str(key string, val string) Context
// ... 其他字段方法
func (c Context) Logger() Logger // 最后再变回 Logger完整调用链的类型变化
zerolog.New(os.Stderr) // → zerolog.Logger
.With() // → zerolog.Context Logger 的方法
.Timestamp() // → zerolog.Context Context 的方法
.Str("app", "demo") // → zerolog.Context
.Logger() // → zerolog.Logger Context 的方法它把 Logger 切换到“字段配置模式”(Context),配置完再通过 .Logger() 切回真正的 Logger,这就是 zerolog 经典的三段式链式写法:
zerolog.New()
↓ 返回 Logger
Logger.With()
↓ 返回 Context
Context.Timestamp() → Context.Str() → ...
↓
Context.Logger()
↓ 返回新的 Logger(已带全局字段)日志级别
zerolog提供以下级别(从低到高)
| 方法 | 级别值 | 说明 | 推荐场景 |
|---|---|---|---|
| log.Trace() | -1 | 临时调试,生产必须关闭 | 详细流程追踪 |
| log.Debug() | 0 | 开发必开,生产可选 | 开发调试、接口入参出参 |
| log.Info() | 1 | 重要业务事件 | 启动完成、关键状态变更、业务里程碑 |
| log.Warn() | 2 | 可容忍、可降级的异常 | 库存不足、重试、超时但可恢复 |
| log.Error() | 3 | 需要关注、会上报的错误 | 业务失败、第三方调用失败 |
| log.Fatal() | 4 | 输出后立即 os.Exit(1) | 配置错误、数据库彻底不可用等无法继续运行 |
| log.Panic() | 5 | 输出后触发 panic(可被 recover) | 逻辑严重错误 |
| log.Log() | - | 不指定级别,由当前 Logger 决定 | 动态级别场景 |
基本用法示例
package main
import "github.com/rs/zerolog/log"
func main() {
log.Print("default msg") //debug
log.Info().Msg("服务器启动了") // 正常业务 Logger.Info
log.Warn().Msg("库存只剩 5 件了") // 警告
log.Error().Msg("支付失败了") // 错误
log.Fatal().Msg("数据库连不上,程序退出了") // 严重错误 → 直接退出 Logger.Fatal() + os.Exit(1)
// log.Panic().Msg("不应该发生的事发生了") // 严重错误 → panic(可被恢复) Logger.Panic() + panic(msg)
}输出
{"level":"debug","time":"2025-11-26T17:09:09+08:00","message":"这其实是 debug 级别"}
{"level":"info","time":"2025-11-26T17:09:09+08:00","message":"服务器已启动,监听 8080 端口"}
{"level":"warn","time":"2025-11-26T17:09:09+08:00","message":"库存只剩 5 件"}
{"level":"error","time":"2025-11-26T17:09:09+08:00","message":"支付失败,订单号 12345"}级别
三层级别分为
- gLevel全局级别
zerolog.SetGlobalLevel(级别数字或常量) 来设置全局级别
zerolog.GlobalLevel() 获取当前全局级别
- 每个Logger的级别
- 消息的级别
zerolog.SetGlobalLevel(zerolog.InfoLevel) // 全局最低门槛:Info级别
logger1 := log.With().Str("module", "A").Logger() // 没设置级别相当于继承全局 Info
logger2 := logger1.Level(zerolog.DebugLevel) // l2试图降到 Debug
logger3 := logger1.Level(zerolog.WarnLevel) // l3升到 Warn更严格
logger1.Debug().Msg("看不到") // Debug(-4) < Info(0) → 拦截
logger1.Info().Msg("能看到")
logger2.Debug().Msg("还是看不到!") // 虽然 logger2 想看 Debug,但全局是 Info 拦截
logger2.Info().Msg("能看到")
logger3.Info().Msg("看不到") // logger3 自己把门槛提到 Warn,Info 被拦
logger3.Warn().Msg("能看到")
特别注意,zerolog.SetGlobalLevel()设置的是全局变量gLevel ,它影响所有Logger。
一条日志消息能否打印 = 消息的级别 >= 当前 Logger 的级别 并且 >= 全局级别(SetGlobalLevel 设置的)
上下文
zerolog 默认把每条日志输出成一行 JSON,里面自带一些标准字段,可以随时往“上下文”里塞任意键值对,这些字段会自动出现在之后所有日志里。
默认自带的字段(全局生效)
// zerolog源代码
// Logger is the global logger.
var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()
{
"level": "info",
"time": "2025-12-02T10:11:12.345Z", // 自动加的时间戳
"message": "user login success"
}
通过 .With()往上下文里加字段
// 一次性加多个字段
log := zerolog.New(os.Stdout).With().
Str("app", "xxxx-service").
Int("pid", os.Getpid()).
Str("env", "production").
Timestamp(). // 让每条日志自动带时间戳(强烈推荐)
Logger()
log.Info().Msg("server started")
// 输出:{"level":"info","app":"xxxx-service","pid":1234,"env":"production","time":"2025-12-02T10:11:12Z","message":"server started"}这些字段一旦加进上下文,后续所有用这个 log 打的日志都会自动带着它们,无需每次重复写。
错误日志
package main
import (
"errors"
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
// 1. 时间用整数时间戳
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix // 秒级时间戳
// zerolog.TimeFieldFormat = "unixmilli" //毫秒时间戳
// 2. 把默认的 "error" 字段改成更熟悉的 "err"
zerolog.ErrorFieldName = "err" // 输出变成 "err":"xxx" 而不是 "error":"xxx"
// 3. 把 message 字段改成 msg,更简洁
// zerolog.MessageFieldName = "msg"
// 4. 加上时间戳、Caller(文件+行号)
log.Logger = log.Output(os.Stderr).With().
Timestamp().
Caller(). // 显示调用文件和行号
Logger()
// 错误日志的 5 种常见写法
errExample := errors.New("自定义的错误") // 标准库 errors.New
// 1. 最常见:只有错误,没有额外 message(推荐)
log.Error().Err(errExample).Send()
// 输出:{"level":"error","err":"自定义的错误","time":1733184000,"caller":"main.go:38"}
// 2. 有错误 + 额外说明(推荐)
log.Error().
Err(errExample).
Msg("用户登录失败") // Msg 必须放最后
// 输出:{"level":"error","err":"自定义的错误","time":1733184000,"message":"用户登录失败",...}
// 3. 没有错误对象,只有字符串
log.Error().Msg("用户登录失败") // 没有 Err() → 没有 err 字段
// 4. 包装了堆栈的错误
errWithStack := errors.New("数据库连接失败")
log.Error().
Err(errWithStack).
Stack(). // 打印调用栈(需要导入 github.com/pkg/errors 或 zerolog 自带的)
Str("db_host", "10.0.0.1").
Msg("数据库连接异常")
// 输出会多一个 "stack" 字段,包含完整调用栈
// 5. Fatal / Panic 级别(程序直接退出)
log.Fatal().
Err(errExample).
Msg("致命错误,服务即将退出") // Fatal 后会自动 os.Exit(1)
// log.Panic().Err(errExample).Msg("触发 panic") // 会 panic,但进程不退出
}
zerolog 的 .Err(err error) 方法签名
func (e *Event) Err(err error) *Event它只要求传入 error 接口类型,不管你是标准库 errors.New、fmt.Errorf、pkg/errors 还是自定义错误,只要实现了 error 接口就能传。
错误日志代码解析
log.Error().Err(errExample).Send()这条链子里到底发生了什么?
| 调用 | 返回类型 | 说明 |
|---|---|---|
log | zerolog.Logger | 全局 Logger 实例(值类型) |
.Error() | zerolog.Event | 创建一个level = error的日志事件对象(关键!) |
.Err(errExample) | zerolog.Event | 给这个事件追加一个字段:{"err":"自定义的错误"}(键名可通过zerolog.ErrorFieldName改) |
.Send() | (无返回值) | 把这个事件真正提交、序列化、写出 |
// 源码
// Logger 是一个 struct(值类型)
type Logger struct {
// ... 很多字段
}
// Error 是 *Logger 的方法(是指针接收者)
func (l *Logger) Error() *Event {
return l.log(ErrorLevel)
}log // 类型:zerolog.Logger(值类型)
log.Error // 实际上是 (&log).Error 的语法糖(Go 自动取地址)
// 所以真正调用的是 *Logger.Error()
// 返回值类型:*zerolog.Event
为什么要有 .Send() ?不调用 Msg("") 行不行?
erolog 故意把有 message 的日志和只有错误、没有 message 的日志分开处理,提供了两种终结方式:
err := errors.New("连接超时")
// 写法 A:用 Msg
log.Error().Err(err).Msg("数据库操作失败")
// → {"level":"error","err":"连接超时","message":"数据库操作失败"}
// 写法 B:用 Send(推荐!更简洁)
log.Error().Err(err).Send()
// → {"level":"error","err":"连接超时"} 没有多余的 message因为错误内容已经足够说明问题了,不想多一个重复的 message 字段污染日志。
全局Logger
// 全局Logger定义如下
var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()
可以覆盖全局Logger,一般不建议覆盖全局Logger。
// 全局日期格式
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
zerolog.TimeFieldFormat = "2006/01/02 15:04:05 -0700"
// With()创建一个全局Logger的子logger
log.Logger = log.With().Str("app", "xxxx").Logger() // 覆盖了全局Logger
log.Info().Send() // {"level":"info","app":"xxxx","time":1223947070}
底层实现
在 zerolog 的设计中,log.Info().Send() 本质上是 全局 Logger 实例的快捷调用方式,其完整形式是 log.Logger.Info().Send()
zerolog/log 包中预定义了全局变量 Logger,并为其实现了包级快捷方法(如 Info())。当调用 log.Info() 时,实际等价于 log.Logger.Info()
例如,在 log.go 源码中可见:
func Info() *zerolog.Event {
return Logger.Info() // 直接返回全局Logger的Info事件
}
因此,log.Info().Send() 本质是通过全局 Logger 记录日志。
覆盖全局 Logger 的影响
当执行 log.Logger = log.With().Str("app", "xxxx").Logger() 时,log.Logger 被替换为新的子 Logger。
此后所有 log.XXX() 调用(包括 log.Info())均基于此新实例
子 Logger 的继承性
log.With() 创建的子 Logger 会继承原 Logger 的配置(如输出目标、日志级别),并添加额外字段(如 "app":"xxxx")。覆盖后,全局所有 log.XXX() 调用均携带这些字段
// 覆盖前:log.Logger 为默认实例
// 覆盖后:log.Logger 变为带 "app" 字段的子实例
log.Info().Send() // 输出 {"level":"info","app":"xxxx","time":...}
谨慎覆盖全局 Logger
覆盖 log.Logger 会影响整个包的日志行为。若需局部定制,建议创建子 Logger 而非修改全局实例:
localLogger := log.With().Str("module", "auth").Logger()
localLogger.Info().Msg("局部日志") // 不影响全局
需要明确日志作用域
自定义Logger
基于全局 Logger创建子 Logger
log.With() 基于全局 Logger 创建子 Logger,继承全局配置(如时间格式、输出目标)并添加新字段
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix // 全局时间格式设为 Unix 时间戳
logger := log.With(). // 基于全局 Logger 创建子 Logger
Str("app", "bbb"). // 添加固定字段 "app"
Caller(). // 添加调用位置信息(文件名+行号)
Logger() // 返回新 Logger 实例
logger.Info().Send() // 输出:{"level":"info","app":"bbb","caller":"main.go:10","time":1700000000}
log.Info().Send() // 输出:{"level":"info","time":1700000000}(全局 Logger 不受影响)
Caller() 添加 caller 字段,记录日志调用处的文件名和行号,便于调试
独立创建新 Logger
zerolog.New(os.Stdout) 创建全新 Logger,与全局 Logger 完全隔离
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
logger := zerolog.New(os.Stdout). // 新建独立 Logger(输出到 os.Stdout)
With(). // 链式调用添加字段
Str("app", "bbb").
Caller().
Logger().
Level(zerolog.ErrorLevel) // 设置日志级别为 Error(仅输出 Error 及以上级别)
fmt.Println(logger.GetLevel()) // 输出:3(zerolog.ErrorLevel 的枚举值)
logger.Info().Send() // 无输出(Info 级别 < Error,被过滤)
logger.Error().Send() // 输出:{"level":"error","app":"bbb","caller":"main.go:15","time":1700000000}
log.Info().Send() // 输出:{"level":"info","time":1700000000}(全局 Logger 独立)
.Level(zerolog.ErrorLevel) 设置该 Logger 仅记录 Error/Fatal/Panic 级别日志,Info 被静默丢弃
独立 Logger 输出至 os.Stdout,而全局 Logger 默认输出到 os.Stderr
写日志文件
多输出核心机制MultiLevelWriter
使用zerolog.MultiLevelWriter 实现日志分流,接收多个 io.Writer 并将日志同时写入所有目标,其内部通过类型断言适配不同写入器:
multi := zerolog.MultiLevelWriter(f, os.Stdout) // 文件 + 控制台日志事件会同时发送到文件和控制台
f *os.File:日志文件写入器
os.Stdout:控制台输出
同时输出到文件与控制台
package main
import (
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log" // 全局logger 此设置对全局生效,所有后续创建的 Logger 均继承该格式
)
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix //Unix 时间戳
// 1. 创建/追加日志文件(O_APPEND 模式)
f, err := os.OpenFile("o:/my.log", os.O_CREATE|os.O_APPEND, os.ModePerm)
if err != nil {
log.Panic().Err(err).Send() // 内部调用 panic 终止程序
}
defer f.Close()
// 2. 创建多路写入器:文件 + 控制台
multi := zerolog.MultiLevelWriter(f, os.Stdout)
// 3. 创建新 Logger(非全局),添加时间戳字段
logger := zerolog.New(multi).With().Timestamp().Logger()
// 4. 输出日志(同时写入文件和控制台)
logger.Info().Msg("日志兵分两路,去控制台stdout,还去日志文件")
}
//文件内容(my.log):
//{"level":"info","time":1700000000,"message":"日志兵分两路..."}
仅输出到文件
// 直接基于文件创建 Logger(无需 MultiLevelWriter)
fileLogger := zerolog.New(f).With().Timestamp().Logger()
fileLogger.Info().Msg("仅写入文件的日志")
//zerolog.New(f).With().Timestamp().Logger()
