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

254 天前
 websong188

快速体验

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

2266 次点击
所在节点    Go 编程语言
25 条回复
linauror
254 天前
@jiangwei2222 @wfhtqp 目前通过写入 GID 来分辨,但是时间范围大或者请求大的时候,还是会重复的
virusdefender
254 天前
好吧,我理解的不太对,其实传 ctx 挺好的 (狗头
linauror
254 天前
@virusdefender 主要是用 ctx 的话,感觉不够优雅,不然每个 service 方法都要传入 ctx 了
mikurasa
254 天前
@dacapoday 哈哈哈我只是感觉像强字段类型的 API 有点恶心,像这样替换标准 log 库也很简单 暴露 API 简单 日志性能我感觉不是并发特别高不是关注点
websong188
254 天前
@zeromake 嗯是的,
本文说的无侵入,更多的的情况是指 原项目使用的 logger 为一个抽象接口,新增的 slog 实现接口就行,对外暴露接口方法

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

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/971958

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX