💡

【golang】slogを雑に使ってみる

2024/06/22に公開

golang1.21から標準パッケージに追加されたslogを雑に使ってみたメモ

今やっているPJに合わせて作ったものなので汎用性は無い。(contextから値取り出してログのパラメータに設定するとか)

slogを直接呼び出す

package logger

import (
	"log/slog"
	"os"
)

func InitLogger(logLevel string, format string) {
	logger := newSlogLogger(logLevel, format)
	slog.SetDefault(logger)
}

func newSlogLogger(logLevel string, format string) *slog.Logger {
	handler := newJSONHandler(logLevel, format)
	return slog.New(handler)
}

func newJSONHandler(logLevel string, format string) slog.Handler {
	var level slog.Level
	switch logLevel {
	case "debug":
		level = slog.LevelDebug
	case "info":
		level = slog.LevelInfo
	case "warn":
		level = slog.LevelWarn
	case "error":
		level = slog.LevelError
	default:
		level = slog.LevelInfo
	}
	opt := slog.HandlerOptions{
		AddSource: true,
		Level:     level,
	}

	switch format {
	case "text":
		return slog.NewTextHandler(os.Stdout, &opt)
	case "json":
		return slog.NewJSONHandler(os.Stdout, &opt)
	default:
		return slog.NewJSONHandler(os.Stdout, &opt)
	}
}

通常のslogの関数slog.Info()等で呼び出す。

自前のロガー経由でslogを呼び出す

PJの思想の中にはcontextにロガーを与えたり、シングルトンなロガーを定義したりというロガーへの要件があることもあります。
その場合は以下のようなロガーstructを定義して使用するのが良いと思います。

以下では各ログレベルのログメソッドの第一引数をmsgとしていますが、これは私の好みなだけなので好きに定義を変えてしまってください。
また、contextから値を呼び出してログに設定する実装は以下には無いです、必要な場合は各メソッドの引数にcontextを追加して

package logger

import (
	"context"
	"log/slog"
	"os"
	"runtime"
	"time"
)

type MyLogger interface {
    Log(...interface{})
	Debug(msg string, args ...interface{})
	Info(msg string, args ...interface{})
	Warn(msg string, args ...interface{})
	Error(msg string, err error, args ...interface{})
}

type SlogLogger struct {
	Logger *slog.Logger
}

func NewSlogLogger(logLevel string, format string) *SlogLogger {
	handler := newJSONHandler(logLevel, format)
	logger := slog.New(handler)
	return &SlogLogger{
		Logger: logger,
	}
}

func newJSONHandler(logLevel string, format string) slog.Handler {
	var level slog.Level
	switch logLevel {
	case "debug":
		level = slog.LevelDebug
	case "info":
		level = slog.LevelInfo
	case "warn":
		level = slog.LevelWarn
	case "error":
		level = slog.LevelError
	default:
		level = slog.LevelInfo
	}
	opt := slog.HandlerOptions{
		AddSource: true,
		Level:     level,
	}

	switch format {
	case "text":
		return slog.NewTextHandler(os.Stdout, &opt)
	case "json":
		return slog.NewJSONHandler(os.Stdout, &opt)
	default:
		return slog.NewJSONHandler(os.Stdout, &opt)
	}
}

func (l SlogLogger) Log(args ...interface{}) {
	if l.Logger == nil {
		return
	}
	if len(args) == 0 {
		return
	}

	// 第一引数がstringの場合は、それをメッセージとして扱う
	if msg, ok := args[0].(string); ok {
		args = args[1:]
		l.Info(msg, args...)
	} else {
		// stringでない場合は、そのままInfoとして扱う
		l.Info("", args...)
	}
}

func (l SlogLogger) Debug(msg string, args ...interface{}) {
	l.log(slog.LevelDebug, msg, nil, args...)
}

func (l SlogLogger) Info(msg string, args ...interface{}) {
	l.log(slog.LevelInfo, msg, nil, args...)
}

func (l SlogLogger) Warn(msg string, args ...interface{}) {
	l.log(slog.LevelWarn, msg, nil, args...)
}

func (l SlogLogger) Error(msg string, err error, args ...interface{}) {
	l.log(slog.LevelError, msg, err, args...)
}

func (l SlogLogger) log(level slog.Level, msg string, err error, args ...interface{}) {
	if l.Logger == nil {
		return
	}

	// トレース情報が欲しいので、runtime.Callersを使って呼び出し元の情報を取得
	// 参考: https://pkg.go.dev/log/slog#hdr-Wrapping_output_methods
	var pcs [1]uintptr
	runtime.Callers(3, pcs[:]) // skipの値は呼び出し方によって変わるため呼び出し階層が変わった場合等に注意

	now := time.Now()
	r := slog.NewRecord(now, level, msg, pcs[0])
	if err != nil {
		r.AddAttrs(slog.Any("err", err))
	}
	r.Add(args...)

	_ = l.Logger.Handler().Handle(context.Background(), r)
}

Discussion