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 · 2749 次点击
    这是一个创建于 476 天前的主题,其中的信息可能已经有所发展或是发生改变。

    快速体验

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

    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 一直传递下去,有什么好的方式吗。
    jiangwei2222
        17
    jiangwei2222  
       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   ·   实用小工具   ·   2705 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 12:55 · PVG 20:55 · LAX 04:55 · JFK 07:55
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.