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

28次阅读

共计 5895 个字符,预计需要花费 15 分钟才能阅读完成。

疾速体验

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

正文完
 0