🐙

Goのバージョン1.26.0から実装されるslogのMultiHandlerについて

に公開

はじめに

Go 1.21で導入された log/slog パッケージに、2月にリリース予定のGo 1.26から MultiHandler が実装されるため、事前のキャッチアップをしておこうと思い記事を書くことになりました!

MultiHandlerについて

MultiHandler は標準の Handler インターフェースを実装しており、設定したすべてのハンドラを順次呼び出します。これにより、1回のログ出力で複数の宛先に同時にログを送信できます。

例えば「標準出力にはテキスト形式で出力しつつ、ファイルにはJSON形式で保存したい」といったユースケースに対応するには、これまで自前でラッパーを実装するか、サードパーティのライブラリに頼る必要がありました。

NewMultiHandler に作成したハンドラを渡すだけで、複数出力が実現できちゃうんです!

// 標準出力用
consoleHandler := slog.NewTextHandler(os.Stdout, nil)

// ファイル出力用
logFile, err := os.OpenFile("/tmp/app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
    log.Fatalf("failed to open log file: %v", err)
}
defer logFile.Close()
fileHandler := slog.NewJSONHandler(logFile, nil)

// これだけで複数出力が実現!
multi := slog.NewMultiHandler(consoleHandler, fileHandler)
logger := slog.New(multi)

logger.Info("login", slog.String("name", "whoami"), slog.Int("id", 42))

Before / After 比較

Before: 自前で MultiHandler を実装

これまでは、複数出力を実現するために自前でラッパーを実装する必要がありました。

type multiHandler struct {
    handlers []slog.Handler
}

func (m *multiHandler) Enabled(ctx context.Context, level slog.Level) bool {
    for _, h := range m.handlers {
        if h.Enabled(ctx, level) {
            return true
        }
    }
    return false
}

func (m *multiHandler) Handle(ctx context.Context, r slog.Record) error {
    var errs []error
    for _, h := range m.handlers {
        if h.Enabled(ctx, r.Level) {
            if err := h.Handle(ctx, r); err != nil {
                errs = append(errs, err)
            }
        }
    }
    return errors.Join(errs...)
}

func (m *multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    newHandlers := make([]slog.Handler, len(m.handlers))
    for i, h := range m.handlers {
        newHandlers[i] = h.WithAttrs(attrs)
    }
    return &multiHandler{handlers: newHandlers}
}

func (m *multiHandler) WithGroup(name string) slog.Handler {
    newHandlers := make([]slog.Handler, len(m.handlers))
    for i, h := range m.handlers {
        newHandlers[i] = h.WithGroup(name)
    }
    return &multiHandler{handlers: newHandlers}
}

func main() {
    consoleHandler := slog.NewTextHandler(os.Stdout, nil)

    logFile, err := os.OpenFile("/tmp/app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    if err != nil {
        log.Fatalf("failed to open log file: %v", err)
    }
    defer logFile.Close()
    fileHandler := slog.NewJSONHandler(logFile, nil)

    logger := slog.New(&multiHandler{
        handlers: []slog.Handler{consoleHandler, fileHandler},
    })

    logger.Info("login", slog.String("name", "whoami"), slog.Int("id", 42))
}
After: 標準ライブラリの MultiHandler を使用
func main() {
    consoleHandler := slog.NewTextHandler(os.Stdout, nil)

    logFile, err := os.OpenFile("/tmp/app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    if err != nil {
        log.Fatalf("failed to open log file: %v", err)
    }
    defer logFile.Close()
    fileHandler := slog.NewJSONHandler(logFile, nil)

    // たった1行で複数出力が実現!
    multi := slog.NewMultiHandler(consoleHandler, fileHandler)
    logger := slog.New(multi)

    logger.Info("login", slog.String("name", "whoami"), slog.Int("id", 42))
}

特徴

1. エラーハンドリング

いずれかのハンドラがエラーを返した場合でも、処理を停止せず残りのハンドラへの出力を継続します。すべてのエラーは errors.Join で結合されて返されます。

infoHandler := slog.NewTextHandler(
    os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo},
)
brokenHandler := &BrokenHandler{
    Handler: infoHandler,
    err:     fmt.Errorf("broken handler"),
}

multi := slog.NewMultiHandler(brokenHandler, infoHandler)
rec := slog.NewRecord(time.Now(), slog.LevelInfo, "hello", 0)

// brokenHandlerがエラーを返しても、infoHandlerへの出力は継続される
err := multi.Handle(context.Background(), rec)
fmt.Println(err) // "broken handler"

2. Enabled メソッド

設定されたハンドラのいずれか1つでも有効であれば true を返します。

infoHandler := slog.NewTextHandler(
    os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo},
)
errorOnlyHandler := slog.NewTextHandler(
    os.Stdout, &slog.HandlerOptions{Level: slog.LevelError},
)
multi := slog.NewMultiHandler(infoHandler, errorOnlyHandler)

// infoHandlerがLevelInfoで有効なのでtrueが返る
enabled := multi.Enabled(context.Background(), slog.LevelInfo)
fmt.Println(enabled) // true

3. WithAttrs / WithGroup

これらのメソッドは、有効化された各ハンドラに対して対応するメソッドを呼び出します。属性やグループの設定がすべてのハンドラに適切に伝播されます。

4. 外部サービスとの連携

Bugsnagなどの外部エラー監視サービスへのカスタムハンドラと組み合わせることも可能です。

consoleHandler := slog.NewJSONHandler(os.Stdout, nil)
bugsnagHandler := NewBugsnagHandler(bugsnagClient, &slog.HandlerOptions{
    Level: slog.LevelError, // エラーのみBugsnagに送信
})

logger := slog.New(slog.NewMultiHandler(
    consoleHandler,
    bugsnagHandler,
))

メリット

観点 内容
標準ライブラリで完結 サードパーティ製パッケージへの依存なしに複数出力が可能
コード量の削減 自前実装が不要になり、ボイラープレートを大幅削減
(補足:ボイラープレートとは、何度も繰り返して書く必要のある定型的なコードの塊のこと)
柔軟な出力形式 宛先ごとに異なるフォーマット(Text/JSON)やログレベルを設定可能
堅牢なエラーハンドリング 1つのハンドラが失敗しても処理を継続
既存コードとの互換性 Handler インターフェースを実装しているため、既存のコードにそのまま組み込める

おわりに

MultiHandler の導入により、これまで自前実装やサードパーティライブラリに頼っていた複数出力のロギングが、標準ライブラリだけでシンプルに実現できるようになりました。

これを知っていれば、よりシンプルに、かつ直感的にログを実装できると感じてます!

参考

https://go.dev/doc/go1.26#logslogpkglogslog

株式会社ドクターズプライム

Discussion