ログの読み方
目次
- 概要
- ログは何を残すものか
- ログレベル
- 構造化ログ
- trace idとcorrelation id
- trace contextを読む
- 障害調査の流れ
- CLIで読む
- 典型的な読み方
- よいログを書く
- ログに入れてはいけないもの
- ログ注入と改ざん
- ログの保存とライフサイクル
- ログとメトリクスとトレース
- ログ基盤の見方
- ログ設計チェックリスト
- OpenTelemetryのログモデル
- ログ基盤別の検索例
- ログのセキュリティ設計
- パフォーマンスと容量
- ロギング戦略の実装パターン
- ログ管理の実務パターン
- パフォーマンス・コスト最適化
- まとめ
- 参考文献
概要
ログは、システムが何をしたかを後から読むための記録です。障害調査、性能分析、セキュリティ監査、ユーザー問い合わせ対応の出発点になります。
ログは「何が起きたか」だけでなく、「いつ、どこで、誰に関係し、どの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 にすべてを詰め込まないことです。provider、duration_ms、http.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_id、requestId、req_id が混在すると、検索式もdashboardも壊れやすくなります。
trace idとcorrelation id
分散システムでは、1つのrequestが複数サービスを通ります。
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_id と span_id が入っていれば、trace画面から該当ログへ飛べます。逆にログからtraceを開くこともできます。
分散システムの障害調査では、ログだけを読むより、traceと往復した方が速いことがあります。どのserviceで遅くなったかをtraceで見て、そのserviceの内部状態をログで読む、という流れです。
障害調査の流れ
ログ調査は、仮説を立てて絞り込む作業です。
最初から全ログを読むのではなく、時間、service、request id、status codeで絞ります。
障害調査では「事実」と「仮説」を混ぜないようにします。
| 段階 | 見るもの | 目的 |
|---|---|---|
| 影響確認 | error rate、status code、対象ユーザー | どれくらい起きているか |
| 時間特定 | deploy時刻、alert時刻、最初のerror | いつからか |
| 経路特定 | trace、service、endpoint | どこで起きたか |
| 原因候補 | error.type、直前ログ、外部依存 | なぜ起きたか |
| 再発防止 | missing field、alert不足、runbook不足 | 次に速く気づけるか |
読み方のコツは、最初に「正常な1件」と「失敗した1件」を並べることです。
失敗ログだけを読むと、普段から出ている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
ログが大きい場合は、less、rg、awk、jq を組み合わせます。
時間帯で切る例です。
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_id、queue、attempt、retry_count、dead_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 だけ消すと、passwd、token、secret、authorization のような別名を取り逃がします。
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して続行するのか、重要イベントだけ同期的に書くのかは、システムの性質で変わります。
ログの保存とライフサイクル
ログは「出す」だけでなく「集める、保存する、検索する、消す」までが運用です。
考えるべき項目です。
| 項目 | 観点 |
|---|---|
| 出力先 | 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 |
ログだけで監視しようとすると、件数集計やしきい値管理が難しくなります。一方で、メトリクスだけでは個別の失敗理由が分かりません。役割を分けると調査が速くなります。
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が高くなります。service、env、cluster のような低cardinality fieldはlabelに向きますが、request_id や user_id はlabelに向きません。
Elastic Common Schemaのような共通field体系を参考にすると、event.action、http.request.method、url.path、user.id、trace.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 では以下を推奨:
- ログは事象のストリーム
- ファイルに書き込まず stdout へ
- ランタイム環境がリダイレクト・保存
- 構造化ログ (JSON preferred)
- タイムスタンプは ISO 8601 + timezone
- ログレベルの使い分け
- 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をそろえると、調査と運用が大きく楽になります。一方で、ログは漏えい、改ざん、コスト増の入口にもなるため、何を残すか、何を残さないか、どれくらい保存するかまで設計する必要があります。
参考文献
公式・標準
- OpenTelemetry: Logs Data Model
- OpenTelemetry Documentation
- OpenTelemetry: Logs
- OWASP Logging Cheat Sheet
- W3C Trace Context