关于go:go-logger-不侵入业务代码-用slog-替换-zap-并实现-callerSkip

疾速体验

以下是 我的项目中 曾经用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 简略示例:

        slog.Info("finished", "key", "value")
    slog.Debug("finished", "key1", "value1")

以下是打印日志 默认slog 输入级别是info以上,所以debug是打印不进去.

2023/09/08 00:27:24 INFO finished key=value

json格式化,设置日志等级,并打印调用函数和文件

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"}

原有 logger zap实现

原有的我的项目曾经实现了一套logger,应用zap log 实现接口

原有代码示例

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,因代码量太大,以下是伪代码,次要提供思路,为下文 slog 无侵入替换zap 预热

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 应用日志

    logger.Debugw("msg1", "k1", "v1") // debug
    logger.SetLevel(DebugLevel)      //设置等级
    logger.Debugw("msg3", "k3", "v3") 
    newLogger := logger.With("name", "song")
    logger.Infow("msg4", "k4", "v4")  // print

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 之类

slog 实现 callerSkip 性能

slog 的 addsource 参数 会打印文件名和行号,但 并不能像 zap 那样反对 callerSkip,也就是说 如果将 slog 封装在 logger 目录的log.go 文件下,应用logger进行打印,展现的文件会一只是log.go

看了 slog 的源码,其实slog 应用了 runtime.Callers 在外部实现了 callerSkip 性能,然而没有对外裸露 callerSkip 参数

我就封装了 ApppendFileLine 办法,应用 runtime.Callers 获取到 文件名 和 行号,减少 file 和 line 的key value到日志

可能会有性能问题,心愿slog能对外提供一个 callerSkip 参数

    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)

阐明

文章中贴的代码不多,次要提供思路,尽管省略了一些办法和 全局logger的实现形式,也写了两个多小时

如要查看logger实现细节,可查看 在文章结尾 疾速体验 援用的包 github.com/webws/go-moda/logger

也能够间接看下我这个 仓库 go-moda 里应用 slog 和 zap 的封装

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理