Golang Go语言中框架 logger 不侵入业务代码 用 slog 替换 zap

发布于 1周前 作者 vueper 来自 Go语言

快速体验

以下是 项目中 已经用 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 的封装


Golang Go语言中框架 logger 不侵入业务代码 用 slog 替换 zap

更多关于Golang Go语言中框架 logger 不侵入业务代码 用 slog 替换 zap的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

26 回复

不侵入业务代码是指啥?看下来也没发现怎么不侵入

更多关于Golang Go语言中框架 logger 不侵入业务代码 用 slog 替换 zap的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


用自带的能满足需求的情况下。就不用第三方

为啥要换,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 的代码,不知道是不是你担心的点 https://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 默认有没有输出,得自己处理,我写了一个小库 https://github.com/virusdefender/slogctx

目前通过写入 GID 来分辨,但是时间范围大或者请求大的时候,还是会重复的

好吧,我理解的不太对,其实传 ctx 挺好的 (狗头

主要是用 ctx 的话,感觉不够优雅,不然每个 service 方法都要传入 ctx 了

哈哈哈我只是感觉像强字段类型的 API 有点恶心,像这样替换标准 log 库也很简单 暴露 API 简单 日志性能我感觉不是并发特别高不是关注点

嗯是的,
本文说的无侵入,更多的的情况是指 原项目使用的 logger 为一个抽象接口,新增的 slog 实现接口就行,对外暴露接口方法

如果 有项目不想 强引入 第三方日志包,也可以用本文 logger 类似 的思路 进行封装

在Go语言中,日志记录是开发过程中不可或缺的一部分,而选择一个合适的日志框架更是至关重要。对于您提到的需求——使用slog替换zap,同时要求框架的logger不侵入业务代码,这里有一些建议:

  1. 抽象日志接口:首先,定义一个通用的日志接口,该接口应包含所有常用的日志方法,如Info、Warn、Error等。然后,无论是zap还是slog,都可以实现这个接口。这样,业务代码只需依赖这个接口,而不关心具体的日志实现。

  2. 依赖注入:通过依赖注入的方式,将日志实现注入到业务代码中。这可以通过构造函数注入、方法注入或全局变量注入等方式实现。这样,在需要替换日志框架时,只需修改注入的实现即可,无需改动业务代码。

  3. 配置文件控制:将日志框架的选择和配置放入配置文件中。在程序启动时,根据配置文件加载相应的日志实现。这样,可以方便地切换日志框架,而无需修改代码。

  4. 使用适配器模式:如果slogzap的API差异较大,可以考虑使用适配器模式,为其中一个框架编写一个适配器,使其符合另一个框架的接口。这样,可以在不改变业务代码的情况下,实现日志框架的替换。

综上所述,通过定义日志接口、依赖注入、配置文件控制和适配器模式等方法,可以有效地实现日志框架的替换,同时保证业务代码的不侵入性。

回到顶部