PR

Go言語(Golang)のログ出力ではslogが使える!コンテキストの引き回しも忘れずに。

基礎

こんにちは。Tomoyuki(@tomoyuki65)です。

Go言語(Golang)でログ出力をする際にはfmt.Printlnなどを使うことがありますが、Goのv1.21からは標準ライブラリとしてlog/slogが使えます。

この記事では、Go言語のログ出力について解説します。

 

Go言語(Golang)のログ出力ではslogが使える!コンテキストの引き回しも忘れずに。

例えばGinを使ってAPIを作る際などには、ミドルウェアを使って一意のIDをリクエストヘッダー「X-Request-Id」にセットすることがあると思います。

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/google/uuid"
)

func httpRequestMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        newUUID, err := uuid.NewRandom()
        if err != nil {
            panic(err)
        }
        c.Request.Header.Set("X-Request-Id", newUUID.String())
    }
}

func main() {
    router := gin.Default()
    router.Use(httpRequestMiddleware())

    router.GET("/", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "Hello World !!",
        })
    })

    router.Run(":8080")
}

 

加えて、APIの実行直後のコード部分ではパラメータにある*gin.Contextのcが使えるため、コンテキストから一意のIDを取り出してログ出力に加えることも可能です。

package main

import (
    "net/http"
    "log/slog"

    "github.com/gin-gonic/gin"
    "github.com/google/uuid"
)

func httpRequestMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        newUUID, err := uuid.NewRandom()
        if err != nil {
            panic(err)
        }
        c.Request.Header.Set("X-Request-Id", newUUID.String())
    }
}

func main() {
    router := gin.Default()
    router.Use(httpRequestMiddleware())

    router.GET("/", func(c *gin.Context) {
        requestId := c.Request.Header.Get("X-Request-Id")
        slog.Info("ログ出力メッセージ1", "requestId", requestId)

        c.JSON(http.StatusOK, gin.H{
            "message": "Hello World !!",
        })
    })

    router.Run(":8080")
}

 

ログ出力結果

go-log-api | 2024/12/11 13:40:34 INFO ログ出力メッセージ1 requestId=496f5424-ff26-4041-a469-d08ed184002e

 

ただし、以下のように途中で関数を使った場合は、そのままだとログに一意のIDを付与することができません。

package main

import (
    "net/http"
    "log/slog"

    "github.com/gin-gonic/gin"
    "github.com/google/uuid"
)

func httpRequestMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        newUUID, err := uuid.NewRandom()
        if err != nil {
            panic(err)
        }
        c.Request.Header.Set("X-Request-Id", newUUID.String())
    }
}

func action() {
    slog.Info("ログ出力メッセージ2")
}

func main() {
    router := gin.Default()
    router.Use(httpRequestMiddleware())

    router.GET("/", func(c *gin.Context) {
        requestId := c.Request.Header.Get("X-Request-Id")
        slog.Info("ログ出力メッセージ1", "requestId", requestId)

        // 関数を実行
        action()

        c.JSON(http.StatusOK, gin.H{
            "message": "Hello World !!",
        })
    })

    router.Run(":8080")
}

 

ログ出力結果

go-log-api  | 2024/12/11 13:46:10 INFO ログ出力メッセージ1 requestId=c0cde5f3-2925-467b-853d-949ab0722226
go-log-api  | 2024/12/11 13:46:10 INFO ログ出力メッセージ2

 

このような場合、Goでは基本的には以下のようにコンテキストを関数に引き回すことになるため、面倒ですが忘れないように注意しましょう。

package main

import (
    "net/http"
    "log/slog"
    "context"

    "github.com/gin-gonic/gin"
    "github.com/google/uuid"
)

func httpRequestMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        newUUID, err := uuid.NewRandom()
        if err != nil {
            panic(err)
        }
        c.Request.Header.Set("X-Request-Id", newUUID.String())
    }
}

func action(ctx context.Context) {
    requestId := ctx.Value("requestId")
    slog.Info("ログ出力メッセージ2", "requestId", requestId)
}

func main() {
    router := gin.Default()
    router.Use(httpRequestMiddleware())

    router.GET("/", func(c *gin.Context) {
        requestId := c.Request.Header.Get("X-Request-Id")
        slog.Info("ログ出力メッセージ1", "requestId", requestId)

        // コンテキストを引き回して関数を実行
        ctx := context.WithValue(c.Request.Context(), "requestId", requestId)
        action(ctx)

        c.JSON(http.StatusOK, gin.H{
            "message": "Hello World !!",
        })
    })

    router.Run(":8080")
}

 

ログ出力結果

go-log-api  | 2024/12/11 13:52:46 INFO ログ出力メッセージ1 requestId=204caad8-1aba-44e9-bc41-0798aec64edb
go-log-api  | 2024/12/11 13:52:46 INFO ログ出力メッセージ2 requestId=204caad8-1aba-44e9-bc41-0798aec64edb

 

また、slogはカスタムロガーを作ることができるので、以下のようにコンテキストを渡して特定の項目をログに追加するような関数を作ることも可能です。

package main

import (
    "net/http"
    "log/slog"
    "context"

    "github.com/gin-gonic/gin"
    "github.com/google/uuid"
)

func httpRequestMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        newUUID, err := uuid.NewRandom()
        if err != nil {
            panic(err)
        }
        c.Request.Header.Set("X-Request-Id", newUUID.String())
    }
}

func action(ctx context.Context) {
    requestId := ctx.Value("requestId")
    slog.Info("ログ出力メッセージ2", "requestId", requestId)
}

type SlogHandler struct {
    slog.Handler
}

func (h *SlogHandler) Handle(ctx context.Context, r slog.Record) error {
    requestId, ok := ctx.Value("requestId").(string)
    if ok {
        r.AddAttrs(slog.Attr{Key: "requestId", Value: slog.String("requestId", requestId).Value})
    }

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

var slogHandler = &SlogHandler{
    slog.NewTextHandler(os.Stdout, nil),
}

var slogLogger = slog.New(slogHandler)

func InfoWithContext(ctx context.Context, message string) {
    slogLogger.InfoContext(ctx, message)
}

func main() {
    router := gin.Default()
    router.Use(httpRequestMiddleware())

    router.GET("/", func(c *gin.Context) {
        requestId := c.Request.Header.Get("X-Request-Id")
        slog.Info("ログ出力メッセージ1", "requestId", requestId)

        // コンテキストを引き回して関数を実行
        ctx := context.WithValue(c.Request.Context(), "requestId", requestId)
        action(ctx)

        // slogのカスタムロガーでログ出力
        InfoWithContext(ctx, "ログ出力メッセージ3")

        c.JSON(http.StatusOK, gin.H{
            "message": "Hello World !!",
        })
    })

    router.Run(":8080")
}

 

ログ出力結果

go-log-api  | 2024/12/11 14:23:04 INFO ログ出力メッセージ1 requestId=c281372d-ef7d-4061-a770-3daa56285d9a
go-log-api  | 2024/12/11 14:23:04 INFO ログ出力メッセージ2 requestId=c281372d-ef7d-4061-a770-3daa56285d9a
go-log-api  | time=2024-12-11T14:23:04.771Z level=INFO msg=ログ出力メッセージ3 requestId=c281372d-ef7d-4061-a770-3daa56285d9a

※カスタムロガーにすると出力形式が変わります。上記はText形式ですが、JSON形式にすることも可能です。

 

尚、上記では「slog.Info」や「slog.InfoContext」を使いましたが、ログレベルを変えたい場合は「slog.Warn」、「slog.WarnContext」、「slog.Error」、「slog.ErrorContext」なども使えます。

 

最後に

今回はログ出力用のslogや注意点についてご紹介しました。

Go言語ではログ出力に一意のIDをつけたい場合はコンテキストの引き回しが必要になったりするため、各種関数を作る際は必ず「context.Context」型の引数を付けておいた方がいいでしょう。

 

この記事を書いた人
Tomoyuki

SE→ブロガーを経て、現在はWeb系エンジニアをしています!

Tomoyukiをフォローする
基礎
スポンサーリンク
Tomoyukiをフォローする

コメント

タイトルとURLをコピーしました