Goのslog：現代的な構造化ログを手軽に

[![表紙](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967563/6925cbe3-c5f1-4bd0-82d8-c8942bf5af65.png)](https://leapcell.io/?lc_t=q_goslog)

## 前書き

go 1.21.0 バージョンでは新しいパッケージ log/slog が導入され、このパッケージは構造化ログの機能を提供します。一般的なログに比べて、構造化ログはより高い可読性を持ち、処理・分析・検索などの面で顕著な利点があります。

## slog パッケージ

slog パッケージは構造化ログを提供します。このログ記録には、メッセージ、重大度レベル、そしてその他さまざまな属性が含まれており、これらの属性はキーと値のペアとして表現されます。

slog パッケージの主な機能は以下の通りです：

- 構造化ログ
- ログの重大度レベル
- ログのカスタム処理
- ログのグループ化

## 初体験

```go
package main

import (
    "context"
    "log/slog"
)

func main() {
    slog.Info("slog msg", "greeting", "hello slog")
    // context を持つ
    slog.InfoContext(context.Background(), "slog msg with context", "greeting", "hello slog")
}
```

上記の例では、パッケージ関数 slog.Info を直接呼び出して info レベルのログを出力しています。この関数内部では、デフォルトで用意されている Logger インスタンスを使ってログの出力処理を行います。それ以外にも、slog.InfoContext を使って context を持たせてログを出力することもできます。

Info() および InfoContext() の他に、Debug()、Warn()、Error() など、さまざまなレベルのログを出力する関数があります。

上記のプログラムを実行すると、以下のような出力が得られます：

```
2025/06/18 21:08:08 INFO slog msg greeting="hello slog"
2025/06/18 21:08:08 INFO slog msg with context greeting="hello slog"
```

## Logger の作成

デフォルトでは、slog パッケージの関数でログを出力すると、ただのテキスト形式になります。もし JSON や key=value 形式での出力を実現したい場合は、slog.New() 関数で Logger インスタンスを作成する必要があります。この関数を使う際は、slog.Handler の実装を渡す必要があり、slog パッケージでは TextHandler と JsonHandler の 2 つの実装が用意されています。

### TextHandler ハンドラ

TextHandler はログ記録用のハンドラで、ログを一連のキーと値のペアとして io.Writer に書き込みます。各ペアは key=value の形式で表され、それぞれ空白で区切られます。

```go
package main

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

func main() {
    textLogger := slog.New(slog.NewTextHandler(os.Stdout, nil))
    textLogger.InfoContext(context.Background(), "TextHandler", "Name", "Leapcell")
}
```

上記の例では、slog.NewTextHandler 関数でログハンドラを作成しています。第 1 引数の os.Stdout はログをコンソールに出力することを意味します。その後、ハンドラを slog.New 関数に渡して Logger インスタンスを作成し、このインスタンスを使ってログ出力を行います。

プログラムの実行結果は以下の通りです：

```
time=2025-06-18T21:09:03.912+00:00 level=INFO msg=TextHandler Name=Leapcell
```

### JsonHandler ハンドラ

JsonHandler は、ログを json 形式で io.Writer に書き込むハンドラです。

```go
package main

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

func main() {
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    jsonLogger.InfoContext(context.Background(), "JsonHandler", "name", "Leapcell")
}
```

上記の例では、slog.NewJsonHandler 関数で json ログハンドラを作成しています。第 1 引数の os.Stdout はログをコンソールに出力することを意味します。その後、ハンドラを slog.New 関数に渡して Logger インスタンスを作成し、このインスタンスを使ってログ出力を行います。

プログラムの実行結果は以下の通りです：

```json
{
  "time": "2025-06-18T21:09:22.614686104+00:00",
  "level": "INFO",
  "msg": "JsonHandler",
  "name": "Leapcell"
}
```

## グローバルな Logger インスタンス

slog にはデフォルトの Logger インスタンスがあります。デフォルトの Logger インスタンスを取得したい場合、以下のコードを参考にしてください：

```go
logger := slog.Default()
```

これまでの例では、作成した Logger インスタンスでログを出力してきました。しかし、毎回特定の Logger インスタンスで記録するのではなく、グローバルに操作したい場合は、slog.SetDefault 関数でデフォルト Logger インスタンスを設定・置換できます。この方法を使うと、ログ記録がより便利で柔軟になります。

```go
package main

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

func main() {
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    slog.SetDefault(jsonLogger)
    slog.InfoContext(context.Background(), "JsonHandler", "name", "Leapcell") //{"time":"2025-06-18T21:11:22.41760604+00:00","level":"INFO","msg":"JsonHandler","name":"Leapcell"}
}
```

## Group グループ化

グループ化とは、ログ記録の関連属性（キーと値のペア）をまとめてグループにすることです。例を見てみましょう：

```go
package main

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

func main() {
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil)).WithGroup("information")
    jsonLogger.InfoContext(context.Background(), "json-log", slog.String("name", "Leapcell"), slog.Int("phone", 1234567890))

    textLogger := slog.New(slog.NewTextHandler(os.Stdout, nil)).WithGroup("information")
    textLogger.InfoContext(context.Background(), "json-log", slog.String("name", "Leapcell"), slog.Int("phone", 1234567890))
}
```

このプログラムを実行すると、以下の結果になります：

```
{"time":"2025-06-18T21:12:23.124255258+00:00","level":"INFO","msg":"json-log","information":{"name":"Leapcell","phone":1234567890}}
time=2025-06-18T21:12:23.127+00:00 level=INFO msg=json-log information.name=Leapcell information.phone=1234567890
```

実行結果からわかるように、JsonHandler ハンドラを持つ Logger インスタンスでグループ化を行うと、出力ログではグループ名（group name）が key となり、value はすべてのキーと値のペアをまとめた json オブジェクトになります。

TextHandler ハンドラを持つ Logger インスタンスでグループ化を行うと、グループ名とすべてのキーが結合され、最終的に groupName.key=value の形式で表示されます。

## LogAttrs 高効率なログ出力

頻繁にログを出力する必要がある場合、これまでの例よりも、slog.LogAttrs 関数と slog.Attr 型を組み合わせてログを出力する方がより効率的です。これは、型の解析処理を減らせるためです。

```go
package main

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

func main() {
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    jsonLogger.LogAttrs(context.Background(), slog.LevelInfo, "高効率なログ出力", slog.String("Name", "Leapcell"), slog.Int("Phone", 12345678901))
}
```

上記の例では、LogAttrs メソッドを使ってログを出力しています。このメソッドのシグネチャは以下の通りです：
`func (l *Logger) LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr)`

シグネチャから分かるように、第 1 引数は context.Context 型のコンテキスト、第 2 引数は Level 型（slog パッケージのログ重大度レベル）、第 3 引数はメッセージ、以降は Attr 型のキーと値のペアです。

他のメソッド（Info など）でログを出力する際は、内部でキーと値のペアを Attr 型に変換しますが、LogAttrs メソッドを使うと Attr 型を直接指定できるため、変換処理が不要となり、より効率的です。

## With 統一属性の設定

もしすべてのログに同じキーと値のペアを含めたい場合は、統一属性を設定することができます。

```go
package main

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

func main() {
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    logger := jsonLogger.With("systemID", "s1")
    logger.LogAttrs(context.Background(), slog.LevelInfo, "json-log", slog.String("k1", "v1"))
    logger.LogAttrs(context.Background(), slog.LevelInfo, "json-log", slog.String("k2", "v2"))
}
```

With メソッドを使って、1 つまたは複数の固定属性を追加し、新しい Logger インスタンスを返すことができます。その後、新インスタンスでログを出力すると、追加した固定属性が常に含まれるため、毎回同じキーと値を追加する必要がなくなります。

このプログラムの出力結果は以下の通りです：

```json
{"time":"2025-06-18T21:19:51.338328238+00:00","level":"INFO","msg":"json-log","systemID":"s1","k1":"v1"}
{"time":"2025-06-18T21:19:51.338604943+00:00","level":"INFO","msg":"json-log","systemID":"s1","k2":"v2"}
```

## HandlerOptions ログハンドラの設定オプション

注意深い読者は、これまでの例で NewJSONHandler や NewTextHandler の第 2 引数が常に nil になっていることに気づいたかもしれません。これはデフォルト設定を使うためです。

この引数の型は \*HandlerOptions です。これを利用することで、ログ文のソースコード位置情報の表示、最低出力レベルの指定、キーと値のペアのカスタム変換などの設定が可能です。

```go
package main

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

func main() {
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        AddSource: true,
        Level: slog.LevelError,
        ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
            if a.Key == slog.TimeKey {
                if t, ok := a.Value.Any().(time.Time); ok {
                    a.Value = slog.StringValue(t.Format(time.DateTime))
                }
            }
            return a
        },
    }))
    jsonLogger.InfoContext(context.Background(), "json-log", slog.String("name", "Leapcell"))
    jsonLogger.ErrorContext(context.Background(), "json-log", slog.String("name", "Leapcell"))
}
```

上記の例では、JsonHandler を持つ Logger インスタンスを作成しています。JsonHandler 作成時に HandlerOptions パラメータを指定して以下の設定を行っています：

- ログ文のソース情報（Source）を出力する設定
- 最低ログレベルを Error に設定
- "time" キーの値のフォーマットを "2006-01-02 15:04:05" 形式に変換する

このプログラムを実行した場合、出力結果は以下のようになります：

```json
{
  "time": "2025-06-18 21:21:31",
  "level": "ERROR",
  "source": { "function": "main.main", "file": "D:/goproject/src/gocode/play/main.go", "line": 24 },
  "msg": "json-log",
  "name": "Leapcell"
}
```

出力は期待通りで、INFO レベルのログは出力されず、Source 情報や "time" キーの値の変換も反映されています。

## key-value ペアの value の値をカスタマイズ

前の例では、HandlerOptions の設定で key-value ペアの value を変更しましたが、slog パッケージは別の方法による value のカスタマイズもサポートしています。

```go
package main

import (
    "context"
    "log/slog"
)

type Password string

func (Password) LogValue() slog.Value {
    return slog.StringValue("REDACTED_PASSWORD")
}

func main() {
    slog.LogAttrs(context.Background(), slog.LevelInfo, "機密データ", slog.Any("password", Password("1234567890")))
}
```

上記の例では、slog.LogValuer インターフェース（特定の型に LogValue() slog.Value メソッドを追加）を実装することで、key-value ペアの value を上書きしています。ログ出力時、value の値は LogValue メソッドの返り値で置き換えられます。

このプログラムの出力結果は以下の通りです：

```
2025/06/18 21:37:11 INFO 機密データ password=REDACTED_PASSWORD
```

出力は期待通りで、password の value が変更されています。

## まとめ

本記事では、Go 言語の slog パッケージについて、基本的な使い方、Logger インスタンスの作成、高効率なログ出力やログ情報のカスタマイズなど、詳細に解説しました。

この記事を読むことで、slog パッケージへの理解がより深まり、ログの管理と記録がより適切に行えるようになるでしょう。

---

### [私たちはLeapcell、Goプロジェクトのホスティングの最適解です。](https://leapcell.io/?lc_t=q_goslog)

[![Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967563/895545c2-9047-45ef-826f-f1a3c2f214a4.png)](https://leapcell.io/?lc_t=q_goslog)

[Leapcell](https://leapcell.io/?lc_t=q_goslog)は、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです：

**複数言語サポート**

- Node.js、Python、Go、Rustで開発できます。

**無制限のプロジェクトデプロイ**

- 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

**比類のないコスト効率**

- 使用量に応じた支払い、アイドル時間は課金されません。
- 例: $25で6.94Mリクエスト、平均応答時間60ms。

**洗練された開発者体験**

- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

**簡単なスケーラビリティと高パフォーマンス**

- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。

[ドキュメントで詳細を確認！](https://docs.leapcell.io/)

[![Try Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967563/78e266fb-3c28-4afa-8c09-53b403523889.png)](https://leapcell.io/?lc_t=q_goslog)

Xでフォローする：[@LeapcellHQ](https://x.com/LeapcellHQ)

---

[ブログでこの記事を読む](https://leapcell.io/blog/hands-on-with-go-slog-package)