go 框架 logger 不侵入业务代码 用 slog 替换 zap - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
websong188
V2EX    Go 编程语言

go 框架 logger 不侵入业务码 用 slog 替换 zap

  •  
  •   websong188 2023-09-08 10:45:21 +08:00 3470 次点击
    这是一个创建于 812 天前的主题,其中的信息可能已经有所发展或是发生改变。

    快速体验

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

    25 条回复    2023-09-08 20:05:40 +08:00
    pennai
        1
    pennai  
       2023-09-08 11:13:39 +08:00
    不侵入业务代码是指啥?看下来也没发现怎么不侵入
    wwek
        2
    wwek  
       2023-09-08 11:16:12 +08:00
    用自带的能满足需求的情况下。就不用第三方
    wwek
        3
    wwek  
       2023-09-08 11:16:19 +08:00
    感谢分享
    lilei2023
        4
    lilei2023  
       2023-09-08 11:21:04 +08:00
    为啥要换,zap 感觉用起来还行啊
    mikurasa
        5
    mikurasa  
       2023-09-08 11:25:12 +08:00   2
    感觉这个库的 API 没有标准库 log 的好用
    我现在用的 zerolog 封装的日志库
    func log.Infof(format string, a ...interface{})
    项目里的 API 非常好用跟打印一样
    mainjzb
        6
    mainjzb  
       2023-09-08 11:29:26 +08:00
    前排提示:1.20 是最后一个支持 win7 的版本 (逃
    websong188
        7
    websong188  
    OP
       2023-09-08 11:45:54 +08:00
    @pennai 我理解的不侵入是在自己项目里引用 logger 包,那个 logger 包 内部实现 是使用 zap,现在改成了 slog
    使用方的业务代码 打印日志依然可以用 原来的方法 比如 logger.infow
    websong188
        8
    websong188  
    OP
       2023-09-08 11:50:06 +08:00
    @lilei2023 zap 其实用起来很行,我在 slog 替换的时候发现,slog 没法 像 zap 那样支持 callerSkip,目前自己实现了一个.
    不知道后面 slog 会不会扩展
    zeromake
        9
    zeromake  
       2023-09-08 11:50:06 +08:00
    @lilei2023 #4 应该指的是 zap 的 zapcore.Field 这些导入,应该是不希望业务里强制导入一个 zap 库,因为有可能出现 zap.Field 格式改动导致所有的业务代码失效(虽然这种情况应该不会发生),什么你不用 zapcore.Field ?那也用不着用 zap 了……
    websong188
        10
    websong188  
    OP
       2023-09-08 12:03:46 +08:00
    @zeromake zapcore.Field 指的是 zap 的 输出字段 key 吗,zap.Config.EncoderConfig 应该是可以指定 key
    这是我之前集成 zap 的代码,不知道是不是你担心的点 https://github.com/webws/go-moda/blob/main/logger/zap_log.go
    websong188
        11
    websong188  
    OP
       2023-09-08 12:09:05 +08:00
    @wwek 是的,没有需求不要制造需求.但自带的 log,用起来是有点一言难尽哦
    zeromake
        12
    zeromake  
       2023-09-08 12:10:55 +08:00
    @websong188 不是说的这个,说的是 zap.String 这些不好直接入侵到业务代码里,你这边不是直接用 any 遮蔽了吗
    logger.Info("failed to fetch URL",
    zap.String("url", url),
    zap.Int("attempt", 3),
    zap.Duration("backoff", time.Second),
    )
    xiaocaiji111
        13
    xiaocaiji111  
       2023-09-08 13:52:58 +08:00
    自己封装一层,业务里使用自己封装的接口,底层 log 想换就换,可以放心用不会影响业务。
    ikaros
        14
    ikaros  
       2023-09-08 13:56:33 +08:00
    zap 的高性能代价就是用 field 强类型换的吧,全部用 any 的话应该性能也和其他的没啥区别,另外我也觉得 zap 用着挺好
    dacapoday
        15
    dacapoday  
       2023-09-08 14:10:08 +08:00
    @mikurasa 非常赞同,而且即使在 zap 的 benchmark 里,zerolog 也是最快的。
    linauror
        16
    linauror  
       2023-09-08 14:13:25 +08:00
    老哥们,借楼问一下。如果想要那种一个请求下来,所有记录的日志都可以记下某一个指定的追踪码,日志中方便查询是同一个请求产生的,不管是在 service, controller 或者 helper 之类的地方都可以记录,但是又不想把 ctx 一直传递下去,有什么好的方式吗。
    AutumnVerse
        17
    AutumnVerse  
       2023-09-08 14:22:09 +08:00   1
    @linauror php 可以,Go 的话必须得有一个变量传下去,无论是 ctx 还是啥。
    monkeyWie
        18
    monkeyWie  
       2023-09-08 14:22:45 +08:00   1
    slog 好像还是不能像 sl4j 一样统一日志门面吧?每个第三方库都一套日志系统真的挺恶心的
    wfhtqp
        19
    wfhtqp  
       2023-09-08 14:25:34 +08:00   1
    没有 ctx 办不了,有歪门获取 gid ,但是需要动源码
    virusdefender
        20
    virusdefender  
       2023-09-08 14:28:56 +08:00   1
    @linauror 可以用 ctx ,但是 slog 默认有没有输出,得自己处理,我写了一个小库 https://github.com/virusdefender/slogctx
    linauror
        21
    linauror  
       2023-09-08 14:29:48 +08:00
    @jiangwei2222 @wfhtqp 目前通过写入 GID 来分辨,但是时间范围大或者请求大的时候,还是会重复的
    virusdefender
        22
    virusdefender  
       2023-09-08 14:30:12 +08:00
    好吧,我理解的不太对,其实传 ctx 挺好的 (狗头
    linauror
        23
    linauror  
       2023-09-08 14:31:13 +08:00
    @virusdefender 主要是用 ctx 的话,感觉不够优雅,不然每个 service 方法都要传入 ctx 了
    mikurasa
        24
    mikurasa  
       2023-09-08 14:42:12 +08:00
    @dacapoday 哈哈哈我只是感觉像强字段类型的 API 有点恶心,像这样替换标准 log 库也很简单 暴露 API 简单 日志性能我感觉不是并发特别高不是关注点
    websong188
        25
    websong188  
    OP
       2023-09-08 20:05:40 +08:00
    @zeromake 嗯是的,
    本文说的无侵入,更多的的情况是指 原项目使用的 logger 为一个抽象接口,新增的 slog 实现接口就行,对外暴露接口方法

    如果 有项目不想 强引入 第三方日志包,也可以用本文 logger 类似 的思路 进行封装
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3530 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 31ms UTC 04:17 PVG 12:17 LAX 20:17 JFK 23:17
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86