ログの読み方

目次

概要

ログは、システムが何をしたかを後から読むための記録です。障害調査、性能分析、セキュリティ監査、ユーザー問い合わせ対応の出発点になります。

要点

ログは「何が起きたか」だけでなく、「いつ、どこで、誰に関係し、どのrequestに属し、結果がどうだったか」を追える形で残すと価値が上がります。

ログは何を残すものか

ログに残す代表的な情報です。

項目
timestamp 2026-04-29T12:34:56Z
level INFO, WARN, ERROR
service api
request_id req-123
user_id u_456
event payment_failed
message payment provider timeout
duration_ms 320
http.method POST
http.status_code 504
error.type TimeoutError

ログは多ければよいわけではありません。調査に必要な情報が、検索しやすい形で残っていることが重要です。

ログは「読む人」と「読む機械」の両方に向けたデータです。人には状況が分かるmessageが必要で、機械には集計できるfieldが必要です。

{
  "timestamp": "2026-04-29T12:34:56.789Z",
  "level": "ERROR",
  "service": "checkout-api",
  "event": "external_api_timeout",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "request_id": "req-123",
  "http.method": "POST",
  "http.route": "/checkout",
  "http.status_code": 504,
  "provider": "payment",
  "duration_ms": 3000,
  "error.type": "TimeoutError",
  "message": "payment provider timed out"
}

ここで大事なのは、message にすべてを詰め込まないことです。providerduration_mshttp.status_code のようにfieldとして分かれていれば、後から検索、集計、alert、dashboardに使えます。

ログレベル

level 使いどころ
DEBUG 開発・詳細調査
INFO 通常の重要イベント
WARN ただちに失敗ではないが注意が必要
ERROR 処理失敗
FATAL プロセス継続不能

ERROR を乱用すると、本当に見るべき問題が埋もれます。逆に、重要な失敗を INFO にするとalertにつながりません。

levelは「開発者の気分」ではなく、運用上の意味で決めます。

判断 level
予定通り成功した重要イベント INFO
retryで回復したが注視したい WARN
requestやjobが失敗した ERROR
processを落とす必要がある FATAL
local調査用の詳細 DEBUG

同じeventの成功と失敗は、できればevent名を対応させます。

{"level":"INFO","event":"payment_authorized","payment_id":"pay_123"}
{"level":"ERROR","event":"payment_authorization_failed","payment_id":"pay_123","error.type":"TimeoutError"}

こうしておくと、失敗率を payment_authorization_failed / payment_authorized のように見られます。

構造化ログ

構造化ログは、messageだけでなくkey-valueで情報を持つログです。

{
  "level": "ERROR",
  "event": "payment_failed",
  "request_id": "req-123",
  "duration_ms": 320,
  "error": "timeout"
}

構造化されていると、jq やログ基盤で検索しやすくなります。

jq 'select(.level == "ERROR") | .request_id' app.jsonl

ログ形式には、主に次の選択肢があります。

形式 特徴 向いている場面
plain text 人が読みやすい 小規模、local debug
JSON Lines 1行1JSONで機械処理しやすい application log
key=value grepしやすい infra、proxy、legacy
syslog OSやnetwork機器で広く使う system log

application logはJSON Linesが扱いやすいことが多いです。1行が1イベントなので、streamとして扱いやすく、壊れた1行があっても周辺のログを読み続けやすいからです。

{"ts":"2026-04-29T12:00:00Z","level":"INFO","event":"request_started","request_id":"req-1"}
{"ts":"2026-04-29T12:00:01Z","level":"INFO","event":"request_finished","request_id":"req-1","duration_ms":812}

field名は揺らさないようにします。request_idrequestIdreq_id が混在すると、検索式もdashboardも壊れやすくなります。

trace idとcorrelation id

分散システムでは、1つのrequestが複数サービスを通ります。

sequenceDiagram participant Browser participant API participant Worker participant DB Browser->>API: request trace_id=abc API->>Worker: job trace_id=abc Worker->>DB: query trace_id=abc

trace idやcorrelation idがあると、複数サービスのログを1本の流れとして追えます。

似た名前のIDが複数出てくるので、役割を分けて覚えると楽です。

ID 役割
trace_id 分散trace全体を識別する
span_id trace内の1区間を識別する
request_id HTTP requestやapplication requestを識別する
correlation_id 複数処理を業務上まとめて追う
job_id 非同期jobを追う
user_id ユーザー単位で追う

trace_id はobservabilityの横断検索に強く、request_id はアプリケーションの問い合わせ対応に強い、という住み分けがよくあります。

trace contextを読む

HTTPでは、分散traceの文脈を traceparent headerで渡す標準があります。

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

これは大まかに次の情報を持ちます。

部分 意味
version 00 format version
trace-id 4bf92f3577b34da6a3ce929d0e0e4736 request全体の流れ
parent-id 00f067aa0ba902b7 直前のspan
flags 01 samplingなどのflag

ログに trace_idspan_id が入っていれば、trace画面から該当ログへ飛べます。逆にログからtraceを開くこともできます。

flowchart LR A["ログ検索<br>trace_id=..."] --> B["trace画面"] B --> C["遅いspanを確認"] C --> D["該当serviceのログへ戻る"]

分散システムの障害調査では、ログだけを読むより、traceと往復した方が速いことがあります。どのserviceで遅くなったかをtraceで見て、そのserviceの内部状態をログで読む、という流れです。

障害調査の流れ

ログ調査は、仮説を立てて絞り込む作業です。

flowchart TD A["症状を確認"] --> B["時間帯を絞る"] B --> C["request id / user idを探す"] C --> D["ERROR / WARNを見る"] D --> E["直前のINFOを見る"] E --> F["関連サービスへ広げる"] F --> G["原因仮説を検証"]

最初から全ログを読むのではなく、時間、service、request id、status codeで絞ります。

障害調査では「事実」と「仮説」を混ぜないようにします。

段階 見るもの 目的
影響確認 error rate、status code、対象ユーザー どれくらい起きているか
時間特定 deploy時刻、alert時刻、最初のerror いつからか
経路特定 trace、service、endpoint どこで起きたか
原因候補 error.type、直前ログ、外部依存 なぜ起きたか
再発防止 missing field、alert不足、runbook不足 次に速く気づけるか

読み方のコツは、最初に「正常な1件」と「失敗した1件」を並べることです。

flowchart TD A["正常request"] --> C["差分を見る"] B["失敗request"] --> C C --> D["違うservice"] C --> E["違うstatus"] C --> F["違うduration"] C --> G["違う外部依存"]

失敗ログだけを読むと、普段から出ているharmless warningに引っ張られることがあります。正常時にも出ているログかどうかを見るだけで、調査の迷路がかなり減ります。

CLIで読む

テキストログです。

grep "ERROR" app.log | tail -100

JSON Linesです。

jq 'select(.level == "ERROR")' app.jsonl

件数を集計します。

jq -r '.event' app.jsonl | sort | uniq -c | sort -nr

ログが大きい場合は、lessrgawkjq を組み合わせます。

時間帯で切る例です。

rg '2026-04-29T12:3[0-9]' app.log

request idを起点に読む例です。

rg -n 'req-123' app.log

JSON Linesからstatus codeごとの件数を出す例です。

jq -r '.["http.status_code"]' app.jsonl | sort | uniq -c | sort -nr

遅いrequestを上位から見る例です。

jq -r 'select(.duration_ms != null) | [.duration_ms, .request_id, .event] | @tsv' app.jsonl \
  | sort -nr \
  | head

error typeを集計する例です。

jq -r 'select(.level == "ERROR") | ."error.type"' app.jsonl \
  | sort \
  | uniq -c \
  | sort -nr

containerの標準出力ログを見る例です。

docker logs --since 30m api
docker logs --tail 200 api
docker compose logs -f api worker

Kubernetesではpodが入れ替わるため、現在のpodだけでなく、直前に落ちたcontainerのログも見ることがあります。

kubectl logs deploy/api --since=30m
kubectl logs pod/api-xxxxx --previous

典型的な読み方

ログを読むときは、検索式をいきなり複雑にしない方が安定します。まず広く見て、少しずつ狭めます。

500エラーが増えた

jq 'select(.["http.status_code"] >= 500)' app.jsonl

次にendpoint、error type、外部依存先で分けます。

jq -r 'select(.["http.status_code"] >= 500) | [.["http.route"], ."error.type", .provider] | @tsv' app.jsonl \
  | sort \
  | uniq -c \
  | sort -nr

latencyが悪化した

平均ではなく、p95やp99に近い「遅い上位」を見ます。

jq -r 'select(.duration_ms != null) | [.duration_ms, .trace_id, .event] | @tsv' app.jsonl \
  | sort -nr \
  | head -50

遅いrequestの trace_id を取り、traceでDB、cache、外部APIのどこが遅いか見ます。

ユーザー問い合わせ

問い合わせ対応では、user id、request id、時刻の3つが入口になります。

jq 'select(.user_id == "u_456")' app.jsonl

ただし、個人情報をログに直接入れすぎると別の問題になります。emailや氏名ではなく、内部IDで追える設計が安全です。

batchやjobの失敗

非同期処理では、HTTP request idだけでは追えません。job_idqueueattemptretry_countdead_letter のようなfieldがあると読みやすくなります。

{
  "level": "ERROR",
  "event": "job_failed",
  "job_id": "job-789",
  "queue": "email",
  "attempt": 3,
  "retry_count": 2,
  "error.type": "SMTPTimeout"
}

よいログを書く

よいログの条件です。

  • event名が安定している
  • timestampがある
  • request idがある
  • user idなどの調査キーがある
  • エラー種別がある
  • secretや個人情報を出しすぎない
  • 機械で集計しやすい

悪い例です。

failed

良い例です。

{"level":"ERROR","event":"payment_failed","request_id":"req-123","provider":"stripe","reason":"timeout"}

ログに入れてはいけないもの

ログは長く保存され、広い範囲の人やシステムに読まれることがあります。そのため、アプリケーション本体よりも漏えい時の影響が大きくなる場合があります。

直接入れない方がよいものです。

種類 対応
secret password、API token、private key 記録しない
session情報 session id、refresh token 記録しない、必要ならhash化
決済情報 card number、CVV 記録しない
sensitive PII 健康情報、政府ID 記録しない
connection string DB URL、credential入りDSN maskする
request body passwordやtokenを含む可能性 allowlist方式で一部だけ
source code stack traceに出る断片 必要性を検討

安全なログ設計では「何を消すか」より「何だけ残すか」を決めます。denylist方式で password だけ消すと、passwdtokensecretauthorization のような別名を取り逃がします。

flowchart TD A["request"] --> B{"allowlist field?"} B -- yes --> C["logに残す"] B -- no --> D["捨てる"] C --> E["mask / hashが必要か確認"]

maskの例です。

{
  "event": "login_failed",
  "user_id": "u_123",
  "email_domain": "example.com",
  "ip_prefix": "203.0.113.0/24"
}

問い合わせ対応に必要な情報は残しつつ、個人を直接特定する値やcredentialは避けます。

ログ注入と改ざん

ログは攻撃対象にもなります。ユーザー入力をそのままログに入れると、改行や区切り文字を使って偽のログ行を作られることがあります。

悪い例です。

login failed: user=alice
ERROR admin login succeeded

これは、ユーザー名に改行を含められると起きるタイプの問題です。対策は、構造化ログにし、制御文字をescapeし、表示側でも適切にencodeすることです。

{"level":"WARN","event":"login_failed","username":"alice\\nERROR admin login succeeded"}

ログの安全性は、主に4つの観点で見ます。

観点 リスク 対策
機密性 ログからsecretやPIIが漏れる mask、権限制御、暗号化
完全性 ログが改ざんされる append-only、署名、監査trail
可用性 ログ増加でdiskや基盤が詰まる rate limit、rotation、sampling
説明責任 誰が何をしたか追えない actor、action、objectを残す

ログ出力に失敗したときの動作も設計対象です。ログ基盤が落ちたからアプリ全体が停止するのか、bufferして続行するのか、重要イベントだけ同期的に書くのかは、システムの性質で変わります。

ログの保存とライフサイクル

ログは「出す」だけでなく「集める、保存する、検索する、消す」までが運用です。

flowchart LR A["application stdout"] --> B["agent / collector"] B --> C["buffer"] C --> D["log store"] D --> E["query / alert"] D --> F["retention / archive / delete"]

考えるべき項目です。

項目 観点
出力先 12-Factorではstdout/stderrにイベントstreamとして出す考え方が基本
収集 agent、sidecar、collector、managed service
buffer ネットワーク断や一時的な基盤障害への耐性
retention 何日検索可能にするか
archive 監査用に低コスト保存するか
削除 個人情報や規約上の保存期限
access control 誰がどのログを読めるか
cost cardinality、保存量、index量

ログは便利なので増えがちですが、保存量、index、検索負荷はコストになります。DEBUG を本番で常時出す、request bodyを丸ごと保存する、高cardinality fieldを無制限にindexする、といった設計は早めに効いてきます。

ログのライフサイクルは、用途ごとに分けます。

用途 保存期間の考え方
障害調査 直近を高速検索できることが重要
セキュリティ監査 改ざん耐性と長期保存が重要
分析 集計済みデータに落とす方がよい場合がある
debug 短期間で十分なことが多い

ログとメトリクスとトレース

Observabilityでは、ログ、メトリクス、トレースを分けて考えます。

種類 得意なこと
ログ 個別イベントの詳細 エラー内容、request id
メトリクス 数値の傾向 p95 latency, error rate
トレース requestの経路 API -> DB -> external API
flowchart LR Incident["障害"] --> Metrics["メトリクスで気づく"] Metrics --> Trace["トレースで経路を見る"] Trace --> Logs["ログで詳細を読む"]

ログだけで監視しようとすると、件数集計やしきい値管理が難しくなります。一方で、メトリクスだけでは個別の失敗理由が分かりません。役割を分けると調査が速くなります。

SREの文脈では、ユーザー影響に近いsignalを重視します。代表的にはlatency、traffic、errors、saturationです。

signal ログで見る例 メトリクスで見る例
latency 遅いrequestの詳細 p95、p99
traffic endpoint別request RPS
errors 失敗requestの理由 error rate
saturation queue待ち、connection不足 CPU、memory、queue length

ログは「なぜ」を読むのが得意で、メトリクスは「どれくらい」を見るのが得意です。ログだけでerror rateを毎回集計するより、error rateはメトリクスで監視し、異常が出たらログで詳細を読む方が自然です。

ログ基盤の見方

ログ基盤を見るときは、検索UIの使い方だけでなく、保存の仕組みを少し知っておくと迷いにくくなります。

基盤の考え方 特徴
full-text index Elasticsearch field検索や全文検索に強い
label + chunk Loki labelで絞り、本文を読む設計
columnar / analytics BigQuery、ClickHouse 大量集計に強い
object storage archive S3など 安いが即時検索は弱い

Lokiのような基盤では、labelを増やしすぎるとcardinalityが高くなります。serviceenvcluster のような低cardinality fieldはlabelに向きますが、request_iduser_id はlabelに向きません。

flowchart TD A["低cardinality<br>service/env/cluster"] --> B["label向き"] C["高cardinality<br>request_id/user_id"] --> D["log body field向き"]

Elastic Common Schemaのような共通field体系を参考にすると、event.actionhttp.request.methodurl.pathuser.idtrace.id のように、後から横断検索しやすい名前を選べます。独自fieldを作る場合も、命名規則をチーム内で固定することが大切です。

ログ設計チェックリスト

ログを追加するときは、次を確認します。

  • event名は安定しているか
  • request id / trace idが入っているか
  • user idなど調査キーが入っているか
  • secretや個人情報を出していないか
  • 失敗理由が機械で分類できるか
  • retry回数や外部依存先が分かるか
  • durationが必要な箇所に入っているか
  • levelは適切か
  • timestampはtimezoneを含んでいるか
  • field名は既存の命名規則と一致しているか
  • 改行や制御文字がescapeされるか
  • high cardinality fieldをlabel/indexにしすぎていないか
  • retentionとアクセス権限が用途に合っているか

悪いログは、調査時に「何か失敗した」以上の情報をくれません。

error happened

よいログは、次に見るべき場所を教えてくれます。

{
  "level": "ERROR",
  "event": "external_api_timeout",
  "service": "checkout",
  "provider": "payment",
  "request_id": "req-123",
  "duration_ms": 3000,
  "retry_count": 2
}

OpenTelemetryのログモデル

OpenTelemetry (OTel, RFC 8446相当) では、ログを統一的に扱うため以下の構造を推奨:

ResourceAttributes: サービス・環境情報
  - service.name: "checkout"
  - service.version: "1.2.3"
  - deployment.environment: "production"

InstrumentationScope: ライブラリ・モジュール
  - name: "payment_processor"
  - version: "0.5.0"

LogRecord: ログレコード本体
  - timestamp: 2024-01-15T10:30:45.123Z
  - severity_number: 11 (ERROR)
  - body: "Payment processing failed"
  - attributes:
      - trace.id: "4bf92f3577b34da6a3ce929d0e0e4736"
      - span.id: "00f067aa0ba902b7"
      - http.method: "POST"
      - http.status_code: 500
      - exception.type: "TimeoutException"
      - exception.message: "Failed to connect within 3000ms"

トレースID と span ID を含むことで、distributed tracing システム全体で request を追跡可能になります。

Elastic Common Schema (ECS) との整合

ECS では以下のように field を標準化:

{
  "@timestamp": "2024-01-15T10:30:45.123Z",
  "log.level": "error",
  "log.logger": "payment_processor",
  "message": "Payment processing failed",
  "event.action": "payment_timeout",
  "event.outcome": "failure",
  "service.name": "checkout",
  "service.environment": "production",
  "trace.id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span.id": "00f067aa0ba902b7",
  "http.request.method": "POST",
  "http.response.status_code": 500,
  "http.response.body.content": "Internal Server Error",
  "error.type": "TimeoutException",
  "error.message": "Failed to connect within 3000ms",
  "error.stack_trace": "at PaymentGateway.process(...)",
  "host.name": "prod-api-001",
  "process.pid": 12345
}

ECS に従うことで、Elasticsearch、Datadog、Splunk など複数の基盤でも同じ field 名で検索可能。

ログ基盤別の検索例

Elasticsearch Query DSL

{
  "query": {
    "bool": {
      "must": [
        { "term": { "service.name": "checkout" } },
        { "range": { "@timestamp": { "gte": "now-1h" } } }
      ],
      "filter": [
        { "term": { "log.level": "ERROR" } },
        { "wildcard": { "error.type": "Timeout*" } }
      ]
    }
  },
  "aggs": {
    "errors_per_minute": {
      "date_histogram": {
        "field": "@timestamp",
        "interval": "1m"
      },
      "aggs": {
        "top_errors": {
          "terms": { "field": "error.type", "size": 5 }
        }
      }
    }
  }
}

Grafana Loki LogQL

{service="checkout", env="production"} 
| json 
| error_count > 10
| stats count() as errors by error_type

LogQL の LogQL の基本:

  • {label="value"} で絞り込み(低cardinality)
  • | json で JSON parse
  • | regexp "pattern" で行フィルタ
  • stats で集計

12 Factor App のログ設計

12factor.net では以下を推奨:

  1. ログは事象のストリーム
    • ファイルに書き込まず stdout へ
    • ランタイム環境がリダイレクト・保存
  2. 構造化ログ (JSON preferred)
  3. タイムスタンプは ISO 8601 + timezone
  4. ログレベルの使い分け
    • ERROR: ユーザーに影響
    • WARN: 潜在的な問題、復旧可能
    • INFO: 主要イベント(起動、デプロイ)
    • DEBUG: 開発デバッグ用

ログのセキュリティ設計

出力してはいけない情報

OWASP Logging Cheat Sheet より:

× Password, API keys, tokens
× Social Security Number, 医療情報
× Session identifier, authentication token
× Database connection string
× Credit card numbers
× Protocol headers (Authorization など)
× User-controlled input (ユーザー入力はエスケープ必須)

アクセス制御

Production ログ:
  - 読取権限: SRE, DevOps, 例外時のDev
  - 保持期間: 30-90日
  - 外部転送: 禁止またはマスキング後のみ

Debug/staging ログ:
  - より詳細な情報を含めてもOK
  - 短期保持(7-30日)

エスケープとインジェクション対策

ログに user input を含める場合:

import json

# 悪い例
log_msg = f"user login: {username}"  # injection 対象

# 良い例
log_entry = {
    "event": "user_login",
    "username": username,  # field として分離
    "timestamp": datetime.utcnow().isoformat()
}
logger.info(json.dumps(log_entry))

JSON 形式なら field が自動的に escapeされます。

パフォーマンスと容量

ログレベル別のボリューム

典型的な本番環境:

DEBUG: 50-100 msg/sec  (開発環境のみ)
INFO:  5-20 msg/sec    (主要イベント)
WARN:  0.1-1 msg/sec   (問題の兆候)
ERROR: 0.01-0.1 msg/sec

1秒あたり 10 msg の環境で、平均 1KB/msg なら 864 GB/日Elasticsearch 等では以下で容量を管理:

日次rotation: リスク
- 昨日分へのアクセスは遅い
- 検索スパンが日をまたぐと複数indexクエリ

時間別rollover: 推奨
- 2時間単位 (or 50GB単位) で自動rotate
- 古い時間帯のindex は自動削除
- SSD / HDD 階層化で cost 削減

Loki では label cardinality を制御することが重要:

良い例:
{service="checkout", env="prod"}
→ index: 2 label values

悪い例:
{service="checkout", user_id="1000"}
→ index: 数百万 label values (out of memory)

ロギング戦略の実装パターン

構造化ロギングの実装

import json
import logging
from datetime import datetime
import uuid

class StructuredLogFormatter(logging.Formatter):
    """JSON形式の構造化ログを出力"""
    
    def format(self, record):
        log_entry = {
            "timestamp": datetime.utcnow().isoformat(),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
            "pathname": record.pathname,
            "line_number": record.lineno
        }
        
        # カスタム属性があれば追加
        if hasattr(record, 'request_id'):
            log_entry['request_id'] = record.request_id
        if hasattr(record, 'trace_id'):
            log_entry['trace_id'] = record.trace_id
        if hasattr(record, 'user_id'):
            log_entry['user_id'] = record.user_id
        if hasattr(record, 'duration_ms'):
            log_entry['duration_ms'] = record.duration_ms
        
        return json.dumps(log_entry)

# ロガー設定
logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
handler.setFormatter(StructuredLogFormatter())
logger.addHandler(handler)

# 使用例
def process_order(order_id, user_id):
    request_id = str(uuid.uuid4())
    trace_id = str(uuid.uuid4())
    start_time = time.time()
    
    try:
        # イベント発生時、コンテキスト情報を付与
        extra_info = {
            'request_id': request_id,
            'trace_id': trace_id,
            'user_id': user_id
        }
        
        logger.info(f"Order processing started: {order_id}", extra=extra_info)
        
        # ビジネスロジック
        result = charge_payment(order_id)
        
        duration_ms = (time.time() - start_time) * 1000
        extra_info['duration_ms'] = duration_ms
        logger.info(f"Order processed successfully: {order_id}", extra=extra_info)
        
        return result
        
    except Exception as e:
        extra_info['error'] = str(e)
        extra_info['error_type'] = type(e).__name__
        logger.error(f"Order processing failed: {order_id}", extra=extra_info)
        raise

ログレベルの実装基準

# DEBUG (10): 開発時のデバッグ情報のみ
logger.debug(f"Variable x={x}, y={y}")  # 本番では無効化

# INFO (20): ビジネスイベント、状態変化
logger.info(f"User registered: {user_id}")
logger.info(f"Payment completed: amount={amount}")

# WARNING (30): 復旧可能なエラー、非推奨の使用
logger.warning(f"Slow query detected: {duration_ms}ms > {threshold}ms")
logger.warning(f"Retry attempt {attempt_count}/3")

# ERROR (40): エラー発生、サービス機能は停止するが他は継続
logger.error(f"Database connection failed", exc_info=True)

# CRITICAL (50): システムが動作継続不可
logger.critical(f"Out of memory")

OpenTelemetry との連携

OpenTelemetry はログ、メトリクス、トレースを統一的に扱う標準:

from opentelemetry import logging as otel_logging, trace
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

# Tracer設定(分散トレース)
jaeger_exporter = JaegerExporter(
    agent_host_name="localhost",
    agent_port=6831,
)
trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(
    BatchSpanProcessor(jaeger_exporter)
)

tracer = trace.get_tracer(__name__)

def process_order(order_id):
    """OpenTelemetry トレース付き"""
    with tracer.start_as_current_span("process_order") as span:
        span.set_attribute("order.id", order_id)
        
        # 子スパン1: 決済処理
        with tracer.start_as_current_span("payment_service") as payment_span:
            payment_span.set_attribute("amount", 99.99)
            result = charge_payment(order_id)
        
        # 子スパン2: 配送処理
        with tracer.start_as_current_span("shipping_service") as shipping_span:
            shipping_span.set_attribute("address", "...")
            ship_order(order_id)
        
        # トレースは自動的にJaeger(またはDatadog等)に送信される

ログ管理の実務パターン

ELK Stack (Elasticsearch, Logstash, Kibana)

アプリケーション
    ↓
   Filebeat (ログ収集)
    ↓
  Logstash (解析・変換)
    ↓
Elasticsearch (インデックス・保存)
    ↓
   Kibana (検索・可視化)

設定例:

# filebeat.yml
filebeat.inputs:
  - type: log
    enabled: true
    paths:
      - /var/log/app/*.log

processors:
  - add_kubernetes_metadata:

output.elasticsearch:
  hosts: ["elasticsearch:9200"]
  index: "app-%{+yyyy.MM.dd}"

# Kibana でのクエリ例
GET app-*/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {"level": "ERROR"}},
        {"range": {"@timestamp": {"gte": "now-1h"}}}
      ]
    }
  },
  "aggs": {
    "error_by_service": {
      "terms": {"field": "service", "size": 10}
    }
  }
}

Grafana Loki でのログ検索

# ラベルによるフィルタ(インデックス効率的)
{service="checkout", env="prod"} 
| json

# フィルタ: ERROR レベルのみ
{service="checkout"} | json | level="ERROR"

# 統計:エラーレート
{service="checkout"} 
| json 
| level="ERROR" 
| unwrap duration_ms 
| rate(5m)

# 複数ラベルの分布
{service="checkout"}
| json
| stats count by level, error_type

OWASP ロギング・チートシート

OWASP Logging Cheat Sheet から実務基準:

何を記録するか:
✓ 認証・認可の成功・失敗
✓ データアクセス(誰が、何を、いつ)
✓ 例外・エラーの詳細
✓ セキュリティイベント(ログイン失敗、権限昇格試行)
✗ パスワード、APIキー、PII(個人識別情報)
✗ セッショントークン(本体)

保存期間:
- セキュリティイベント: 最低90日
- アクセスログ: 30日~1年
- エラーログ: 7~30日
- 監査ログ: 1年~7年(コンプライアンス要件による)

保護:
- ログの改ざん防止: デジタル署名, WORM(Write Once Read Many)
- アクセス制限: 最小権限の原則
- 暗号化: 転送中(TLS), 保存時(AES-256)

ログローテーション戦略の比較

戦略        | 単位      | 利点              | 欠点
------      | ----      | ----              | ----
時間ベース  | 1時間     | 予測可能          | サイズ不均
サイズベース| 100MB     | 容量コントロール  | バースト対応弱い
ハイブリッド| 1時間+50MB| サイズと時間両立  | 設定複雑

設定例:

# logrotate 設定
/var/log/app/*.log {
    daily                    # 日次ローテーション
    rotate 30               # 30世代保持
    compress                # gzip圧縮
    delaycompress          # 翌日圧縮開始
    missingok              # ファイルなくても続行
    notifempty             # 空ファイルはスキップ
    create 0640 app appgroup  # 新ファイル権限
    postrotate            # ローテーション後実行
        systemctl reload app
    endscript
}

パフォーマンス・コスト最適化

ログ出力時のパフォーマンス低下

# 悪い例: 毎ループで複雑な文字列構築
for i in range(1000000):
    logger.debug(f"Processing item {i} with data {expensive_function()}")
    # expensive_function()は毎回実行される!

# 良い例: ログレベルで判定
if logger.isEnabledFor(logging.DEBUG):
    logger.debug(f"Processing item {i}")  # DEBUGが有効な時だけ実行

# ベストプラクティス: 遅延評価
logger.debug(f"Processing: %s", expensive_function)  # %記法で遅延
logger.debug("Processing: %s", lazy_string())  # 関数は呼び出さない

ログ保存コスト削減

ログ量削減戦略:
1. DEBUG・TRACE ログを本番で無効化
2. リクエスト成功時は要約ログのみ
3. サンプリング: 成功リクエスト10%、エラー100%
4. ログレベル別保持期間: ERROR 90日、INFO 30日、DEBUG 7日
5. 圧縮: gzip → 元サイズの10%
6. ストレージ階層化: ホット(直近7日) → コールド(90日)

コスト試算:
- ログ生成: 1リクエスト 1KB
- 日100万リクエスト: 1GB/日
- 圧縮後: 100MB/日
- 30日保持: 3GB × $0.023/GB/月 = $0.07/月
- 年額: $1未満

まとめ

ログは、障害時に過去を読むための観測データです。timestamp、level、event、request id、trace id、構造化されたkey-valueをそろえると、調査と運用が大きく楽になります。一方で、ログは漏えい、改ざん、コスト増の入口にもなるため、何を残すか、何を残さないか、どれくらい保存するかまで設計する必要があります。

参考文献

公式・標準

講義・記事

書籍

解説・補助