go 框架 logger 不侵入业务代码 用 slog 替换 zap
快速体验
以下是 项目中 已经用 slog 替换 zap 后的 logger 使用方法,与替换前使用方式相同,无任何感知
package main
import "github.com/webws/go-moda/logger"
func main() {
// 格式化打印 {"time":"2023-09-08T01:25:21.313463+08:00","level":"INFO","msg":"info hello slog","key":"value","file":"/Users/xxx/w/pro/go-moda/example/logger/main.go","line":6}
logger.Infow("info hello slog", "key", "value") // 打印 json
logger.Debugw("debug hello slog", "key", "value") // 不展示
logger.SetLevel(logger.DebugLevel) // 设置等级
logger.Debugw("debug hello slog", "key", "value") // 设置了等级之后展示 debug
// with
newLog := logger.With("newkey", "newValue")
newLog.Debugw("new hello slog") // 会打印 newkey:newValue
logger.Debugw("old hello slog") // 不会打印 newkey:newValue
}
slog 基础使用
Go 1.21 版本中 将 golang.org/x/exp/slog 引入了 go 标准库 路径为 log/slog 。
新项目的 如果不使用第三方包,可以直接用 slog 当你的 logger
slog 简单示例:
默认 输出级别是 info 以上,所以 debug 是打印不出来.
import "log/slog"
func main() {
slog.Info("finished", "key", "value")
slog.Debug("finished", "key", "value")
}
输出
2023/09/08 00:27:24 INFO finished key=value
slog 格式化
HandlerOptions Level:设置日志等级 AddSource:打印文件相关信息
func main() {
opts := &slog.HandlerOptions{AddSource: true, Level: slog.LevelInfo}
logger := slog.New(slog.NewJSONHandler(os.Stdout, opts))
logger.Info("finished", "key", "value")
}
输出
{"time":"2023-09-08T00:34:22.035962+08:00","level":"INFO","source":{"function":"callvis/slog.TestLogJsonHandler","file":"/Users/websong/w/pro/go-note/slog/main_test.go","line":39},"msg":"finished","key":"value"}
slog 切换日志等级
看 slog 源码 HandlerOptions 的 Level 是一个 interface,slog 自带的 slog.LevelVar 实现了这个 interface,也可以自己定义实现 下面是部分源码
type Leveler interface {
Level() Level
}
type LevelVar struct {
val atomic.Int64
}
// Level returns v's level.
func (v *LevelVar) Level() Level {
return Level(int(v.val.Load()))
}
// Set sets v's level to l.
func (v *LevelVar) Set(l Level) {
v.val.Store(int64(l))
}
通过 slog.LevelVar 设置 debug 等级后,第二次的 debug 日志是可以打印出来
func main() {
levelVar := &slog.LevelVar{}
levelVar.Set(slog.LevelInfo)
opts := &slog.HandlerOptions{AddSource: true, Level: levelVar}
logger := slog.New(slog.NewJSONHandler(os.Stdout, opts))
logger.Info("finished", "key", "value")
levelVar.Set(slog.LevelDebug)
logger.Debug("finished", "key", "value")
}
想要实现 文章开头 通过 logger.SetLevel(logger.DebugLevel) 快速切换等级,可以选择将 slog.Logger 与 slog.LevelVar 封装到同一结构,比如
type SlogLogger struct {
logger *slog.Logger
level *slog.LevelVar
}
下文 slog 替换 zap 有详细代码体现
原有 logger zap 实现
原有项目已经实现了一套 logger,使用 zap log 以下代码都是在 logger 包下 github.com/webws/go-moda/logger
原 zap 代码
logger interface LoggerInterface
package logger
type LoggerInterface interface {
Debugw(msg string, keysAndValues ...interface{})
Infow(msg string, keysAndValues ...interface{})
Errorw(msg string, keysAndValues ...interface{})
Fatalw(msg string, keysAndValues ...interface{})
SetLevel(level Level)
With(keyValues ...interface{}) LoggerInterface
}
zap log 实现 LoggerInterface
type ZapSugaredLogger struct {
logger *zap.SugaredLogger
zapConfig *zap.Config
}
func buildZapLog(level Level) LoggerInterface {
encoderConfig := zapcore.EncoderConfig{
TimeKey: "ts",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
zapConfig := &zap.Config{
Level: zap.NewAtomicLevelAt(zapcore.Level(level)),
Development: true,
DisableCaller: false,
DisableStacktrace: true,
Sampling: &zap.SamplingConfig{Initial: 100, Thereafter: 100},
Encoding: "json",
EncoderConfig: encoderConfig,
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
l, err := zapConfig.Build(zap.AddCallerSkip(2))
if err != nil {
fmt.Printf("zap build logger fail err=%v", err)
return nil
}
return &ZapSugaredLogger{
logger: l.Sugar(),
zapConfig: zapConfig,
}
func (l *ZapSugaredLogger) Debugw(msg string, keysAndValues ...interface{}) {
l.logger.Debugw(msg, keysAndValues...)
}
func (l *ZapSugaredLogger) Errorw(msg string, keysAndValues ...interface{}) {
l.logger.Errorw(msg, keysAndValues...)
}
// ...省略 info 之类其他实现接口的方法
}
全局初始化 logger,因代码量太大,以下是伪代码,主要提供思路
package logger
// 全局 log,也可以单独 NewLogger 获取新的实例
var globalog = newlogger(DebugLevel)
func newlogger(level Level) *Logger {
l := &Logger{logger: buildZapLog(level)}
return l
}
func Infow(msg string, keysAndValues ...interface{}) {
globalog.logger.Infow(msg, keysAndValues...)
}
// ...省略其他全局方法,比如 DebugW 之类
在项目中通过 如下使用 logger
import "github.com/webws/go-moda/logger"
func main() {
logger.Infow("hello", "key", "value") // 打印 json
}
slog 不侵入业务 替换 zap
logger interface 接口保持不变
slog 实现 代码
package logger
import (
"log/slog"
"os"
"runtime"
)
var _ LoggerInterface = (*SlogLogger)(nil)
type SlogLogger struct {
logger *slog.Logger
level *slog.LevelVar
// true 代表使用 slog 打印文件路径,false 会使用自定的方法给日志 增加字段 file line
addSource bool
}
// newSlog
func newSlog(level Level, addSource bool) LoggerInterface {
levelVar := &slog.LevelVar{}
levelVar.Set(slog.LevelInfo)
opts := &slog.HandlerOptions{AddSource: addSource, Level: levelVar}
logger := slog.New(slog.NewJSONHandler(os.Stdout, opts))
return &SlogLogger{
logger: logger,
level: levelVar,
}
}
func (l *SlogLogger) Fatalw(msg string, keysAndValues ...interface{}) {
keysAndValues = l.ApppendFileLine(keysAndValues...)
l.logger.Error(msg, keysAndValues...)
os.Exit(1)
}
func (l *SlogLogger) Infow(msg string, keysAndValues ...interface{}) {
keysAndValues = l.ApppendFileLine(keysAndValues...)
l.logger.Info(msg, keysAndValues...)
}
// 省略继承接口的其他方法 DebugW 之类的
func (l *SlogLogger) SetLevel(level Level) {
zapLevelToSlogLevel(level)
l.level.Set(slog.Level(zapLevelToSlogLevel(level)))
}
//
func (l *SlogLogger) With(keyValues ...interface{}) LoggerInterface {
newLog := l.logger.With(keyValues...)
return &SlogLogger{
logger: newLog,
level: l.level,
}
}
// ApppendFileLine 获取调用方的文件和文件号
// slog 原生 暂不支持 callerSkip,使用此函数啃根会有性能问题,最好等 slog 提供 CallerSkip 的参数
func (l *SlogLogger) ApppendFileLine(keyValues ...interface{}) []interface{} {
l.addSource = false
if !l.addSource {
var pc uintptr
var pcs [1]uintptr
// skip [runtime.Callers, this function, this function's caller]
runtime.Callers(4, pcs[:])
pc = pcs[0]
fs := runtime.CallersFrames([]uintptr{pc})
f, _ := fs.Next()
keyValues = append(keyValues, "file", f.File, "line", f.Line)
return keyValues
}
return keyValues
}
全局初始化 logger,以下伪代码
package logger
// 全局 log,也可以单独 NewLogger 获取新的实例
var globalog = newlogger(DebugLevel)
func newlogger(level Level) *Logger {
l := &Logger{logger: newSlog(level, false)}
return l
}
func Infow(msg string, keysAndValues ...interface{}) {
globalog.logger.Infow(msg, keysAndValues...)
}
// ...省略其他全局方法,比如 DebugW 之类
一样可以 通过 如下使用 logger,与使用 zap 时一样
import "github.com/webws/go-moda/logger"
func main() {
logger.Infow("hello", "key", "value") // 打印 json
}
slog 实现 callerSkip 功能
slog 的 addsource 参数 会打印文件名和行号,但 并不能像 zap 那样支持 callerSkip,也就是说 如果将 slog 封装在 logger 目录的 log.go 文件下,使用 logger 进行打印,展示的文件会一只是 log.go
看了 slog 的源码, 使用了 runtime.Callers 在内部实现了 callerSkip 功能,但是没有对外暴露 callerSkip 参数
可以看我上面代码 自己封装了一个方法: ApppendFileLine, 使用 runtime.Callers 获取到 文件名 和 行号,增加 file 和 line 的 key value 到日志
可能会有性能问题,希望 slog 能对外提供一个 callerSkip 参数
说明
文章中贴的代码不多,主要提供思路,虽然省略了一些方法和 全局 logger 的实现方式
如要查看 logger 实现细节,可查看 在文章开头 快速体验 引用的包 github.com/webws/go-moda/logger
也可以直接看下我这个 仓库 go-moda 里使用 slog 和 zap 的封装
不侵入业务代码是指啥?看下来也没发现怎么不侵入
用自带的能满足需求的情况下。就不用第三方
感谢分享
为啥要换,zap 感觉用起来还行啊
感觉这个库的 API 没有标准库 log 的好用我现在用的 zerolog 封装的日志库func log.Infof(format string, a ...interface{})项目里的 API 非常好用跟打印一样
前排提示:1.20 是最后一个支持 win7 的版本 (逃
我理解的不侵入是在自己项目里引用 logger 包,那个 logger 包 内部实现 是使用 zap,现在改成了 slog使用方的业务代码 打印日志依然可以用 原来的方法 比如 logger.infow
zap 其实用起来很行,我在 slog 替换的时候发现,slog 没法 像 zap 那样支持 callerSkip,目前自己实现了一个.不知道后面 slog 会不会扩展
#4 应该指的是 zap 的 zapcore.Field 这些导入,应该是不希望业务里强制导入一个 zap 库,因为有可能出现 zap.Field 格式改动导致所有的业务代码失效(虽然这种情况应该不会发生),什么你不用 zapcore.Field ?那也用不着用 zap 了……
zapcore.Field 指的是 zap 的 输出字段 key 吗,zap.Config.EncoderConfig 应该是可以指定 key这是我之前集成 zap 的代码,不知道是不是你担心的点 github.com/webws/go-moda/blob/main/logger/zap_log.go
是的,没有需求不要制造需求.但自带的 log,用起来是有点一言难尽哦
不是说的这个,说的是 zap.String 这些不好直接入侵到业务代码里,你这边不是直接用 any 遮蔽了吗logger.Info("failed to fetch URL", zap.String("url", url), zap.Int("attempt", 3), zap.Duration("backoff", time.Second),)
自己封装一层,业务里使用自己封装的接口,底层 log 想换就换,可以放心用不会影响业务。
zap 的高性能代价就是用 field 强类型换的吧,全部用 any 的话应该性能也和其他的没啥区别,另外我也觉得 zap 用着挺好
非常赞同,而且即使在 zap 的 benchmark 里,zerolog 也是最快的。
老哥们,借楼问一下。如果想要那种一个请求下来,所有记录的日志都可以记下某一个指定的追踪码,日志中方便查询是同一个请求产生的,不管是在 service, controller 或者 helper 之类的地方都可以记录,但是又不想把 ctx 一直传递下去,有什么好的方式吗。
php 可以,Go 的话必须得有一个变量传下去,无论是 ctx 还是啥。
slog 好像还是不能像 sl4j 一样统一日志门面吧?每个第三方库都一套日志系统真的挺恶心的
没有 ctx 办不了,有歪门获取 gid ,但是需要动源码
可以用 ctx ,但是 slog 默认有没有输出,得自己处理,我写了一个小库 github.com/virusdefender/slogctx
目前通过写入 GID 来分辨,但是时间范围大或者请求大的时候,还是会重复的
好吧,我理解的不太对,其实传 ctx 挺好的 (狗头
主要是用 ctx 的话,感觉不够优雅,不然每个 service 方法都要传入 ctx 了
哈哈哈我只是感觉像强字段类型的 API 有点恶心,像这样替换标准 log 库也很简单 暴露 API 简单 日志性能我感觉不是并发特别高不是关注点
嗯是的,本文说的无侵入,更多的的情况是指 原项目使用的 logger 为一个抽象接口,新增的 slog 实现接口就行,对外暴露接口方法如果 有项目不想 强引入 第三方日志包,也可以用本文 logger 类似 的思路 进行封装
突然有个困惑。 感觉现在写代码越来越依靠 AI 辅助生成,跟 AI 不断对话,得到基本框架,然后自己在做一些微调。导致现在拿到一些需求之后,反而不知道该如何自己写了。 这种情况…
Win11 史诗级更新,状态栏窗口不再合并。快去尝试。 2023-适用于 Windows 11 Version 22H2 的 09 累积更新,适合基于 x64 的系统 (KB5…
之前对华为系统的产品和服务总是有种莫名的感觉,不能说喜欢还是不喜欢,总之就是看得太多自媒体在讲多么牛~多么牛~~这反而给的感觉宣传得过头了。昨天 2024-09-25 ,想注册…