いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-490 (slogメモ-12)(context.Contextとの連携)

関連記事

Goメモ-477 (slogメモ-01)(基本的な使い方) - いろいろ備忘録日記

Goメモ-478 (slogメモ-02)(構造化ログの出力) - いろいろ備忘録日記

Goメモ-479 (slogメモ-03)(デフォルトロガー) - いろいろ備忘録日記

Goメモ-480 (slogメモ-04)(従来のlogパッケージとの連携) - いろいろ備忘録日記

Goメモ-482 (slogメモ-05)(テキスト形式のログ) - いろいろ備忘録日記

Goメモ-483 (slogメモ-06)(JSON形式のログ) - いろいろ備忘録日記

Goメモ-484 (slogメモ-07)(動的にログレベルを変更) - いろいろ備忘録日記

Goメモ-485 (slogメモ-08)(グループ (1)) - いろいろ備忘録日記

Goメモ-486 (slogメモ-09)(グループ (2)) - いろいろ備忘録日記

Goメモ-487 (slogメモ-10)(機密情報などのマスキング) - いろいろ備忘録日記

Goメモ-488 (slogメモ-11)(カスタムログレベル) - いろいろ備忘録日記

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ

概要

以下、自分用のメモです。

今更ながら、Go 1.21で導入された log/slog を使ってみたりしています。

少しづつメモしていきます。今回はcontext.Contextとの連携について。

slogでは、InfoContext メソッドなど、context.Context を渡せる関数がありますが、

コンテキストの中の情報をどのようにログに出力するかは自分で処理しないといけません。

つまり、カスタムハンドラを作成する必要があります。

サンプル

ctxkey.go

package main

import "context"

type ctxKey struct{}

var (
    msgKey = ctxKey{}
)

func ctxValue(ctx context.Context) string {
    return ctx.Value(msgKey).(string)
}

func setCtxValue(ctx context.Context, message string) context.Context {
    return context.WithValue(ctx, msgKey, message)
}

handler.go

package main

import (
    "context"
    "log/slog"
)

// ContextHandler は、[context.Context]のキーをログの出力に加えるカスタムハンドラです。
type ContextHandler struct {
    slog.Handler
}

// NewContextHandler は、指定されたハンドラを元に [ContextHandler] を作成します。
func NewContextHandler(handler slog.Handler) *ContextHandler {
    return &ContextHandler{handler}
}

// Handle implements [slog.Handler.Handle].
func (me *ContextHandler) Handle(ctx context.Context, r slog.Record) error {
    if message := ctxValue(ctx); message != "" {
        r.AddAttrs(slog.String("ctxkey", message))
    }

    return me.Handler.Handle(ctx, r)
}

// WithAttrs implements [slog.Handler.WithAttrs].
func (h *ContextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    return &ContextHandler{h.Handler.WithAttrs(attrs)}
}

// WithGroup implements [slog.Handler.WithGroup].
func (h *ContextHandler) WithGroup(name string) slog.Handler {
    return &ContextHandler{h.Handler.WithGroup(name)}
}

main.go

package main

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

const (
    MainTimeout = time.Second
    ProcTimeout = 50 * time.Millisecond
)

var (
    ErrMainTooSlow = errors.New("(MAIN) TOO SLOW")
    ErrProcTooSlow = errors.New("(PROC) TOO SLOW")
)

func init() {
    log.SetFlags(0)
}

func main() {
    var (
        rootCtx          = context.Background()
        mainCtx, mainCxl = context.WithTimeoutCause(rootCtx, time.Second, ErrMainTooSlow)
        procCtx          = run(mainCtx)
        err              error
    )
    defer mainCxl()

    select {
    case <-mainCtx.Done():
        err = context.Cause(mainCtx)
    case <-procCtx.Done():
        if err = context.Cause(procCtx); errors.Is(err, context.Canceled) {
            err = nil
        }
    }

    if err != nil {
        log.Fatal(err)
    }
}

func run(pCtx context.Context) context.Context {
    var (
        ctx, cxl = context.WithCancelCause(pCtx)
    )

    go func() {
        cxl(proc(ctx))
    }()
    go func() {
        <-time.After(ProcTimeout)
        cxl(ErrProcTooSlow)
    }()

    return ctx
}

func proc(pCtx context.Context) error {
    //
    // slog には、context.Context を受け取るログ出力メソッドも存在する。
    //
    // - DebugContext
    // - InfoContext
    // - WarnContext
    // - ErrorContext
    // - Log
    //
    // 上記のメソッドには、context.Context を指定できるが
    // 指定しただけだと context.Context の中に設定されている
    // キー/値 は、何も出力されない。
    //
    // 自前で設定した context.Context のキー/値 をログに出力するには
    // カスタムハンドラを作成する必要がある。
    //
    // カスタムハンドラを作成する場合の注意点として
    // Handleメソッドのみをオーバーライドして利用しているサンプルが
    // 結構あるが、これだと slog.Logger.With() や slog.Logger.WithGroup() などで
    // 情報を追加したロガーを利用する場合に、元となるハンドラが利用されてしまい
    // context.Contextの情報が出力されなくなってしまうことに注意が必要。
    // slog.Handler.WithAttrs(), slog.Handler.WithGroup() もオーバーライドしておく。
    //

    var (
        ctx = setCtxValue(pCtx, "helloworld")

        level = slog.LevelInfo
        opt   = &slog.HandlerOptions{
            Level:       level,
            ReplaceAttr: replaceAttr,
        }
        writer     = os.Stdout
        handler    = NewContextHandler(slog.NewJSONHandler(writer, opt))
        rootLogger = slog.New(handler)
        logger     = rootLogger.With("id", 1)
    )

    logger.DebugContext(ctx, "Call DebugContext")
    logger.InfoContext(ctx, "Call InfoContext")
    logger.WarnContext(ctx, "Call WarnContext")
    logger.ErrorContext(ctx, "Call ErrorContext")

    return nil
}

func replaceAttr(group []string, a slog.Attr) slog.Attr {
    if a.Key == slog.TimeKey {
        return slog.Attr{}
    }

    return a
}

実行すると以下のように出力されます。

Taskfile.yml

# https://taskfile.dev

version: '3'

tasks:
  default:
    cmds:
      - task: run
  run:
    cmds:
      - go run .

shell

$ task
task: [run] go run .
{"level":"INFO","msg":"Call InfoContext","id":1,"ctxkey":"helloworld"}
{"level":"WARN","msg":"Call WarnContext","id":1,"ctxkey":"helloworld"}
{"level":"ERROR","msg":"Call ErrorContext","id":1,"ctxkey":"helloworld"}

try-golang/examples/slog at main · devlights/try-golang · GitHub

参考情報

Goのおすすめ書籍


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。