SoFunction
Updated on 2025-03-05

go logger does not invade business code and replace zap with slog and implement callerSkip detailed explanation

Quick experience

The following is the usage method of logger after replacing zap with slog in the project. It is the same as before replacing, without any perception.

package main
import "/webws/go-moda/logger"
func main() {
    // Format printing {"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/","line":6}    ("info hello slog", "key", "value")   // Print json    ("debug hello slog", "key", "value") // Not displayed    ()                // Set level    ("debug hello slog", "key", "value") // Show debug after setting the level    // with
    newLog := ("newkey", "newValue")
    ("new hello slog") // Will print newkey:newValue    ("old hello slog") // Will not print newkey:newValue}

Basic use of slog

In Go version 1.21, /x/exp/slog was introduced into the go standard library. The path is log/slog.

If you do not use third-party packages, you can use slog as your logger directly

slog simple example

The default output level is info or above, so debug cannot be printed.

import "log/slog"
func main() {
    ("finished", "key", "value")
    ("finished", "key", "value")
}

Output

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

slog formatting

HandlerOptions Level: Set log level AddSource: Print file-related information

func main() {
    opts := &{AddSource: true, Level: }
    logger := ((, opts))
    ("finished", "key", "value")
}

Output

{"time":"2023-09-08T00:34:22.035962+08:00","level":"INFO","source":{"function":"callvis/","file":"/Users/websong/w/pro/go-note/slog/main_test.go","line":39},"msg":"finished","key":"value"}

slog switch log level

Look at the slog source code. The Level of HandlerOptions is an interface. Slog comes with this interface. You can also define it yourself. The following are some source codes.

type Leveler interface {
    Level() Level
}
type LevelVar struct {
    val atomic.Int64
}
// Level returns v's level.
func (v *LevelVar) Level() Level {
    return Level(int(()))
}
// Set sets v's level to l.
func (v *LevelVar) Set(l Level) {
    (int64(l))
}

After setting the debug level, the second debug log can be printed out

func main() {
    levelVar := &{}
    ()
    opts := &{AddSource: true, Level: levelVar}
    logger := ((, opts))
    ("finished", "key", "value")
    ()
    ("finished", "key", "value")
}

To implement the beginning of the article, you can choose to encapsulate the same structure as the article, for example

type SlogLogger struct {
    logger *
    level  *
}

The following slog replaces zap with detailed code

Original logger zap implementation

The original project has implemented a set of loggers, and the following codes are all under the logger package

Original zap code

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 implementation LoggerInterface

type ZapSugaredLogger struct {
    logger    *
    zapConfig *
}
func buildZapLog(level Level) LoggerInterface {
    encoderConfig := {
        TimeKey:        "ts",
        LevelKey:       "level",
        NameKey:        "logger",
        CallerKey:      "caller",
        MessageKey:     "msg",
        StacktraceKey:  "stacktrace",
        LineEnding:     ,
        EncodeDuration: ,
        EncodeTime:     zapcore.ISO8601TimeEncoder,
        EncodeLevel:    ,
        EncodeCaller:   ,
    }
    zapConfig := &{
        Level:             ((level)),
        Development:       true,
        DisableCaller:     false,
        DisableStacktrace: true,
        Sampling:          &{Initial: 100, Thereafter: 100},
        Encoding:          "json",
        EncoderConfig:     encoderConfig,
        OutputPaths:       []string{"stderr"},
        ErrorOutputPaths:  []string{"stderr"},
    }
    l, err := ((2))
    if err != nil {
        ("zap build logger fail err=%v", err)
        return nil
    }
    return &ZapSugaredLogger{
        logger:    (),
        zapConfig: zapConfig,
    }
    func (l *ZapSugaredLogger) Debugw(msg string, keysAndValues ...interface{}) {
    (msg, keysAndValues...)
    }
    func (l *ZapSugaredLogger) Errorw(msg string, keysAndValues ...interface{}) {
        (msg, keysAndValues...)
    }
    // ...Omit other methods to implement interfaces such as info}

Global initialization logger, because the code volume is too large, the following is pseudo-code, which mainly provides ideas

package logger
// Global log, you can also get new instances with NewLogger alonevar globalog = newlogger(DebugLevel)
func newlogger(level Level) *Logger {
    l := &Logger{logger: buildZapLog(level)}
    return l
}
func Infow(msg string, keysAndValues ...interface{}) {
    (msg, keysAndValues...)
}
// ...Omit other global methods,for exampleDebugW What's more

In the project, use logger as follows

import "/webws/go-moda/logger"
func main() {
    ("hello", "key", "value")   // Print json}

slog does not intrude into business Replace zap

The logger interface remains unchanged

slog implementation code

package logger
import (
    "log/slog"
    "os"
    "runtime"
)
var _ LoggerInterface = (*SlogLogger)(nil)
type SlogLogger struct {
    logger *
    level  *
    // true means to use slog to print the file path, false will use a custom method to add fields to the log file line    addSource bool
}
// newSlog
func newSlog(level Level, addSource bool) LoggerInterface {
    levelVar := &{}
    ()
    opts := &{AddSource: addSource, Level: levelVar}
    logger := ((, opts))
    return &SlogLogger{
        logger: logger,
        level:  levelVar,
    }
}
func (l *SlogLogger) Fatalw(msg string, keysAndValues ...interface{}) {
    keysAndValues = (keysAndValues...)
    (msg, keysAndValues...)
    (1)
}
func (l *SlogLogger) Infow(msg string, keysAndValues ...interface{}) {
    keysAndValues = (keysAndValues...)
    (msg, keysAndValues...)
}
//Omit other methods of inheriting interfaces such as DebugWfunc (l *SlogLogger) SetLevel(level Level) {
    zapLevelToSlogLevel(level)
    ((zapLevelToSlogLevel(level)))
}
// 
func (l *SlogLogger) With(keyValues ...interface{}) LoggerInterface {
    newLog := (keyValues...)
    return &SlogLogger{
        logger: newLog,
        level:  ,
    }
}
// ApppendFileLine Gets the caller's file and file number// Slog is native and does not support callerSkip. Using this function to chew on the root will cause performance problems. It is best to wait for slog to provide the parameters of CallerSkipfunc (l *SlogLogger) ApppendFileLine(keyValues ...interface{}) []interface{} {
     = false
    if ! {
        var pc uintptr
        var pcs [1]uintptr
        // skip [, this function, this function's caller]
        (4, pcs[:])
        pc = pcs[0]
        fs := ([]uintptr{pc})
        f, _ := ()
        keyValues = append(keyValues, "file", , "line", )
        return keyValues
    }
    return keyValues
}

Global initialization logger, the following pseudo-code

package logger
// Global log, you can also get new instances with NewLogger alonevar globalog = newlogger(DebugLevel)
func newlogger(level Level) *Logger {
    l := &Logger{logger: newSlog(level, false)}
    return l
}
func Infow(msg string, keysAndValues ...interface{}) {
    (msg, keysAndValues...)
}
// ...Omit other global methods,for exampleDebugW What's more

You can also use logger as follows, just like when using zap

import "/webws/go-moda/logger"
func main() {
    ("hello", "key", "value")   // Print json}

slog implements callerSkip function

The addsource parameter of slog will print the file name and line number, but it cannot support callerSkip like zap. That is to say, if slog is encapsulated under a file in the logger directory and printed with logger, the displayed file will be just

I read the source code of slog and used the callerSkip function internally, but did not expose the callerSkip parameters to the outside world.

You can see my code above and encapsulate a method myself: AppendFileLine, use AppendFileLine to get the file name and line number, and add the key value of file and line to the log

There may be performance problems, I hope slog can provide a callerSkip parameter to the outside world

illustrate

There are not many codes posted in the article, mainly providing ideas, although some methods and global logger implementation methods are omitted

To view logger implementation details, you can view the package quoted at the beginning of the article Quick experience

You can also check out my warehouse directlygo-modaUse slog and zap encapsulation

The above is the detailed explanation of go logger's business code that does not invade business code and uses slog to replace zap and implement callerSkip. For more information about go logger slog to replace zap, please follow my other related articles!