---
title: "構造化ログでフィールドから検索できるログの書式とログレベルを整える"
description: "構造化ログは、各値を文の中ではなく専用のフィールドに入れます。1 件の注文を 3 通りの方法で LogBrew に送り、order_id でどれを見つけ直せるかを示します。"
canonical: "https://logbrew.co/ja/guides/structured-logging"
last_updated: "2026-11-10"
---

# 構造化ログでフィールドから検索できるログの書式とログレベルを整える

構造化ログは、各値を文の中ではなく専用のフィールドに入れます。1 件の注文を 3 通りの方法で LogBrew に送り、`order_id` でどれを見つけ直せるかを示します。

2026年11月10日 に Python 3.13.5、logbrew-sdk 0.1.29 で検証

執筆 [Furkan Erday](https://github.com/furkanerday). Furkan Erday は LogBrew の創業者です。毎日 Claude Code と Codex を使って LogBrew を開発しています。

Canonical: https://logbrew.co/ja/guides/structured-logging

## `order_id` で見つけ直せるログ行はどれか

| id を入れた場所 | テキスト検索 | フィールドフィルター |
| --- | --- | --- |
| メッセージ本文の中 | 見つかった | 見つからない |
| `extra={}` のフィールド | 見つからない | 見つかった |
| `extra={}` 内の dict | 見つからない | 見つからない |

Python のハンドラーで 1 件の注文を 3 通りの方法で送り、検索しました。カップが満杯なら、検索でその行が見つかったことを示します。

## フィルターできるログの書式を選ぶ

### メッセージは固定し、値はフィールドに入れる

フィールドは名前と型を保持します。メッセージ本文の中の値は、ただの部分文字列です。Python では `extra={}` でフィールドを渡し、メッセージはどの注文でも同じにしておきます。

```bash
log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
```

### プログラムが読むログは JSON 行で書く

`Formatter` のサブクラスで、各レコードを標準フィールドと `extra` の各値を持つ 1 行 1 オブジェクトの JSON にします。ログコレクターは正規表現なしでその行を解析できます。

```bash
import json
import logging

RECORD_FIELDS = set(vars(logging.LogRecord("", 0, "", 0, "", (), None))) | {"message", "asctime"}


class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        line = {
            "time": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
        }
        line.update({k: v for k, v in vars(record).items() if k not in RECORD_FIELDS})
        if record.exc_info:
            line["exception"] = self.formatException(record.exc_info)
        return json.dumps(line)


handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logging.basicConfig(level=logging.INFO, handlers=[handler])
log = logging.getLogger("taproom")

log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
log.warning("keg below 10%", extra={"keg": "ipa", "percent": 8})
```

出力

```text
{"time": "2026-11-10T08:34:04+0300", "level": "INFO", "logger": "taproom", "message": "order paid", "order_id": "ord_42", "amount_cents": 1999}
{"time": "2026-11-10T08:34:04+0300", "level": "WARNING", "logger": "taproom", "message": "keg below 10%", "keg": "ipa", "percent": 8}
```

### フィールドを LogBrew に送る

`LogBrewLoggingHandler` は、各レコードを `extra` の値をフィールドとして送ります。同じ注文を、メッセージ本文の中、`extra` のフィールド、`extra` 内の dict の 3 通りで送りました。

```bash
import logging
import os

from logbrew_sdk import HttpTransport, LogBrewClient, LogBrewLoggingHandler

client = LogBrewClient.create(api_key=os.environ["LOGBREW_SERVER_API_KEY"])
transport = HttpTransport()
log = logging.getLogger("taproom")
log.addHandler(LogBrewLoggingHandler(client, transport, metadata={"service": "taproom"}))
log.setLevel(logging.INFO)

logging.getLogger("taproom.text").info("order %s paid, %d cents", "ord_42", 1999)
logging.getLogger("taproom.fields").info(
    "order paid", extra={"order_id": "ord_42", "amount_cents": 1999}
)
logging.getLogger("taproom.nested").info(
    "order paid", extra={"order": {"id": "ord_42", "amount_cents": 1999}}
)

response = client.flush(transport)
print("HTTP", response.status_code, "accepted", response.accepted_events, "logs")
```

出力

```text
HTTP 200 accepted 3 logs
```

### フィールドでその行を見つけ直す

`GET /api/logs` は、フィールドのパス、型、演算子、値を持つ `attribute_filters` を受け付けます。`order_id` のフィルターで見つかるのは、`extra` で送った行だけです。ハンドラーは文字列、数値、真偽値、`None` だけを保持するため、`extra` 内の dict は捨てられます。

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode 'attribute_filters=[{"path":"/metadata/order_id","type":"string","operator":"eq","value":"ord_42"}]' \
  | jq -c '.[] | {message, order_id: .attributes.metadata.order_id}'
```

出力

```text
{"message":"order paid","order_id":"ord_42"}
```

## すべてのサービスでログレベルを同じように使う

### 本番環境では 4 つのレベルを使う

`info` は起きたこと、`warning` は問題が起きたものの対処済みのこと、`error` は失敗したリクエストやジョブ、`critical` はサービスを止めた事象に使います。`debug` はローカル実行のために取っておきます。LogBrew のハンドラーは DEBUG レコードを `info` として送ります。

### レベルは起動時に 1 回だけ設定する

レベルは環境変数から読み込みます。そうすれば、1 台のホストだけを DEBUG で動かし、残りは INFO のままにできます。Python では `logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO"))` がその役割を果たします。

### 数値は数値として記録する

`amount_cents: 1999` は型を保持するため、`amount_cents >= 1000` のフィルターで見つかります。`amount_cents: "1999"` をテキストとしても送りましたが、数値フィルターはその行をスキップしました。

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode 'attribute_filters=[{"path":"/metadata/amount_cents","type":"number","operator":"gte","value":1000}]' \
  | jq -c '.[] | {message, amount_cents: .attributes.metadata.amount_cents}'
```

出力

```text
{"message":"order paid","amount_cents":1999}
```

## 実際に試して確認したこと

Python のハンドラーで 1 件の注文を 3 通りの方法で送り、LogBrew の API で検索しました。テキスト検索 `search=ord_42` は 3 行のうち 1 行、メッセージの中に id を含む行だけを見つけました。

`order_id = ord_42` のフィルターは、`extra={"order_id": "ord_42"}` で送った 1 行を見つけました。`amount_cents >= 1000` のフィルターも同じ行を見つけました。ハンドラーが 1999 を数値として保存していたためです。

`extra={"order": {...}}` で送った行は、どの条件にも一致しませんでした。ハンドラーは `extra` から文字列、数値、真偽値、`None` を送り、dict やリストはスキップします。

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode "search=ord_42" \
  | jq -c '.[] | {message, order_id: .attributes.metadata.order_id}'
```

出力

```text
{"message":"order ord_42 paid, 1999 cents","order_id":null}
```

## プレーンテキストと JSON のログ書式

通常の `%(message)s` 書式は、`extra` の値をすべて捨てます。下の行は注文 id のない `INFO order paid` で終わるため、後からその注文を見つける手段がありません。

JSON 行はフィールドを型ごと保持し、ログコレクターは正規表現なしで解析できます。人には読みにくいので、メッセージは短く、フィールド名は固定しておきます。

Python には標準の JSON フォーマッターがありません。python/cpython#77691 は 2018 年にこれを求めましたが、ドキュメントにすでに書き方が載っているとして閉じられました。上の 15 行の `Formatter` がそのレシピです。

```bash
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("taproom")

log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
```

出力

```text
2026-11-10 08:34:04,955 INFO order paid
```

## LogBrew が記録する内容

各ログは、レベル、ロガー名、`extra` のすべてのプリミティブなフィールド、そしてハンドラーの `metadata={}` のフィールドを保持します。API は、フィールドのパス、型、演算子でフィルターします。テキストは `eq`、`ne`、`contains`、`starts_with` を、数値は `gt`、`gte`、`lt`、`lte` を受け付けます。

LogBrew は、名前に `password`、`secret`、`token`、`cookie`、`authorization` を含むフィールドをインデックスしないため、フィルターでは決して見つかりません。それでも、秘密情報はログ行に入れないでください。

トレースの中で送ったログは、トレース ID とスパン ID を持つため、`order_id` のフィルターからその行を生んだリクエストにたどり着けます。

## よくある質問

### 構造化ログとは何ですか？

すべての値が、文の中の単語ではなく、JSON またはログレコードのフィールドとして、型を持つ名前付きフィールドになっているログです。検索ツールは部分文字列ではなくフィールドでフィルターできます。

### ログレベルの順序は？

DEBUG、INFO、WARNING、ERROR、CRITICAL で、Python では 10 から 50 の番号が付きます。INFO に設定したロガーは INFO 以上を通し、DEBUG を捨てます。

### ログは JSON とプレーンテキストのどちらで書くべきですか？

コレクター、ファイル、検索ツールなどのプログラムが読む行なら JSON です。人だけが読むターミナルならプレーンテキストです。どちらでも同じフィールドを使います。

### Python のログ行にフィールドを追加するには？

ログ呼び出しに `extra={"order_id": "ord_42"}` を渡します。標準のフォーマッターは extra のフィールドを無視します。JSON フォーマッターや LogBrew のハンドラーはそれを保持します。

### LogBrew でログをフィールドで検索できますか？

できます。`GET /api/logs` は最大 4 つの `attribute_filters` を受け付け、それぞれにフィールドのパス、型、`eq` や `gte` などの演算子を指定します。

## ログを任意のフィールドでフィルターする

LogBrew は記録したすべてのフィールドを保持し、そのフィールドからログ行を見つけ直せます。行が属するトレースや問題も一緒に確認できます。十分な無料プランで始められ、カードは不要です。

[無料で始める](https://logbrew.co/ja)

## 出典

- [Python docs, Logging Cookbook](https://docs.python.org/3/howto/logging-cookbook.html)
- [Python docs, LogRecord attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes)
- [python/cpython#77691](https://github.com/python/cpython/issues/77691)

## エージェントと Python のその他のガイド

- [Claude Code と Codex に MCP サーバーを追加する](https://logbrew.co/ja/guides/mcp-server-claude-code-codex)
- [Claude Code と Codex で AGENTS.md を使う](https://logbrew.co/ja/guides/agents-md)
- [Claude の overloaded エラー 529 とレート制限エラー 429 を解決する](https://logbrew.co/ja/guides/claude-overloaded-rate-limit-error)
- [Python の logging を正しいログレベルと例外の記録つきで設定する](https://logbrew.co/ja/guides/python-logging)
- [Claude Code、Codex、JavaScript、Python のエラーを解決する](https://logbrew.co/ja/guides)
