オブザーバビリティとモニタリング
目次
- 概要
- 1. オブザーバビリティとは何か
- 2. モニタリングの進化と歴史
- 3. オブザーバビリティの4本柱(メトリクス・ログ・トレース・プロファイル)
- 4. オブザーバビリティ vs モニタリング vs テレメトリ
- 5. メトリクスの基礎と設計
- 6. Prometheus
- 7. PromQL詳細リファレンス
- 8. Grafanaダッシュボード設計
- 9. ログ管理の基礎
- 10. Loki
- 11. LogQLリファレンス
- 12. ELKスタック(Elasticsearch/Logstash/Kibana)
- 13. 構造化ログ設計
- 14. 分散トレーシングの基礎
- 15. OpenTelemetry
- 16. Jaeger / Tempo 実践ガイド
- 17. 継続的プロファイリング
- 18. Pyroscope / Parca 実践
- 19. アラート設計の理論と実践
- 20. SLOベースのアラート(Burn Rate)
- 21. PagerDuty / OpsGenie 設計
- 22. Grafana統合スタック(LGTM)
- 23. OpenTelemetry Collector
- 24. 長期ストレージ(Thanos / Mimir)
- 25. Kubernetes環境のオブザーバビリティ
- 26. サービスメッシュのオブザーバビリティ
- 27. eBPFによるカーネルレベル観測
- 28. RUM(Real User Monitoring)とWeb Vitals
- 29. 合成監視(Synthetic Monitoring)
- 30. AIOpsと異常検知
- 31. オブザーバビリティのコスト最適化
- 32. マイクロサービスのオブザーバビリティ
- 33. 具体的な障害パターンとデバッグ手順
- 34. SRE視点のオブザーバビリティ実践
- 35. ツール選定ガイドと比較
- 付録A: よく使うコマンドリファレンス
- 付録B: アラートルールのベストプラクティス集
- 付録C: オブザーバビリティ成熟度チェックリスト
- まとめ
- 参考文献
概要
まず、この章の中心構造を図で確認します。細部に入る前に、どの概念がどこへつながるかをつかむための地図です。
コード例は、そのまま写すためだけのものではありません。直前の本文で「何を確かめる例か」を押さえ、直後の説明で「どの性質が見えるか」を確認してください。実務では、ここに入力の境界、失敗時の挙動、依存する実行環境を足して読むと判断しやすくなります。
オブザーバビリティは、メトリクス・ログ・トレース・プロファイルなどのテレメトリから、システム内部の状態を調査できるようにする設計です。
モニタリングは既知の異常を検知するための仕組みであり、オブザーバビリティは未知の問題を調査できる状態を作るための考え方です。現代の分散システムでは、単一のCPU使用率やエラー率だけでは原因に届きません。リクエスト、サービス、ログ、メトリクス、デプロイイベントをつなげて読むことが重要です。
この章で重視すること
- メトリクス・ログ・トレース・プロファイルの役割を分けて理解する
- Prometheus、Grafana、Loki、Tempo、OpenTelemetryを一つの観測基盤として捉える
- アラートをユーザー影響とSLOに結びつける
- コスト、カーディナリティ、サンプリングを設計上の制約として扱う
1. オブザーバビリティとは何か
この章で重視すること
- オブザーバビリティの定義と語源
- なぜ現代システムにオブザーバビリティが必要なのか
- オブザーバビリティが解決する問題
- 「観測可能なシステム」の特性
- オブザーバビリティ成熟度モデル
1.1 オブザーバビリティの定義
オブザーバビリティ(Observability) という概念はもともと制御理論から来ている。1960年、数学者Rudolf E. Kálmánは「システムの外部出力から内部状態を推測できる度合い」としてこの言葉を定義した。
ソフトウェアエンジニアリングにおいて、この概念は次のように解釈される:
オブザーバビリティとは、システムが生成する外部シグナル(メトリクス・ログ・トレース)を分析することで、システム内部で何が起きているかを理解できる能力である。
重要な点は「理解できる」という部分だ。単に「何かがおかしい」と検知するだけでなく、「なぜおかしいのか」「どこで問題が発生しているのか」を外部からの観測だけで推測できることがオブザーバビリティの本質である。
オブザーバビリティの3つの問いかけ
| 問い | 低いオブザーバビリティ | 高いオブザーバビリティ |
|---|---|---|
| 何が起きているか | アラートで「エラーが増えた」とわかる | どのエンドポイント、どのユーザー層で何%増えたかわかる |
| なぜ起きているか | ログを手動で検索するしかない | トレースで根本原因のサービス・行を特定できる |
| どう直すか | 経験と勘に頼る | 過去の類似インシデントと自動で関連付けられる |
1.2 現代システムの複雑性とオブザーバビリティの必要性
2010年代以前のシステムはシンプルだった。3層アーキテクチャ(フロントエンド、アプリケーション、データベース)で構成され、問題が起きれば原因の候補は限られていた。
現代のシステムは根本的に異なる:
【現代のマイクロサービスアーキテクチャの例】
[CDN / Edge]
|
[API Gateway / Load Balancer]
/ | | | \
[Auth] [User] [Order] [Payment] [Notify]
| | | | |
[DB] [DB] [DB] [DB] [Queue]
| |
[Inventory] [Email]
|
[DB]
このような環境では:
- 単一リクエストが数十のサービスを経由する
- 障害が連鎖的に伝播する(Cascading Failures)
- デプロイが頻繁に行われる(1日に何十回も)
- インフラが動的に変化する(オートスケール、コンテナの生死)
従来の「監視」アプローチでは、こうした環境での問題解決は困難だ。「CPU使用率が高い」というアラートを受けても、どのコンテナの、どのコードパスで、どのユーザーリクエストを処理しているときに高くなるのかがわからなければ、対処のしようがない。
1.3 オブザーバビリティが解決する問題
問題1: Unknown Unknowns(未知の未知)
従来のモニタリングは「Known Unknowns」—つまり、起きると予測できる問題—にしか対応できない。CPUが閾値を超えたらアラート、エラーレートが増えたらアラート、という形だ。
しかし実際の障害の多くはUnknown Unknowns—予測していなかった問題の組み合わせ—から発生する。
例:
- マイクロサービスAのメモリリークが、ガベージコレクションの一時停止を増やし、マイクロサービスBへのレスポンスタイムを悪化させ、リトライが増加し、マイクロサービスCへの負荷が増大する
このような「誰も予測していなかった問題」を調査するには、高いオブザーバビリティが必要だ。
問題2: 高いカーディナリティへの対応
現代のシステムでは、問題が「どのユーザーIDで」「どのリージョンで」「どのバージョンで」発生しているかを知ることが重要だ。これには「高カーディナリティ」なデータの探索能力が必要になる。
# 高カーディナリティの例
# ユーザーID(数百万)× リージョン(数十)× サービスバージョン(数十)
# = 数十億の組み合わせ
# 従来の監視では事前集計しか保存できない
avg(response_time) # 全体平均しかわからない
# 高いオブザーバビリティでは
# 任意の次元での任意の絞り込みが可能
response_time{user_tier="premium", region="ap-northeast-1", version="2.3.1"}
問題3: デプロイとの相関
問題がデプロイ後に発生した場合、どのデプロイが原因かを特定する必要がある。オブザーバビリティ基盤はデプロイイベントとメトリクスを相関させ、「このデプロイ後にエラーレートが0.1%から2%に増加した」と即座に判断できる。
1.4 オブザーバビリティ成熟度モデル
システムのオブザーバビリティは段階的に向上する。以下のモデルを参考に、現在地を把握し、次のステップを計画しよう。
レベル5: プロアクティブ(予測的)
├── 機械学習による異常検知
├── SLOバーンレートによる予測アラート
└── カオスエンジニアリングとの統合
レベル4: 相関分析
├── メトリクス・ログ・トレースの横断的分析
├── 自動根本原因分析(RCA)
└── デプロイとメトリクス変化の自動相関
レベル3: 分散トレーシング
├── エンドツーエンドのリクエスト追跡
├── サービス依存関係の自動マッピング
└── レイテンシのボトルネック特定
レベル2: 構造化ログ + メトリクス
├── JSON形式のログ
├── アプリケーションメトリクス
└── 基本的なダッシュボード
レベル1: 基本監視
├── インフラメトリクス(CPU・メモリ)
├── 非構造化ログ
└── 単純な閾値ベースアラート
レベル0: なし
└── ログなし、監視なし
| レベル | 特徴 | MTTR(平均復旧時間) |
|---|---|---|
| 0 | ログなし・監視なし | 数時間〜数日 |
| 1 | 基本監視 | 1〜4時間 |
| 2 | 構造化ログ・アプリメトリクス | 30分〜1時間 |
| 3 | 分散トレーシング | 10〜30分 |
| 4 | 相関分析・自動RCA | 5〜15分 |
| 5 | 予測的オブザーバビリティ | 1〜5分(または予防) |
1.5 オブザーバビリティ実現のための3つの原則
原則1: テレメトリデータの3本柱(+ α)
高いオブザーバビリティを実現するには、メトリクス・ログ・トレース(そして最近ではプロファイル)の4種類のデータを適切に組み合わせる必要がある。
┌─────────────────────────────────────────────────────┐
│ オブザーバビリティの柱 │
├─────────────┬──────────────┬──────────────┬─────────┤
│ Metrics │ Logs │ Traces │Profiles │
│ メトリクス │ ログ │ トレース │プロファイル│
├─────────────┼──────────────┼──────────────┼─────────┤
│ 何が起きて │ 何が起きたか │ どこで起きて │ なぜ重い │
│ いるか │ の詳細記録 │ いるか │ のか │
├─────────────┼──────────────┼──────────────┼─────────┤
│ Prometheus │ Loki/ES │ Tempo/Jaeger │Pyroscope│
└─────────────┴──────────────┴──────────────┴─────────┘
原則2: コンテキストの維持
各シグナルは単体では不完全だ。メトリクスでエラーレートの上昇を検知したとき、関連するログやトレースに即座にジャンプできることが重要だ。これを テレメトリの相関(Correlation) という。
原則3: カーディナリティと粒度の設計
データの粒度が粗すぎると問題を見落とし、細かすぎるとコストと運用負荷が増大する。適切な設計が必要だ。
1.6 まとめ
- オブザーバビリティは「システム内部を外部から推測できる能力」
- 現代の分散システムでは、監視だけでは不十分でオブザーバビリティが必要
- Unknown Unknownsへの対応、高カーディナリティの探索、デプロイとの相関が主な価値
- 成熟度モデルを参考に段階的に向上させる
- メトリクス・ログ・トレース・プロファイルの4本柱を組み合わせる
2. モニタリングの進化と歴史
この章で重視すること
- システム監視の歴史的変遷(1990年代〜現在)
- 各時代のツールとアプローチ
- クラウドネイティブ時代のモニタリング課題
- オブザーバビリティへの必然的な移行
2.1 第1世代: ポーリングベース監視(1990年代〜2000年代初頭)
Nagios時代
1999年にリリースされた Nagios は、システム監視ツールの原型を作った。その仕組みはシンプルで強力だった:
# Nagiosの基本的な動作
while True:
for host in monitored_hosts:
status = check_host(host) # ping, HTTP, TCP等のチェック
if status != OK:
send_alert(host, status)
sleep(60) # 1分間隔でポーリング
特徴:
- プラグインベース:
check_http、check_ping、check_diskなどのプラグインでチェック - ステータスベース: OK / WARNING / CRITICAL / UNKNOWN の4状態
- 受動的通知: 問題が起きてから通知
# nagios.cfg 設定例
define host {
use linux-server
host_name web-server-01
alias Web Server 01
address 192.168.1.10
check_command check-host-alive
max_check_attempts 3
check_interval 5
retry_interval 1
notification_interval 30
}
define service {
use generic-service
host_name web-server-01
service_description HTTP
check_command check_http!-p 80
check_interval 5
retry_interval 1
}
限界:
- スケールしない(数百台が限界)
- メトリクスの時系列保存ができない
- アプリケーション内部の可視性ゼロ
- 設定が複雑で保守が困難
2.2 第2世代: RRDtoolとグラフ化(2000年代)
Nagiosに時系列グラフ機能を追加した Cacti(2001年)や Munin(2004年)が登場した。これらは RRDtool(Round-Robin Database)を使って時系列データを保存し、グラフを自動生成した。
# RRDtoolの基本操作
# データベース作成(固定サイズ、古いデータは自動的に上書き)
rrdtool create traffic.rrd \
--step 300 \
DS:input:COUNTER:600:0:U \
DS:output:COUNTER:600:0:U \
RRA:AVERAGE:0.5:1:600 \
RRA:AVERAGE:0.5:6:700 \
RRA:AVERAGE:0.5:24:775
# データ更新
rrdtool update traffic.rrd N:1234567:891011
# グラフ生成
rrdtool graph traffic.png \
DEF:in=traffic.rrd:input:AVERAGE \
DEF:out=traffic.rrd:output:AVERAGE \
LINE1:in#00FF00:Input \
LINE1:out#FF0000:Output
この時代の課題:
- データ保存期間が固定(古いデータは粗い粒度に圧縮)
- ラベルやタグの概念なし
- クエリ言語が貧弱
- サーバー台数の増加に対応困難
2.3 第3世代: クラウド時代のメトリクス(2010年代前半)
AWSの登場(2006年)とクラウドの普及により、インフラが「コード」になった。このとき台頭したのが:
Graphite + StatsD(2008〜2012年)
# StatsDを使ったアプリケーションメトリクス送信
import statsd
client = statsd.StatsClient('localhost', 8125)
# カウンター
client.incr('web.requests.total')
# タイマー
with client.timer('db.query.duration'):
result = db.execute(query)
# ゲージ
client.gauge('queue.length', queue.size())
Graphiteはドット区切りのメトリクス名(web.requests.total)を使い、階層的にデータを管理した。しかしラベルの概念がなく、メトリクスの多次元分析ができなかった。
InfluxDB(2013年)
InfluxDBはタグ(ラベル)の概念を導入した最初のTSDB(時系列データベース)の一つだ:
-- InfluxQL でのクエリ
SELECT mean("response_time")
FROM "http_requests"
WHERE "method" = 'GET' AND "status" = '200'
AND time > now() - 1h
GROUP BY "endpoint", time(5m)
2.4 第4世代: Prometheus革命(2012年〜現在)
2012年、SoundCloudがGoogleのBorgmon(内部モニタリングシステム)にインスパイアされて開発した Prometheus が登場した。
Prometheusが革新的だった理由:
- Pullモデル: PrometheusがHTTPエンドポイントをスクレイピング(能動的取得)
- 多次元データモデル: メトリクス名 + ラベルの組み合わせ
- PromQL: 強力なクエリ言語
- サービスディスカバリ: Kubernetes、Consul、EC2等と自動統合
- アラートマネージャー: アラートのルーティング・グループ化・抑制
┌─────────────────────────────────────────────────────────┐
│ Prometheusエコシステム │
│ │
│ [Kubernetes] [Consul] [EC2] ← サービスディスカバリ │
│ ↓ ↓ ↓ │
│ [Pod/App] [Service] [VM] ← /metrics エンドポイント │
│ ↑ ↑ ↑ │
│ └────────────┴───────┘ │
│ スクレイピング(pull) │
│ ↓ │
│ [Prometheus Server] │
│ ┌──────────────────┐ │
│ │ TSDB(ローカル)│ │
│ │ Retrieval │ │
│ │ Rule Engine │ │
│ └──────────────────┘ │
│ ↓ ↓ │
│ [Alertmanager] [Grafana] │
└─────────────────────────────────────────────────────────┘
2016年にCNCF(Cloud Native Computing Foundation)に寄贈され、Kubernetesと並ぶクラウドネイティブ時代の標準モニタリングツールとなった。
2.5 第5世代: 分散トレーシングの登場(2010年代後半)
マイクロサービスの普及に伴い、単一サービスの監視では不十分になった。Googleが2010年に発表した Dapper(内部分散トレーシングシステム)の論文が業界に影響を与え、オープンソース実装が登場した。
| 年 | 出来事 |
|---|---|
| 2010 | Google、Dapper論文を発表 |
| 2012 | Twitter、Zipkinをオープンソース公開 |
| 2015 | OpenZipkinとして標準化が進む |
| 2016 | Uber、Jaegerを開発・公開(2017年にOSS化) |
| 2016 | OpenTracingプロジェクト発足(CNCF) |
| 2019 | OpenCensusとOpenTracingが統合、OpenTelemetry誕生 |
| 2021 | OpenTelemetry Traces仕様がGA(安定版)に |
| 2023 | OpenTelemetry MetricsがGA、Logsがbeta |
| 2025 | OpenTelemetryが事実上の業界標準に |
2.6 第6世代: 統合オブザーバビリティ(2020年代)
現在、業界は「モニタリングツールの集合体」から「統合オブザーバビリティプラットフォーム」への移行期にある。
Grafana LGTMスタック
Grafana Labsが提案する統合スタック:
L = Loki(ログ)
G = Grafana(可視化)
T = Tempo(トレース)
M = Mimir(メトリクスの長期保存)
これに OpenTelemetry Collector を加えた構成が、現代的なアーキテクチャとなっている。
主要クラウドベンダーの統合ソリューション
| ベンダー | サービス |
|---|---|
| AWS | CloudWatch / X-Ray / Managed Grafana |
| Google Cloud | Cloud Monitoring / Cloud Trace / Cloud Logging |
| Azure | Azure Monitor / Application Insights |
| Datadog | 統合SaaSオブザーバビリティプラットフォーム |
| Grafana Cloud | マネージドLGTMスタック |
2.7 モニタリング進化の全体像
1990s 2000s 2010s 2020s
│ │ │ │
Nagios Graphite Prometheus OpenTelemetry
(ポーリング) (時系列グラフ) (ラベルベース) (統合標準化)
│ │ │ │
└─手動設定 └─固定スキーマ └─サービスディスカバリ └─自動計装
Zipkin/Jaeger LGTM Stack
(分散トレーシング) AIops
2.8 まとめ
- 監視の歴史はNagiosのポーリングから始まり、時系列DB、Prometheus、統合オブザーバビリティへと進化
- 各世代の限界が次世代のイノベーションを生んだ
- 現代はOpenTelemetryを中心とした標準化の時代
- Grafana LGTMスタックが業界の事実上の標準として定着しつつある
3. オブザーバビリティの4本柱(メトリクス・ログ・トレース・プロファイル)
この章で重視すること
- メトリクス・ログ・トレース・プロファイルの特性と違い
- 各シグナルをいつ使うべきか
- 4本柱の相関と組み合わせ方
- 各シグナルに適したツール
3.1 第1の柱: メトリクス(Metrics)
定義と特性
メトリクスとは、時間とともに変化する数値データのことだ。特定の時刻における「状態のスナップショット」を集積したものと言える。
メトリクスの構造:
名前 + ラベル集合 + タイムスタンプ + 値
例:
http_requests_total{
method="GET",
endpoint="/api/users",
status="200",
service="user-service"
} 12345 @ 1715000000
メトリクスの4種類(Prometheusモデル)
Counter(カウンター)
# 単調増加する累積値
# リセットされるのはプロセス再起動時のみ
http_requests_total{status="200"} 102345
http_requests_total{status="500"} 234
# 使用例: リクエスト数、エラー数、バイト転送量
# PromQLでの使い方:
rate(http_requests_total{status="500"}[5m]) # 1秒あたりのエラー数
Gauge(ゲージ)
# 上下する瞬間値
node_memory_MemAvailable_bytes 4294967296 # 4GB
node_cpu_temperature_celsius{cpu="0"} 72.5
queue_pending_messages{queue="orders"} 1423
# 使用例: CPU使用率、メモリ使用量、キュー長
# PromQLでの使い方:
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100 # メモリ使用率%
Histogram(ヒストグラム)
# 値の分布を記録するためのバケット
http_request_duration_seconds_bucket{le="0.005"} 24054
http_request_duration_seconds_bucket{le="0.01"} 33444
http_request_duration_seconds_bucket{le="0.025"} 100392
http_request_duration_seconds_bucket{le="0.05"} 129389
http_request_duration_seconds_bucket{le="0.1"} 133988
http_request_duration_seconds_bucket{le="0.25"} 148970
http_request_duration_seconds_bucket{le="0.5"} 150001
http_request_duration_seconds_bucket{le="1"} 150001
http_request_duration_seconds_bucket{le="+Inf"} 150001
http_request_duration_seconds_sum 149.372337592
http_request_duration_seconds_count 150001
# p99レイテンシを計算:
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
Summary(サマリー)
# クライアント側でパーセンタイルを計算(分散集計不可)
rpc_duration_seconds{quantile="0.5"} 0.012
rpc_duration_seconds{quantile="0.9"} 0.034
rpc_duration_seconds{quantile="0.99"} 0.089
rpc_duration_seconds_sum 1234.567
rpc_duration_seconds_count 45678
# 注意: 基本的にHistogramを推奨(後からクエリで任意のパーセンタイルを計算可能)
メトリクスの適用場面
| ユースケース | 適切なメトリクスタイプ | 例 |
|---|---|---|
| リクエスト数の追跡 | Counter | http_requests_total |
| エラーレートの計算 | Counter | http_errors_total / http_requests_total |
| レイテンシ分布 | Histogram | http_request_duration_seconds |
| 現在のリソース使用量 | Gauge | node_cpu_usage_percent |
| キュー長の監視 | Gauge | queue_pending_jobs |
3.2 第2の柱: ログ(Logs)
定義と特性
ログとは、システム内で発生したイベントの時系列記録だ。不変で、タイムスタンプを持ち、詳細な文脈情報を含む。
# 非構造化ログ(旧来の形式)
2024-01-15 10:23:45 ERROR Failed to connect to database: Connection refused
2024-01-15 10:23:46 WARN Retrying database connection (attempt 2/3)
2024-01-15 10:23:48 ERROR Database connection failed after 3 attempts
# 構造化ログ(現代の形式: JSON)
{
"timestamp": "2024-01-15T10:23:45Z",
"level": "error",
"service": "user-service",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"message": "Database connection failed",
"error": "connection refused",
"db_host": "postgres-primary:5432",
"retry_count": 3,
"user_id": "usr_123456",
"request_id": "req_789abc"
}
ログレベルの設計
TRACE - 最も詳細。デバッグ時のみ有効。本番では無効にすべき。
DEBUG - 開発・デバッグ用。本番では通常無効。
INFO - 通常の動作記録。サービスの開始/終了、重要なビジネスイベント。
WARN - 注意が必要だが処理は継続。デグレードした動作。
ERROR - エラーが発生。処理に失敗したがサービスは継続。
FATAL - 致命的エラー。サービス停止につながる。
ログの使用場面
import structlog
import json
logger = structlog.get_logger()
def process_payment(payment_id: str, amount: float, user_id: str):
logger.info(
"payment_processing_started",
payment_id=payment_id,
amount=amount,
user_id=user_id,
currency="JPY"
)
try:
result = payment_gateway.charge(payment_id, amount)
logger.info(
"payment_processing_succeeded",
payment_id=payment_id,
transaction_id=result.transaction_id,
duration_ms=result.duration_ms
)
return result
except PaymentDeclinedException as e:
logger.warn(
"payment_declined",
payment_id=payment_id,
reason=str(e),
error_code=e.code
)
raise
except Exception as e:
logger.error(
"payment_processing_failed",
payment_id=payment_id,
error=str(e),
exc_info=True # スタックトレースを含む
)
raise
3.3 第3の柱: トレース(Traces)
定義と特性
分散トレースとは、単一のリクエストが複数のサービスを経由する様子を記録したものだ。リクエストのエンドツーエンドの経路と、各ステップの所要時間を可視化する。
トレースの構造:
Trace ID: 4bf92f3577b34da6a3ce929d0e0e4736
│
├── Span: API Gateway (0ms - 450ms)
│ ├── Span: Auth Service (5ms - 25ms)
│ │ └── Span: Token Validation (6ms - 20ms)
│ ├── Span: User Service (30ms - 150ms)
│ │ ├── Span: DB Query (users) (35ms - 90ms)
│ │ └── Span: Cache Write (100ms - 110ms)
│ └── Span: Order Service (160ms - 440ms)
│ ├── Span: DB Query (orders) (165ms - 280ms)
│ ├── Span: Inventory Check (290ms - 360ms)
│ └── Span: Payment Validation (370ms - 430ms)
│ └── Span: External Payment API (375ms - 425ms)
Spanの属性
# OpenTelemetryのSpan属性例
span:
trace_id: "4bf92f3577b34da6a3ce929d0e0e4736"
span_id: "00f067aa0ba902b7"
parent_span_id: "b9c7c989f97918e1"
operation_name: "HTTP GET /api/orders"
service_name: "order-service"
start_time: "2024-01-15T10:23:45.000000Z"
end_time: "2024-01-15T10:23:45.280000Z"
duration_ms: 280
status: "OK"
attributes:
http.method: "GET"
http.url: "/api/orders/12345"
http.status_code: 200
db.system: "postgresql"
db.statement: "SELECT * FROM orders WHERE id = $1"
db.duration_ms: 115
events:
- name: "cache_miss"
timestamp: "2024-01-15T10:23:45.050000Z"
- name: "db_query_start"
timestamp: "2024-01-15T10:23:45.065000Z"
3.4 第4の柱: プロファイル(Profiles)
定義と特性
継続的プロファイリングとは、本番環境で動作するアプリケーションのCPU・メモリ・Goroutine・スレッドなどのリソース消費を継続的に記録する技術だ。
プロファイリングが答える問い:
メトリクス: 「CPU使用率が高い」← 何が起きているか
ログ: 「DatabaseQuery実行中」← どのコードを実行しているか
トレース: 「order-serviceが遅い」← どこが遅いか
プロファイル: 「sql.Scan()が全体の73%のCPUを消費」← なぜ重いか
# CPUプロファイルのフレームグラフ(テキスト表現)
main() 100%
└── handleRequest() 98%
├── processOrder() 85%
│ ├── db.Query() 45%
│ │ └── sql.Scan() 43% ← ここがボトルネック
│ ├── validateItems() 25%
│ └── calculateTax() 15%
└── writeResponse() 13%
3.5 4本柱の比較と使い分け
| 特性 | メトリクス | ログ | トレース | プロファイル |
|---|---|---|---|---|
| データ量 | 低 | 高 | 中〜高 | 中 |
| コスト | 低 | 中〜高 | 中〜高 | 中 |
| クエリ速度 | 高速 | 中程度 | 低〜中 | 中程度 |
| カーディナリティ | 低〜中 | 非常に高 | 非常に高 | 中 |
| 適したトラブルシューティング | 何が問題か | 何が起きたか | どこが問題か | なぜ重いか |
| 主なツール | Prometheus | Loki, ES | Tempo, Jaeger | Pyroscope |
| 保存期間(標準) | 15日〜1年 | 7〜90日 | 7〜30日 | 7〜30日 |
どのシグナルを使うべきか判断フロー
アラートが発生 → メトリクスでトレンドを確認
↓
問題のサービスを特定 → トレースで原因サービスを絞り込む
↓
エラーの詳細を調査 → ログで具体的なエラーメッセージを確認
↓
パフォーマンス問題がある → プロファイルで具体的なコード行を特定
3.6 テレメトリ相関(Exemplars)
理想的なオブザーバビリティでは、各シグナル間を素早く移動できる。これを可能にするのが Exemplar(エグゼンプラー)という仕組みだ。
# PrometheusのメトリクスにトレースIDを埋め込む
http_request_duration_seconds_bucket{
le="0.1",
method="GET",
endpoint="/api/orders"
} 12345 {trace_id="4bf92f3577b34da6a3ce929d0e0e4736"} 1715000000
# Grafanaはこのtrace_idを使ってTempoのトレースに自動ジャンプできる
相関のワークフロー:
- Grafanaダッシュボードでエラーレートスパイクを発見
- そのスパイク期間のExemplarをクリック → Tempoのトレースが開く
- 問題のSpanをクリック → Lokiの関連ログが開く
- パフォーマンス問題があれば → Pyroscopeのプロファイルと相関
3.7 まとめ
- メトリクスは「何が起きているか」を数値で素早く把握するためのもの
- ログは「何が起きたか」の詳細な記録で、問題の詳細調査に使う
- トレースは「どこで問題が起きているか」をリクエスト単位で追跡する
- プロファイルは「なぜ重いのか」をコードレベルで特定する
- 4つを組み合わせることで初めて実践的なオブザーバビリティが実現する
4. オブザーバビリティ vs モニタリング vs テレメトリ
この章で重視すること
- オブザーバビリティ・モニタリング・テレメトリの正確な定義
- 各概念の関係性と違い
- 「ホワイトボックス」vs「ブラックボックス」監視
- SRE視点での使い分け
4.1 概念の整理
これらの言葉は混同されがちだが、明確な違いがある:
┌─────────────────────────────────────────────────────────────┐
│ オブザーバビリティ │
│ (システムの内部状態を理解できる能力という"性質") │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ モニタリング │ │
│ │ (既知の状態を継続的に観測するという"実践") │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ テレメトリ │ │ │
│ │ │ (システムが生成・送信するデータそのもの) │ │ │
│ │ │ メトリクス / ログ / トレース / プロファイル │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
4.2 テレメトリ(Telemetry)
テレメトリとは、システムが自動的に収集・送信するデータのことだ。ギリシャ語で「遠く」(tele)と「測定」(metry)を組み合わせた言葉で、もともとは宇宙探査機などからデータを遠隔収集する技術を指していた。
ソフトウェアの文脈では:
- アプリケーションが自ら生成するメトリクス・ログ・トレース
- インフラが生成するシステムメトリクス
- エージェントが収集して送信するデータ
# OpenTelemetryでのテレメトリデータのフロー
Application
↓ (SDK instrumentation)
OpenTelemetry SDK
↓ (OTLP protocol)
OpenTelemetry Collector
↓ (routing)
┌───────────┬──────────┬──────────┐
│ Prometheus │ Loki │ Tempo │
│ (Metrics) │ (Logs) │ (Traces) │
└───────────┴──────────┴──────────┘
↓
Grafana (Visualization)
4.3 モニタリング(Monitoring)
モニタリングとは、テレメトリデータを収集・分析して、システムの既知の問題を検出する実践だ。
重要なのは「既知の問題」という部分だ。モニタリングは事前に「何を監視するか」「どの閾値でアラートを出すか」を人間が定義する必要がある。
モニタリングのワークフロー:
定義 → テレメトリ収集 → 閾値チェック → アラート発砲 → 対応
(事前) (継続的) (自動) (自動) (人間)
例:
「HTTPエラーレートが5%を超えたらアラート」
→ Prometheus Alert Rule定義
→ 継続的にメトリクスを収集
→ 閾値超過を検知
→ Alertmanager経由でPagerDutyに通知
→ エンジニアが対応
モニタリングの限界
モニタリングは「知っている問題」の検出は得意だが:
- 新種の障害パターンは事前に定義できない
- 複数の症状の組み合わせを自動的に相関させられない
- なぜ問題が起きているかを教えてくれない
4.4 オブザーバビリティ(Observability)
オブザーバビリティは、テレメトリデータを使って「事前に定義していなかった問題」も調査できる能力だ。
| 比較軸 | モニタリング | オブザーバビリティ |
|---|---|---|
| アプローチ | 事前定義した状態を監視 | 任意の質問を後から投げかけられる |
| 適した問題 | Known Unknowns | Unknown Unknowns |
| 調査方法 | ダッシュボードを見る | 仮説を立ててデータを探索する |
| データ設計 | 事前に集計してグラフ化 | 生データを保存して後から分析 |
| ツール | Nagios, Zabbix | OpenTelemetry, Honeycomb |
実例: 同じ状況への対応の違い
状況: ECサイトで特定のユーザーだけ決済に失敗している
モニタリングアプローチ:
- 全体のエラーレートは0.01%以下 → アラート発砲なし
- ダッシュボードを見ても何も異常なし
- ユーザーからの問い合わせで初めて気づく
- ログを手動で `grep error payment.log` して調査
- 数時間後に原因を特定
オブザーバビリティアプローチ:
- 分散トレースで「payment_id」を持つSpanをフィルタリング
- エラーになったトレースのみを抽出
- 「特定のカード種類(Amex)のみ失敗」を数分で発見
- 関連するログにジャンプして詳細を確認
- 15分で原因(カード番号の長さ検証バグ)を特定
4.5 ホワイトボックス監視 vs ブラックボックス監視
ブラックボックス監視
システムの内部を知らなくても行える監視。外側から「動いているか」を確認する。
# ブラックボックス監視の例
# HTTPエンドポイントへのリクエストが成功するか
curl -s -o /dev/null -w "%{http_code}" https://api.example.com/health
200 # ← 正常
# Blackbox Exporterによる監視
# prometheus.yml
scrape_configs:
- job_name: 'blackbox'
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://api.example.com/health
- https://api.example.com/checkout
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
ホワイトボックス監視
システムの内部状態をアプリケーション自身が公開する監視。
# Pythonアプリケーションでのホワイトボックス計装例
from prometheus_client import Counter, Histogram, Gauge, start_http_server
import time
# メトリクスの定義
REQUEST_COUNT = Counter(
'http_requests_total',
'Total HTTP requests',
['method', 'endpoint', 'status_code']
)
REQUEST_LATENCY = Histogram(
'http_request_duration_seconds',
'HTTP request latency',
['method', 'endpoint'],
buckets=[.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10]
)
DB_CONNECTIONS = Gauge(
'db_connections_active',
'Active database connections',
['pool', 'database']
)
def track_request(method, endpoint, status_code, duration):
REQUEST_COUNT.labels(
method=method,
endpoint=endpoint,
status_code=str(status_code)
).inc()
REQUEST_LATENCY.labels(
method=method,
endpoint=endpoint
).observe(duration)
4.6 SREにとっての実践的な意味
Google SREブックでは、モニタリングの目的として4つのゴールシグナル(Four Golden Signals)を定義している:
| シグナル | 説明 | メトリクス例 |
|---|---|---|
| Latency(レイテンシ) | リクエストの処理時間 | p50, p99 response time |
| Traffic(トラフィック) | システムへの負荷 | requests per second |
| Errors(エラー) | 失敗したリクエストの割合 | error rate % |
| Saturation(飽和) | リソースの利用率 | CPU, memory, disk % |
これらを計測・監視するのがモニタリングの基本であり、これらのデータを使って「なぜエラーが増えているのか」「どのサービスがボトルネックか」を調査するのがオブザーバビリティだ。
- Four Golden Signals → SLI/SLOの定義
- SLI/SLO → アラートの設計
- アラート発砲 → オブザーバビリティによる調査
- 調査 → 根本原因の特定と修正
4.7 まとめ
- テレメトリはシステムが生成するデータそのもの
- モニタリングはそのデータで既知の問題を検出する実践
- オブザーバビリティはそのデータで未知の問題も調査できる能力
- モニタリングとオブザーバビリティは対立ではなく補完関係
- ブラックボックス(外部)とホワイトボックス(内部)の両方を組み合わせる
5. メトリクスの基礎と設計
この章で重視すること
- メトリクス設計の原則
- 命名規則とベストプラクティス
- カーディナリティの管理
- RED・USE・Four Golden Signalsメソッド
- メトリクス設計のアンチパターン
5.1 メトリクス設計の原則
良いメトリクスを設計するには、計測する前に「なぜこれを計測するのか」を明確にする必要がある。
BREWフレームワーク
Behavior(振る舞い)、Resource(リソース)、Event(イベント)、Workload(ワークロード)の観点でメトリクスを設計する。
Behavior: サービスの振る舞い
- リクエスト数、エラー率、レイテンシ
- 例: http_requests_total, http_request_duration_seconds
Resource: リソース消費
- CPU、メモリ、ディスク、ネットワーク
- 例: process_cpu_seconds_total, process_resident_memory_bytes
Event: 重要なビジネスイベント
- 注文数、支払い処理数、ユーザー登録数
- 例: orders_created_total, payments_processed_total
Workload: システムへの入力
- キュー長、バッチサイズ、並行接続数
- 例: queue_pending_jobs, db_connections_active
5.2 Prometheusメトリクス命名規則
Prometheusには標準的な命名規則がある。これに従うことでメトリクスの意味が一目でわかるようになる。
# 命名規則のパターン:
{namespace}_{subsystem}_{name}_{unit}
# 良い例:
http_requests_total # namespace=http, name=requests, unit=total(counter)
http_request_duration_seconds # unit=seconds (SI単位系)
node_memory_MemAvailable_bytes # bytes
process_cpu_seconds_total # seconds, total
database_connections_active # ゲージ(_total不要)
kafka_consumer_lag_messages # ゲージ
# 悪い例:
RequestCount # CamelCase(使わない)
http_req_dur # 略語(避ける)
response_time_ms # ミリ秒(SI単位はseconds推奨)
errors # namespace・subsystemなし
単位の規則
# Prometheusの推奨単位
時間: seconds(秒)
サイズ: bytes(バイト)
割合: ratio(0〜1の小数)または percentage(0〜100の場合はラベルで明示)
個数: total(カウンター)、何もなし(ゲージ)
# 例:
http_request_duration_seconds # 秒(ミリ秒ではない)
node_network_receive_bytes_total # バイト
node_filesystem_usage_ratio # 0〜1の割合(%ではない)
5.3 カーディナリティ管理
カーディナリティとは、メトリクスのユニークな時系列の数だ。ラベルの値の組み合わせがカーディナリティを決定する。
# カーディナリティの計算例
http_requests_total{
method: GET/POST/PUT/DELETE = 4通り
endpoint: 50エンドポイント
status_code: 200/201/400/401/403/404/500/503 = 8通り
}
カーディナリティ = 4 × 50 × 8 = 1,600 時系列
# これは問題ない
# 危険なカーディナリティの例
http_requests_total{
user_id: 1,000,000ユーザー ← これを絶対に追加してはいけない
}
カーディナリティ = 4 × 50 × 8 × 1,000,000 = 1,600,000,000 時系列!
# Prometheusが数分でOOMでクラッシュする
カーディナリティの安全基準
| レベル | 時系列数 | 状態 |
|---|---|---|
| 安全 | < 100,000 | 問題なし |
| 注意 | 100,000〜1,000,000 | メモリに注意 |
| 危険 | > 1,000,000 | Prometheusが不安定になる可能性 |
| 重大 | > 10,000,000 | クラッシュの危険 |
高カーディナリティのラベルを使いたい場合
# NG: Prometheusのラベルに高カーディナリティ値を使う
http_requests_total{user_id="usr_123456"}
# OK: 低カーディナリティのバケット化
http_requests_total{user_tier="premium"} # premium/standard/free の3通り
# OK: 高カーディナリティはログ・トレースに任せる
# メトリクス: サービス全体のエラーレート(低カーディナリティ)
http_requests_total{service="payment", status="error"}
# ログ: 特定のエラーの詳細(高カーディナリティ)
{"level":"error", "user_id":"usr_123456", "error":"card_declined"}
# トレース: リクエスト単位の詳細(高カーディナリティ)
span.attributes["user.id"] = "usr_123456"
5.4 REDメソッド
REDメソッドはマイクロサービスのメトリクス設計に使われるフレームワークだ:
- Rate: リクエストレート(1秒あたりのリクエスト数)
- Errors: エラーレート(失敗したリクエストの割合)
- Duration: リクエスト処理時間(レイテンシのパーセンタイル)
# REDメトリクスの実装例(Python + OpenTelemetry)
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
meter = metrics.get_meter("user-service")
# Rate: リクエストカウンター
request_counter = meter.create_counter(
"http_requests_total",
description="Total HTTP requests",
unit="1"
)
# Errors: エラーカウンター
error_counter = meter.create_counter(
"http_errors_total",
description="Total HTTP errors",
unit="1"
)
# Duration: ヒストグラム
request_duration = meter.create_histogram(
"http_request_duration_seconds",
description="HTTP request duration",
unit="s"
)
def handle_request(method: str, endpoint: str):
start = time.time()
try:
result = process(method, endpoint)
request_counter.add(1, {
"method": method,
"endpoint": endpoint,
"status": "success"
})
return result
except Exception as e:
error_counter.add(1, {
"method": method,
"endpoint": endpoint,
"error_type": type(e).__name__
})
raise
finally:
duration = time.time() - start
request_duration.record(duration, {
"method": method,
"endpoint": endpoint
})
5.5 USEメソッド
USEメソッドはインフラ・リソースのメトリクス設計に使われる:
- Utilization: リソースの使用率(%)
- Saturation: 過負荷の度合い(キュー長など)
- Errors: エラーの数・割合
# USEメソッドの適用例
CPU:
Utilization: node_cpu_usage_percent = 73%
Saturation: node_load1 / node_cpu_count = 1.2(1を超えると飽和)
Errors: machine_check_exception_total
メモリ:
Utilization: node_memory_usage_percent = 84%
Saturation: node_vmstat_pgmajfault(ページングが多い = 飽和)
Errors: oom_kill_total
ディスク:
Utilization: node_disk_io_time_seconds_total(io wait %)
Saturation: node_disk_io_time_weighted_seconds_total(io wait queue)
Errors: node_disk_read_errors_total
ネットワーク:
Utilization: node_network_transmit_bytes_total / interface_bandwidth
Saturation: node_network_transmit_drop_total(パケットドロップ)
Errors: node_network_transmit_errs_total
5.6 Four Golden Signals(Googleのアプローチ)
Google SRE Bookが提唱する、すべてのサービスに適用できる4つのシグナル:
1. Latency(レイテンシ)
- 成功リクエストのレイテンシ
- 失敗リクエストのレイテンシ(別々に追跡!)
- p50, p95, p99 を測定
- 例: p99 < 200ms がSLO
2. Traffic(トラフィック)
- 1秒あたりのリクエスト数
- ピーク時と通常時の比較
- 例: 通常 1,000 RPS、ピーク 5,000 RPS
3. Errors(エラー)
- HTTPの明示的エラー(5xx)
- 暗黙的エラー(200だが間違った内容)
- 意図的なエラー(400系)との分離
- 例: 5xxエラーレート < 0.1%
4. Saturation(飽和)
- 最も制約されているリソースの使用率
- 将来の枯渇予測
- 例: CPU < 70%, メモリ < 85%
5.7 メトリクス設計のアンチパターン
アンチパターン1: 過剰なラベル
# 悪い例: カーディナリティ爆発
http_requests_total{
user_id: "usr_123456", # ← 高カーディナリティ
ip_address: "1.2.3.4", # ← 高カーディナリティ
request_id: "req_abc123", # ← 超高カーディナリティ
}
# 良い例: 低カーディナリティのラベルのみ
http_requests_total{
service: "api",
endpoint: "/users",
method: "GET",
status_code: "200",
region: "ap-northeast-1"
}
アンチパターン2: ゲージの誤用
# 悪い例: カウンターにゲージを使う
requests_gauge.set(requests_gauge.get() + 1) # スレッドセーフでない
# 良い例: カウンターを使う
requests_counter.inc() # アトミックに増加
アンチパターン3: 計算済み値を保存する
# 悪い例: 計算済みの比率を保存する
error_rate_gauge.set(errors / total) # 後で集計できない
# 良い例: 生の数値を保存してPromQLで計算する
errors_counter.inc()
total_counter.inc()
# PromQLでエラーレートを計算:
# rate(errors_counter[5m]) / rate(total_counter[5m])
5.8 まとめ
- メトリクスはCounter・Gauge・Histogram・Summaryの4種類
- 命名規則は
{namespace}_{subsystem}_{name}_{unit}に従う - カーディナリティを常に意識し、高カーディナリティ値はラベルに入れない
- REDメソッド(サービス向け)とUSEメソッド(インフラ向け)を使い分ける
- Four Golden Signalsはすべてのサービスに適用できる基本フレームワーク
6. Prometheus
この章で重視すること
- Prometheusのアーキテクチャと各コンポーネント
- インストールと基本設定
- サービスディスカバリの設定
- エクスポーターの種類と使い方
- アラートルールの設計
- 高可用性構成
- パフォーマンスチューニング
6.1 Prometheusのアーキテクチャ
┌──────────────────────────────────────────────────────────────────┐
│ Prometheus エコシステム │
│ │
│ 【ディスカバリ】 【ターゲット】 │
│ Kubernetes API ──────────→ Pod /metrics │
│ Consul ──────────→ Service /metrics │
│ EC2 API ──────────→ Node Exporter │
│ Static Config ──────────→ Custom App │
│ ↑ │
│ HTTP Pull (スクレイピング) │
│ │ │
│ ┌──────────────────────┴───────────────────────────────────┐ │
│ │ Prometheus Server │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │ │
│ │ │ Retrieval │ │ TSDB │ │ HTTP API │ │ │
│ │ │ (スクレイパー)│ │ (ストレージ) │ │ (クエリ) │ │ │
│ │ └──────────────┘ └──────────────┘ └───────────────┘ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ Rule Engine (ルール評価) │ │ │
│ │ │ Recording Rules / Alerting Rules │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ ↓ │
│ [Alertmanager] [Grafana] │
│ ┌──────────────┐ ┌───────────────┐ │
│ │ Routes │ │ Dashboards │ │
│ │ Silences │ │ Alerts │ │
│ │ Inhibitions │ │ Explore │ │
│ └──────────────┘ └───────────────┘ │
│ ↓ │
│ [PagerDuty / Slack / Email] │
└──────────────────────────────────────────────────────────────────┘
6.2 Prometheusのインストール
Docker Composeでの起動(開発・テスト環境)
# docker-compose.yml
version: '3.8'
services:
prometheus:
image: prom/prometheus:v2.51.0
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- ./rules:/etc/prometheus/rules
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=30d'
- '--storage.tsdb.retention.size=10GB'
- '--web.enable-lifecycle'
- '--web.enable-admin-api'
restart: unless-stopped
alertmanager:
image: prom/alertmanager:v0.27.0
container_name: alertmanager
ports:
- "9093:9093"
volumes:
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml
restart: unless-stopped
node-exporter:
image: prom/node-exporter:v1.8.0
container_name: node-exporter
ports:
- "9100:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($|/)'
restart: unless-stopped
volumes:
prometheus_data:
Kubernetes での Prometheus Operator インストール
# Helm を使ったkube-prometheus-stackのインストール
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
# カスタム値ファイルを作成
cat > values.yaml << 'EOF'
prometheus:
prometheusSpec:
retention: 30d
retentionSize: "50GB"
resources:
requests:
memory: "4Gi"
cpu: "500m"
limits:
memory: "8Gi"
cpu: "2"
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: gp3
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Gi
grafana:
adminPassword: "change-me-in-production"
persistence:
enabled: true
size: 10Gi
alertmanager:
alertmanagerSpec:
resources:
requests:
memory: "256Mi"
cpu: "100m"
EOF
helm install prometheus prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--create-namespace \
--values values.yaml
6.3 prometheus.yml の詳細設定
# prometheus.yml
global:
scrape_interval: 15s # デフォルトのスクレイピング間隔
evaluation_interval: 15s # ルール評価間隔
scrape_timeout: 10s # タイムアウト
# 全メトリクスに付加される外部ラベル(Thanosやリモートライト用)
external_labels:
cluster: 'production'
region: 'ap-northeast-1'
# ルールファイルの読み込み
rule_files:
- "/etc/prometheus/rules/*.yml"
# アラートマネージャーの設定
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
timeout: 10s
api_version: v2
# リモートライト(Thanos/Mimirへの転送)
remote_write:
- url: "http://mimir:9009/api/v1/push"
queue_config:
max_samples_per_send: 10000
max_shards: 200
capacity: 2500
scrape_configs:
# Prometheus自身を監視
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Node Exporter(ホストメトリクス)
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
relabel_configs:
- source_labels: [__address__]
regex: '(.+):.*'
target_label: instance
replacement: '$1'
# Kubernetes Podの自動ディスカバリ
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
# prometheus.io/scrape: "true" アノテーションがあるPodのみ対象
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
# prometheus.io/path アノテーションでパスを指定可能
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
# prometheus.io/port アノテーションでポートを指定可能
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
# Podのラベルをメトリクスのラベルとして追加
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
# namespace, pod名をラベルとして追加
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_pod_name]
action: replace
target_label: kubernetes_pod_name
# Kubernetes Serviceの自動ディスカバリ
- job_name: 'kubernetes-services'
kubernetes_sd_configs:
- role: endpoints
relabel_configs:
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
action: replace
target_label: __scheme__
regex: (https?)
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
action: replace
target_label: __address__
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
# Blackbox Exporter(外形監視)
- job_name: 'blackbox-http'
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://api.example.com/health
- https://www.example.com/
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
# カスタムアプリケーション
- job_name: 'app-backend'
scrape_interval: 10s
static_configs:
- targets:
- 'backend-1:8080'
- 'backend-2:8080'
- 'backend-3:8080'
metric_relabel_configs:
# 不要なメトリクスを除外してカーディナリティを削減
- source_labels: [__name__]
regex: 'go_gc_.*'
action: drop
6.4 主要なエクスポーター
Node Exporter(ホストメトリクス)
# インストール
wget https://github.com/prometheus/node_exporter/releases/download/v1.8.0/node_exporter-1.8.0.linux-amd64.tar.gz
tar xvfz node_exporter-1.8.0.linux-amd64.tar.gz
cd node_exporter-1.8.0.linux-amd64
./node_exporter
# systemdサービス設定
cat > /etc/systemd/system/node_exporter.service << 'EOF'
[Unit]
Description=Prometheus Node Exporter
After=network.target
[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/local/bin/node_exporter \
--collector.filesystem.mount-points-exclude="^/(dev|proc|sys|var/lib/docker/.+)($|/)" \
--collector.netclass.ignored-devices="^(veth.*){{CONTENT}}quot; \
--collector.netdev.device-exclude="^(veth.*){{CONTENT}}quot;
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now node_exporter
主要なメトリクス一覧
# CPU
node_cpu_seconds_total{mode="idle|user|system|iowait|irq|softirq|steal"}
# メモリ
node_memory_MemTotal_bytes
node_memory_MemAvailable_bytes
node_memory_MemFree_bytes
node_memory_Buffers_bytes
node_memory_Cached_bytes
# ディスク
node_filesystem_size_bytes{mountpoint="/"}
node_filesystem_avail_bytes{mountpoint="/"}
node_disk_read_bytes_total{device="sda"}
node_disk_written_bytes_total{device="sda"}
node_disk_io_time_seconds_total{device="sda"}
# ネットワーク
node_network_receive_bytes_total{device="eth0"}
node_network_transmit_bytes_total{device="eth0"}
node_network_receive_errs_total{device="eth0"}
# システム負荷
node_load1
node_load5
node_load15
PostgreSQL Exporter
# docker-compose.yml に追加
postgres-exporter:
image: prometheuscommunity/postgres-exporter:v0.15.0
container_name: postgres-exporter
environment:
DATA_SOURCE_NAME: "postgresql://prometheus:password@postgres:5432/mydb?sslmode=disable"
ports:
- "9187:9187"
restart: unless-stopped
# カスタムクエリの設定 queries.yaml
pg_replication:
query: "SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))::INT as lag"
master: true
metrics:
- lag:
usage: "GAUGE"
description: "Replication lag in seconds"
pg_slow_queries:
query: |
SELECT count(*) as count
FROM pg_stat_activity
WHERE state = 'active'
AND query_start < now() - interval '5 minutes'
master: true
metrics:
- count:
usage: "GAUGE"
description: "Number of queries running longer than 5 minutes"
Redis Exporter
# Redis Exporterの起動
docker run -d \
--name redis-exporter \
-p 9121:9121 \
oliver006/redis_exporter \
--redis.addr=redis://redis-host:6379 \
--redis.password=your-password
カスタムExporterの作成(Python)
#!/usr/bin/env python3
"""カスタムExporterの実装例: 外部APIの状態を監視"""
import time
import requests
from prometheus_client import start_http_server, Gauge, Counter, Histogram
from prometheus_client.core import REGISTRY
# メトリクスの定義
API_RESPONSE_TIME = Histogram(
'external_api_response_seconds',
'External API response time in seconds',
['endpoint', 'status'],
buckets=[.01, .05, .1, .25, .5, 1, 2.5, 5]
)
API_STATUS = Gauge(
'external_api_up',
'External API availability (1=up, 0=down)',
['endpoint']
)
API_REQUESTS_TOTAL = Counter(
'external_api_requests_total',
'Total external API requests',
['endpoint', 'status']
)
ENDPOINTS = [
'https://api.payment.com/health',
'https://api.inventory.com/status',
'https://api.shipping.com/ping',
]
def check_endpoints():
for endpoint in ENDPOINTS:
try:
start = time.time()
response = requests.get(endpoint, timeout=5)
duration = time.time() - start
status = 'success' if response.ok else 'error'
API_RESPONSE_TIME.labels(
endpoint=endpoint,
status=status
).observe(duration)
API_STATUS.labels(endpoint=endpoint).set(
1 if response.ok else 0
)
API_REQUESTS_TOTAL.labels(
endpoint=endpoint,
status=status
).inc()
except requests.exceptions.RequestException as e:
API_STATUS.labels(endpoint=endpoint).set(0)
API_REQUESTS_TOTAL.labels(
endpoint=endpoint,
status='timeout'
).inc()
if __name__ == '__main__':
start_http_server(8000)
print("Custom exporter running on :8000")
while True:
check_endpoints()
time.sleep(30)
6.5 Recording Rules(記録ルール)
高コストなPromQLクエリを事前計算して保存する仕組み。ダッシュボードのクエリを高速化し、サーバー負荷を削減する。
# rules/recording_rules.yml
groups:
- name: http_metrics
interval: 30s # デフォルトのevaluation_intervalを上書き
rules:
# サービス別のリクエストレート(5分間)
- record: job:http_requests_total:rate5m
expr: |
sum by (job, instance, method, status_code) (
rate(http_requests_total[5m])
)
# サービス別のエラーレート
- record: job:http_errors:rate5m
expr: |
sum by (job, instance) (
rate(http_requests_total{status_code=~"5.."}[5m])
)
# エラー比率(0〜1)
- record: job:http_error_ratio:rate5m
expr: |
job:http_errors:rate5m
/
sum by (job, instance) (rate(http_requests_total[5m]))
# p99レイテンシ
- record: job:http_request_duration_p99:rate5m
expr: |
histogram_quantile(0.99,
sum by (job, instance, le) (
rate(http_request_duration_seconds_bucket[5m])
)
)
# CPU使用率(ノード別)
- record: node:cpu_usage:rate5m
expr: |
1 - avg by (instance) (
rate(node_cpu_seconds_total{mode="idle"}[5m])
)
# メモリ使用率
- record: node:memory_usage:ratio
expr: |
1 - (
node_memory_MemAvailable_bytes
/ node_memory_MemTotal_bytes
)
6.6 アラートルールの設計
# rules/alerting_rules.yml
groups:
- name: service_alerts
rules:
# 高エラーレートアラート
- alert: HighErrorRate
expr: |
(
sum by (job, instance) (rate(http_requests_total{status_code=~"5.."}[5m]))
/
sum by (job, instance) (rate(http_requests_total[5m]))
) > 0.05
for: 5m
labels:
severity: critical
team: backend
annotations:
summary: "High error rate detected: {{ $labels.job }}"
description: |
Service {{ $labels.job }} on {{ $labels.instance }} has an error rate of
{{ $value | humanizePercentage }} over the last 5 minutes.
runbook_url: "https://wiki.example.com/runbooks/high-error-rate"
dashboard_url: "https://grafana.example.com/d/service-overview"
# 高レイテンシアラート
- alert: HighLatencyP99
expr: |
histogram_quantile(0.99,
sum by (job, instance, le) (
rate(http_request_duration_seconds_bucket[5m])
)
) > 1.0
for: 10m
labels:
severity: warning
team: backend
annotations:
summary: "High p99 latency: {{ $labels.job }}"
description: |
p99 latency for {{ $labels.job }} is {{ $value | humanizeDuration }}.
SLO threshold is 1 second.
# サービスダウン検知
- alert: ServiceDown
expr: up == 0
for: 1m
labels:
severity: critical
pagerduty: true
annotations:
summary: "Service down: {{ $labels.job }}"
description: "{{ $labels.instance }} of job {{ $labels.job }} is down."
- name: infrastructure_alerts
rules:
# CPU使用率警告
- alert: HighCPUUsage
expr: node:cpu_usage:rate5m > 0.85
for: 15m
labels:
severity: warning
annotations:
summary: "High CPU usage on {{ $labels.instance }}"
description: "CPU usage is {{ $value | humanizePercentage }} (threshold: 85%)"
# メモリ不足警告
- alert: LowMemory
expr: node:memory_usage:ratio > 0.90
for: 5m
labels:
severity: warning
annotations:
summary: "Low memory on {{ $labels.instance }}"
description: "Memory usage is {{ $value | humanizePercentage }}"
# ディスク容量警告(4時間以内に枯渇予測)
- alert: DiskWillFillIn4Hours
expr: |
predict_linear(
node_filesystem_avail_bytes{mountpoint="/"}[1h], 4 * 3600
) < 0
for: 5m
labels:
severity: critical
annotations:
summary: "Disk will be full in 4 hours: {{ $labels.instance }}"
6.7 Alertmanagerの設定
# alertmanager.yml
global:
smtp_smarthost: 'smtp.example.com:587'
smtp_from: 'alertmanager@example.com'
smtp_auth_username: 'alertmanager@example.com'
smtp_auth_password: 'password'
# PagerDutyの設定
pagerduty_url: 'https://events.pagerduty.com/v2/enqueue'
# Slackの設定
slack_api_url: 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK'
# ルーティング設定
route:
# デフォルトのルート
receiver: 'default-receiver'
group_by: ['alertname', 'cluster', 'service']
group_wait: 30s # 初回アラートを送る前に待機(グループ化のため)
group_interval: 5m # 同じグループの更新通知間隔
repeat_interval: 4h # アラートが継続している場合の再通知間隔
routes:
# Criticalアラートは即座にPagerDutyへ
- match:
severity: critical
receiver: 'pagerduty-critical'
group_wait: 0s
repeat_interval: 1h
# Warningアラートはチームのslackへ
- match:
severity: warning
receiver: 'slack-warning'
group_wait: 1m
repeat_interval: 8h
# pagerduty: trueのラベルがついたアラートはPagerDutyへ
- match:
pagerduty: "true"
receiver: 'pagerduty-critical'
# バックエンドチームのアラート
- match:
team: backend
receiver: 'slack-backend-team'
# レシーバーの定義
receivers:
- name: 'default-receiver'
slack_configs:
- channel: '#alerts'
title: '{{ .GroupLabels.alertname }}'
text: '{{ range .Alerts }}{{ .Annotations.description }}{{ end }}'
- name: 'pagerduty-critical'
pagerduty_configs:
- service_key: 'your-pagerduty-service-key'
description: '{{ .GroupLabels.alertname }}: {{ .CommonAnnotations.summary }}'
details:
firing: '{{ .Alerts.Firing | len }}'
resolved: '{{ .Alerts.Resolved | len }}'
- name: 'slack-warning'
slack_configs:
- channel: '#alerts-warning'
color: 'warning'
title: '⚠️ {{ .GroupLabels.alertname }}'
title_link: '{{ .CommonAnnotations.dashboard_url }}'
text: |
{{ range .Alerts }}
*Alert:* {{ .Annotations.summary }}
*Description:* {{ .Annotations.description }}
*Runbook:* {{ .Annotations.runbook_url }}
{{ end }}
- name: 'slack-backend-team'
slack_configs:
- channel: '#backend-alerts'
api_url: 'https://hooks.slack.com/services/BACKEND/TEAM/WEBHOOK'
# 抑制ルール(サービスダウン時にその他の関連アラートを抑制)
inhibit_rules:
- source_match:
severity: 'critical'
alertname: 'ServiceDown'
target_match:
severity: 'warning'
equal: ['job', 'instance']
6.8 高可用性構成
# Prometheusの高可用性(HA)アーキテクチャ
同一設定の2台のPrometheusが独立してスクレイピング
[Application] ←── scrape ── [Prometheus-1] ──→ [Alertmanager Cluster]
←── scrape ── [Prometheus-2] ──→ [Alertmanager Cluster]
[Grafana] ──→ query ──→ [Prometheus-1] or [Prometheus-2]
# Kubernetes での Prometheus HA 設定(PrometheusOperator使用)
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
name: prometheus-ha
namespace: monitoring
spec:
replicas: 2 # 2台で同一設定
replicaExternalLabelName: "__replica__" # レプリカを区別するラベル
alerting:
alertmanagers:
- namespace: monitoring
name: alertmanager-main
port: web
# Thanosサイドカー(長期ストレージ連携)
thanos:
image: quay.io/thanos/thanos:v0.35.0
objectStorageConfig:
key: thanos.yaml
name: thanos-object-storage-config
storage:
volumeClaimTemplate:
spec:
storageClassName: gp3
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Gi
6.9 パフォーマンスチューニング
# prometheus.yml のパフォーマンス設定
global:
scrape_interval: 30s # 15s → 30s に変更でストレージ削減
scrape_timeout: 15s
# ストレージ設定(起動オプション)
# --storage.tsdb.retention.time=30d
# --storage.tsdb.retention.size=50GB
# --storage.tsdb.wal-compression # WAL圧縮(メモリとCPUを少し使うが、ディスク使用量を削減)
# --storage.tsdb.chunk-encoding=zstd # Prometheus 2.50以降
# クエリパフォーマンス設定
# --query.max-concurrency=20
# --query.timeout=2m
# 不要なメトリクスの削除(メトリクスのフィルタリング)
# scrape_configs内のmetric_relabel_configs
metric_relabel_configs:
# 高カーディナリティなgo_gorutineラベルを削除
- source_labels: [__name__]
regex: 'go_gc_duration_seconds.*'
action: drop
# 特定のラベル値を持つシリーズを削除
- source_labels: [__name__, status_code]
regex: 'http_requests_total;(1|2)\d\d' # 1xx, 2xxの詳細は不要
action: drop
# 高カーディナリティなラベルを削除(値は残す)
- regex: 'request_id|session_id|trace_id'
action: labeldrop
6.10 まとめ
- Prometheusはpull型のアーキテクチャで、サービスディスカバリと組み合わせて動的に監視対象を管理する
prometheus.ymlでスクレイピング設定、Recording Rules、Alerting Rulesを定義する- Node Exporter、PostgreSQL Exporter、Blackbox Exporterなど目的別のエクスポーターを使い分ける
- 高可用性はレプリカを2台立てることで実現し、長期保存にはThanos/Mimirを使う
- カーディナリティの管理がパフォーマンスの鍵
7. PromQL詳細リファレンス
この章で重視すること
- PromQLの基本構文と型システム
- セレクター・マッチャーの使い方
- 集計演算子の完全リファレンス
- 関数の詳細と使い分け
- 実践的なクエリパターン集
- クエリ最適化テクニック
7.1 PromQLの型システム
PromQLには4つのデータ型がある:
1. Instant Vector(瞬間ベクター)
単一の時刻における、ラベルセット→値のマッピング
例: http_requests_total
2. Range Vector(範囲ベクター)
時間範囲内の、ラベルセット→値の系列マッピング
例: http_requests_total[5m]
※ 直接グラフ表示はできない。rate()等の関数に渡す
3. Scalar(スカラー)
単一の浮動小数点数
例: 3.14
4. String(文字列)
文字列リテラル(ほとんど使われない)
例: "hello"
7.2 セレクターとマッチャー
# 基本的なセレクター
http_requests_total # メトリクス名のみ
http_requests_total{job="api-server"} # ラベルで絞り込み
# マッチャーの種類
http_requests_total{status_code="200"} # = 完全一致
http_requests_total{status_code!="200"} # != 不一致
http_requests_total{status_code=~"2.."} # =~ 正規表現一致
http_requests_total{status_code!~"2.."} # !~ 正規表現不一致
# 複数のマッチャーを組み合わせ(AND条件)
http_requests_total{
job="api-server",
status_code=~"5..",
method="POST"
}
# 正規表現の例
{endpoint=~"/api/v[12]/.*"} # v1またはv2のAPIパス
{job=~"(frontend|backend|payment)"} # 3つのサービスのどれか
{instance!~"test-.*"} # testで始まるインスタンスを除外
# 範囲ベクター(時間窓)
http_requests_total[5m] # 過去5分
http_requests_total[1h] # 過去1時間
http_requests_total[24h] # 過去24時間
# オフセット(過去のデータと比較)
http_requests_total offset 1d # 1日前の値
rate(http_requests_total[5m] offset 1h) # 1時間前のレート
7.3 集計演算子
# sum - 合計
sum(http_requests_total) # 全ラベルの合計
sum by (job) (http_requests_total) # jobごとの合計
sum without (instance) (http_requests_total) # instanceを除いてグループ化
# avg - 平均
avg by (job) (http_request_duration_seconds)
# max / min
max by (instance) (node_cpu_usage_percent)
min by (datacenter) (node_memory_available_bytes)
# count - 時系列の数(値ではなく本数)
count by (job) (up == 1) # 稼働中のインスタンス数
# count_values - 値ごとのカウント
count_values("version", app_version) # バージョンごとの本数
# topk / bottomk - 上位/下位K件
topk(5, http_requests_total) # 最もリクエストが多い5つのラベルセット
bottomk(3, node_memory_available_bytes) # 残りメモリが少ない3台
# quantile - パーセンタイル(要注意:summaryメトリクス用)
quantile(0.95, http_request_duration_seconds) # p95
# stddev / stdvar
stddev(http_request_duration_seconds)
# group(稀な使用例)
group by (job) (up) # 値を1に置き換えてグループ化
7.4 重要な関数
rate() と irate()
# rate() - 時間範囲内の1秒あたりの平均変化率
# カウンターのリセットを自動処理
# グラフ・アラート両方に適している
rate(http_requests_total[5m])
# irate() - 直近2サンプルの瞬間変化率
# 揮発性の高いメトリクスに向く(CPU使用率など)
# アラートには向かない(ノイジーになる)
irate(cpu_seconds_total[5m])
# 使い分けの原則:
# - 通常のグラフとアラート → rate()
# - 高解像度の瞬間値が必要 → irate()
# - 長期トレンド → rate() with longer window
increase()
# increase() - 時間範囲内の増加量(rate() × 秒数)
increase(http_requests_total[1h]) # 1時間のリクエスト増加数
increase(errors_total[24h]) # 24時間のエラー数
histogram_quantile()
# Histogramからパーセンタイルを計算
histogram_quantile(0.99,
rate(http_request_duration_seconds_bucket[5m])
) # p99レイテンシ
# サービス別のp99レイテンシ(重要:sum by (le) が必要)
histogram_quantile(0.99,
sum by (job, le) (
rate(http_request_duration_seconds_bucket[5m])
)
)
# 複数パーセンタイルを一つのクエリで(Grafana変数活用)
histogram_quantile($quantile,
sum by (job, le) (
rate(http_request_duration_seconds_bucket[5m])
)
)
predict_linear()
# 線形回帰でN秒後の値を予測
# ディスク容量の枯渇予測に最もよく使われる
# 4時間後のディスク空き容量を予測
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[1h], 4 * 3600)
# 0を下回る(=ディスクフル)まで何秒かを計算
(
node_filesystem_avail_bytes{mountpoint="/"}
/
-deriv(node_filesystem_avail_bytes{mountpoint="/"}[1h])
)
label_replace() と label_join()
# label_replace() - ラベルの値を正規表現で変換
label_replace(
http_requests_total,
"short_endpoint", # 新しいラベル名
"$1", # 置換文字列
"endpoint", # ソースラベル名
"/api/v[0-9]+/(.*)" # 正規表現
)
# label_join() - 複数ラベルを連結
label_join(
http_requests_total,
"instance_job", # 新しいラベル名
"-", # セパレータ
"instance", "job" # 連結するラベル
)
time-based関数
# time() - 現在のUnixタイムスタンプ
time()
# timestamp() - 各サンプルのタイムスタンプ(staleness検出に使用)
timestamp(up)
# 最後にスクレイプされてからの経過時間
time() - timestamp(up{job="api-server"})
# day_of_week(), hour() - 時刻関数(ビジネス時間帯アラートに使用)
# 平日9-18時のみアラート
(
http_error_rate > 0.05
and on()
(hour() >= 9 and hour() < 18 and day_of_week() >= 1 and day_of_week() <= 5)
)
7.5 実践的なクエリパターン集
エラーレートの計算
# 全体のエラーレート(%)
sum(rate(http_requests_total{status_code=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
* 100
# サービス別エラーレート
sum by (job) (rate(http_requests_total{status_code=~"5.."}[5m]))
/
sum by (job) (rate(http_requests_total[5m]))
# エンドポイント別エラーレート(上位5件)
topk(5,
sum by (endpoint) (rate(http_requests_total{status_code=~"5.."}[5m]))
/
sum by (endpoint) (rate(http_requests_total[5m]))
)
レイテンシの分析
# p50, p95, p99の同時計算
histogram_quantile(0.50, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
# SLOしきい値を超えたリクエストの割合(例: 200ms以内がSLO)
(
sum(rate(http_request_duration_seconds_bucket{le="0.2"}[5m]))
/
sum(rate(http_request_duration_seconds_count[5m]))
)
# 遅いリクエストの数
sum(rate(http_request_duration_seconds_bucket{le="+Inf"}[5m]))
-
sum(rate(http_request_duration_seconds_bucket{le="1.0"}[5m]))
インフラメトリクスのクエリ
# CPU使用率(モード別・ノード別)
sum by (instance, mode) (
rate(node_cpu_seconds_total[5m])
) / count by (instance) (node_cpu_seconds_total{mode="idle"}) * 100
# 実際のCPU使用率(100% - idle)
(1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))) * 100
# メモリ使用率
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100
# ディスクI/Oレート
rate(node_disk_read_bytes_total[5m])
rate(node_disk_written_bytes_total[5m])
# ネットワーク帯域使用率(例: 1Gbps NICの場合)
rate(node_network_receive_bytes_total{device="eth0"}[5m]) / 125000000 * 100 # % of 1Gbps
# TCP接続数
node_netstat_Tcp_CurrEstab
# ファイルディスクリプタ使用率
process_open_fds / process_max_fds * 100
比率・変化率の計算
# 前日との比較(同じ時刻の1日前と比較)
rate(http_requests_total[5m])
/
rate(http_requests_total[5m] offset 1d)
- 1 # 変化率(正: 増加、負: 減少)
# 先週同曜日との比較
rate(http_requests_total[5m])
/
rate(http_requests_total[5m] offset 1w)
# 急激な増加を検知(通常の2倍以上)
rate(http_errors_total[5m])
> 2 * avg_over_time(rate(http_errors_total[5m])[1h:5m])
SLO関連クエリ
# 30日間のエラーバジェット消費率(SLO: 99.9%)
# エラーバジェット = 1 - 0.999 = 0.001
# エラーレートが高すぎる場合のバーンレート
(
sum(rate(http_requests_total{status_code=~"5.."}[1h]))
/
sum(rate(http_requests_total[1h]))
) / 0.001 # SLO のエラーバジェット比率
# エラーバジェットの残り(%)
(1 - (
sum(increase(http_requests_total{status_code=~"5.."}[30d]))
/
sum(increase(http_requests_total[30d]))
)) / 0.001 * 100
7.6 二項演算子と集合演算
# 二項演算子(算術)
node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes # 使用中メモリ
node_filesystem_size_bytes - node_filesystem_avail_bytes # ディスク使用量
# ラベルが一致する場合のみ演算(デフォルト動作)
http_requests_total / http_requests_total offset 1h # ラベルで自動マッチング
# on() と ignoring() でラベルマッチングを制御
method_code:http_errors:rate5m{code="500"}
/
ignoring(code) # codeラベルを無視してマッチング
method:http_requests:rate5m
# group_left / group_right(多対一の結合)
node_cpu_seconds_total
* on(instance) group_left(nodename)
node_uname_info{nodename!=""}
# 集合演算
# and - 両方に存在するシリーズ
http_requests_total{method="GET"} and http_requests_total{status_code="200"}
# or - どちらかに存在するシリーズ
http_requests_total or grpc_requests_total
# unless - 左辺にのみ存在するシリーズ
http_requests_total unless http_requests_total{status_code=~"2.."}
ベクトルマッチング(Vector Matching)の詳細
PromQLの二項演算でのベクトルマッチングは、異なるメトリクスを結合する際に重要だ。デフォルトの「1対1マッチング」では、両方のベクターで完全に同じラベルセットを持つシリーズのみが演算される。
# 例: CPUとメモリの使用率を計算したい場合
# 両方に同じラベル(instance, job)があれば1対1マッチング(デフォルト)
node_cpu_usage_percent / node_memory_usage_percent
# 実行結果:
# {instance="node1", job="app"}: 20 / 60 = 0.33
# {instance="node2", job="app"}: 15 / 50 = 0.30
# instanceラベルが一つだけ違う場合は両辺で除外されたシリーズは演算されない
on()修飾子: マッチングに使うラベルを明示指定
# on()で指定したラベルのみを使ってマッチング
node_cpu_usage_percent
/
on(instance) # instanceラベルのみでマッチング、他のラベルは無視
node_memory_usage_percent
# 複数ラベルでマッチング
on(instance, job)
ignoring()修飾子: マッチングから除外するラベルを指定
# 「全ラベルを使うが、これらのラベルは無視」という意味
http_requests_total
/
ignoring(path, handler) # pathとhandlerは無視してマッチング
http_requests_total offset 1h
# 上記は以下と等価:
http_requests_total
/
on(instance, job, method, status_code) # 他は全部除外
http_requests_total offset 1h
group_left/group_rightによる多対1マッチング
単純な二項演算では「1対1マッチング」しか許さないが、時に「多対1」の結合が必要だ。その場合がgroup_leftとgroup_rightで明示する。
# 例: ノード情報(1件)を複数のメトリクス(複数件)に付与する
# これはエラー(多対1で右辺の1件に対して左辺の複数件がマッチ)
node_cpu_usage_percent * node_uname_info
# group_leftで許可(左辺が複数、右辺が1)
node_cpu_usage_percent
* on(instance) group_left(nodename, os_version)
node_uname_info{os_version!=""}
# group_rightの場合(左辺が1、右辺が複数)
node_uname_info
* on(instance) group_right(cpu_percent, memory_percent)
node_cpu_usage_percent
実践例: ノード情報を複数のメトリクスに付与
# ノード1台(instance)につき1行のノード情報テーブル
# node_uname_info{instance="node1", nodename="prod-app-01", os="Linux"}
# node_uname_info{instance="node2", nodename="prod-app-02", os="Darwin"}
# CPUメトリクス(複数行)にノード名を付与
rate(node_cpu_seconds_total[5m])
* on(instance) group_left(nodename, os)
node_uname_info
# 実行結果(nodename, osが追加されている):
# {instance="node1", cpu="0", nodename="prod-app-01", os="Linux"}: 0.12
# {instance="node1", cpu="1", nodename="prod-app-01", os="Linux"}: 0.08
# {instance="node2", cpu="0", nodename="prod-app-02", os="Darwin"}: 0.15
7.7 subquery(サブクエリ)
# 過去1時間の5分おきのrate()の最大値
max_over_time(rate(http_requests_total[5m])[1h:5m])
# 構文: range_query[range:step]
# - range: subqueryが評価される時間範囲
# - step: サンプリング間隔
# 過去24時間でのp99レイテンシのピーク
max_over_time(
histogram_quantile(0.99,
sum by (le) (
rate(http_request_duration_seconds_bucket[5m])
)
)[24h:5m]
)
# 過去1時間で一度でも閾値を超えたか
max_over_time(
(http_error_rate > 0.01)[1h:5m]
) > 0
7.8 クエリの最適化
# 悪い例: 集計前にrateを計算してから集計(正しい順序)
sum(rate(http_requests_total[5m])) # OK: rateしてからsum
# 悪い例: sumしてからrate(間違い!ラベルが消えてしまう)
rate(sum(http_requests_total)[5m]) # NG: 一般的なミス
# Recording Rulesで事前計算
# 重いクエリはRecording Ruleで事前計算して軽量化
# rules/recording_rules.yml で定義済みなら:
job:http_request_duration_p99:rate5m # 軽量な参照
# vs 毎回計算(重い):
histogram_quantile(0.99,
sum by (job, le) (
rate(http_request_duration_seconds_bucket[5m])
)
)
# 不要なラベルを除外してカーディナリティを下げる
sum without (instance, pod) (http_requests_total)
# クエリの並列化(Grafanaで複数パネルを使う場合)
# 同一のRecording Ruleを複数パネルで参照することでDB負荷を削減
7.9 実践デバッグTips
# メトリクスが存在するかを確認
count(http_requests_total)
# 特定ラベルの値を確認
group by (endpoint) (http_requests_total)
# カーディナリティの高いメトリクスを特定
topk(10, count by (__name__) ({__name__!=""}))
# ラベルのユニーク値を確認(Grafanaのexploreで便利)
label_values(http_requests_total, endpoint)
# 値が0のシリーズを除外
http_requests_total > 0
# NaN / Infを除外
http_requests_total != NaN
# デバッグ用: 生の時系列を表示(Table形式で見る)
sort_desc(http_requests_total)
7.10 まとめ
- PromQLにはInstant Vector・Range Vector・Scalar・Stringの4型がある
rate()は通常のグラフとアラートに使い、irate()は瞬間値が必要な場合のみhistogram_quantile()を使う際は必ずsum by (le)を含める- 集計はrateの後に行う(rateしてからsum、sumしてからrateはNG)
- 重いクエリはRecording Rulesで事前計算する
8. Grafanaダッシュボード設計
この章で重視すること
- Grafanaのアーキテクチャと主要機能
- ダッシュボード設計の原則
- パネルタイプの使い分け
- 変数とテンプレートの活用
- アラートの設定
- ダッシュボードをコードで管理(Grafana as Code)
8.1 Grafanaのインストールと基本設定
# docker-compose.yml(Grafana単体)
services:
grafana:
image: grafana/grafana:11.0.0
container_name: grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=secure-password
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SERVER_DOMAIN=grafana.example.com
- GF_SERVER_ROOT_URL=https://grafana.example.com
# SMTP設定(アラートメール用)
- GF_SMTP_ENABLED=true
- GF_SMTP_HOST=smtp.example.com:587
- GF_SMTP_USER=grafana@example.com
- GF_SMTP_PASSWORD=smtp-password
# Slack通知用
- GF_UNIFIED_ALERTING_ENABLED=true
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
- ./grafana/dashboards:/var/lib/grafana/dashboards
restart: unless-stopped
volumes:
grafana_data:
データソースのプロビジョニング
# grafana/provisioning/datasources/datasources.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
jsonData:
timeInterval: "15s"
queryTimeout: "60s"
exemplarTraceIdDestinations:
- datasourceUid: tempo
name: trace_id
- name: Loki
type: loki
access: proxy
url: http://loki:3100
jsonData:
derivedFields:
- datasourceUid: tempo
matcherRegex: "trace_id=(\\w+)"
name: TraceID
url: '${__value.raw}'
- name: Tempo
type: tempo
access: proxy
url: http://tempo:3200
jsonData:
tracesToLogsV2:
datasourceUid: loki
spanStartTimeShift: '-1h'
spanEndTimeShift: '1h'
tags:
- key: service.name
value: job
serviceMap:
datasourceUid: prometheus
search:
hide: false
lokiSearch:
datasourceUid: loki
8.2 ダッシュボード設計の原則
原則1: ストーリーを語る
良いダッシュボードは「ストーリー」を持つ。閲覧者が上から下へと読み進めると、自然に問題の全体像から詳細へとズームインできる設計にする。
【推奨レイアウト: 上から下へのズームイン】
行1: サービス全体の健全性(大きな数字・ステータス)
├── 可用性 99.98%
├── p99レイテンシ 145ms
└── エラーレート 0.02%
行2: 過去24時間のトレンド(時系列グラフ)
├── リクエストレート
├── エラーレート
└── レイテンシ(p50/p95/p99)
行3: インフラリソース状況
├── CPU使用率(ノード別)
├── メモリ使用率
└── ディスクI/O
行4: 詳細な内訳
├── エンドポイント別レイテンシ(Top10)
└── エラーログ(Loki)
原則2: 適切なビジュアライゼーションを選ぶ
| データの性質 | 適したパネルタイプ |
|---|---|
| 時系列データのトレンド | Time series(折れ線グラフ) |
| 現在の値(ゲージ的) | Stat, Gauge |
| 比較・ランキング | Bar chart |
| 割合 | Pie chart(ただし多用しない) |
| 多次元のヒートマップ | Heatmap |
| テキスト・ステータス | Table, State timeline |
| ログ表示 | Logs panel |
原則3: 色を意味に使う
緑: 正常
黄: 警告(注意が必要)
オレンジ: 異常(対応必要)
赤: 重大(今すぐ対応)
# Grafanaでのしきい値設定(panel.json)
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "green", "value": null},
{"color": "yellow", "value": 80},
{"color": "orange", "value": 90},
{"color": "red", "value": 95}
]
}
8.3 変数(Variables)の活用
変数を使うとダッシュボードを再利用可能にできる。
# 変数の設定(UIで設定)
1. クラスター選択変数
Name: cluster
Type: Query
Query: label_values(up, cluster)
Multi-value: true
Include All: true
2. ネームスペース変数(クラスター変数に連動)
Name: namespace
Type: Query
Query: label_values(up{cluster="$cluster"}, namespace)
Multi-value: true
3. サービス変数(ネームスペースに連動)
Name: service
Type: Query
Query: label_values(up{cluster="$cluster", namespace="$namespace"}, job)
変数を使ったクエリ
# 変数を埋め込んだクエリ
sum by (endpoint) (
rate(http_requests_total{
cluster="$cluster",
namespace="$namespace",
job=~"$service"
}[5m])
)
# Grafana内部変数
# $__interval: パネルの時間範囲に基づいた自動調整間隔
# $__rate_interval: rate()に最適化された間隔(最低4倍のscrape_interval)
# $__from / $__to: 時間範囲の開始・終了
rate(http_requests_total[${__rate_interval}]) # 推奨
8.4 実践的なダッシュボードJSON
{
"title": "Service Overview Dashboard",
"uid": "service-overview",
"tags": ["service", "sre", "production"],
"time": {"from": "now-3h", "to": "now"},
"refresh": "30s",
"templating": {
"list": [
{
"name": "cluster",
"type": "query",
"query": "label_values(up, cluster)",
"multi": false,
"includeAll": false
},
{
"name": "service",
"type": "query",
"query": "label_values(up{cluster=\"$cluster\"}, job)",
"multi": true,
"includeAll": true,
"current": {"selected": true, "text": "All", "value": "$__all"}
}
]
},
"panels": [
{
"id": 1,
"title": "Availability (SLO: 99.9%)",
"type": "stat",
"gridPos": {"h": 4, "w": 6, "x": 0, "y": 0},
"targets": [
{
"expr": "(1 - sum(rate(http_requests_total{cluster=\"$cluster\", job=~\"$service\", status_code=~\"5..\"}[24h])) / sum(rate(http_requests_total{cluster=\"$cluster\", job=~\"$service\"}[24h]))) * 100",
"legendFormat": "Availability"
}
],
"options": {
"reduceOptions": {"calcs": ["lastNotNull"]},
"colorMode": "background"
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"decimals": 3,
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "red", "value": null},
{"color": "yellow", "value": 99.5},
{"color": "green", "value": 99.9}
]
}
}
}
},
{
"id": 2,
"title": "p99 Latency",
"type": "stat",
"gridPos": {"h": 4, "w": 6, "x": 6, "y": 0},
"targets": [
{
"expr": "histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{cluster=\"$cluster\", job=~\"$service\"}[5m]))) * 1000",
"legendFormat": "p99 Latency"
}
],
"fieldConfig": {
"defaults": {
"unit": "ms",
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "green", "value": null},
{"color": "yellow", "value": 500},
{"color": "red", "value": 1000}
]
}
}
}
},
{
"id": 3,
"title": "Request Rate",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 4},
"targets": [
{
"expr": "sum by (job) (rate(http_requests_total{cluster=\"$cluster\", job=~\"$service\"}[$__rate_interval]))",
"legendFormat": "{{job}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "reqps",
"custom": {
"lineWidth": 2,
"fillOpacity": 10
}
}
}
},
{
"id": 4,
"title": "Error Rate",
"type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 4},
"targets": [
{
"expr": "sum by (job) (rate(http_requests_total{cluster=\"$cluster\", job=~\"$service\", status_code=~\"5..\"}[$__rate_interval])) / sum by (job) (rate(http_requests_total{cluster=\"$cluster\", job=~\"$service\"}[$__rate_interval])) * 100",
"legendFormat": "{{job}}"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent",
"custom": {
"lineWidth": 2,
"fillOpacity": 10,
"thresholdsStyle": {"mode": "line+area"}
},
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "green", "value": null},
{"color": "red", "value": 5}
]
}
}
}
}
]
}
8.5 Grafana as Code(Grafonnet / Jsonnet)
ダッシュボードをコードとして管理することで、バージョン管理・レビュー・再利用が可能になる。
// dashboard.jsonnet
local grafana = import 'grafonnet/grafana.libsonnet';
local dashboard = grafana.dashboard;
local row = grafana.row;
local singlestat = grafana.singlestat;
local graphPanel = grafana.graphPanel;
local prometheus = grafana.prometheus;
local template = grafana.template;
dashboard.new(
'Service Overview',
tags=['service', 'sre'],
refresh='30s',
time_from='now-3h',
uid='service-overview',
)
.addTemplate(
template.datasource(
'datasource',
'prometheus',
'Prometheus',
label='Datasource',
)
)
.addTemplate(
template.new(
'service',
'$datasource',
'label_values(up, job)',
label='Service',
multi=true,
includeAll=true,
)
)
.addPanel(
graphPanel.new(
'Request Rate',
datasource='$datasource',
format='reqps',
span=12,
)
.addTarget(
prometheus.target(
'sum by (job) (rate(http_requests_total{job=~"$service"}[$__rate_interval]))',
legendFormat='{{job}}',
)
),
gridPos={h: 8, w: 12, x: 0, y: 0},
)
# Jsonnetのコンパイルとデプロイ
# インストール
go install github.com/google/go-jsonnet/cmd/jsonnet@latest
jb init
jb install github.com/grafana/grafonnet-lib/grafonnet
# コンパイル
jsonnet -J vendor dashboard.jsonnet > dashboard.json
# Grafana APIでデプロイ
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $GRAFANA_API_KEY" \
-d @dashboard.json \
https://grafana.example.com/api/dashboards/db
8.6 Grafana Alertingの設定
# grafana/provisioning/alerting/rules.yml
apiVersion: 1
groups:
- orgId: 1
name: Service Alerts
folder: SRE Alerts
interval: 1m
rules:
- uid: high-error-rate
title: High Error Rate
condition: C
data:
- refId: A
relativeTimeRange:
from: 300
to: 0
datasourceUid: prometheus
model:
expr: |
sum(rate(http_requests_total{status_code=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
intervalMs: 1000
maxDataPoints: 43200
instant: true
- refId: C
datasourceUid: "__expr__"
model:
type: threshold
conditions:
- evaluator:
params: [0.05]
type: gt
operator:
type: and
query:
params: [A]
noDataState: NoData
execErrState: Error
for: 5m
labels:
severity: critical
team: backend
annotations:
summary: "Error rate exceeds 5%"
description: "Current error rate: {{ $values.A.Value | humanizePercentage }}"
runbook_url: "https://wiki.example.com/runbooks/high-error-rate"
8.7 よく使うダッシュボードのパターン
USEダッシュボード(インフラ用)
# CPU Utilization(利用率)
(1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))) * 100
# CPU Saturation(飽和)
avg by (instance) (node_load1) / count by (instance) (node_cpu_seconds_total{mode="idle"}) * 100
# Memory Utilization
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100
# Memory Saturation(ページング)
rate(node_vmstat_pgmajfault[5m])
# Disk I/O Utilization
rate(node_disk_io_time_seconds_total[5m]) * 100
# Network Saturation(エラーとドロップ)
rate(node_network_transmit_drop_total[5m]) + rate(node_network_receive_drop_total[5m])
REDダッシュボード(サービス用)
# Rate(リクエストレート)
sum by (service) (rate(http_requests_total[$__rate_interval]))
# Errors(エラーレート)
sum by (service) (rate(http_requests_total{status_code=~"5.."}[$__rate_interval]))
/
sum by (service) (rate(http_requests_total[$__rate_interval]))
# Duration(p50/p95/p99)
histogram_quantile(0.50, sum by (service, le) (rate(http_request_duration_seconds_bucket[$__rate_interval])))
histogram_quantile(0.95, sum by (service, le) (rate(http_request_duration_seconds_bucket[$__rate_interval])))
histogram_quantile(0.99, sum by (service, le) (rate(http_request_duration_seconds_bucket[$__rate_interval])))
8.8 まとめ
- ダッシュボードはストーリーを持ち、上から下へとズームインする設計にする
- 変数(テンプレート)を使ってダッシュボードを再利用可能にする
- 色はステータスを表すために一貫して使う
- ダッシュボードはJsonnet/Grafonnetでコード管理する
- REDダッシュボード(サービス)とUSEダッシュボード(インフラ)を組み合わせる
9. ログ管理の基礎
この章で重視すること
- ログとは何か、なぜ重要か
- ログの種類とフォーマット
- 構造化ログ vs 非構造化ログ
- ログのライフサイクル管理
- ログ収集のアーキテクチャ
9.1 ログとは何か
ログはシステムが生成するイベント記録の集合体だ。ソフトウェアエンジニアリングにおけるログの価値は:
- デバッグ: 問題が発生したときの詳細な状況記録
- 監査: セキュリティ・コンプライアンス上の記録
- 分析: ビジネスイベントやユーザー行動の分析
- パフォーマンス: スロークエリやボトルネックの特定
9.2 ログのフォーマット進化
第1世代: 非構造化ログ
# Apache Combined Log Format(古典的)
192.168.1.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326
192.168.1.2 - - [10/Oct/2000:13:55:37 -0700] "POST /api/login HTTP/1.1" 401 512
# 問題点:
# - grepでしか検索できない
# - フィールドの抽出がパーサー依存
# - ツールごとにフォーマットが違う
第2世代: 構造化ログ(JSON)
{
"timestamp": "2024-01-15T10:23:45.123Z",
"level": "info",
"service": "api-gateway",
"version": "2.3.1",
"environment": "production",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"message": "Request processed successfully",
"http": {
"method": "POST",
"url": "/api/v1/orders",
"status_code": 201,
"duration_ms": 145,
"bytes_sent": 1024,
"client_ip": "203.0.113.1",
"user_agent": "Mozilla/5.0..."
},
"user": {
"id": "usr_123456",
"tier": "premium"
},
"order": {
"id": "ord_789abc",
"item_count": 3,
"total_amount": 15800
}
}
9.3 ログレベルの適切な使用
TRACE - 最も詳細。関数の入出力、ループ内の状態など
本番では絶対に有効にしない
DEBUG - デバッグ用詳細情報。変数の値、処理ステップなど
本番では通常無効。障害調査時に一時的に有効化
INFO - 重要なビジネスイベント。正常フローのマイルストーン
「何かが起きた」を記録する
例: ユーザーログイン、注文作成、決済完了
WARN - 予期しない状況だが処理は継続
例: キャッシュミス、リトライ、設定値の異常
アラートの候補(頻発する場合)
ERROR - 操作が失敗。ユーザーへの影響あり
例: DBクエリ失敗、外部APIタイムアウト
アラートが必要
FATAL - プロセスが続行できない致命的エラー
例: 設定ファイル読み込み失敗、必須DBへの接続不能
即座のアラートが必要
# Python での適切なログレベル使用例
import structlog
logger = structlog.get_logger()
class OrderService:
def create_order(self, user_id: str, items: list) -> dict:
logger.debug("create_order called", user_id=user_id, item_count=len(items))
# キャッシュのチェック
cached = self.cache.get(f"user:{user_id}:active_order")
if not cached:
logger.debug("cache miss", key=f"user:{user_id}:active_order")
# 在庫チェック
for item in items:
stock = self.inventory.check(item.sku)
if stock.quantity < item.quantity:
logger.warning(
"low_stock_detected",
sku=item.sku,
available=stock.quantity,
requested=item.quantity
)
try:
order = self.db.create_order(user_id, items)
logger.info(
"order_created",
order_id=order.id,
user_id=user_id,
item_count=len(items),
total_amount=order.total
)
return order
except DatabaseException as e:
logger.error(
"order_creation_failed",
user_id=user_id,
error=str(e),
error_type=type(e).__name__
)
raise
9.4 ログ収集アーキテクチャ
【ログ収集の全体像】
┌────────────────────────────────────────────────────────────┐
│ アプリケーション層 │
│ [App A] [App B] [App C] → stdout/stderr │
│ ↓ │
│ コンテナランタイム(Docker/containerd) │
│ /var/log/containers/*.log │
└────────────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────────┐
│ ログ収集層(各ノードにDaemonSetで配置) │
│ [Promtail / Fluent Bit / Fluentd / Vector] │
│ - ファイル読み込み(tail) │
│ - ラベル付与(Pod名・ネームスペース・アプリ名) │
│ - フィルタリング・変換 │
│ - 転送(Push to Loki/Elasticsearch) │
└────────────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────────┐
│ ログ集約・保存層 │
│ [Loki / Elasticsearch / CloudWatch Logs] │
│ - 圧縮保存 │
│ - インデックス(Lokiはラベルのみ) │
│ - 検索・クエリAPI │
└────────────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────────────┐
│ 可視化・分析層 │
│ [Grafana / Kibana] │
│ - ログ検索UI │
│ - ダッシュボード │
│ - アラート │
└────────────────────────────────────────────────────────────┘
Promtailの設定例(Kubernetes環境)
# promtail-config.yml
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
batch_wait: 1s
batch_size: 1048576 # 1MB
scrape_configs:
# Kubernetes Podのログ収集
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
pipeline_stages:
# コンテナランタイムのログフォーマットをパース
- cri: {}
# JSON形式のログをパース
- json:
expressions:
level: level
trace_id: trace_id
service: service
# ラベルとして抽出
- labels:
level:
trace_id:
service:
# 特定のログを除外(ヘルスチェック)
- drop:
expression: '.*GET /health.*'
drop_counter_reason: health_check_filtered
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod
- source_labels: [__meta_kubernetes_pod_container_name]
target_label: container
- source_labels: [__meta_kubernetes_pod_label_app]
target_label: app
- source_labels: [__meta_kubernetes_pod_label_version]
target_label: version
- replacement: /var/log/pods/*$1/*.log
separator: /
source_labels:
- __meta_kubernetes_pod_uid
- __meta_kubernetes_pod_container_name
target_label: __path__
Fluent Bitの設定例
# fluent-bit.conf
[SERVICE]
Flush 5
Log_Level info
HTTP_Server On
HTTP_Listen 0.0.0.0
HTTP_Port 2020
[INPUT]
Name tail
Tag kube.*
Path /var/log/containers/*.log
multiline.parser docker, cri
DB /var/log/flb_kube.db
Mem_Buf_Limit 50MB
Skip_Long_Lines On
Refresh_Interval 10
[FILTER]
Name kubernetes
Match kube.*
Kube_URL https://kubernetes.default.svc:443
Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token
Kube_Tag_Prefix kube.var.log.containers.
Merge_Log On
Keep_Log Off
K8S-Logging.Parser On
K8S-Logging.Exclude Off
[FILTER]
Name grep
Match kube.*
Exclude log /health|/ready|/metrics
[OUTPUT]
Name loki
Match kube.*
Host loki
Port 3100
Labels job=fluentbit, app=$kubernetes['labels']['app'], namespace=$kubernetes['namespace_name']
Auto_Kubernetes_Labels On
Line_Format json
Retry_Limit False
9.5 ログ保持とコスト管理
【ログ保持期間の設計原則】
Hot Storage(高速・高コスト)
└── 直近7日: 全ログ、高速検索可能
Warm Storage(中速・中コスト)
└── 7日〜90日: 圧縮保存、クエリは数秒〜数十秒
Cold Storage(低速・低コスト)
└── 90日〜数年: S3/GCSなどオブジェクトストレージ、コンプライアンス用
# Lokiの保持期間設定
limits_config:
retention_period: 744h # 31日
compactor:
working_directory: /loki/compactor
shared_store: s3
retention_enabled: true
# ストリーム別の保持期間(Loki 2.8以降)
ruler:
storage:
type: local
# 保持ポリシー(ストリームレベル)
# per_stream_rate_limit: 10MB # ストリームごとのレート制限
9.6 まとめ
- ログは「何が起きたか」の詳細記録で、デバッグ・監査・分析に使う
- 構造化ログ(JSON)を使うことで機械可読性が上がり、フィルタリング・集計が容易になる
- ログレベルを適切に使い、本番のDEBUGログは無効にする
- PromtailまたはFluent BitでKubernetesのログを自動収集する
- 保持期間は Hot/Warm/Cold の3段階で設計してコストを最適化する
10. Loki
この章で重視すること
- Lokiのアーキテクチャと設計思想
- Prometheusインスパイアのラベルベースアプローチ
- デプロイメントモードの選択
- Lokiの設定ファイル詳解
- パフォーマンスチューニング
10.1 Lokiの設計思想
「Prometheusのログ版」 というのがLokiのコンセプトだ。ログの全文インデックスを作らず、ラベルのみをインデックス化し、ログ本文はオブジェクトストレージに圧縮保存する。
【ElasticsearchとLokiの比較】
Elasticsearch:
- ログの全文をインデックス化
- 任意のフィールドで高速検索
- 高コスト(インデックスはログの数倍のサイズ)
- 大規模環境では運用が複雑
Loki:
- ラベル(メタデータ)のみをインデックス化
- ログ本文の検索はgrep的なフィルタリング
- 低コスト(オブジェクトストレージに圧縮保存)
- Prometheusと同じラベルモデル → 相関が容易
10.2 Lokiのアーキテクチャ
┌────────────────────────────────────────────────────────────────┐
│ Lokiアーキテクチャ(マイクロサービスモード) │
│ │
│ ┌──────────┐ │
│ │ Promtail │──→ [Distributor] ──→ [Ingester × N] ──→ [Chunk] │
│ │ Fluent │ ↓ ↓ │
│ │ Vector │ 一貫性ハッシュ ┌──────────────┐ │
│ └──────────┘ (Consistent Hash) │ Object Storage│ │
│ │ (S3/GCS/MinIO)│ │
│ ┌──────────┐ └──────────────┘ │
│ │ Grafana │──→ [Query Frontend] ──→ [Querier × N]──→↑ │
│ │ LogCLI │ ↓ │
│ └──────────┘ キャッシュ・分割 [Index Store] │
│ 並列クエリ (BoltDB/DynamoDB) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Compactor(バックグラウンド) │ │
│ │ チャンクの圧縮・保持期間管理 │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
コンポーネントの役割
| コンポーネント | 役割 |
|---|---|
| Distributor | ログを受信し、Ingesterに分配(一貫性ハッシュ) |
| Ingester | ログをメモリに保持し、定期的にS3に書き込む |
| Querier | クエリを実行してIngesterとS3からデータを取得 |
| Query Frontend | クエリを分割・並列化し、キャッシュを管理 |
| Compactor | S3上のチャンクを圧縮・保持期間を適用 |
| Ruler | アラートルールを評価 |
10.3 Lokiのデプロイメントモード
モノリシックモード(小規模・開発環境)
# loki-monolithic.yml
auth_enabled: false
server:
http_listen_port: 3100
common:
instance_addr: 127.0.0.1
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100
schema_config:
configs:
- from: 2020-10-24
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
ruler:
alertmanager_url: http://alertmanager:9093
マイクロサービスモード(本番環境・大規模)
# loki-distributed.yml
auth_enabled: true
server:
http_listen_port: 3100
grpc_listen_port: 9095
grpc_server_max_recv_msg_size: 104857600 # 100MB
grpc_server_max_send_msg_size: 104857600
# インジェスター設定
ingester:
chunk_idle_period: 2h
chunk_block_size: 262144 # 256KB
chunk_target_size: 1536000 # 1.5MB
chunk_retain_period: 1m
max_transfer_retries: 0
wal:
enabled: true
dir: /loki/wal
lifecycler:
ring:
kvstore:
store: consul
consul:
host: consul:8500
replication_factor: 3
# ストレージ(S3)
storage_config:
aws:
s3: s3://ap-northeast-1/loki-chunks
bucketnames: loki-chunks
region: ap-northeast-1
access_key_id: ${AWS_ACCESS_KEY_ID}
secret_access_key: ${AWS_SECRET_ACCESS_KEY}
s3forcepathstyle: false
tsdb_shipper:
active_index_directory: /loki/index
cache_location: /loki/index_cache
cache_ttl: 24h
shared_store: s3
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: s3
schema: v13
index:
prefix: loki_index_
period: 24h
compactor:
working_directory: /loki/compactor
shared_store: s3
retention_enabled: true
limits_config:
retention_period: 744h # 31日
ingestion_rate_mb: 16
ingestion_burst_size_mb: 32
max_streams_per_user: 10000
max_global_streams_per_user: 50000
split_queries_by_interval: 30m # クエリの並列分割
max_query_parallelism: 32
max_query_length: 721h # クエリできる最大時間範囲
cardinality_limit: 100000
per_stream_rate_limit: 10MB
per_stream_rate_limit_burst: 30MB
querier:
max_concurrent: 20
query_ingesters_within: 3h
query_range:
parallelise_shardable_queries: true
cache_results: true
results_cache:
cache:
redis:
endpoint: redis:6379
expiration: 1h
10.4 Lokiのベストプラクティス
ラベル設計
# 良いラベル設計(低カーディナリティ)
{
env: "production",
cluster: "ap-northeast-1-prod",
namespace: "payment",
app: "payment-service",
pod: "payment-service-7d4f8c-xk2pq" # ← これは高カーディナリティ
}
# ラベルのカーディナリティ目安:
# env: 3-5 (prod/staging/dev...)
# cluster: 5-20
# namespace: 10-50
# app: 50-200
# pod: 数千〜数万(podは慎重に)
# ルール: 1テナントあたりのユニークストリーム数 < 10,000
# Promtailでのラベル最適化
pipeline_stages:
- json:
expressions:
level: level
trace_id: trace_id
# trace_idはラベルではなく構造化ログフィールドとして扱う
# (ラベルにすると無限にカーディナリティが増える)
- labels:
level: "" # levelはラベル(3〜7種類なので低カーディナリティ)
# trace_idはラベルにしない!
クエリパフォーマンスの最適化
# 速いクエリ(ラベルでフィルタリング後にテキスト検索)
{app="payment-service", env="production"} |= "error"
# 遅いクエリ(ラベルを指定せずテキスト検索)
{} |= "payment error" # 全ストリームを検索するので遅い!
# さらに遅い(正規表現)
{app="payment-service"} |~ "payment.*error.*declined"
# 最速(ラベルでしっかり絞り込んでから検索)
{app="payment-service", env="production", level="error"}
|= "declined"
10.5 まとめ
- Lokiはラベルのみインデックス化することで低コストのログ集約を実現
- Prometheusと同じラベルモデルなので、メトリクスとの相関が容易
- ラベルのカーディナリティを低く保つことが重要
- 本番環境ではマイクロサービスモードでS3をバックエンドに使う
11. LogQLリファレンス
この章で重視すること
- LogQLの基本構文
- ログフィルタリングパイプライン
- メトリクス集計クエリ
- パーサーの種類と使い分け
- 実践的なクエリパターン
11.1 LogQLの基本構文
LogQLクエリの構造:
{stream selector} | pipeline_stage1 | pipeline_stage2 | ...
例:
{app="payment-service", env="prod"}
| json
| level = "error"
| line_format "{{.message}}: {{.error}}"
ストリームセレクター
# ラベルによるストリーム選択
{app="payment-service"} # 完全一致
{app!="payment-service"} # 不一致
{app=~"payment.*"} # 正規表現一致
{app!~"test.*"} # 正規表現不一致
# 複数条件(AND)
{app="api-gateway", env="production", level="error"}
# 全ストリーム(非推奨: パフォーマンス問題)
{job=~".+"}
11.2 フィルタリングパイプライン
# テキストフィルター
{app="api"} |= "error" # 含む
{app="api"} != "healthcheck" # 含まない
{app="api"} |~ "error|ERROR" # 正規表現一致
{app="api"} !~ "GET /health" # 正規表現不一致
# 複数フィルターの組み合わせ(AND)
{app="payment"}
|= "error"
!= "connection_pool"
|~ "timeout|refused"
# パーサー: JSON
{app="api"}
| json
| status_code >= 500 # パース後にフィールドでフィルタ
| duration > 1000 # ミリ秒
# パーサー: logfmt
{app="worker"}
| logfmt
| level="error"
| queue="orders"
# パーサー: pattern(正規表現よりシンプル)
{app="nginx"}
| pattern `<ip> - <user> [<timestamp>] "<method> <path> <protocol>" <status> <bytes>`
| status >= 500
# パーサー: regexp
{app="legacy"}
| regexp `(?P<level>\w+) (?P<message>.*)`
| level = "ERROR"
# パーサー: unpack(OTelのログを展開)
{app="otel-collector"} | unpack
11.3 メトリクスクエリ(LogQL Metrics)
# count_over_time: 時間窓内のログ行数
count_over_time({app="api", level="error"}[5m])
# rate: 1秒あたりのログ行数
rate({app="api", level="error"}[5m])
# 集計(sum/avg/max/min/count)
sum by (app) (rate({env="prod", level="error"}[5m]))
# サービス別エラーレート
sum by (app) (rate({env="prod"} |= "error" [5m]))
/
sum by (app) (rate({env="prod"} [5m]))
# bytes_rate: 1秒あたりのバイト数
bytes_rate({app="nginx"}[5m])
# bytes_over_time: 時間窓内のバイト数
bytes_over_time({app="nginx"}[5m])
# quantile_over_time: パーセンタイル計算
# (JSON/logfmtパース後の数値フィールドに使用)
quantile_over_time(0.99, {app="api"} | json | unwrap duration [5m]) by (endpoint)
11.4 unwrap を使った高度な集計
# JSON ログから数値を抽出してパーセンタイルを計算
quantile_over_time(
0.99,
{app="payment-service"}
| json
| unwrap duration_ms [5m]
) by (endpoint)
# 平均レイテンシ
avg_over_time(
{app="api"}
| json
| unwrap response_time_ms [5m]
) by (service)
# 最大値
max_over_time(
{app="worker"}
| logfmt
| unwrap queue_depth [1m]
) by (queue_name)
# 合計バイト数
sum_over_time(
{app="nginx"}
| pattern `<_> <_> <_> <_> <_> <_> <status> <bytes> <_>`
| unwrap bytes [5m]
)
11.5 実践的なクエリパターン
エラー調査
# 直近1時間のエラーログを表示
{env="production", level="error"} [1h]
# 特定のトレースIDに関連するすべてのログ
{env="production"} | json | trace_id = "4bf92f3577b34da6a3ce929d0e0e4736"
# エラーが多いサービスTOP5
topk(5, sum by (app) (rate({env="production", level="error"}[5m])))
# スロークエリの検出(JSONログ、duration_ms > 1000)
{app="api", env="production"}
| json
| duration_ms > 1000
| line_format "SLOW: {{.endpoint}} took {{.duration_ms}}ms - user={{.user_id}}"
パフォーマンス分析
# p99レイテンシのトレンド(グラフ用)
quantile_over_time(
0.99,
{app="payment-service", env="production"}
| json
| unwrap duration_ms [$__interval]
) by (endpoint)
# エンドポイント別の平均レイテンシ
avg_over_time(
{app="api", env="production"}
| json
| unwrap response_time_ms [5m]
) by (endpoint)
セキュリティ監査
# 認証失敗の検出
{app="auth-service"}
| json
| event = "login_failed"
| line_format "FAILED LOGIN: user={{.username}} ip={{.ip_address}}"
# 管理者操作のログ
{app="admin-api"}
| json
| user_role = "admin"
# 同一IPからの大量リクエスト(1分間に100回以上)
sum by (ip) (
rate(
{app="nginx"}
| pattern `<ip> - <_>`
[1m]
)
) > 100
11.6 LogQL vs PromQLの比較
| 機能 | LogQL | PromQL |
|---|---|---|
| データ型 | ログ行(文字列) | 数値メトリクス |
| フィルタリング | パイプライン(パーサー+条件) | ラベルセレクター |
| 集計 | sum/count/rate等 | sum/avg/rate等(同じ) |
| 学習コスト | PromQL既知なら低い | - |
| 適したデータ | 詳細なイベント情報 | 集計された数値 |
11.7 まとめ
- LogQLはPromQLにインスパイアされた設計で、Prometheusユーザーには学習しやすい
- ストリームセレクター→パーサー→フィルター→フォーマットの順でパイプラインを組む
- メトリクスクエリ(rate/count_over_time)でログからメトリクスを生成できる
unwrapを使うと数値フィールドのパーセンタイルや平均を計算できる
12. ELKスタック(Elasticsearch/Logstash/Kibana)
この章で重視すること
- ELKスタックのアーキテクチャ
- Elasticsearchのインデックス設計
- Logstashのパイプライン設定
- Kibanaでのログ検索とダッシュボード
- LokiとELKの使い分け
12.1 ELKスタックの概要
ELK(またはElastic Stack)は、Elasticsearch・Logstash・Kibanaの頭文字を取ったログ管理スタックだ。近年はBeatsも加わりELKBと呼ばれることもある。
┌────────────────────────────────────────────────────────────────┐
│ ELKアーキテクチャ │
│ │
│ ┌──────────────┐ ┌──────────┐ ┌──────────────────────┐ │
│ │ Filebeat │──→│ │ │ Elasticsearch │ │
│ │ (ファイル) │ │Logstash │──→│ (検索・保存) │ │
│ │ Metricbeat │──→│ (変換・ │ │ │ │
│ │ (メトリクス) │ │ エンリッチ)│ │ ┌────────────────┐ │ │
│ │ Heartbeat │──→│ │ │ │ Index: logs-2024│ │ │
│ │ (死活監視) │ └──────────┘ │ │ Index: logs-2025│ │ │
│ └──────────────┘ │ └────────────────┘ │ │
│ └──────────────────────┘ │
│ ↓ │
│ ┌─────────┐ │
│ │ Kibana │ │
│ │ (UI) │ │
│ └─────────┘ │
└────────────────────────────────────────────────────────────────┘
12.2 Elasticsearchのインデックス設計
// インデックステンプレートの設定
PUT _index_template/logs-template
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"index.lifecycle.name": "logs-policy",
"index.lifecycle.rollover_alias": "logs"
},
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
},
"level": {
"type": "keyword"
},
"service": {
"type": "keyword"
},
"trace_id": {
"type": "keyword"
},
"message": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"duration_ms": {
"type": "long"
},
"http": {
"properties": {
"status_code": {"type": "integer"},
"method": {"type": "keyword"},
"url": {"type": "keyword"}
}
}
}
}
}
}
ILM(Index Lifecycle Management)の設定
// ログのライフサイクル管理
PUT _ilm/policy/logs-policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_size": "50gb",
"max_age": "1d"
}
}
},
"warm": {
"min_age": "7d",
"actions": {
"shrink": {
"number_of_shards": 1
},
"forcemerge": {
"max_num_segments": 1
}
}
},
"cold": {
"min_age": "30d",
"actions": {
"freeze": {}
}
},
"delete": {
"min_age": "90d",
"actions": {
"delete": {}
}
}
}
}
}
12.3 Logstashパイプライン
# logstash.conf - 本番向けパイプライン例
input {
beats {
port => 5044
}
kafka {
bootstrap_servers => "kafka:9092"
topics => ["application-logs"]
codec => json
}
}
filter {
# タイムスタンプのパース
date {
match => ["timestamp", "ISO8601"]
target => "@timestamp"
remove_field => ["timestamp"]
}
# JSONログのパース
if [message] =~ /^\{/ {
json {
source => "message"
target => "parsed"
}
mutate {
rename => {
"[parsed][level]" => "level"
"[parsed][trace_id]" => "trace_id"
"[parsed][service]" => "service"
"[parsed][duration_ms]" => "duration_ms"
"[parsed][message]" => "message"
}
remove_field => ["parsed"]
}
}
# レベルの正規化
mutate {
uppercase => ["level"]
}
# GeoIPの付加(クライアントIPから地域情報)
if [http][client_ip] {
geoip {
source => "[http][client_ip]"
target => "geoip"
fields => ["city_name", "country_code2", "latitude", "longitude"]
}
}
# 不要フィールドの削除
mutate {
remove_field => ["agent", "ecs", "input", "tags"]
}
# スロークエリにタグを付与
if [duration_ms] and [duration_ms] > 1000 {
mutate {
add_tag => ["slow_query"]
}
}
}
output {
if "slow_query" in [tags] {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "slow-queries-%{+YYYY.MM.dd}"
}
}
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "logs-%{+YYYY.MM.dd}"
pipeline => "logs-ingest-pipeline"
}
# デバッグ用(本番では無効化)
# stdout { codec => rubydebug }
}
12.4 LokiとELKの使い分け
| 比較軸 | Loki | Elasticsearch |
|---|---|---|
| コスト | 低(S3に圧縮保存) | 高(全文インデックス) |
| 検索速度 | ラベル検索は速い、テキスト検索は遅め | 全文検索が高速 |
| スケール | 容易(オブジェクトストレージ) | 複雑(シャード管理) |
| 学習コスト | 低(Prometheusと同モデル) | 高(独自概念が多い) |
| 適した用途 | クラウドネイティブ、コスト重視 | 複雑な全文検索、ビジネス分析 |
| GrafanaとのUI | ネイティブ統合 | Kibana(別UI) |
推奨:
- Kubernetes + コスト重視 → Loki
- 複雑なログ分析 + 既存ELS投資あり → Elasticsearch
- コンプライアンス・長期保存重視 → 両方(Lokiで高速検索、S3コールドストレージ)
12.5 まとめ
- ELKスタックは全文インデックスによる高速検索が強みだが、コストが高い
- インデックステンプレートとILMポリシーでデータ管理を自動化する
- Logstashでログの変換・エンリッチメントを行う
- 小規模またはコスト重視環境ではLokiが推奨
13. 構造化ログ設計
この章で重視すること
- 構造化ログの設計原則
- 言語別の実装方法
- ログとトレースの相関(Trace ID埋め込み)
- センシティブデータのマスキング
- ログサンプリング戦略
13.1 構造化ログの設計原則
原則1: フィールドの一貫性
// 全サービスで統一すべきフィールド
{
"timestamp": "2024-01-15T10:23:45.123Z", // ISO 8601
"level": "info", // trace/debug/info/warn/error/fatal
"service": "payment-service", // サービス名
"version": "2.3.1", // アプリバージョン
"environment": "production", // 環境
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", // OpenTelemetry trace_id
"span_id": "00f067aa0ba902b7", // OpenTelemetry span_id
"message": "Payment processed" // 人間が読むメッセージ
}
原則2: イベントに意味のある名前をつける
# 悪い例
logger.info("OK")
logger.error("Error occurred")
logger.info("Done")
# 良い例
logger.info("payment_charge_succeeded",
payment_id=payment_id,
amount=amount,
currency="JPY")
logger.error("payment_charge_failed",
payment_id=payment_id,
error_code="insufficient_funds",
bank_response_code="51")
logger.info("order_fulfillment_completed",
order_id=order_id,
fulfillment_time_ms=elapsed_ms)
13.2 言語別の実装
Python: structlog
import structlog
import logging
import sys
# structlogの設定
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.processors.JSONRenderer() # JSON出力
],
wrapper_class=structlog.stdlib.BoundLogger,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
# OpenTelemetryとの統合(trace_idの自動付与)
from opentelemetry import trace
def add_trace_info(logger, method, event_dict):
"""現在のトレース情報をログに付与するprocessor"""
current_span = trace.get_current_span()
if current_span.is_recording():
ctx = current_span.get_span_context()
event_dict["trace_id"] = format(ctx.trace_id, "032x")
event_dict["span_id"] = format(ctx.span_id, "016x")
return event_dict
# アプリケーションでの使用
logger = structlog.get_logger().bind(
service="payment-service",
version="2.3.1",
environment="production"
)
def process_payment(payment_id: str, amount: float) -> dict:
log = logger.bind(payment_id=payment_id, amount=amount)
log.info("payment_processing_started")
try:
result = charge_card(payment_id, amount)
log.info("payment_processing_succeeded",
transaction_id=result.id,
duration_ms=result.duration_ms)
return result
except CardDeclinedException as e:
log.warning("payment_declined",
reason=e.reason,
decline_code=e.code)
raise
except Exception as e:
log.error("payment_processing_failed",
error=str(e),
exc_info=True)
raise
Go: slog(標準ライブラリ)
package main
import (
"context"
"log/slog"
"os"
"time"
"go.opentelemetry.io/otel/trace"
)
// OpenTelemetryのtrace情報を付与するhandler
type OTelHandler struct {
slog.Handler
}
func (h *OTelHandler) Handle(ctx context.Context, r slog.Record) error {
span := trace.SpanFromContext(ctx)
if span.IsRecording() {
sc := span.SpanContext()
r.AddAttrs(
slog.String("trace_id", sc.TraceID().String()),
slog.String("span_id", sc.SpanID().String()),
)
}
return h.Handler.Handle(ctx, r)
}
func main() {
// JSON形式での出力設定
baseHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// levelをlowercaseに変換
if a.Key == slog.LevelKey {
a.Value = slog.StringValue(strings.ToLower(a.Value.String()))
}
return a
},
})
logger := slog.New(&OTelHandler{Handler: baseHandler}).With(
"service", "payment-service",
"version", "2.3.1",
"environment", "production",
)
slog.SetDefault(logger)
// 使用例
ctx := context.Background()
start := time.Now()
err := processPayment(ctx, paymentID, amount)
duration := time.Since(start).Milliseconds()
if err != nil {
slog.ErrorContext(ctx, "payment_processing_failed",
"payment_id", paymentID,
"error", err.Error(),
"duration_ms", duration,
)
} else {
slog.InfoContext(ctx, "payment_processing_succeeded",
"payment_id", paymentID,
"duration_ms", duration,
)
}
}
13.3 センシティブデータのマスキング
import re
import structlog
# マスキングprocessor
SENSITIVE_PATTERNS = {
'credit_card': re.compile(r'\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b'),
'email': re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'),
'phone': re.compile(r'\b0[0-9]{1,4}-[0-9]{1,4}-[0-9]{4}\b'),
'jwt': re.compile(r'eyJ[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*'),
}
SENSITIVE_KEYS = {'password', 'secret', 'token', 'api_key', 'private_key', 'ssn'}
def mask_sensitive_data(logger, method, event_dict):
"""センシティブデータをマスクするprocessor"""
for key, value in list(event_dict.items()):
# キー名がセンシティブな場合はマスク
if key.lower() in SENSITIVE_KEYS:
event_dict[key] = "***MASKED***"
continue
# 文字列値のパターンマッチング
if isinstance(value, str):
for pattern_name, pattern in SENSITIVE_PATTERNS.items():
if pattern.search(value):
event_dict[key] = pattern.sub("[MASKED]", value)
return event_dict
# 設定に追加
structlog.configure(
processors=[
mask_sensitive_data, # ← マスキングをprocessorチェーンに追加
# ... 他のprocessor
structlog.processors.JSONRenderer()
]
)
13.4 ログサンプリング戦略
import random
import structlog
class SamplingProcessor:
"""ログをサンプリングするprocessor"""
def __init__(self, sample_rates: dict):
# デフォルトのサンプリングレート
# level -> サンプリング率(1.0 = 100%、0.1 = 10%)
self.sample_rates = sample_rates
def __call__(self, logger, method, event_dict):
level = event_dict.get("level", "info")
rate = self.sample_rates.get(level, 1.0)
if rate < 1.0 and random.random() > rate:
raise structlog.DropEvent() # イベントを破棄
return event_dict
# 設定例
# DEBUG: 1%、INFO: 10%、WARNING: 100%、ERROR: 100%
sampler = SamplingProcessor({
"debug": 0.01,
"info": 0.10,
"warning": 1.0,
"error": 1.0,
"critical": 1.0
})
# 重要なイベントはサンプリングしない(force_log=Trueフラグで)
def sampling_processor(logger, method, event_dict):
if event_dict.get("force_log", False):
return event_dict # サンプリングしない
return sampler(logger, method, event_dict)
13.5 まとめ
- フィールド名はサービス間で統一し、trace_id/span_idを必ず含める
- イベント名は動詞_名詞_過去形の形式(payment_processing_succeeded)
- センシティブデータはログに出力する前にマスキングする
- 高ボリュームのINFOログはサンプリングしてコストを削減する
14. 分散トレーシングの基礎
この章で重視すること
- 分散トレーシングの概念と必要性
- TraceとSpanの詳細
- コンテキスト伝播の仕組み
- サンプリング戦略
- トレーシングのアンチパターン
14.1 分散トレーシングとは
マイクロサービスアーキテクチャでは、単一のユーザーリクエストが複数のサービスを経由する。分散トレーシングは、このリクエストの旅全体を記録し可視化する仕組みだ。
【ECサイトでの注文フロー例】
ユーザー: 「注文確定」ボタンをクリック
↓ 450ms
[API Gateway]
↓ 認証チェック 20ms
[Auth Service]
↓
[Order Service] ← 200ms
│ ├── [Inventory DB] 80ms
│ ├── [Price Service] 30ms
│ └── [Cart Service] 45ms
↓
[Payment Service] ← 180ms
│ ├── [Fraud Detection] 50ms
│ └── [Payment Gateway API] 120ms(外部API)
↓
[Notification Service] 30ms
├── [Email Queue] 5ms
└── [SMS Service] 25ms
総所要時間: 450ms
最大ボトルネック: Payment Gateway API (120ms)
このフローで「なぜ450msかかったのか」を知るには分散トレーシングが必須だ。
14.2 TraceとSpanの詳細
Traceの構造
Trace = 一連のSpanのツリー
Trace ID: 4bf92f3577b34da6a3ce929d0e0e4736
│
├── Root Span: api-gateway.handle_request (0-450ms)
│ │ SpanID: aabbcc001
│ │ Attributes:
│ │ http.method: POST
│ │ http.url: /api/v1/orders
│ │ http.status_code: 201
│ │ http.user_agent: Chrome/120
│ │ Events:
│ │ - request_validated (5ms)
│ │ - auth_completed (25ms)
│ │
│ ├── Child Span: auth-service.validate_token (5-25ms)
│ │ SpanID: aabbcc002
│ │ ParentSpanID: aabbcc001
│ │
│ ├── Child Span: order-service.create_order (30-230ms)
│ │ │ SpanID: aabbcc003
│ │ │ ParentSpanID: aabbcc001
│ │ │
│ │ ├── Child Span: postgres.query (35-115ms)
│ │ │ SpanID: aabbcc004
│ │ │ Attributes:
│ │ │ db.system: postgresql
│ │ │ db.statement: INSERT INTO orders...
│ │ │ db.rows_affected: 1
│ │ │
│ │ └── Child Span: cache.set (120-135ms)
│ │ SpanID: aabbcc005
│ │
│ └── Child Span: payment-service.charge (240-420ms)
│ │ SpanID: aabbcc006
│ │
│ ├── Child Span: fraud.check (245-295ms)
│ │ SpanID: aabbcc007
│ │
│ └── Child Span: stripe.charge (300-420ms) ← 外部API
│ SpanID: aabbcc008
│ Status: OK
│ Attributes:
│ stripe.charge_id: ch_xxx
│ stripe.amount: 15800
Spanのステータスコード
# OpenTelemetryのSpanステータス
status:
code: OK # OK / ERROR / UNSET
message: "" # エラーの場合のメッセージ
# OK: 正常終了
# ERROR: エラー(スタックトレースやエラーメッセージを含むこと)
# UNSET: 未設定(デフォルト)
14.3 コンテキスト伝播
分散トレーシングのポイントは、複数のサービスにまたがる「文脈(Context)」をリクエストとともに伝播させることだ。
【W3C Trace Context(標準仕様)】
HTTPヘッダーに付与:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
フォーマット:
00: バージョン
4bf92f3577b34da6a3ce929d0e0e4736: Trace ID(16バイト)
00f067aa0ba902b7: Parent Span ID(8バイト)
01: フラグ(01=サンプリング対象)
tracestate: vendor1=value1,vendor2=value2
(ベンダー固有の追加情報)
// Goでのコンテキスト伝播例
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"net/http"
)
// サービスA: HTTPリクエストを送信する際にコンテキストを伝播
func callServiceB(ctx context.Context, url string) (*http.Response, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
// コンテキストをHTTPヘッダーに注入
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
// → traceparent: 00-4bf92...-01 がヘッダーに付与される
return http.DefaultClient.Do(req)
}
// サービスB: HTTPリクエストを受信する際にコンテキストを抽出
func handleRequest(w http.ResponseWriter, r *http.Request) {
// HTTPヘッダーからコンテキストを抽出
ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
// → 同じTrace IDのSpanが継続する
tracer := otel.Tracer("service-b")
ctx, span := tracer.Start(ctx, "service-b.handle_request")
defer span.End()
// 以降の処理はすべて同じTrace IDで追跡される
}
14.4 サンプリング戦略
すべてのリクエストをトレースすると、データ量とコストが爆発的に増大する。サンプリングによって適切な比率でトレースを収集する。
ヘッドベースサンプリング(Head-based Sampling)
リクエストの入口(Head)でサンプリングを決定
- 固定レートサンプリング: 10%のリクエストをトレース
- 確率的サンプリング: 乱数で決定
メリット: 低オーバーヘッド
デメリット: 「重要なリクエスト」を見逃す可能性
# OpenTelemetryでの設定例
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
sampler = TraceIdRatioBased(0.1) # 10%をサンプリング
テールベースサンプリング(Tail-based Sampling)
リクエスト完了後(Tail)に重要性を判断してサンプリング
- エラーがあったトレース: 100%保持
- 遅いトレース(p99超え): 100%保持
- 正常かつ高速: 1%保持
メリット: 重要なトレースを確実に保持
デメリット: 全トレースを一時保持→高メモリ消費
実装: OpenTelemetry Collectorのtailsamplingプロセッサー
# OpenTelemetry CollectorのTail-based Sampling設定
processors:
tail_sampling:
decision_wait: 10s # 判断待機時間(全Spanが揃うまで)
num_traces: 100000 # メモリに保持するトレース数
expected_new_traces_per_sec: 1000
policies:
# エラーのあるトレースは全部保持
- name: error-policy
type: status_code
status_code: {status_codes: [ERROR]}
# レイテンシが1秒を超えるトレースを保持
- name: latency-policy
type: latency
latency: {threshold_ms: 1000}
# 正常なトレースは1%をサンプリング
- name: normal-policy
type: probabilistic
probabilistic: {sampling_percentage: 1}
14.5 トレーシングのアンチパターン
アンチパターン1: 全てのSpanにデータを詰め込む
# 悪い例: 大量のデータをSpan属性に
span.set_attribute("response_body", json.dumps(large_response)) # MB単位のデータ
span.set_attribute("db_result", str(all_rows)) # 全行のデータ
# 良い例: 要約情報のみ
span.set_attribute("response_size_bytes", len(response_body))
span.set_attribute("db_rows_returned", len(all_rows))
アンチパターン2: 高カーディナリティのSpan名
# 悪い例: URLをそのままSpan名に使う(ユーザーIDが入る)
span_name = f"GET /api/users/usr_{user_id}/orders" # 無限のSpan名
# 良い例: パラメータを除いた静的なSpan名
span_name = "GET /api/users/:user_id/orders"
span.set_attribute("user.id", user_id) # IDは属性に
14.6 まとめ
- 分散トレーシングはマイクロサービスでのリクエスト追跡に不可欠
- TraceはSpanのツリー構造で表現される
- W3C Trace Contextがコンテキスト伝播の標準仕様
- サンプリングはコストとデータ品質のバランス(テールベースを推奨)
15. OpenTelemetry
この章で重視すること
- OpenTelemetryのアーキテクチャと主要コンポーネント
- 言語別の手動・自動計装
- OTLP(OpenTelemetry Protocol)
- OpenTelemetry Collectorの設計
- 本番環境への導入戦略
15.1 OpenTelemetryとは
OpenTelemetry(OTel) は、テレメトリデータ(メトリクス・ログ・トレース)の計装・生成・収集・エクスポートのためのベンダーニュートラルなオープンスタンダードです。公式ドキュメントでも、OpenTelemetryはバックエンドそのものではなく、アプリケーションや基盤からテレメトリを出すためのフレームワークとツールキットとして位置づけられています。
2019年にOpenTracingとOpenCensusが統合して誕生し、現在はCNCFのgraduatedプロジェクトです。
導入時に大切なのは、「すべてのログを集める」ことではなく、未知の障害に対して追加計装なしで問いを立てられる状態を作ることです。trace id、span id、service name、deployment environment、versionをそろえると、ログ・メトリクス・トレースを同じリクエストやリリースに結び付けられます。
【OpenTelemetryのゴール】
Before: 各ベンダーが独自SDKを提供
Datadog SDK → Datadog
New Relic SDK → New Relic
Jaeger SDK → Jaeger
ベンダー変更のたびにコード変更が必要
After: OpenTelemetryが標準APIを提供
OpenTelemetry SDK → OpenTelemetry Collector → [Prometheus / Loki / Tempo / Datadog / ...]
ベンダー変更はCollectorの設定変更だけでOK
15.2 OpenTelemetryのアーキテクチャ
┌──────────────────────────────────────────────────────────────┐
│ OpenTelemetryのコンポーネント │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ API │ │
│ │ (言語ごとのインターフェース定義。実装はSDKが行う) │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ SDK │ │
│ │ (APIの実装。サンプリング・処理・エクスポートを担当) │ │
│ │ │ │
│ │ TracerProvider MeterProvider LoggerProvider │ │
│ │ Sampler MetricReader LogRecordProcessor │ │
│ │ SpanProcessor MetricExporter LogRecordExporter │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↓ OTLP │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ OpenTelemetry Collector │ │
│ │ │ │
│ │ Receivers → Processors → Exporters │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │Prometheus│ │ Loki │ │ Tempo │ │ Datadog │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────┘
15.3 Python での計装(手動計装)
# requirements.txt
# opentelemetry-api==1.24.0
# opentelemetry-sdk==1.24.0
# opentelemetry-exporter-otlp-proto-grpc==1.24.0
# opentelemetry-instrumentation-fastapi==0.45b0
# opentelemetry-instrumentation-sqlalchemy==0.45b0
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
# リソース情報(全テレメトリに付与されるメタデータ)
resource = Resource.create({
SERVICE_NAME: "payment-service",
SERVICE_VERSION: "2.3.1",
"deployment.environment": "production",
"host.name": os.hostname(),
"k8s.namespace.name": os.getenv("K8S_NAMESPACE", "default"),
"k8s.pod.name": os.getenv("K8S_POD_NAME", "unknown"),
})
# トレース設定
trace_exporter = OTLPSpanExporter(
endpoint="http://otel-collector:4317",
insecure=True
)
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(
BatchSpanProcessor(
trace_exporter,
max_export_batch_size=512,
max_queue_size=2048,
export_timeout_millis=30000,
)
)
trace.set_tracer_provider(tracer_provider)
# メトリクス設定
metric_exporter = OTLPMetricExporter(
endpoint="http://otel-collector:4317",
insecure=True
)
metric_reader = PeriodicExportingMetricReader(
metric_exporter,
export_interval_millis=60000 # 60秒ごとにエクスポート
)
meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
metrics.set_meter_provider(meter_provider)
# トレーサーとメーターの取得
tracer = trace.get_tracer(__name__)
meter = metrics.get_meter(__name__)
# メトリクスの定義
request_counter = meter.create_counter(
"payment_requests_total",
description="Total payment requests",
unit="1"
)
request_duration = meter.create_histogram(
"payment_request_duration_seconds",
description="Payment request duration",
unit="s",
explicit_bucket_boundaries=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5]
)
# 手動計装
def process_payment(payment_id: str, amount: float, currency: str = "JPY") -> dict:
with tracer.start_as_current_span(
"payment.process",
attributes={
"payment.id": payment_id,
"payment.amount": amount,
"payment.currency": currency,
}
) as span:
start_time = time.time()
try:
# 詐欺検知
with tracer.start_as_current_span("fraud.check") as fraud_span:
fraud_result = fraud_service.check(payment_id, amount)
fraud_span.set_attribute("fraud.score", fraud_result.score)
fraud_span.set_attribute("fraud.risk_level", fraud_result.risk_level)
if fraud_result.blocked:
span.set_attribute("payment.result", "fraud_blocked")
span.set_status(trace.Status(trace.StatusCode.ERROR, "Fraud detected"))
raise PaymentFraudException(fraud_result)
# 決済処理
with tracer.start_as_current_span("stripe.charge") as stripe_span:
result = stripe.charge(payment_id, amount, currency)
stripe_span.set_attribute("stripe.charge_id", result.id)
stripe_span.set_attribute("stripe.status", result.status)
span.set_attribute("payment.result", "success")
span.set_attribute("payment.transaction_id", result.id)
request_counter.add(1, {"result": "success", "currency": currency})
return result
except Exception as e:
span.record_exception(e)
span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
request_counter.add(1, {"result": "error", "error_type": type(e).__name__})
raise
finally:
duration = time.time() - start_time
request_duration.record(duration, {"currency": currency})
15.4 Python での自動計装(Zero-Code Instrumentation)
# 自動計装のインストール
pip install opentelemetry-distro opentelemetry-exporter-otlp
# 利用するフレームワークの計装をインストール
opentelemetry-bootstrap -a install
# 環境変数で設定
export OTEL_SERVICE_NAME=payment-service
export OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=2.3.1
export OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1 # 10%サンプリング
# 自動計装で起動
opentelemetry-instrument python app.py
# または uvicorn/gunicornと組み合わせ
opentelemetry-instrument uvicorn main:app --host 0.0.0.0 --port 8080
15.5 Go での計装
package main
import (
"context"
"log"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"go.opentelemetry.io/otel/trace"
)
func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
// OTLPエクスポーターの設定
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("otel-collector:4317"),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
// リソース情報
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName("payment-service"),
semconv.ServiceVersion("2.3.1"),
attribute.String("deployment.environment", "production"),
),
)
if err != nil {
return nil, err
}
// TracerProviderの設定
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(
sdktrace.ParentBased(
sdktrace.TraceIDRatioBased(0.1), // 10%サンプリング
),
),
)
otel.SetTracerProvider(tp)
return tp, nil
}
// サービス実装
type PaymentService struct {
tracer trace.Tracer
}
func (s *PaymentService) ProcessPayment(ctx context.Context, paymentID string, amount float64) error {
ctx, span := s.tracer.Start(ctx, "payment.process",
trace.WithAttributes(
attribute.String("payment.id", paymentID),
attribute.Float64("payment.amount", amount),
),
)
defer span.End()
// データベース操作のトレース
ctx, dbSpan := s.tracer.Start(ctx, "db.insert_payment")
err := s.db.InsertPayment(ctx, paymentID, amount)
dbSpan.End()
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
// 外部APIのトレース
ctx, apiSpan := s.tracer.Start(ctx, "stripe.charge",
trace.WithAttributes(
attribute.String("payment.gateway", "stripe"),
),
)
result, err := s.stripe.Charge(ctx, amount)
if err != nil {
apiSpan.RecordError(err)
apiSpan.SetStatus(codes.Error, err.Error())
apiSpan.End()
return err
}
apiSpan.SetAttributes(attribute.String("stripe.charge_id", result.ID))
apiSpan.End()
span.SetAttributes(attribute.String("payment.result", "success"))
return nil
}
15.6 リソース属性とセマンティック規約
OpenTelemetryのセマンティック規約(Semantic Conventions)は、テレメトリデータの属性命名を標準化する。これにより、異なるベンダー・言語間でデータを一貫性を持たせて解釈できる。
リソース属性の標準命名
リソース属性は、テレメトリの生成元を記述するメタデータだ。以下は主要なリソース属性のカテゴリ:
【サービスレベル】
service.name: サービス名(必須)
service.version: サービスバージョン
service.instance.id: インスタンスID(複数インスタンスがある場合に区別)
service.namespace: サービスが属する論理グループ
【デプロイメント】
deployment.environment: 環境(production/staging/development)
deployment.version: デプロイメントバージョン
【ホスト情報】
host.name: ホスト名
host.id: マシン一意識別子
host.type: マシン種別(vm, container, baremetal)
【Kubernetesリソース】
k8s.cluster.name: クラスター名
k8s.node.name: ノード名
k8s.namespace.name: Namespaceパス(e.g., "payment-prod")
k8s.pod.name: Pod名
k8s.pod.uid: Pod UID
k8s.deployment.name: Deployment名
k8s.statefulset.name: StatefulSet名
【プロセス情報】
process.pid: プロセスID
process.runtime.name: ランタイム名(e.g., "CPython")
process.runtime.version: ランタイムバージョン
【コンテナ】
container.id: コンテナID
container.image.name: イメージ名
container.image.tag: イメージタグ
例えば、PaymentサービスをKubernetes上で実行する場合のリソース属性の完全例:
resource:
service.name: payment-service
service.version: 2.3.1
service.namespace: financial-services
deployment.environment: production
k8s.cluster.name: production-asia
k8s.namespace.name: payment-prod
k8s.pod.name: payment-service-7d4f2b5x8
k8s.deployment.name: payment-service
host.name: node-12
container.id: abc123def456
process.runtime.name: CPython
process.runtime.version: 3.11.2
このリソース属性は、生成されたすべてのスパン・メトリクス・ログに自動的に付与される。これにより、Grafanaで「production環境のpayment-serviceからのエラー」といった多次元フィルタリングが可能になる。
スパン属性の標準化
スパン属性は、特定の操作の詳細を記述する。OpenTelemetryはHTTP・DB・キュー・RPC等の一般的なシナリオに対して標準属性を定義している。
【HTTP属性】
http.request.method: HTTP メソッド(GET, POST, etc)
http.response.status_code: レスポンスステータスコード(200, 500, etc)
http.url: 完全なURL
http.target: URLのパス部(クエリを含む)
http.host: ホストヘッダ値
http.scheme: httpまたはhttps
http.flavor: HTTP/1.1, HTTP/2.0, etc
【データベース属性】
db.system: DBMS種別(postgresql, mysql, mongodb, redis, etc)
db.name: データベース名
db.user: 接続ユーザー
db.statement: SQLまたはクエリステートメント
db.operation: クエリ種別(select, insert, update, delete)
db.sql.table: テーブル名
【メッセージング属性】
messaging.system: メッセージング実装(kafka, rabbitmq, sqs, etc)
messaging.destination.name: キュー/トピック名
messaging.message.id: メッセージID
messaging.operation: 操作(publish, receive, process)
【RPC属性】
rpc.system: rpc実装(grpc, jsonrpc, etc)
rpc.service: サービス名
rpc.method: メソッド名
例えば、PaymentサービスがPostgresに決済データを挿入する場合:
with tracer.start_as_current_span("db.insert") as span:
span.set_attribute("db.system", "postgresql")
span.set_attribute("db.name", "payment_db")
span.set_attribute("db.user", "payment_app")
span.set_attribute("db.operation", "insert")
span.set_attribute("db.sql.table", "payments")
span.set_attribute("db.statement", "INSERT INTO payments (id, amount) VALUES (?, ?)")
# 実際のDB操作実行
これらの標準属性により、「PostgreSQLのinsert操作で500ms以上かかったスパンを検索」といったクエリが可能になる。
15.7 サンプリング戦略(Head vs Tail)
OpenTelemetryのサンプリングは、トレース量を削減しながら代表性を保つための重要な設計だ。2つのサンプリング戦略を理解する必要がある。
Head Sampling(ヘッドサンプリング)
Head Samplingは、リクエストの入口時点で(すべてのスパンの内容を見ずに)サンプリング決定を行う。
代表的なHead Sampling戦略:
- Consistent Probability Sampling(確率的一貫サンプリング)
- Trace IDのハッシュに基づいて確率的にサンプリング決定
- 同じTrace IDなら同じサービス間で一貫性を保証
- 実装:
TraceIdRatioBased(0.1)→ 10%サンプリング
# Python OpenTelemetry example
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
# 10%のトレースをサンプリング(残り90%は破棄)
tracer_provider = TracerProvider(
sampler=TraceIdRatioBased(0.1)
)
利点:
- 決定はリクエスト入口で確定(後続サービスで統一)
- 計算コスト最小(ハッシュ計算のみ)
- 予測可能: 100万リクエストなら約10万トレースが必ず保存
欠点:
- エラートレースでも破棄される可能性(重要な障害を見落とす危険)
- レイテンシが長いトレースを優先できない
- 動的な優先度判断ができない
Tail Sampling(テイルサンプリング)
Tail Samplingは、リクエストの終了時点で(全スパンの内容を見た後に)サンプリング決定を行う。
代表的なTail Sampling戦略:
- Error-based Tail Sampling
- トレースにエラースパンがあれば100%保存
- 正常なトレースは確率的に保存
# OpenTelemetry Collector設定
tail_sampling:
policies:
- name: error_sampler
type: status_code
status_codes: [ERROR] # エラーなら100%保存
- name: healthy_sampler
type: probabilistic
probabilistic:
sampling_percentage: 10 # 正常なら10%保存
- Latency-based Tail Sampling
- 遅いトレースを優先的に保存(p99以上など)
- 速いトレースは少なく保存
tail_sampling:
policies:
- name: slow_traces
type: latency
latency:
threshold_ms: 1000 # 1秒以上なら100%保存
- name: normal_traces
type: probabilistic
probabilistic:
sampling_percentage: 5 # 1秒未満なら5%保存
- Composite Tail Sampling
- 複数条件を組み合わせ
tail_sampling:
policies:
# 優先度1: エラーは100%
- name: errors
type: status_code
status_codes: [ERROR]
# 優先度2: 遅い(>1s)は100%
- name: slow
type: latency
latency:
threshold_ms: 1000
# 優先度3: VIP顧客のリクエストは100%
- name: vip_customers
type: span_count
span_count:
min_spans: 1
max_spans: 10000
and_sample_rate: 1.0
# デフォルト: 2%保存
- name: default
type: probabilistic
probabilistic:
sampling_percentage: 2
利点:
- エラーや遅いリクエストを確実に保存
- 重要なトレースの見落としなし
- 不要な正常トレースは削減できる
欠点:
- エッジケースの発見が難しい(設定した条件外の問題)
- メモリ使用量が増加(全スパン情報一時保持)
- 決定遅延(スパンが完了するまで待機)
実装のバランス
【推奨戦略】
高トラフィック(1000+ req/s):
→ Tail Sampling推奨
→ エラー・遅延は100%, 正常は確率的
低トラフィック(<100 req/s):
→ Head Sampling推奨
→ 単純さ重視, 全トレース保存でもコスト許容可能
ハイブリッド:
Head Sampling(50%) → Tail Sampling(エラー100%, 正常5%)
→ 入口で半減 → 出口で条件付けで再減
15.8 Java での計装(Spring Boot)
// build.gradle
dependencies {
implementation "io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter:2.3.0"
implementation "io.opentelemetry:opentelemetry-exporter-otlp:1.38.0"
}
# application.yml
otel:
service:
name: payment-service
exporter:
otlp:
endpoint: http://otel-collector:4317
traces:
sampler: parentbased_traceidratio
sampler:
arg: "0.1"
resource:
attributes:
deployment.environment: production
service.version: 2.3.1
// Javaエージェントを使った自動計装(コード変更不要)
// JVM起動オプションに追加するだけ
// java -javaagent:/path/to/opentelemetry-javaagent.jar \
// -Dotel.service.name=payment-service \
// -Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
// -jar payment-service.jar
// 手動でカスタムSpanを追加する場合
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.GlobalOpenTelemetry;
@Service
public class PaymentService {
private final Tracer tracer = GlobalOpenTelemetry.getTracer("payment-service");
public Payment processPayment(String paymentId, BigDecimal amount) {
Span span = tracer.spanBuilder("payment.process")
.setAttribute("payment.id", paymentId)
.setAttribute("payment.amount", amount.doubleValue())
.startSpan();
try (Scope scope = span.makeCurrent()) {
Payment result = doProcessPayment(paymentId, amount);
span.setAttribute("payment.result", "success");
return result;
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
}
15.9 OpenTelemetry Collectorの設定
# otel-collector-config.yml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
# Prometheusメトリクスの収集
prometheus:
config:
scrape_configs:
- job_name: 'otel-collector'
static_configs:
- targets: ['localhost:8888']
processors:
# バッチ処理(エクスポート効率化)
batch:
timeout: 1s
send_batch_size: 1024
send_batch_max_size: 2048
# メモリ使用量制限
memory_limiter:
check_interval: 1s
limit_mib: 512
spike_limit_mib: 128
# リソース属性の追加・変換
resource:
attributes:
- key: env
value: production
action: upsert
# 属性の変換
attributes:
actions:
# センシティブな属性を削除
- key: http.request.header.authorization
action: delete
# 属性名の変換
- key: http.url
action: hash # URLをハッシュ化
# Tail-based Sampling
tail_sampling:
decision_wait: 10s
num_traces: 100000
policies:
- name: errors-policy
type: status_code
status_code: {status_codes: [ERROR]}
- name: slow-traces-policy
type: latency
latency: {threshold_ms: 1000}
- name: sample-policy
type: probabilistic
probabilistic: {sampling_percentage: 10}
exporters:
# Tempoへのトレース送信
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
# Prometheusへのメトリクス送信
prometheusremotewrite:
endpoint: http://prometheus:9090/api/v1/write
# Lokiへのログ送信
loki:
endpoint: http://loki:3100/loki/api/v1/push
labels:
resource:
service.name: "service"
service.version: "version"
# デバッグ用(本番では無効化)
debug:
verbosity: detailed
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling, batch]
exporters: [otlp/tempo]
metrics:
receivers: [otlp, prometheus]
processors: [memory_limiter, batch]
exporters: [prometheusremotewrite]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [loki]
extensions: [health_check, pprof, zpages]
telemetry:
logs:
level: info
metrics:
level: basic
address: 0.0.0.0:8888
15.10 まとめ
- OpenTelemetryはベンダーニュートラルなテレメトリ標準で、広く使われる業界標準
- API(インターフェース)とSDK(実装)が分離されているので、ベンダーロックインしない
- 自動計装(Zero-code)で始めて、必要に応じて手動計装を追加する
- OpenTelemetry Collectorでデータのルーティング・変換・サンプリングを行う
16. Jaeger / Tempo 実践ガイド
この章で重視すること
- JaegerとTempoの違いと選択基準
- Grafana Tempoのセットアップ
- TraceQLによるトレース検索
- メトリクス・ログとの相関設定
- 実践的なデバッグワークフロー
16.1 JaegerとTempoの比較
| 比較軸 | Jaeger | Grafana Tempo |
|---|---|---|
| ストレージ | Cassandra / Elasticsearch | オブジェクトストレージ(S3/GCS) |
| コスト | 高(Cassandraの運用コスト) | 低(S3は安価) |
| スケール | 複雑 | 容易(Lokiと同様の設計) |
| インデックス | あり(検索が速い) | なし(メトリクス・ログ経由で検索) |
| Grafana統合 | 部分的 | ネイティブ |
推奨の考え方: 既存のGrafana/Loki基盤と統合しやすい場合はTempoを選びやすい。
16.2 Grafana Tempoのセットアップ
# tempo-config.yml
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
jaeger:
protocols:
thrift_http:
endpoint: 0.0.0.0:14268
grpc:
endpoint: 0.0.0.0:14250
ingester:
trace_idle_period: 10s
flush_check_period: 10s
max_block_bytes: 1_000_000
max_block_duration: 5m
compactor:
compaction:
compaction_window: 1h
block_retention: 336h # 14日
storage:
trace:
backend: s3
s3:
bucket: tempo-traces
endpoint: s3.ap-northeast-1.amazonaws.com
region: ap-northeast-1
access_key: ${AWS_ACCESS_KEY_ID}
secret_key: ${AWS_SECRET_ACCESS_KEY}
wal:
path: /var/tempo/wal
local:
path: /var/tempo/blocks
querier:
max_concurrent_queries: 20
search:
max_result_limit: 10000
default_result_limit: 100
# メトリクス生成(スパンからメトリクスを生成)
metrics_generator:
registry:
external_labels:
source: tempo
cluster: production
storage:
path: /tmp/tempo/generator/wal
remote_write:
- url: http://prometheus:9090/api/v1/write
processors:
- service-graphs # サービス間の依存関係グラフ
- span-metrics # スパンからメトリクスを生成
overrides:
metrics_generator_processors:
- service-graphs
- span-metrics
16.3 TraceQL リファレンス
TraceQLはTempoのトレース検索クエリ言語だ。
# 基本構文: { span_selectors } | pipeline
# すべてのトレースを検索
{}
# 特定サービスのエラートレース
{.service.name = "payment-service" && status = error}
# レイテンシが1秒以上のトレース
{duration > 1s}
# 特定の属性を持つスパン
{.http.status_code = 500}
{.db.system = "postgresql" && .db.statement =~ "SELECT.*orders"}
# 複合条件
{.service.name = "payment-service" && .http.method = "POST" && status = error}
# 特定のトレースIDで検索
{rootSpan.name = "POST /api/orders" && duration > 500ms}
# パイプライン(集計)
{.service.name = "payment-service"} | avg(duration)
{status = error} | count() by (.service.name)
{duration > 1s} | histogram(duration, "50", "95", "99")
16.4 実践的なデバッグワークフロー
シナリオ: 決済サービスのp99レイテンシが急上昇
Step 1: Grafanaのメトリクスダッシュボードでスパイクを発見
- payment-serviceのp99が200ms → 2000msに上昇
- エラーレートに変化なし(正常リクエストが遅くなっている)
Step 2: Exemplarをクリックしてトレースにジャンプ
- 遅いトレースのTraceIDを取得
- Tempoのトレースビューを開く
Step 3: トレースの分析
- 全体: 2100ms
- payment-service.process: 2090ms
- fraud-detection.check: 50ms (正常)
- stripe.charge: 2030ms ← ボトルネック!
- http.client.request: 2025ms
Step 4: Lokiでstripe関連のログを検索
{app="payment-service"} | json | trace_id = "4bf92f..."
→ "Stripe API rate limit hit, retry after 2s"
Step 5: 原因特定
→ Stripeのレート制限に達している
→ 直前にバッチ処理が走っており、大量のAPIコールが発生
Step 6: 対応
→ バッチ処理のレート制限を追加
→ Stripeのレート制限エラーを専用メトリクスで監視追加
16.5 サービスグラフ
Tempoのサービスグラフ機能で、マイクロサービス間の依存関係とレイテンシを可視化できる。
# metrics_generatorの設定でservice-graphsを有効化
processors:
service_graphs:
wait: 10s
max_items: 10000
dimensions:
- service.namespace
- http.status_code
# 生成されたサービスグラフメトリクスでのクエリ例
# サービス間のリクエストレート
rate(traces_service_graph_request_total[5m])
# サービス間のエラーレート
rate(traces_service_graph_request_failed_total[5m])
/
rate(traces_service_graph_request_total[5m])
# サービス間のp99レイテンシ
histogram_quantile(0.99,
rate(traces_service_graph_request_server_seconds_bucket[5m])
)
16.6 まとめ
- Grafana TempoはS3バックエンドの低コストなトレースストレージ
- TraceQLで柔軟なトレース検索が可能
- メトリクスのExemplar → Tempo → Lokiの相関フローで効率的なデバッグ
- サービスグラフでマイクロサービスの依存関係を自動可視化
17. 継続的プロファイリング
この章で重視すること
- 継続的プロファイリングの概念
- プロファイルの種類
- フレームグラフの読み方
- 本番環境でのプロファイリング
- 他のシグナルとの相関
17.1 継続的プロファイリングとは
従来のプロファイリングは「開発環境で、問題が発生したときに手動で実行するもの」だった。継続的プロファイリング(Continuous Profiling) は、本番環境で常時低オーバーヘッドでプロファイルを収集し続けるアプローチだ。
【継続的プロファイリングが解決する問題】
問題: 本番でのみ発生するパフォーマンス問題
「なぜCPU使用率が高いのか」← メトリクスではわからない
「なぜメモリが増え続けるのか」← ログではわからない
「なぜGCが多発するのか」← トレースでは不十分
解決: プロファイルで「どのコードが重いか」を特定
「sql.Scan()が全体CPUの73%を消費」
「Orderオブジェクトがヒープの45%を占有」
「非効率なループがGCを頻発させている」
17.2 プロファイルの種類
| プロファイル種類 | 計測対象 | 用途 |
|---|---|---|
| CPU Profile | CPU時間の消費 | CPUボトルネックの特定 |
| Memory/Heap Profile | メモリ割り当て | メモリリーク・大量確保の特定 |
| Goroutine/Thread Profile | スレッド状態 | デッドロック・リークの特定 |
| Block Profile | ブロック待機時間 | 競合・I/O待ちの特定 |
| Mutex Profile | Mutex競合 | ロック競合の特定 |
17.3 フレームグラフの読み方
【フレームグラフの構造】
幅 = そのフレームでの実行時間(広いほど時間がかかっている)
縦 = コールスタック(下が呼び出し元、上が呼び出し先)
色 = 任意(ライブラリ/ユーザーコード等で色分けされることが多い)
例:
main() ████████████████████ 100%
└── handleRequest() ███████████████████ 98%
├── processOrder() ████████████████ 82%
│ ├── db.Query() ████████ 41%
│ │ └── sql.Scan() ████████ 41% ← ここが重い!
│ ├── json.Marshal() ████ 21%
│ └── validateItems() ████ 21%
└── writeResponse() ███ 15%
分析:
- sql.Scan()がCPUの41%を消費している
- 全行のデータを取得してからフィルタリングしている可能性
- 改善案: SQLクエリにWHERE句を追加して取得行数を削減
17.4 Goでのプロファイリング実装
package main
import (
"net/http"
_ "net/http/pprof" // これだけでpprofエンドポイントが有効になる
"runtime"
)
func main() {
// pprofエンドポイントを有効化
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// メモリプロファイリングの設定
runtime.MemProfileRate = 1 // 全割り当てを記録(本番では注意)
// アプリケーションのメインロジック
startServer()
}
// プロファイルの取得コマンド
// CPU: go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
// Memory: go tool pprof http://localhost:6060/debug/pprof/heap
// Goroutine: go tool pprof http://localhost:6060/debug/pprof/goroutine
// Block: go tool pprof http://localhost:6060/debug/pprof/block
18. Pyroscope / Parca 実践
この章で重視すること
- PyroscopeとParcaの比較
- Pyroscopeのセットアップと設定
- 言語別のSDK設定
- GrafanaとのID連携
18.1 Pyroscope vs Parca
| 比較軸 | Pyroscope(Grafana) | Parca |
|---|---|---|
| eBPF対応 | あり | より強力 |
| 言語サポート | 10+言語 | 主にGo・eBPF |
| Grafana統合 | ネイティブ | 部分的 |
| マルチテナント | あり | あり |
| クラウド対応 | Grafana Cloud | セルフホスト |
| 推奨環境 | Grafana LGTMスタック | 完全OSS環境 |
18.2 Pyroscopeのセットアップ
# docker-compose.yml
services:
pyroscope:
image: grafana/pyroscope:1.5.0
container_name: pyroscope
ports:
- "4040:4040"
volumes:
- pyroscope_data:/data
command:
- "-config.file=/etc/pyroscope/config.yml"
configs:
- source: pyroscope_config
target: /etc/pyroscope/config.yml
configs:
pyroscope_config:
content: |
server:
http_listen_port: 4040
storage:
backend: s3
s3:
bucket: pyroscope-profiles
endpoint: s3.amazonaws.com
region: ap-northeast-1
compactor:
compaction_blocks_retention_period: 720h # 30日
Python SDK
import pyroscope
pyroscope.configure(
application_name="payment-service",
server_address="http://pyroscope:4040",
# 環境別タグ
tags={
"env": "production",
"version": "2.3.1",
"region": "ap-northeast-1",
},
# サンプリング設定(デフォルト: 100Hz = 10ms間隔)
sample_rate=100,
)
# 特定コードのプロファイリング
with pyroscope.tag_wrapper({"endpoint": "/api/orders", "user_tier": "premium"}):
result = process_order(order_id)
Go SDK
import "github.com/grafana/pyroscope-go"
func main() {
profiler, err := pyroscope.Start(pyroscope.Config{
ApplicationName: "payment-service",
ServerAddress: "http://pyroscope:4040",
Tags: map[string]string{
"env": "production",
"version": "2.3.1",
},
ProfileTypes: []pyroscope.ProfileType{
pyroscope.ProfileCPU,
pyroscope.ProfileAllocObjects,
pyroscope.ProfileAllocSpace,
pyroscope.ProfileInuseObjects,
pyroscope.ProfileInuseSpace,
pyroscope.ProfileGoroutines,
},
})
if err != nil {
log.Fatal(err)
}
defer profiler.Stop()
// アプリケーション起動
startServer()
}
18.3 GrafanaとPyroscopeの統合
# Grafana データソース設定
datasources:
- name: Pyroscope
type: grafana-pyroscope-datasource
url: http://pyroscope:4040
jsonData:
profileTypes:
- process_cpu:cpu
- memory:alloc_objects
- memory:alloc_space
- memory:inuse_objects
- memory:inuse_space
- goroutine:goroutine
Tempoとの相関(Trace → Profile)
# Tempoの設定にPyroscopeを追加
querier:
trace_by_id:
query_timeout: 30s
# GrafanaでのTrace→Profileリンク設定
# datasources/tempo.yml
jsonData:
tracesToProfiles:
datasourceUid: pyroscope
profileTypeId: process_cpu:cpu
customQuery: true
query: 'service.name="${__span.tags["service.name"]}"'
18.4 まとめ
- 継続的プロファイリングは「なぜ重いのか」をコードレベルで特定する
- Pyroscope(Grafana)はLGTMスタックへの統合が容易
- eBPFベースのプロファイリングはコード変更不要で強力
- Trace IDとの相関でトレース→プロファイルのドリルダウンが可能
19. アラート設計の理論と実践
この章で重視すること
- アラート設計の基本原則
- アラート疲労とその対策
- アラートの種類と使い分け
- 優れたアラートの条件
- Runbook(手順書)の設計
19.1 アラート設計の基本原則
原則1: アクショナブルなアラートのみ作る
悪いアラート:
- 「CPUが70%を超えた」← 何をすればいい?
- 「メモリが増加している」← 正常な動作かも
- 「リクエスト数が増えた」← 良いことかも
良いアラート:
- 「p99レイテンシがSLOの1秒を超えている(現在: 2.3秒)」← SLOを改善するために対応が必要
- 「エラーレートが0.1%のSLOを超えている(現在: 2.1%)」← 明確に対応が必要
- 「ディスクが4時間後に満杯になる予測」← 計画的な対応が必要
原則2: 症状に基づくアラート(原因ではなく)
原因ベースのアラート(悪い):
- CPUが高い、メモリが高い、DBコネクションが多い
症状ベースのアラート(良い):
- ユーザーへの影響(エラーレート、レイテンシ、可用性)
↑ これが高い場合のみアラート
「ユーザーが困っているか?」がアラートの判断基準
原則3: アラート疲労を防ぐ
アラート疲労のメカニズム:
1. 多すぎるアラート
2. エンジニアが毎回「これは無視していい」と判断
3. 重要なアラートも無視するようになる
4. 本物のインシデントを見逃す
対策:
- アラートの数を減らす(多くても30個以下を目標)
- 定期的なアラートレビュー(月次)
- 「このアラートを受けた人が最後にアクションを取ったのはいつか」を追跡
- 90日間アクションが取られなかったアラートは削除
19.2 アラートの種類
1. ページングアラート(今すぐ対応が必要)
- 重大度: Critical / P1
- PagerDutyで夜中でも起こす
- 例: サービス完全停止、エラーレート > 5%
- 基準: 30分以内に対応しないとSLO違反
2. チケットアラート(業務時間内に対応)
- 重大度: Warning / P2
- Slackに通知、翌朝対応でも可
- 例: CPU > 85%が1時間、ディスク使用量 > 80%
- 基準: 24時間以内に対応
3. 情報アラート(記録・傾向の把握)
- 重大度: Info / P3
- ログや週次レポートに記録
- 例: デプロイ通知、スケールアップイベント
- 基準: 対応不要(トレンド把握のみ)
19.3 優れたアラートの6要素
# 優れたアラートルールの例
- alert: PaymentServiceHighErrorRate
expr: |
(
sum(rate(http_requests_total{service="payment", status_code=~"5.."}[5m]))
/
sum(rate(http_requests_total{service="payment"}[5m]))
) > 0.05
for: 5m # 5分間継続して初めて発火(フラップ防止)
labels:
severity: critical # 重大度
team: payment # 担当チーム
service: payment # 影響サービス
pagerduty: "true" # PagerDuty通知
annotations:
# ①: 要約(何が起きているか、1行で)
summary: "Payment service error rate > 5% for 5 minutes"
# ②: 詳細説明(何が・どこで・どのくらい)
description: |
Payment service error rate is {{ $value | humanizePercentage }}
(SLO: < 0.1%, current SLO breach).
Affecting: {{ $labels.instance }}
# ③: Runbook URL(対応手順書へのリンク)
runbook_url: "https://wiki.example.com/runbooks/payment-high-error-rate"
# ④: ダッシュボードURL(視覚的確認用)
dashboard_url: "https://grafana.example.com/d/payment-service"
# ⑤: 影響範囲(ユーザーへの影響)
impact: "Users cannot complete checkout. Revenue impact: ~¥500,000/hour"
# ⑥: 関連アラート・依存関係
related_alerts: "PaymentDBConnectionPoolExhausted, StripeAPIHighLatency"
19.4 Runbook(手順書)の設計
# Runbook: PaymentServiceHighErrorRate
## 概要
決済サービスのエラーレートが閾値(5%)を超えた場合の対応手順。
## 発火条件
- アラート名: PaymentServiceHighErrorRate
- 条件: 5分間のエラーレート > 5%
- SLO影響: 現在のエラーレートで30日SLO(99.9%)が X時間で枯渇
## 初動対応(5分以内)
### Step 1: 現状確認
1. Grafanaダッシュボードを開く: [Payment Service Overview](https://grafana.example.com/d/payment)
2. 以下を確認:
- エラーレートの推移(いつから上昇したか)
- 影響を受けているエンドポイント
- エラーの種類(5xx のどのコード?)
### Step 2: 最近の変更確認
```bash
# 直近1時間のデプロイ履歴
kubectl rollout history deployment/payment-service -n payment
# 直近のConfigMap変更
kubectl get events -n payment --sort-by='.lastTimestamp' | tail -20
```
### Step 3: ログ確認
```logql
# Grafana Exploreで実行
{app="payment-service", env="production", level="error"} | json
```
## 診断フローチャート
```
エラーレート上昇
↓
直前にデプロイがあったか?
Yes → ロールバックを検討
No ↓
外部APIのエラーか? (Stripe等)
Yes → Stripeのステータスページを確認
No ↓
DBの問題か?
Yes → PaymentDBRunbookを参照
No ↓
SRE Escalation
```
## ロールバック手順
```bash
# 前のバージョンにロールバック
kubectl rollout undo deployment/payment-service -n payment
# ロールバック確認
kubectl rollout status deployment/payment-service -n payment
```
## エスカレーション
- 15分以内に解決しない場合: @payment-lead をメンション
- 30分以内に解決しない場合: インシデント宣言 → #incident チャンネルへ
- 外部APIが原因の場合: Stripe/ベンダーサポートへ連絡
## ポストモーテム
解決後は48時間以内にポストモーテムを実施すること。
[ポストモーテムテンプレート](https://wiki.example.com/postmortem-template)
19.5 アラートのテスト
# Prometheusでアラートのテスト
# amtool を使ってAlertmanagerを直接テスト
# アラートのシミュレーション
amtool alert add alertname=TestAlert severity=critical \
--annotation summary="Test alert" \
--annotation description="This is a test"
# アラートの確認
amtool alert query
# サイレンス(アラートの一時抑制)
amtool silence add alertname=PaymentServiceHighErrorRate \
--duration=2h \
--comment="Planned maintenance"
# サイレンスの確認
amtool silence query
19.6 まとめ
- アクショナブルかどうかがアラート設計の基本基準
- 症状ベース(ユーザー影響)のアラートを優先する
- アラートにはRunbook URLとダッシュボードURLを必ず含める
- 定期的なアラートレビューでアラート疲労を防ぐ
- アラートはコードとして管理し、PRレビューを行う
20. SLOベースのアラート(Burn Rate)
この章で重視すること
- SLI/SLO/エラーバジェットの復習
- バーンレートの概念
- マルチウィンドウ・マルチバーンレートアラート
- Prometheusでの実装
- エラーバジェットダッシュボード
20.1 SLI/SLO/エラーバジェットの復習
SLI(Service Level Indicator)= 計測できる指標
例: リクエストの成功率 = 成功リクエスト数 / 全リクエスト数
SLO(Service Level Objective)= SLIの目標値
例: 「30日間で99.9%の成功率を維持する」
エラーバジェット = 許容できる失敗の量
例: 99.9%のSLO → 0.1%失敗してもOK
30日間 × 60分 × 60秒 = 2,592,000秒
エラーバジェット = 2,592,000 × 0.001 = 2,592秒(約43分)
20.2 バーンレート(Burn Rate)
バーンレートとは、エラーバジェットをどのくらいの速さで消費しているかを示す指標だ。
バーンレート = 現在のエラーレート / エラーバジェットのレート
バーンレート = 1: ちょうど30日間でエラーバジェットを使い切る(正常)
バーンレート = 2: 15日でエラーバジェットを使い切る(早い)
バーンレート = 10: 3日でエラーバジェットを使い切る(非常に早い)
バーンレート = 36: 20時間でエラーバジェットを使い切る(緊急)
【バーンレートの計算】
SLO: 99.9%
エラーバジェット: 0.1% = 0.001
バーンレート = 実際のエラーレート / エラーバジェットレート
= 0.05 / 0.001
= 50
→ バーンレート50は、1時間でエラーバジェットの全量を消費する速度!
20.3 マルチウィンドウ・マルチバーンレートアラート
Google SRE Workbookが推奨するアラート設計:
【アラートの設計】
Fast Burn(緊急):
- 短期ウィンドウ(1時間)でバーンレート > 14.4
- 短期ウィンドウ(5分)でもバーンレート > 14.4
⟹ 2時間で月次エラーバジェットの5%を消費するペース
⟹ 即座にページング
Slow Burn(警告):
- 長期ウィンドウ(6時間)でバーンレート > 6
- 短期ウィンドウ(30分)でもバーンレート > 6
⟹ 5日で月次エラーバジェットの5%を消費するペース
⟹ Slackに通知、業務時間内に対応
【バーンレートとアラートウィンドウの設定値(SLO: 99.9%)】
バーンレート | ウィンドウ | 検出時間 | エラーバジェット消費
14.4 | 1h/5m | 5分 | 月次の5%(2%失効)
6 | 6h/30m | 30分 | 月次の5%(10%失効)
20.4 Prometheusでの実装
# rules/slo_alerting.yml
groups:
- name: slo-payment-service
rules:
# SLIの計算(Recording Rules)
# 1分間のエラーレート
- record: slo:payment:error_rate1m
expr: |
sum(rate(http_requests_total{service="payment", status_code=~"5.."}[1m]))
/
sum(rate(http_requests_total{service="payment"}[1m]))
# 5分間のエラーレート
- record: slo:payment:error_rate5m
expr: |
sum(rate(http_requests_total{service="payment", status_code=~"5.."}[5m]))
/
sum(rate(http_requests_total{service="payment"}[5m]))
# 30分間のエラーレート
- record: slo:payment:error_rate30m
expr: |
sum(rate(http_requests_total{service="payment", status_code=~"5.."}[30m]))
/
sum(rate(http_requests_total{service="payment"}[30m]))
# 1時間のエラーレート
- record: slo:payment:error_rate1h
expr: |
sum(rate(http_requests_total{service="payment", status_code=~"5.."}[1h]))
/
sum(rate(http_requests_total{service="payment"}[1h]))
# 6時間のエラーレート
- record: slo:payment:error_rate6h
expr: |
sum(rate(http_requests_total{service="payment", status_code=~"5.."}[6h]))
/
sum(rate(http_requests_total{service="payment"}[6h]))
# 30日間のエラーレート(SLOコンプライアンス計算用)
- record: slo:payment:error_rate30d
expr: |
sum(rate(http_requests_total{service="payment", status_code=~"5.."}[30d]))
/
sum(rate(http_requests_total{service="payment"}[30d]))
# バーンレートの計算(SLO: 99.9% → エラーバジェット: 0.001)
- record: slo:payment:burn_rate1h
expr: slo:payment:error_rate1h / 0.001
- record: slo:payment:burn_rate6h
expr: slo:payment:error_rate6h / 0.001
# エラーバジェット残量(%)
- record: slo:payment:error_budget_remaining
expr: |
(1 - (
sum(increase(http_requests_total{service="payment", status_code=~"5.."}[30d]))
/
sum(increase(http_requests_total{service="payment"}[30d]))
)) / 0.001 * 100
- name: slo-alerts-payment
rules:
# Fast Burn Alert(緊急: 2時間で5%のエラーバジェット消費)
- alert: PaymentSLOFastBurn
expr: |
(slo:payment:burn_rate1h > 14.4)
and
(slo:payment:error_rate5m / 0.001 > 14.4)
for: 2m
labels:
severity: critical
slo: payment
burn_type: fast
pagerduty: "true"
annotations:
summary: "Payment SLO: Fast burn rate detected"
description: |
Burn rate: {{ $value | humanize }}x (threshold: 14.4x)
At this rate, the monthly error budget will be exhausted in
{{ div 720 $value | humanizeDuration }}.
Error budget remaining: {{ with query "slo:payment:error_budget_remaining" }}{{ . | first | value | humanize }}%{{ end }}
runbook_url: "https://wiki.example.com/runbooks/payment-slo-burn"
# Slow Burn Alert(警告: 5日で5%のエラーバジェット消費)
- alert: PaymentSLOSlowBurn
expr: |
(slo:payment:burn_rate6h > 6)
and
(slo:payment:error_rate30m / 0.001 > 6)
for: 15m
labels:
severity: warning
slo: payment
burn_type: slow
annotations:
summary: "Payment SLO: Slow burn rate detected"
description: |
Burn rate: {{ $value | humanize }}x (threshold: 6x)
Error budget remaining: {{ with query "slo:payment:error_budget_remaining" }}{{ . | first | value | humanize }}%{{ end }}
runbook_url: "https://wiki.example.com/runbooks/payment-slo-burn"
20.4.5 エラーバジェットの詳細計算とバーンレート式
基本的なエラーバジェット計算
エラーバジェットは、SLO目標値から逆算した「許容される失敗の割合・時間」だ。
【エラーバジェット = (100 - SLO%) × 計測期間の総時間】
例: 99.9% SLO で30日間の場合:
許容失敗率 = 100 - 99.9 = 0.1% = 0.001
計測期間 = 30日 × 24時間 × 3600秒 = 2,592,000秒
エラーバジェット = 2,592,000 × 0.001 = 2,592秒(約43分)
【SLOごとのエラーバジェット一覧(30日間)】
SLO | 許容失敗率 | 合計時間 | エラーバジェット時間
99.0% | 1% | 30日 | ~7.2時間
99.5% | 0.5% | 30日 | ~3.6時間
99.9% | 0.1% | 30日 | ~43分
99.95%| 0.05% | 30日 | ~21分
99.99%| 0.01% | 30日 | ~4分
バーンレート計算式
バーンレートは、現在のエラー速度がエラーバジェットをどの速度で消費しているかを示す。
【バーンレート計算式】
バーンレート = (実際のエラーレート / 許容エラーレート) × 100
実例:
SLO: 99.9% (許容エラーレート = 0.1%)
実際のエラーレート: 0.5%
バーンレート = (0.5% / 0.1%) × 100 = 500%
→ 許容エラーレートの5倍の速度でエラーが発生している
→ エラーバジェットを5倍の速度で消費中
バーンレートと消費時間の対応表
【エラーバジェット消費時間の計算】
消費時間 = エラーバジェット / バーンレート
SLO: 99.9%, 月次エラーバジェット: 43分の場合:
バーンレート | 意味 | エラーバジェット消費時間
1x | 正常 | 43日(1ヶ月)
5x | やや高い | 8.6日
10x | 高い | 4.3日
14.4x | Fast Burn(1h/5m)| 3時間
36x | 非常に高い | 1.2日
50x | 緊急 | ~52分(月次全消費)
100x | 極度の緊急 | ~26分
Prometheus でのバーンレート式(詳細版)
# Step 1: SLO目標値を定数として定義
# SLO: 99.9% → 許容エラーレート: 0.1%
slo_target_errors = 0.001
# Step 2: 現在のエラーレート(複数ウィンドウ)
error_rate_5m = sum(rate(http_requests_total{status_code=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m]))
error_rate_1h = sum(rate(http_requests_total{status_code=~"5.."}[1h]))
/ sum(rate(http_requests_total[1h]))
error_rate_6h = sum(rate(http_requests_total{status_code=~"5.."}[6h]))
/ sum(rate(http_requests_total[6h]))
# Step 3: バーンレート計算(複数ウィンドウ)
# Fast Burn(短期)
burn_rate_fast_5m = error_rate_5m / slo_target_errors
# Slow Burn(長期)
burn_rate_slow_6h = error_rate_6h / slo_target_errors
# Step 4: アラート条件
# Fast Burn: 1時間と5分で両方ともバーンレート > 14.4
- alert: SLOBurnRateFast
expr: |
(
(sum(rate(http_requests_total{status_code=~"5.."}[1h]))
/ sum(rate(http_requests_total[1h]))) / 0.001
) > 14.4
and
(
(sum(rate(http_requests_total{status_code=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m]))) / 0.001
) > 14.4
for: 5m
labels:
severity: critical
annotations:
summary: "Fast burn rate detected"
description: "Error budget will be exhausted in ~2 hours"
# Slow Burn: 6時間と30分で両方ともバーンレート > 6
- alert: SLOBurnRateSlow
expr: |
(
(sum(rate(http_requests_total{status_code=~"5.."}[6h]))
/ sum(rate(http_requests_total[6h]))) / 0.001
) > 6
and
(
(sum(rate(http_requests_total{status_code=~"5.."}[30m]))
/ sum(rate(http_requests_total[30m]))) / 0.001
) > 6
for: 30m
labels:
severity: warning
annotations:
summary: "Slow burn rate detected"
description: "Error budget will be exhausted in ~5 days"
マルチサービス対応のバーンレート計算
複数のサービスがある場合、サービスごとにSLOを設定し、バーンレートを計算する:
# Recording Rule でサービスごとにSLOとバーンレートを計算
# サービスの成功率(SLI)
- record: sli:request_success_rate:5m
expr: |
sum by (service) (
rate(http_requests_total{status_code=~"2.."}[5m])
)
/
sum by (service) (
rate(http_requests_total[5m])
)
# エラーレート
- record: sli:error_rate:5m
expr: |
1 - sli:request_success_rate:5m
# SLO定数マッピング(ファイルで定義されたvalues.json)
# {
# "payment": 0.001, // 99.9%
# "auth": 0.0001, // 99.99%
# "ui": 0.01 // 99%
# }
# バーンレート(サービス別, 1時間ウィンドウ)
- record: slo:burn_rate:1h
expr: |
sum by (service) (
sli:error_rate:5m / on(service) group_left()
vector(slo_targets{slo="error_rate"})
)
# バーンレートアラート(サービス別)
- alert: SLOBurnRate
expr: slo:burn_rate:1h > 14.4
for: 5m
labels:
severity: critical
annotations:
summary: "High burn rate for {{ $labels.service }}"
dashboard: "https://grafana.example.com/d/slo-{{ $labels.service }}"
20.5 エラーバジェットダッシュボード
# エラーバジェット残量(0〜100%)
slo:payment:error_budget_remaining
# エラーバジェット消費率(過去30日)
(1 - slo:payment:error_budget_remaining / 100) * 100
# 現在のバーンレート
slo:payment:burn_rate1h
# バーンレートのトレンド(過去24時間)
slo:payment:burn_rate1h
# SLOコンプライアンス率
(1 - slo:payment:error_rate30d) * 100
# エラーバジェット枯渇予測(現在のバーンレートで何日後に枯渇するか)
# エラーバジェット残量[%] / (バーンレート × 100 / 30日) [%/日]
slo:payment:error_budget_remaining
/
(slo:payment:burn_rate1h * 100 / 720) # 720 = 30日 × 24時間
20.6 マルチサービスSLO管理
# 複数サービスのSLOを一括管理するRecording Rules
groups:
- name: slo-multi-service
rules:
# 各サービスのSLO設定
# サービス名 → エラーバジェット(1 - SLO目標)
# payment: 99.9% → 0.001
# checkout: 99.5% → 0.005
# inventory: 99.0% → 0.010
# 汎用バーンレート計算(service ラベルで区別)
- record: job:slo_burn_rate:1h
expr: |
label_replace(
sum by (service) (rate(http_requests_total{status_code=~"5.."}[1h]))
/
sum by (service) (rate(http_requests_total[1h])),
"burn_rate", "1h", "", ""
)
20.7 まとめ
- バーンレートはエラーバジェットの消費速度を示す重要指標
- マルチウィンドウ・マルチバーンレートアラートで真に重要な問題のみをアラート
- Fast Burn(バーンレート>14.4)は即座にページング、Slow Burn(>6)は通知のみ
- Recording Rulesでエラーバジェット計算を効率化する
21. PagerDuty / OpsGenie 設計
この章で重視すること
- オンコール管理ツールの選択
- PagerDutyの設定設計
- エスカレーションポリシーの設計
- オンコールローテーションのベストプラクティス
- インシデント対応のワークフロー
21.1 オンコール管理ツールの比較
| 比較軸 | PagerDuty | incident.io | Rootly |
|---|---|---|---|
| 機能 | 豊富 | モダン | モダン |
| 価格 | 高め | 中程度 | 中程度 |
| Slack統合 | あり | ネイティブ | ネイティブ |
| AI機能 | あり | あり | あり |
| 日本での普及 | 高い | 普及中 | 普及中 |
OpsGenieはサービス提供方針の変化に注意が必要なため、新規選定では移行性も確認する。
21.2 PagerDutyの設計原則
サービス構成
PagerDutyのサービス設計:
サービス = アラートのルーティング先
推奨設計:
payment-service → payment-team 担当
checkout-service → checkout-team 担当
infrastructure → sre-team 担当
security → security-team 担当
アンチパターン:
all-alerts → 全員に全部届く(アラート疲労の原因)
エスカレーションポリシー
【推奨エスカレーションポリシー】
レベル1(Primary On-call):
- 通知方法: Push通知 → SMS → 電話(5分以内に応答なし)
- 対応時間: アラート発生から30分以内
レベル2(Secondary / Manager):
- 条件: レベル1が30分以内に応答しない
- 通知方法: SMS → 電話
- 対応時間: アラート発生から45分以内
レベル3(エンジニアリングマネージャー):
- 条件: レベル2が15分以内に応答しない
- 通知方法: 電話
オンコールローテーションの設計
【週次ローテーションの例】
チーム: 5人(engineer-a, b, c, d, e)
Primary: 月曜9時から翌週月曜9時まで(7日間)
Secondary: 異なるメンバー
週 Primary Secondary
1 engineer-a engineer-b
2 engineer-b engineer-c
3 engineer-c engineer-d
4 engineer-d engineer-e
5 engineer-e engineer-a
【フォロー・ザ・サン(グローバルチーム向け)】
東京チーム: 09:00-18:00 JST
ロンドンチーム: 09:00-18:00 GMT(18:00-03:00 JST)
ニューヨークチーム: 09:00-18:00 ET(23:00-08:00 JST)
→ 24時間をカバーし、誰も夜中に起こされない
21.3 AlertmanagerとPagerDutyの統合
# alertmanager.yml
receivers:
- name: pagerduty-critical
pagerduty_configs:
- routing_key: 'YOUR_PAGERDUTY_INTEGRATION_KEY' # Events API v2
description: '{{ template "pagerduty.default.description" . }}'
severity: critical
client: 'Alertmanager'
client_url: '{{ template "pagerduty.default.clientURL" . }}'
details:
firing: '{{ .Alerts.Firing | len }}'
resolved: '{{ .Alerts.Resolved | len }}'
num_firing: '{{ .Alerts.Firing | len }}'
links:
- href: '{{ .CommonAnnotations.dashboard_url }}'
text: Dashboard
- href: '{{ .CommonAnnotations.runbook_url }}'
text: Runbook
21.4 オンコールのベストプラクティス
アラートの品質管理
【週次オンコールレビューの議題】
1. 今週のアラート統計
- 総アラート数: 47件
- ページング: 3件(Critical)
- Slack通知: 44件
- 誤検知(Noisy): 12件
2. 誤検知の分析
- 「DiskWillFill」: 4件(バッチ処理終了後に改善)
→ バッチ時間中はsilenceを設定する
- 「HighCPU」: 8件(デプロイ後の一時的スパイク)
→ デプロイ時に5分のsilenceを自動付与
3. アクション
- DiskWillFillをfor: 30mに変更(現在: 5m)
- デプロイパイプラインにAlertmanager silence APIを追加
インシデントコマンダーの役割
【インシデント対応のロール】
インシデントコマンダー(IC):
- 指揮を取る(自分でデバッグはしない)
- 全員の状況を把握し、リソースを配分
- ステークホルダーへの通知
- 復旧の判断
テクニカルリード:
- 実際の調査・修正を担当
- ICに定期的に状況報告(5分ごと)
コミュニケーター:
- ユーザー・ステークホルダーへの定期更新
- ステータスページの更新
- Slack #incident チャンネルの管理
21.5 まとめ
- PagerDutyはサービス別にエスカレーションポリシーを設計する
- 週次ローテーションで公平なオンコール負荷分散
- グローバルチームではフォロー・ザ・サンで夜間ページングを防ぐ
- 週次レビューでアラート品質を継続的に改善する
22. Grafana統合スタック(LGTM)
この章で重視すること
- LGTMスタックの全体像
- 各コンポーネントの役割と連携
- Docker ComposeとKubernetesでのセットアップ
- データの相関設定
- セキュリティと認証
22.1 LGTMスタックとは
LGTMはGrafana Labsが提供するオブザーバビリティスタックの頭文字:
L = Loki (ログ集約)
G = Grafana (可視化・アラート)
T = Tempo (分散トレーシング)
M = Mimir (メトリクスの長期保存)
+ OpenTelemetry Collector (テレメトリ収集・ルーティング)
+ Prometheus (メトリクス収集)
これに加えてPyroscopeを追加するとLGTMP(またはLGTM+Profiles)となる。
┌─────────────────────────────────────────────────────────────────┐
│ LGTMアーキテクチャ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ アプリケーション層 │ │
│ │ [Service A] [Service B] [Service C] [Service D] │ │
│ │ ↓ OTLP ↓ OTLP ↓ stdout ↓ /metrics │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ OpenTelemetry Collector (Gateway) │ │
│ │ Receivers → Processors → Exporters │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ OTLP ↓ Remote Write ↓ Push ↓ OTLP │
│ ┌────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Tempo │ │ Mimir │ │ Loki │ │Pyroscope │ │
│ │(Traces) │ │(Metrics) │ │ (Logs) │ │(Profiles)│ │
│ └────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ↑ ↑ ↑ ↑ │
│ └──────────────┴───────────────┴───────────────┘ │
│ ↑ │
│ ┌─────────┐ │
│ │ Grafana │ │
│ └─────────┘ │
└─────────────────────────────────────────────────────────────────┘
22.2 Docker Compose設定
# docker-compose.yml(LGTMフルスタック)
version: '3.8'
x-logging: &default-logging
driver: "json-file"
options:
max-size: "50m"
max-file: "3"
services:
# ==================== Grafana ====================
grafana:
image: grafana/grafana:11.0.0
container_name: grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin}
- GF_AUTH_ANONYMOUS_ENABLED=false
- GF_FEATURE_TOGGLES_ENABLE=traceqlEditor
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
depends_on:
- prometheus
- loki
- tempo
logging: *default-logging
# ==================== Prometheus ====================
prometheus:
image: prom/prometheus:v2.51.0
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./prometheus/rules:/etc/prometheus/rules:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=7d'
- '--web.enable-lifecycle'
- '--web.enable-remote-write-receiver'
logging: *default-logging
# ==================== Loki ====================
loki:
image: grafana/loki:3.0.0
container_name: loki
ports:
- "3100:3100"
volumes:
- ./loki/loki-config.yml:/etc/loki/loki-config.yml:ro
- loki_data:/loki
command: -config.file=/etc/loki/loki-config.yml
logging: *default-logging
# ==================== Tempo ====================
tempo:
image: grafana/tempo:2.4.0
container_name: tempo
ports:
- "3200:3200" # Tempo HTTP API
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "14268:14268" # Jaeger HTTP
volumes:
- ./tempo/tempo-config.yml:/etc/tempo/tempo-config.yml:ro
- tempo_data:/var/tempo
command: -config.file=/etc/tempo/tempo-config.yml
logging: *default-logging
# ==================== Pyroscope ====================
pyroscope:
image: grafana/pyroscope:1.5.0
container_name: pyroscope
ports:
- "4040:4040"
volumes:
- pyroscope_data:/data
logging: *default-logging
# ==================== OpenTelemetry Collector ====================
otel-collector:
image: otel/opentelemetry-collector-contrib:0.100.0
container_name: otel-collector
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "8888:8888" # Collector metrics
- "8889:8889" # Prometheus exporter
volumes:
- ./otel-collector/otel-config.yml:/etc/otelcol/config.yaml:ro
depends_on:
- prometheus
- loki
- tempo
logging: *default-logging
# ==================== Promtail ====================
promtail:
image: grafana/promtail:3.0.0
container_name: promtail
volumes:
- ./promtail/promtail-config.yml:/etc/promtail/config.yml:ro
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
command: -config.file=/etc/promtail/config.yml
logging: *default-logging
# ==================== Node Exporter ====================
node-exporter:
image: prom/node-exporter:v1.8.0
container_name: node-exporter
ports:
- "9100:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
pid: host
logging: *default-logging
# ==================== cAdvisor(コンテナメトリクス) ====================
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.47.0
container_name: cadvisor
ports:
- "8080:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
privileged: true
logging: *default-logging
volumes:
grafana_data:
prometheus_data:
loki_data:
tempo_data:
pyroscope_data:
networks:
default:
name: observability
22.3 Grafanaのプロビジョニング設定
# grafana/provisioning/datasources/all.yml
apiVersion: 1
datasources:
# Prometheus
- name: Prometheus
type: prometheus
uid: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
jsonData:
httpMethod: POST
exemplarTraceIdDestinations:
- datasourceUid: tempo
name: trace_id
urlDisplayLabel: View in Tempo
# Loki
- name: Loki
type: loki
uid: loki
access: proxy
url: http://loki:3100
jsonData:
derivedFields:
- datasourceUid: tempo
matcherRegex: '"trace_id":\s*"(\w+)"'
name: TraceID
url: '${__value.raw}'
urlDisplayLabel: View in Tempo
# Tempo
- name: Tempo
type: tempo
uid: tempo
access: proxy
url: http://tempo:3200
jsonData:
httpMethod: GET
serviceMap:
datasourceUid: prometheus
search:
hide: false
tracesToLogsV2:
datasourceUid: loki
spanStartTimeShift: '-1h'
spanEndTimeShift: '1h'
filterByTraceID: true
filterBySpanID: false
customQuery: true
query: '{app="${__span.tags["service.name"]}"} | json | trace_id="${__trace.traceId}"'
tracesToMetrics:
datasourceUid: prometheus
spanStartTimeShift: '-1h'
spanEndTimeShift: '1h'
tags:
- key: service.name
value: service
queries:
- name: Request Rate
query: 'sum(rate(http_requests_total{service="${__tags.service}"}[$__rate_interval]))'
- name: Error Rate
query: 'sum(rate(http_requests_total{service="${__tags.service}", status_code=~"5.."}[$__rate_interval]))'
# Pyroscope
- name: Pyroscope
type: grafana-pyroscope-datasource
uid: pyroscope
access: proxy
url: http://pyroscope:4040
22.4 Kubernetesへのデプロイ
# Helmを使ったLGTMスタックのデプロイ
# 1. Loki
helm repo add grafana https://grafana.github.io/helm-charts
helm install loki grafana/loki \
--namespace monitoring \
--set loki.auth_enabled=false \
--set loki.storage.type=s3 \
--set loki.storage.s3.region=ap-northeast-1 \
--set loki.storage.s3.bucketNames.chunks=loki-chunks \
--set loki.storage.s3.bucketNames.ruler=loki-ruler \
--set loki.storage.s3.bucketNames.admin=loki-admin
# 2. Tempo
helm install tempo grafana/tempo-distributed \
--namespace monitoring \
--set storage.trace.backend=s3 \
--set storage.trace.s3.bucket=tempo-traces \
--set storage.trace.s3.region=ap-northeast-1
# 3. Mimir(長期メトリクスストレージ)
helm install mimir grafana/mimir-distributed \
--namespace monitoring \
--set mimir.structuredConfig.common.storage.backend=s3 \
--set mimir.structuredConfig.common.storage.s3.bucket_name=mimir-metrics \
--set mimir.structuredConfig.common.storage.s3.region=ap-northeast-1
# 4. Grafana
helm install grafana grafana/grafana \
--namespace monitoring \
--values grafana-values.yaml
22.5 まとめ
- LGTMスタックはGrafana Labsが提供する統合オブザーバビリティプラットフォーム
- 各コンポーネントはS3をバックエンドとして使い、水平スケールが容易
- データソース間のリンク設定でシームレスな相関分析が可能
- HelmとKubernetesで本番グレードのデプロイが可能
23. OpenTelemetry Collector
この章で重視すること
- Collectorのアーキテクチャパターン(Agent vs Gateway)
- 主要なReceiver・Processor・Exporterの詳細
- 本番環境でのデプロイ戦略
- パフォーマンスチューニング
- トラブルシューティング
23.1 Agent vs Gateway パターン
Collectorを挟むと、アプリケーションはバックエンドごとの差異を意識しにくくなります。サンプリング、属性の正規化、秘匿情報の除去、複数バックエンドへの配送をCollector側へ寄せられるため、計装と運用の責務を分けやすくなります。
【Agentパターン】
各ノード/Podにサイドカーまたはデーモンセットとして配置
[App Pod]
├── [App Container] → OTLP → [OTel Collector Sidecar]
│ ↓ 直接バックエンドへ
└── [OTel Collector] ─────────────→ [Tempo/Loki/Prometheus]
メリット: シンプル、低レイテンシ
デメリット: バックエンドへの接続がノード数分増える
【Gatewayパターン(推奨)】
集中型のCollectorクラスターにルーティング
[App Pod] → OTLP → [OTel Collector Agent(DaemonSet)]
↓ OTLP
[OTel Collector Gateway(Deployment)]
├── Tail Sampling
├── Data Transformation
└── Multi-backend Routing
↓
[Tempo / Loki / Prometheus / Datadog / ...]
メリット: 集中的な処理(サンプリング・変換)、スケールしやすい
デメリット: 構成が複雑、Gatewayが単障害点になり得る
23.2 Receiver の種類
receivers:
# OTLP(最も一般的)
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
max_recv_msg_size_mib: 32
http:
endpoint: 0.0.0.0:4318
cors:
allowed_origins:
- "https://*.example.com"
# Prometheus スクレイピング
prometheus:
config:
scrape_configs:
- job_name: 'app-metrics'
scrape_interval: 15s
static_configs:
- targets: ['app:8080']
# Jaeger(移行期)
jaeger:
protocols:
thrift_http:
endpoint: 0.0.0.0:14268
grpc:
endpoint: 0.0.0.0:14250
# Zipkin(移行期)
zipkin:
endpoint: 0.0.0.0:9411
# Kafka(大規模環境)
kafka:
brokers:
- kafka:9092
topic: otlp-traces
encoding: otlp_proto
# Filelog(ファイルからログ収集)
filelog:
include:
- /var/log/pods/*/*/*.log
start_at: beginning
include_file_path: true
include_file_name: false
operators:
- type: router
id: get-format
routes:
- output: parser-docker
expr: 'body matches "^\\{"'
- output: parser-crio
expr: 'body matches "^[^ Z]+ "'
- type: json_parser
id: parser-docker
output: extract-metadata-from-filepath
- type: regex_parser
id: parser-crio
regex: '^(?P<time>[^ Z]+) (?P<stream>stdout|stderr) (?P<logtag>[^ ]*) ?(?P<log>.*){{CONTENT}}#x27;
output: extract-metadata-from-filepath
- type: regex_parser
id: extract-metadata-from-filepath
regex: '^.*\/(?P<namespace>[^_]+)_(?P<pod_name>[^_]+)_(?P<uid>[a-f0-9\-]{36})\/(?P<container_name>[^\._]+)\/(?P<restart_count>\d+)\.log{{CONTENT}}#x27;
parse_from: attributes["log.file.path"]
23.3 Processor の詳細
processors:
# バッチ処理(必須)
batch:
timeout: 1s
send_batch_size: 1024
send_batch_max_size: 2048
# メモリ制限(必須)
memory_limiter:
check_interval: 1s
limit_mib: 1024
spike_limit_mib: 256
# リソース属性の追加・変換
resource:
attributes:
- key: env
value: production
action: upsert
- key: collector.version
from_attribute: collector.version
action: insert
# 属性の操作
attributes:
actions:
# センシティブ情報の削除
- key: http.request.header.authorization
action: delete
- key: db.statement
action: hash # SQLをハッシュ化(本番DBのSQLを隠す)
# ラベルの追加
- key: environment
value: production
action: insert
# Kubernetes メタデータの付与(k8sattributes)
k8sattributes:
auth_type: serviceAccount
passthrough: false
filter:
node_from_env_var: KUBE_NODE_NAME
extract:
metadata:
- k8s.pod.name
- k8s.pod.uid
- k8s.deployment.name
- k8s.namespace.name
- k8s.node.name
- k8s.pod.start_time
labels:
- tag_name: app
key: app
from: pod
- tag_name: version
key: version
from: pod
annotations:
- tag_name: git_sha
key: git-sha
from: pod
# フィルタリング
filter:
error_mode: ignore
traces:
span:
# ヘルスチェックをフィルタリング
- 'attributes["http.route"] == "/health"'
- 'attributes["http.route"] == "/ready"'
- 'attributes["http.route"] == "/metrics"'
logs:
log_record:
# DEBUGログをフィルタリング
- 'severity_number < SEVERITY_NUMBER_INFO'
# サンプリング(ヘッドベース)
probabilistic_sampler:
sampling_percentage: 10 # 10%
# テールベースサンプリング
tail_sampling:
decision_wait: 10s
num_traces: 100000
expected_new_traces_per_sec: 1000
policies:
- name: errors
type: status_code
status_code: {status_codes: [ERROR]}
- name: slow-traces
type: latency
latency: {threshold_ms: 1000}
- name: important-services
type: string_attribute
string_attribute:
key: service.name
values: [payment-service, checkout-service]
- name: sample-rest
type: probabilistic
probabilistic: {sampling_percentage: 5}
23.4 健全性確認コマンド
# Collector の健全性確認
curl http://localhost:13133/ # health_check extension
# メトリクス確認
curl http://localhost:8888/metrics | grep otelcol
# zpages(Webダッシュボード)
# http://localhost:55679/debug/tracez
# http://localhost:55679/debug/pipelinez
# http://localhost:55679/debug/extensionz
# ログでのトラブルシューティング
kubectl logs -n monitoring deployment/otel-collector -f
# 一般的なエラー
# "exporter queue is full": batch サイズを増やす or エクスポーター追加
# "memory_limiter is blocking": limitを増やす or スケールアウト
# "context deadline exceeded": エクスポーター先のタイムアウトを確認
23.5 まとめ
- CollectorはAgent(ノードごと)とGateway(集中型)の2パターン
- 本番環境ではGatewayパターン + テールベースサンプリングを推奨
- k8sattributesプロセッサーでKubernetesメタデータを自動付与
- memory_limiterとbatchは必須のプロセッサー
24. 長期ストレージ(Thanos / Mimir)
この章で重視すること
- PrometheusのスケールアウトとHA構成の限界
- ThanosとMimirの違いと選択基準
- Thanosのコンポーネントと設定
- Mimir のアーキテクチャと設定
- S3をバックエンドにした長期保存
24.1 Prometheusのスケール限界
Prometheusの限界:
- シングルノード: 数百万の時系列が上限
- 保存期間: デフォルト15日(ディスク次第で延長可能だが限界あり)
- クラスタリング: ネイティブでサポートなし
- マルチテナント: サポートなし
解決策:
Thanos → 既存のPrometheusを維持しながら長期保存・高可用性を追加
Mimir → PrometheusをフルマネージドなTSDBに置き換え(より統合的)
24.2 ThanosとMimirの比較
| 比較軸 | Thanos | Grafana Mimir |
|---|---|---|
| アーキテクチャ | 既存Prometheusにサイドカー追加 | 完全マイクロサービス |
| マイグレーション | 容易(既存環境を維持) | 設定変更が必要 |
| スケール | 数億時系列 | 10億以上の時系列 |
| マルチテナント | 限定的 | ネイティブサポート |
| クエリパフォーマンス | 良好 | 優秀(Query Sharding) |
| 推奨シナリオ | 既存Prometheus環境 | 新規構築・大規模 |
24.3 Thanosのアーキテクチャ
┌──────────────────────────────────────────────────────────────┐
│ Thanosアーキテクチャ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Prometheus HA-1 │ │ Prometheus HA-2 │ │
│ │ +Thanos Sidecar │ │ +Thanos Sidecar │ │
│ └──────────┬───────┘ └───────┬──────────┘ │
│ │ │ │
│ └──────────┬────────┘ │
│ ↓ gRPC (StoreAPI) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Thanos Query │ │
│ │ (複数のStore APIを束ねてクエリを実行) │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↑ │
│ ┌───────────────┐ │ ┌───────────────────────┐ │
│ │ Thanos Store │──────┘ │ Thanos Ruler │ │
│ │ (S3のデータを │ │ (Recording/Alerting │ │
│ │ クエリ可能に) │ │ Rulesの評価) │ │
│ └───────────────┘ └───────────────────────┘ │
│ ↑ │
│ ┌─────────────────┐ │
│ │ Thanos Compactor│ ← S3上のブロックを圧縮・ダウンサンプリング │
│ └─────────────────┘ │
│ ↑↓ │
│ ┌──────────────────┐ │
│ │ S3 Bucket │ │
│ │ (長期保存) │ │
│ └──────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Thanos Sidecarの設定
# prometheus.yml
global:
external_labels:
cluster: 'production'
replica: '$(POD_NAME)' # Kubernetesのpod名
# Kubernetes DeploymentでThanosサイドカーを追加
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: prometheus
spec:
template:
spec:
containers:
- name: prometheus
image: prom/prometheus:v2.51.0
args:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=2d" # 短期間(Thanosが長期保存)
- "--storage.tsdb.min-block-duration=2h"
- "--storage.tsdb.max-block-duration=2h"
- "--web.enable-lifecycle"
# Thanosサイドカー
- name: thanos-sidecar
image: quay.io/thanos/thanos:v0.35.0
args:
- "sidecar"
- "--prometheus.url=http://localhost:9090"
- "--tsdb.path=/prometheus"
- "--grpc-address=0.0.0.0:10901"
- "--http-address=0.0.0.0:10902"
- "--objstore.config-file=/etc/thanos/object-store.yml"
volumeMounts:
- name: prometheus-data
mountPath: /prometheus
# object-store.yml(S3設定)
type: S3
config:
bucket: thanos-metrics
endpoint: s3.ap-northeast-1.amazonaws.com
region: ap-northeast-1
access_key: ${AWS_ACCESS_KEY_ID}
secret_key: ${AWS_SECRET_ACCESS_KEY}
insecure: false
signature_version2: false
put_user_metadata: {}
http_config:
idle_conn_timeout: 90s
response_header_timeout: 2m
insecure_skip_verify: false
trace:
enable: false
part_size: 134217728 # 128 * 1024 * 1024
24.4 Grafana Mimirのアーキテクチャ
# mimir-config.yml
# Mimir のシングルバイナリ設定(小規模)
multitenancy_enabled: false
# S3ストレージ
common:
storage:
backend: s3
s3:
bucket_name: mimir-metrics
endpoint: s3.amazonaws.com
region: ap-northeast-1
access_key_id: ${AWS_ACCESS_KEY_ID}
secret_access_key: ${AWS_SECRET_ACCESS_KEY}
blocks_storage:
s3:
bucket_name: mimir-blocks
tsdb:
dir: /data/tsdb
retention_period: 1y # ブロック保持期間(S3上)
compactor:
data_dir: /data/compactor
sharding_ring:
kvstore:
store: memberlist
distributor:
ring:
kvstore:
store: memberlist
ingester:
ring:
kvstore:
store: memberlist
replication_factor: 3
ruler_storage:
s3:
bucket_name: mimir-ruler
memberlist:
join_members:
- mimir-0.mimir-headless.monitoring.svc.cluster.local
- mimir-1.mimir-headless.monitoring.svc.cluster.local
- mimir-2.mimir-headless.monitoring.svc.cluster.local
# PrometheusのRemote Writeでデータを受け取る設定
limits:
ingestion_rate: 10000
ingestion_burst_size: 200000
max_global_series_per_user: 1500000
compactor_blocks_retention_period: 1y
24.5 PrometheusからMimirへのRemote Write
# prometheus.yml
remote_write:
- url: "http://mimir:9009/api/v1/push"
# 認証(マルチテナントの場合)
headers:
X-Scope-OrgID: tenant-production
queue_config:
capacity: 10000
max_shards: 200
min_shards: 1
max_samples_per_send: 2000
batch_send_deadline: 5s
min_backoff: 30ms
max_backoff: 5s
write_relabel_configs:
# 特定のメトリクスのみ送信(コスト削減)
- source_labels: [__name__]
regex: 'node_.*|http_.*|go_.*|process_.*'
action: keep
24.6 長期保存のコスト最適化
【Thanos/Mimirのダウンサンプリング】
生データ(0〜48時間): 元の解像度で保存
5分サンプリング(48時間以降): 5分単位に集計
1時間サンプリング(2週間以降): 1時間単位に集計
効果:
生データ 100GB → 5分サンプリング 15GB → 1時間サンプリング 2GB
長期間データのクエリは自動的にダウンサンプリングを使用
# Thanos Compactorでダウンサンプリングを実行
thanos compact \
--data-dir=/tmp/thanos-compact \
--objstore.config-file=object-store.yml \
--retention.resolution-raw=48h \
--retention.resolution-5m=14d \
--retention.resolution-1h=1y \
--downsampling.disable=false
24.7 まとめ
- Prometheusは単体では長期保存・高可用性に限界がある
- Thanosは既存環境への追加が容易、Mimirは新規大規模環境向け
- S3をバックエンドにすることで低コストな長期保存を実現
- ダウンサンプリングで長期データのクエリを高速化しコストを削減
25. Kubernetes環境のオブザーバビリティ
この章で重視すること
- Kubernetesのメトリクス体系
- kube-prometheus-stackの設定
- Kubernetes特有のログ収集
- Kubernetesイベントの活用
- Kubernetes SLOの設計
25.1 Kubernetesのメトリクス体系
Kubernetesのメトリクス階層:
Level 1: クラスターレベル
- Node: CPU, メモリ, ディスク, ネットワーク
- Control Plane: API Server, etcd, Scheduler, Controller Manager
Level 2: ワークロードレベル
- Deployment: Pod数, replicaの状態
- Pod: CPU/メモリ使用量, 再起動回数
- Container: リソース使用量, OOM回数
Level 3: アプリケーションレベル
- カスタムメトリクス(/metrics エンドポイント)
- SLI/SLO メトリクス
Level 4: ネットワーク/サービスメッシュ
- Service間のレイテンシ・エラーレート
- Ingress メトリクス
主要なKubernetesメトリクス
# Pod の再起動回数(OOMやクラッシュの検知)
increase(kube_pod_container_status_restarts_total[1h])
# Deploymentの不健全なレプリカ数
kube_deployment_status_replicas_unavailable > 0
# ノードの状態
kube_node_status_condition{condition="Ready", status="true"} == 0
# PVCの使用率
kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes * 100
# CPU Throttling(CPUリソースが制限されているか)
rate(container_cpu_throttled_seconds_total[5m]) > 0.25
# HPA(Horizontal Pod Autoscaler)の状態
kube_horizontalpodautoscaler_status_current_replicas
kube_horizontalpodautoscaler_spec_max_replicas
# etcdの健全性
etcd_server_has_leader # 1であれば正常
etcd_disk_wal_fsync_duration_seconds # I/Oレイテンシ
# API Serverのレイテンシ
apiserver_request_duration_seconds_bucket
# Scheduler のスケジューリングレイテンシ
scheduler_scheduling_attempt_duration_seconds
25.2 kube-prometheus-stackの設定
# values.yaml for kube-prometheus-stack
prometheus:
prometheusSpec:
# 追加のスクレイプ設定
additionalScrapeConfigs:
- job_name: 'custom-apps'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
# Recording Rules
ruleSelectorNilUsesHelmValues: false
ruleSelector:
matchLabels:
role: alert-rules
resources:
requests:
memory: "4Gi"
cpu: "500m"
limits:
memory: "8Gi"
cpu: "2000m"
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: gp3
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 200Gi
# アラートルールのカスタマイズ
defaultRules:
rules:
kubeScheduler: true
kubeControllerManager: true
etcd: true
alertmanager: true
kubeProxy: true
kubeApiserver: true
# Grafana の設定
grafana:
enabled: true
persistence:
enabled: true
size: 10Gi
# 追加のダッシュボード
dashboardProviders:
dashboardproviders.yaml:
apiVersion: 1
providers:
- name: 'custom'
orgId: 1
folder: 'Custom'
type: file
options:
path: /var/lib/grafana/dashboards/custom
dashboardsConfigMaps:
custom: "grafana-dashboards-custom"
25.3 Kubernetesイベントの監視
Kubernetesイベントはクラスターの状態変化を記録する重要なシグナルだ。
# イベントの確認
kubectl get events -n production --sort-by='.lastTimestamp'
# Warning イベントのみ
kubectl get events --field-selector type=Warning -A
# 特定のPodのイベント
kubectl describe pod payment-service-xxx -n production
# event-exporter for Prometheus
# kube-state-meticsのイベントエクスポート設定
apiVersion: apps/v1
kind: Deployment
metadata:
name: kubernetes-event-exporter
spec:
template:
spec:
containers:
- name: event-exporter
image: ghcr.io/resmoio/kubernetes-event-exporter:latest
args:
- "-conf=/data/config.yaml"
volumeMounts:
- name: config
mountPath: /data
volumes:
- name: config
configMap:
name: event-exporter-cfg
---
apiVersion: v1
kind: ConfigMap
metadata:
name: event-exporter-cfg
data:
config.yaml: |
logLevel: info
logFormat: json
route:
routes:
- match:
- receiver: loki
receivers:
- name: loki
loki:
streamLabels:
app: event-exporter
env: production
url: http://loki:3100/loki/api/v1/push
25.4 Kubernetes SLOの設計
# Kubernetes Deployment のSLO設計
# 目標: Deploymentの希望レプリカ数が常に利用可能であること
# SLI: 実際のREADY Pod数 / 希望Pod数
# SLO: 99.9%の時間帯で100%のレプリカが利用可能
# Recording Rules
- record: namespace_deployment:replicas_available_ratio
expr: |
min by (namespace, deployment) (
kube_deployment_status_replicas_available
/
kube_deployment_spec_replicas
)
# アラートルール
- alert: DeploymentReplicasNotAvailable
expr: |
namespace_deployment:replicas_available_ratio < 1.0
for: 5m
labels:
severity: warning
annotations:
summary: "Deployment {{ $labels.namespace }}/{{ $labels.deployment }} has unavailable replicas"
description: "{{ $value | humanizePercentage }} of replicas are available"
25.5 まとめ
- Kubernetesのメトリクスはクラスター・ワークロード・アプリケーション・ネットワークの4層で考える
- kube-prometheus-stackで主要なメトリクスとアラートを自動設定できる
- Pod再起動・CPU Throttling・PVC使用率などKubernetes特有の問題を監視する
- Kubernetesイベントもテレメトリとして活用する
26. サービスメッシュのオブザーバビリティ
この章で重視すること
- サービスメッシュがオブザーバビリティに提供する価値
- Istioのオブザーバビリティ設定
- Linkerdの組み込みオブザーバビリティ
- Envoyメトリクスの活用
- サービスグラフの自動生成
26.1 サービスメッシュとオブザーバビリティ
サービスメッシュ(Istio/Linkerd)が提供するオブザーバビリティ:
アプリコードの変更なしで以下を自動取得:
✓ サービス間のREDメトリクス(Rate/Errors/Duration)
✓ 分散トレーシング(HTTPヘッダー自動伝播)
✓ サービスの依存関係マップ(Kiali等で可視化)
✓ mTLS(サービス間の暗号化通信)の状態
✓ リトライ・サーキットブレーカーの動作状況
メリット:
- コードレベルの計装が不要
- 全サービスで一貫したメトリクス
デメリット:
- サイドカーのCPU/メモリオーバーヘッド
- ネットワークレイテンシの微増
- 学習・運用コスト
26.2 IstioのTelemetry設定
# Istioのテレメトリ設定(Telemetry API)
apiVersion: telemetry.istio.io/v1alpha1
kind: Telemetry
metadata:
name: default
namespace: istio-system
spec:
# アクセスログの設定
accessLogging:
- providers:
- name: envoy # デフォルト
filter:
expression: "response.code >= 400" # 4xx以上のみログ
# トレーシングの設定
tracing:
- providers:
- name: otel
randomSamplingPercentage: 10.0 # 10%サンプリング
customTags:
app_version:
literal:
value: "2.3.1"
# メトリクスのカスタマイズ
metrics:
- providers:
- name: prometheus
reportingInterval: 15s
overrides:
# カスタムラベルを追加
- match:
metric: ALL_METRICS
tagOverrides:
request_protocol:
operation: UPSERT
value: "request.protocol"
# Istio + OTel Collector 設定
apiVersion: v1
kind: ConfigMap
metadata:
name: istio-config
namespace: istio-system
data:
mesh: |
defaultConfig:
tracing:
openCensusAgent:
address: "otel-collector.monitoring:55678"
sampling: 10
proxyMetadata:
BOOTSTRAP_XDS_AGENT: "true"
26.3 Linkerdの組み込みオブザーバビリティ
Linkerdは設定なしで詳細なメトリクスを提供する。
# Linkerdのインストール
curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/install | sh
linkerd check --pre # 前提条件のチェック
linkerd install --crds | kubectl apply -f -
linkerd install | kubectl apply -f -
linkerd check
# 可視化コンポーネントのインストール
linkerd viz install | kubectl apply -f -
linkerd viz check
# サービスをメッシュに追加
kubectl get deploy -n production -o yaml | linkerd inject - | kubectl apply -f -
# Linkerd Dashboardを開く
linkerd viz dashboard &
# Linkerd CLIでのリアルタイムメトリクス確認
linkerd viz stat deploy -n production
# NAME MESHED SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99
# payment-service 1/1 98.23% 150.2rps 8ms 43ms 68ms
# order-service 1/1 99.81% 380.5rps 5ms 20ms 35ms
# checkout-service 1/1 99.95% 95.3rps 3ms 12ms 18ms
# リアルタイムのトップコマンド
linkerd viz top deploy/payment-service -n production
# サービス間のトラフィック確認
linkerd viz tap deploy/payment-service -n production \
--to deploy/postgres \
--path /query
26.4 Envoyメトリクスの活用
# Istio/Envoyが生成するメトリクス(サービスメッシュ共通)
# サービス間のリクエストレート
rate(istio_requests_total{reporter="destination"}[5m])
# サービス間のエラーレート
sum by (destination_service_name) (
rate(istio_requests_total{response_code=~"5.."}[5m])
)
/
sum by (destination_service_name) (
rate(istio_requests_total[5m])
)
# サービス間のp99レイテンシ
histogram_quantile(0.99,
sum by (destination_service_name, le) (
rate(istio_request_duration_milliseconds_bucket[5m])
)
) / 1000 # ミリ秒 → 秒
# アウトバウンドのソケット接続数
envoy_cluster_upstream_cx_active
# リトライ回数(高い場合はサービスが不安定な可能性)
rate(istio_requests_total{response_flags="RI"}[5m]) # RI = Retry
26.5 まとめ
- サービスメッシュはアプリコードの変更なしでオブザーバビリティを提供
- Istioは高機能だが複雑、Linkerdはシンプルで低オーバーヘッド
- Envoyメトリクスでサービス間のREDメトリクスを自動収集
- OpenTelemetryとの統合でトレースを自動伝播
27. eBPFによるカーネルレベル観測
この章で重視すること
- eBPFの仕組みと安全性
- オブザーバビリティへの応用
- bpftrace・BCC・Pixieの使い方
- eBPFベースのプロファイリング
- 本番環境でのeBPF活用
27.1 eBPFとは
eBPF(extended Berkeley Packet Filter) は、Linuxカーネル内でサンドボックス化されたプログラムを実行する技術だ。カーネルのソースコードを変更することなく、カーネル内のイベントにフックしてデータを収集できる。
【eBPFの位置づけ】
従来のアプローチの問題:
カーネルモジュール: 安全性が低い(バグがカーネルパニックを引き起こす)
strace: オーバーヘッドが大きい
uprobe: バイナリレベルで複雑
eBPFの利点:
✓ JITコンパイルで高速
✓ Verifierによる安全性保証(無限ループや不正メモリアクセスを防止)
✓ コードを変更せずにカーネル内イベントを観測
✓ ユーザー空間への低オーバーヘッドなデータ転送
オーバーヘッド:
通常: CPU使用率の < 5%(本番環境で許容可能)
27.2 eBPFのオブザーバビリティへの応用
【eBPFで観測できること】
システムコール:
- ファイルオープン/読み書き
- ネットワーク接続(connect/accept)
- プロセス起動/終了
- メモリ割り当て
カーネル関数:
- スケジューリングイベント
- ページフォルト
- TCP/IPスタックの内部状態
ユーザー空間プログラム:
- 関数の呼び出しと戻り値(uprobes)
- CPU使用量(perf_events)
- メモリ割り当て(malloc等)
ネットワーク:
- パケットの捕捉・フィルタリング
- XDP(eXpress Data Path)でのパケット処理
27.3 bpftraceによるトレーシング
# bpftraceのインストール
sudo apt install bpftrace # Ubuntu/Debian
sudo yum install bpftrace # RHEL/CentOS
# 基本的な使い方
# 全てのsyscallをトレース(高負荷、短時間のみ)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'
# openat syscall でオープンされるファイルを監視
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_openat
{
printf("%-6d %-16s %s\n", pid, comm, str(args->filename));
}
'
# スロークエリの特定(MySQLの場合)
sudo bpftrace -e '
uprobe:/usr/sbin/mysqld:dispatch_command
{
@start[tid] = nsecs;
}
uretprobe:/usr/sbin/mysqld:dispatch_command
/@start[tid] != 0 && (nsecs - @start[tid]) > 1000000000/
{
printf("Slow query: %d ms\n", (nsecs - @start[tid]) / 1000000);
delete(@start[tid]);
}
'
# HTTPレイテンシの測定
sudo bpftrace -e '
kprobe:tcp_sendmsg /comm == "nginx"/ { @start[tid] = nsecs; }
kretprobe:tcp_sendmsg /@start[tid]/ {
@latency = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}
'
# ネットワーク接続の監視
sudo bpftrace -e '
kprobe:tcp_connect
{
printf("%s → %s\n", comm, ntop(args->sk->__sk_common.skc_daddr));
}
'
# CPU時間をプロセスごとに集計
sudo bpftrace -e '
profile:hz:99
{
@cpu[comm] = count();
}
interval:s:10
{
print(@cpu);
clear(@cpu);
}
'
27.4 BCCツールキット
BCC(BPF Compiler Collection) は、eBPFプログラムをPythonで書けるようにするツールキットだ。
# BCC のインストール
sudo apt install bpfcc-tools linux-headers-$(uname -r) # Ubuntu
sudo yum install bcc-tools kernel-devel # RHEL
# よく使うBCCツール
# ファイルI/Oの詳細(どのプロセスがどのファイルにアクセスしているか)
sudo opensnoop
# スロークエリの検出(ディスクI/Oが25ms以上)
sudo biolatency -m -D
sudo biosnoop -Q
# TCP接続の監視
sudo tcptracer
sudo tcpconnect
sudo tcpaccept
# HTTPリクエストのトレース(Golangアプリ向け)
sudo gethostlatency
# Pythonのメモリアロケーション
sudo memleak -p $(pgrep python)
# CPU使用率の高い関数を特定
sudo profile -F 99 -f -d 10 # 10秒間プロファイリング
27.5 Pixieによる自動オブザーバビリティ
PixieはeBPFを使ってKubernetes環境のオブザーバビリティを自動的に提供するOSSツールだ。アプリケーションコードの変更が一切不要だ。
# Pixieのインストール
px auth login
px deploy
# Pixieで確認できること(自動):
# - HTTP/HTTPSのリクエスト・レスポンス
# - DNSクエリ
# - MySQLクエリ
# - PostgreSQLクエリ
# - Kafkaメッセージ
# - gRPC呼び出し
# PxLスクリプト(Pixieのクエリ言語)
px run px/http_data # HTTPリクエストの一覧
# カスタムスクリプト
import px
def http_errors():
df = px.DataFrame(table='http_events', start_time='-5m')
df = df[df.resp_status >= 400]
df = df.groupby(['service', 'req_path', 'resp_status']).agg(
count=('resp_status', px.count)
)
df = df.sort(by='count', ascending=False)
return df
px.display(http_errors())
27.6 eBPFのセキュリティとコンプライアンス
eBPFのセキュリティリスクと対策:
リスク:
- 特権(root)が必要な場合がある
- カーネルの内部状態へのアクセス
- 不正利用によるセキュリティ情報の窃取
対策:
- CAP_BPF capability を使って最小権限で実行
- BPFフィルタリングルールでプログラムを制限
- eBPFプログラムの署名・検証
- 本番環境でのeBPFツール使用は監査ログに記録
# Kubernetes でのeBPF Pod 最小権限設定
securityContext:
capabilities:
add:
- BPF
- PERFMON
- NET_ADMIN
privileged: false
27.7 まとめ
- eBPFはカーネルを変更せずにカーネル内のイベントを観測する技術
- アプリコードの変更なしにHTTP・DB・ネットワークを自動観測可能
- bpftrace/BCCで個別の問題調査、Pixieで包括的な自動オブザーバビリティ
- 本番環境でのオーバーヘッドは通常5%以下
28. RUM(Real User Monitoring)とWeb Vitals
この章で重視すること
- RUMとCore Web Vitalsの重要性
- OpenTelemetry Browser SDKでのRUM実装
- Core Web Vitalsの計測と改善
- バックエンドとの相関
- RUMのコスト最適化
28.1 RUMとは
Real User Monitoring(RUM) は、実際のユーザーのブラウザ上でのパフォーマンスを計測するアプローチだ。ラボテスト(合成監視)とは異なり、実際のネットワーク環境・デバイス・地域での体験を計測する。
【Lab Testing vs RUM】
Lab Testing(合成監視):
- 制御された環境(固定帯域・固定デバイス)
- 24時間定期実行
- 結果が再現可能・比較可能
- 問題の早期発見に適している
RUM:
- 実際のユーザーの環境
- ユーザーが訪れたときのみ計測
- 多様なデバイス・ネットワーク
- ユーザー体験の真の計測
→ 両方を組み合わせることが推奨
28.2 Core Web Vitals
Googleが定義するWebパフォーマンスの主要指標:
| 指標 | 説明 | 良い値 | 要改善 |
|---|---|---|---|
| LCP (Largest Contentful Paint) | 最大コンテンツの表示時間 | < 2.5秒 | > 4秒 |
| INP (Interaction to Next Paint) | 操作への応答時間 | < 200ms | > 500ms |
| CLS (Cumulative Layout Shift) | レイアウトのずれ | < 0.1 | > 0.25 |
| TTFB (Time to First Byte) | 最初のバイト受信時間 | < 800ms | > 1800ms |
| FCP (First Contentful Paint) | 最初のコンテンツ表示 | < 1.8秒 | > 3秒 |
28.3 OpenTelemetry Browser SDKによるRUM実装
<!-- index.html: Web Vitalsの計測 -->
<script type="module">
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';
const endpoint = 'https://otel-collector.example.com:4318';
function sendToOTel(metric) {
// OpenTelemetry形式でメトリクスを送信
fetch(`${endpoint}/v1/metrics`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
resourceMetrics: [{
resource: {
attributes: [
{key: 'service.name', value: {stringValue: 'web-frontend'}},
{key: 'service.version', value: {stringValue: '2.3.1'}},
{key: 'user.country', value: {stringValue: navigator.language}},
{key: 'browser.name', value: {stringValue: getBrowserName()}},
]
},
scopeMetrics: [{
metrics: [{
name: `webvitals.${metric.name.toLowerCase()}`,
description: `Core Web Vital: ${metric.name}`,
gauge: {
dataPoints: [{
attributes: [
{key: 'rating', value: {stringValue: metric.rating}},
{key: 'page.url', value: {stringValue: window.location.pathname}},
],
timeUnixNano: Date.now() * 1000000,
asDouble: metric.value
}]
}
}]
}]
}]
})
});
}
// Core Web Vitalsの計測
onCLS(sendToOTel);
onINP(sendToOTel);
onLCP(sendToOTel);
onFCP(sendToOTel);
onTTFB(sendToOTel);
</script>
// React/Next.js アプリでの実装
// pages/_app.tsx または app/layout.tsx
import { useEffect } from 'react'
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals'
import { trace, metrics } from '@opentelemetry/api'
const meter = metrics.getMeter('web-frontend')
const lcpHistogram = meter.createHistogram('webvitals.lcp', {
description: 'Largest Contentful Paint',
unit: 'ms'
})
const inpHistogram = meter.createHistogram('webvitals.inp', {
description: 'Interaction to Next Paint',
unit: 'ms'
})
const clsHistogram = meter.createHistogram('webvitals.cls', {
description: 'Cumulative Layout Shift'
})
export function reportWebVitals(metric: any) {
const commonAttributes = {
page: window.location.pathname,
rating: metric.rating, // good/needs-improvement/poor
device_type: getDeviceType(),
connection_type: (navigator as any).connection?.effectiveType ?? 'unknown'
}
switch (metric.name) {
case 'LCP':
lcpHistogram.record(metric.value, commonAttributes)
break
case 'INP':
inpHistogram.record(metric.value, commonAttributes)
break
case 'CLS':
clsHistogram.record(metric.value, commonAttributes)
break
}
}
28.4 RUMとバックエンドトレースの相関
// フロントエンドでTraceIDを生成し、バックエンドに伝播
import { trace, context, propagation } from '@opentelemetry/api'
import { W3CTraceContextPropagator } from '@opentelemetry/core'
const tracer = trace.getTracer('web-frontend')
async function fetchOrders(): Promise<Order[]> {
return tracer.startActiveSpan('fetch_orders', async (span) => {
try {
const headers: Record<string, string> = {}
// W3C Trace ContextをHTTPヘッダーに注入
propagation.inject(context.active(), headers)
// → traceparent: 00-4bf92f...-01 がヘッダーに付与される
// バックエンドの同じTrace IDでトレースが継続する
const response = await fetch('/api/orders', { headers })
span.setAttributes({
'http.status_code': response.status,
'page': window.location.pathname
})
if (!response.ok) {
span.setStatus({ code: SpanStatusCode.ERROR })
}
return response.json()
} catch (error) {
span.recordException(error as Error)
throw error
} finally {
span.end()
}
})
}
28.5 RUM用Grafanaダッシュボード
# Core Web Vitalsのp75(Googleの評価基準はp75)
histogram_quantile(0.75,
sum by (page, le) (
rate(webvitals_lcp_bucket[1h])
)
) # < 2500ms が "Good"
# ページ別のLCP分布
histogram_quantile(0.75,
sum by (page, le) (rate(webvitals_lcp_bucket[$__rate_interval]))
) * on(page) group_left()
# Good/Needs-Improvement/Poor の割合
sum by (page, rating) (
rate(webvitals_lcp_count[1h])
)
# 地域別のWeb Vitals
histogram_quantile(0.75,
sum by (region, le) (rate(webvitals_lcp_bucket[1h]))
)
28.6 まとめ
- RUMは実際のユーザーのブラウザパフォーマンスを計測する
- Core Web Vitals(LCP/INP/CLS)がGoogleの評価指標
- OpenTelemetry Browser SDKでバックエンドと統一したテレメトリ基盤を構築
- W3C Trace Contextでフロント→バックエンドのトレースを連結
29. 合成監視(Synthetic Monitoring)
この章で重視すること
- 合成監視の種類と使い分け
- Grafana Synthetic Monitoringの設定
- シナリオベースの監視設計
- グローバル監視の設計
- アラートの設計
29.1 合成監視の種類
合成監視の種類:
1. Uptime Check(死活監視)
- HTTP GET でステータスコードを確認
- 間隔: 1分
- 用途: サービスが起動しているかの基本確認
2. API Functional Test(機能確認)
- APIのレスポンスの内容を確認
- 間隔: 5分
- 用途: エンドポイントが正しいレスポンスを返すか
3. Transaction Monitoring(シナリオ監視)
- 実際のユーザー操作をシミュレート
- ログイン → 商品検索 → カート追加 → 決済
- 間隔: 10〜30分
- 用途: ユーザーフローが正常に機能するか
4. DNS Check(DNS監視)
- DNSレコードの解決を確認
- 間隔: 5分
- 用途: DNSの問題を早期検知
5. SSL Certificate Check(証明書監視)
- SSL証明書の有効期限を確認
- 間隔: 1日
- 用途: 証明書失効を事前に検知
29.2 Grafana Synthetic Monitoringの設定
# synthetic-monitoring.yml
# Grafana Cloud Synthetic Monitoringの設定
checks:
# HTTP監視(基本的な死活確認)
- job: "api-health"
target: "https://api.example.com/health"
interval: 60s
timeout: 10s
probe_locations:
- Tokyo
- Singapore
- Frankfurt
- NewYork
http:
ip_version: "V4"
valid_status_codes: [200]
valid_http_versions: ["HTTP/1.1", "HTTP/2"]
fail_if_ssl: false
fail_if_not_ssl: true
tls_config:
insecure_skip_verify: false
headers:
- name: "Accept"
value: "application/json"
body: ""
# レスポンスに含まれるべき文字列
fail_if_body_not_matches_regexp: ['"status":"ok"']
# マルチステップシナリオ(k6スクリプト)
- job: "checkout-flow"
target: "https://www.example.com"
interval: 300s # 5分ごと
timeout: 60s
probe_locations:
- Tokyo
- Singapore
scripted:
script: |
import http from 'k6/http';
import { check, sleep } from 'k6';
export default function() {
// Step 1: トップページを確認
let res = http.get('https://www.example.com');
check(res, {
'homepage status 200': (r) => r.status === 200,
'homepage contains products': (r) => r.body.includes('products'),
});
sleep(1);
// Step 2: 商品検索
res = http.get('https://api.example.com/v1/products?q=laptop');
check(res, {
'search status 200': (r) => r.status === 200,
'search has results': (r) => JSON.parse(r.body).total > 0,
'search response time OK': (r) => r.timings.duration < 2000,
});
sleep(1);
// Step 3: カート追加
const headers = {'Content-Type': 'application/json'};
res = http.post(
'https://api.example.com/v1/cart',
JSON.stringify({product_id: 'prod_123', quantity: 1}),
{headers: headers}
);
check(res, {
'cart add status 201': (r) => r.status === 201,
});
}
29.3 Blackbox Exporterの詳細設定
# blackbox.yml
modules:
# HTTP監視
http_2xx:
prober: http
timeout: 10s
http:
valid_http_versions: ["HTTP/1.1", "HTTP/2"]
valid_status_codes: [] # デフォルト: 2xx
method: GET
follow_redirects: true
fail_if_ssl: false
fail_if_not_ssl: false
tls_config:
insecure_skip_verify: false
# HTTPS監視(SSL証明書の確認も含む)
https_2xx:
prober: http
timeout: 10s
http:
valid_http_versions: ["HTTP/1.1", "HTTP/2"]
fail_if_not_ssl: true
fail_if_ssl: false
tls_config:
insecure_skip_verify: false
# API監視(JSONレスポンスの確認)
http_post_api:
prober: http
timeout: 10s
http:
method: POST
headers:
Content-Type: application/json
Authorization: Bearer ${API_TOKEN}
body: '{"query": "health"}'
valid_status_codes: [200]
fail_if_body_not_matches_regexp:
- '"status":\s*"ok"'
# TCP監視
tcp_connect:
prober: tcp
timeout: 10s
# DNS監視
dns_check:
prober: dns
timeout: 10s
dns:
preferred_ip_protocol: ip4
query_name: "api.example.com"
query_type: "A"
valid_rcodes:
- NOERROR
# ICMP(ping)
icmp:
prober: icmp
timeout: 10s
icmp:
preferred_ip_protocol: ip4
# Prometheusからのスクレイプ設定(Blackbox Exporter)
scrape_configs:
- job_name: 'blackbox-http'
metrics_path: /probe
params:
module: [https_2xx]
static_configs:
- targets:
- https://api.example.com/health
- https://api.example.com/v1/products
- https://checkout.example.com/
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
- job_name: 'blackbox-ssl'
metrics_path: /probe
params:
module: [https_2xx]
static_configs:
- targets:
- https://api.example.com
- https://www.example.com
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
SSL証明書期限のアラート
# SSL証明書の期限アラート
- alert: SSLCertExpiringSoon
expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 30 # 30日以内
for: 1h
labels:
severity: warning
annotations:
summary: "SSL certificate expiring soon: {{ $labels.instance }}"
description: "Certificate expires in {{ $value | humanizeDuration }}"
- alert: SSLCertExpiresCritical
expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 7 # 7日以内
for: 1h
labels:
severity: critical
pagerduty: "true"
annotations:
summary: "SSL certificate expires within 7 days: {{ $labels.instance }}"
29.4 グローバル監視の設計
【グローバル監視の地域選択基準】
1. ユーザーの地理的分布に基づく
日本のECサイト:
- Tokyo(主要ユーザー)
- Singapore(アジア太平洋)
- Frankfurt(ヨーロッパ在住の日本人)
グローバルSaaS:
- North America(East/West)
- Europe(Frankfurt/London)
- Asia Pacific(Tokyo/Singapore/Sydney)
- South America(São Paulo)
2. アラートの設計(複数地域からの確認)
悪い設計: 1箇所から失敗でアラート(誤検知が多い)
良い設計: 3箇所中2箇所以上が失敗でアラート
PromQLでの複数地域確認:
count by (job) (probe_success == 0) >= 2
29.5 まとめ
- 合成監視はRUMの補完で、問題を先行して検知する
- HTTP確認から始めて、シナリオ監視(k6スクリプト)へと段階的に拡充
- SSL証明書の期限監視を忘れずに(期限切れは重大インシデント)
- グローバル監視は複数地域からの確認で誤検知を防ぐ
30. AIOpsと異常検知
この章で重視すること
- AIOpsの概念と適用範囲
- 統計的異常検知
- 機械学習による異常検知
- LLMを活用したインシデント分析
- AIOpsツールの評価
30.1 AIOpsとは
AIOps(AI for IT Operations) は、AIと機械学習をIT運用に適用することで、問題の検知・診断・解決を自動化・高速化する取り組みだ。
AIOpsが解決する問題:
問題1: アラートの洪水(Alert Storm)
→ 関連アラートを相関させて根本原因を特定
→ ノイズを除去して重要なアラートを優先
問題2: パターン認識の困難さ
→ 過去のインシデントパターンから異常を検知
→ 正常なトラフィックパターン(日次・週次)を学習
問題3: 根本原因分析の時間
→ ログ・メトリクス・トレースを自動的に相関
→ 過去のインシデントからの学習で原因候補を提示
問題4: キャパシティプランニング
→ 将来のリソース需要を予測
→ 自動スケーリングのしきい値最適化
30.2 統計的異常検知
機械学習を使わなくても、統計的手法で有効な異常検知が可能だ。
Z-scoreによる異常検知
import numpy as np
from scipy import stats
def detect_anomaly_zscore(values: list[float], threshold: float = 3.0) -> list[bool]:
"""
Z-scoreを使った異常検知
|z-score| > threshold の場合に異常とみなす
"""
z_scores = np.abs(stats.zscore(values))
return [z > threshold for z in z_scores]
# Prometheusクエリでの実装
# (単純な標準偏差を使った異常検知)
#
# 異常スコア = |現在値 - 移動平均| / 移動標準偏差
# これがしきい値を超えたらアラート
zscore_query = """
abs(
http_requests_total
- avg_over_time(http_requests_total[1h])
)
/ stddev_over_time(http_requests_total[1h]) > 3
"""
Prometheusでの異常検知クエリ
# 1週間前の同時刻と比較して2倍以上の乖離
(
rate(http_requests_total[5m])
/
rate(http_requests_total[5m] offset 1w)
) > 2 # 200%以上の増加
# 移動平均からの乖離(Holt-Winters近似)
# 過去7日間の平均を基準に現在の値を評価
(
rate(http_error_rate[5m])
>
avg_over_time(rate(http_error_rate[5m])[7d:5m]) * 3
)
# 時系列の季節性を考慮した異常検知
# 同曜日・同時刻の過去4週間との比較
abs(
rate(http_requests_total[5m])
-
(
rate(http_requests_total[5m] offset 1w)
+ rate(http_requests_total[5m] offset 2w)
+ rate(http_requests_total[5m] offset 3w)
+ rate(http_requests_total[5m] offset 4w)
) / 4
) > 500 # 差が500 RPS以上の場合
30.3 機械学習による異常検知
# Prophet を使った時系列予測と異常検知
from prophet import Prophet
import pandas as pd
import numpy as np
class AnomalyDetector:
def __init__(self, interval_width: float = 0.99):
self.model = Prophet(
interval_width=interval_width, # 99%信頼区間
yearly_seasonality=False,
weekly_seasonality=True,
daily_seasonality=True,
# トレンド変化点の感度
changepoint_prior_scale=0.05,
)
self.is_fitted = False
def fit(self, df: pd.DataFrame):
"""
df: columns=['ds' (datetime), 'y' (value)]
"""
self.model.fit(df)
self.is_fitted = True
def detect_anomalies(self, df: pd.DataFrame) -> pd.DataFrame:
"""実績値が予測区間を外れた点を異常とする"""
if not self.is_fitted:
raise ValueError("Model not fitted. Call fit() first.")
forecast = self.model.predict(df)
df = df.copy()
df['yhat'] = forecast['yhat']
df['yhat_lower'] = forecast['yhat_lower']
df['yhat_upper'] = forecast['yhat_upper']
df['is_anomaly'] = (
(df['y'] < df['yhat_lower']) |
(df['y'] > df['yhat_upper'])
)
df['anomaly_score'] = (
(df['y'] - df['yhat']) /
(df['yhat_upper'] - df['yhat_lower'])
).abs()
return df
# 使用例
# Prometheusから1週間分のデータを取得
import requests
def fetch_prometheus_data(query: str, start: str, end: str) -> pd.DataFrame:
response = requests.get(
'http://prometheus:9090/api/v1/query_range',
params={
'query': query,
'start': start,
'end': end,
'step': '5m'
}
)
data = response.json()['data']['result'][0]['values']
df = pd.DataFrame(data, columns=['ds', 'y'])
df['ds'] = pd.to_datetime(df['ds'], unit='s')
df['y'] = df['y'].astype(float)
return df
# 異常検知の実行
df = fetch_prometheus_data(
'sum(rate(http_requests_total[5m]))',
'now-7d',
'now'
)
detector = AnomalyDetector()
detector.fit(df[:-288]) # 直近1日以外でトレーニング
anomalies = detector.detect_anomalies(df)
print(anomalies[anomalies['is_anomaly']])
30.4 LLMを活用したインシデント分析
# LLMを使ったインシデント自動分析
import anthropic
import json
class IncidentAnalyzer:
def __init__(self, api_key: str):
self.client = anthropic.Anthropic(api_key=api_key)
def analyze_incident(
self,
alert_info: dict,
recent_logs: list[str],
metric_trends: dict,
recent_deployments: list[dict]
) -> dict:
"""インシデントの自動分析"""
prompt = f"""
あなたはSREエキスパートです。以下のインシデント情報を分析し、根本原因の候補と推奨アクションを提示してください。
## アラート情報
{json.dumps(alert_info, ensure_ascii=False, indent=2)}
## 直近のログ(エラーのみ、最新50件)
{chr(10).join(recent_logs[-50:])}
## メトリクストレンド(直近1時間)
{json.dumps(metric_trends, ensure_ascii=False, indent=2)}
## 直近のデプロイ
{json.dumps(recent_deployments, ensure_ascii=False, indent=2)}
以下の形式で回答してください:
1. **インシデントサマリー**: 1行で何が起きているか
2. **根本原因の候補**: 可能性の高い順に3つ
3. **推奨する初動対応**: 具体的なコマンドを含む手順
4. **エスカレーション判断**: いつ誰にエスカレーションすべきか
5. **監視すべき追加メトリクス**: 状況把握に役立つPromQLクエリ
"""
response = self.client.messages.create(
model="claude-opus-4-5",
max_tokens=2000,
messages=[{"role": "user", "content": prompt}]
)
return {
"analysis": response.content[0].text,
"input_tokens": response.usage.input_tokens,
"output_tokens": response.usage.output_tokens
}
# Slack Botとの統合
def handle_incident_command(alert_data: dict) -> str:
analyzer = IncidentAnalyzer(api_key=os.getenv("ANTHROPIC_API_KEY"))
# データの収集
logs = fetch_recent_error_logs(alert_data['service'])
metrics = fetch_metric_trends(alert_data['service'])
deployments = fetch_recent_deployments(alert_data['service'])
result = analyzer.analyze_incident(alert_data, logs, metrics, deployments)
return f"🤖 AIインシデント分析:\n{result['analysis']}"
30.5 AIOpsツールの評価基準
| ツール | 特徴 | 適したシナリオ |
|---|---|---|
| Grafana MLObs | Grafana統合、異常検知 | LGTMスタック環境 |
| Datadog AI | 高度なアラート相関・根本原因分析 | Datadogユーザー |
| Dynatrace Davis AI | 完全自動AI根本原因分析 | エンタープライズ |
| New Relic Applied Intelligence | アラート品質改善 | New Relicユーザー |
| 自社実装(Prophet + LLM) | カスタマイズ最大 | 技術力があるチーム |
30.6 まとめ
- AIOpsはアラート洪水・根本原因分析・キャパシティ予測を自動化する
- 統計的手法(Z-score・Holt-Winters)から始め、必要に応じてMLを適用
- LLMを使ったインシデント自動分析で対応速度を向上させる
- AIOpsはエンジニアを置き換えるものではなく、意思決定を支援するもの
31. オブザーバビリティのコスト最適化
この章で重視すること
- オブザーバビリティのコスト構造
- メトリクス・ログ・トレースの各コスト削減手法
- サンプリング戦略とコストのバランス
- OpenTelemetryによるコスト最適化
- ROIの計算方法
31.1 オブザーバビリティのコスト構造
典型的なオブザーバビリティコスト(中規模SaaS、月次):
データ保存コスト:
メトリクス(Prometheus/Mimir): ¥300,000
ログ(Loki/ES): ¥500,000
トレース(Tempo): ¥200,000
プロファイル(Pyroscope): ¥100,000
SaaSコスト(Datadog/New Relic等):
ホスト単位課金: ¥1,500,000
ログインジェスト: ¥800,000
合計: ¥3,400,000/月
コスト削減の余地:
- 不要なメトリクス: 30〜50%削減可能
- ログのサンプリング: 40〜60%削減可能
- トレースのサンプリング: 70〜90%削減可能
31.2 メトリクスのコスト最適化
# 1. 不要なメトリクスを削除(metric_relabel_configs)
# prometheus.yml
metric_relabel_configs:
# デフォルトで生成されるが使わないメトリクスを削除
- source_labels: [__name__]
regex: 'go_memstats_gc_sys_bytes|go_memstats_other_sys_bytes'
action: drop
# 高カーディナリティのラベルを削除
- regex: 'quantile'
action: labeldrop
# テスト/ステージング環境のメトリクスを除外
- source_labels: [environment]
regex: 'test|staging'
action: drop
# 2. スクレイプ間隔を調整(頻度を下げる)
# 重要度の低いメトリクスは長い間隔に
scrape_configs:
- job_name: 'critical-services'
scrape_interval: 15s # 重要サービスは15秒
- job_name: 'non-critical'
scrape_interval: 60s # 非重要は60秒に延長 → 75%削減
# 3. Recording Rulesで集計済みメトリクスのみを長期保存
# 生の高カーディナリティメトリクスは短期間のみ保持
remote_write:
- url: "http://mimir:9009/api/v1/push"
write_relabel_configs:
# Recording Rulesで作成した集計メトリクスのみ長期保存に送信
- source_labels: [__name__]
regex: 'job:.*|slo:.*|node:.*' # Recording Rules の命名規則
action: keep
31.3 ログのコスト最適化
# Promtailでのログフィルタリング
pipeline_stages:
# ヘルスチェックログを除外(ノイズの60%を占めることがある)
- drop:
expression: 'GET /health|GET /ready|GET /metrics|GET /ping'
drop_counter_reason: health_check_noise
# DEBUGログを除外(本番環境)
- match:
selector: '{level="debug"}'
action: drop
drop_counter_reason: debug_log_filtered
# 繰り返しエラーをサンプリング(10件に1件)
- sampling:
rate: 0.1
match_selector: '{level="error"} |= "connection timeout"'
# ログレベルのサンプリング設定(Python)
import structlog
import random
def sampling_processor(logger, method, event_dict):
level = event_dict.get('level', 'info')
# サンプリングレート(本番環境の推奨値)
rates = {
'trace': 0.001, # 0.1%
'debug': 0.01, # 1%
'info': 0.10, # 10%
'warning': 1.0, # 100%
'error': 1.0, # 100%
'critical': 1.0 # 100%
}
rate = rates.get(level, 1.0)
# 重要なイベントは必ず保存
if event_dict.get('force_log') or event_dict.get('business_critical'):
return event_dict
if random.random() > rate:
raise structlog.DropEvent()
return event_dict
31.4 トレースのコスト最適化
# OpenTelemetry Collectorでの費用対効果の高いサンプリング設定
processors:
tail_sampling:
decision_wait: 10s
num_traces: 100000
policies:
# エラーは全て保存(最も重要)
- name: errors
type: status_code
status_code: {status_codes: [ERROR]}
# SLO閾値を超えるトレースは保存
- name: slow-traces
type: latency
latency: {threshold_ms: 1000} # p99 SLOの値に合わせる
# 重要サービスは高めのサンプリングレート
- name: critical-services
type: string_attribute
string_attribute:
key: service.name
values: [payment-service, auth-service]
# その他は1%のみ
- name: default
type: probabilistic
probabilistic: {sampling_percentage: 1}
# 効果:
# Before: 全トレース保存 → 100GB/日
# After: サンプリング後 → 5GB/日(95%削減)
31.5 コスト最適化のROI計算
# コスト最適化のROI計算ツール
class ObservabilityCostCalculator:
def calculate_savings(
self,
# 現在のコスト
current_metrics_cost: float, # 月次
current_log_cost: float,
current_trace_cost: float,
# 削減率(%)
metrics_reduction_pct: float = 30,
log_reduction_pct: float = 60,
trace_reduction_pct: float = 90,
# 実装コスト(人件費等)
implementation_hours: float = 40,
hourly_rate: float = 10000 # 円/時
) -> dict:
# 月次削減額
metrics_savings = current_metrics_cost * metrics_reduction_pct / 100
log_savings = current_log_cost * log_reduction_pct / 100
trace_savings = current_trace_cost * trace_reduction_pct / 100
total_monthly_savings = metrics_savings + log_savings + trace_savings
# 実装コスト
implementation_cost = implementation_hours * hourly_rate
# 回収期間(月)
payback_months = implementation_cost / total_monthly_savings
# 年間ROI(%)
annual_savings = total_monthly_savings * 12
roi = (annual_savings - implementation_cost) / implementation_cost * 100
return {
"monthly_savings": total_monthly_savings,
"annual_savings": annual_savings,
"implementation_cost": implementation_cost,
"payback_months": payback_months,
"annual_roi_pct": roi,
"breakdown": {
"metrics": metrics_savings,
"logs": log_savings,
"traces": trace_savings
}
}
# 例の計算
calculator = ObservabilityCostCalculator()
result = calculator.calculate_savings(
current_metrics_cost=300000,
current_log_cost=500000,
current_trace_cost=200000,
implementation_hours=80
)
print(f"月次削減額: ¥{result['monthly_savings']:,.0f}")
print(f"年間削減額: ¥{result['annual_savings']:,.0f}")
print(f"回収期間: {result['payback_months']:.1f}ヶ月")
print(f"年間ROI: {result['annual_roi_pct']:.0f}%")
31.6 まとめ
- オブザーバビリティコストの主な削減対象は不要なメトリクス・ログ・トレース
- メトリクス: 不要なメトリクスのdrop、スクレイプ間隔の最適化
- ログ: DEBUGログのdrop、INFOログのサンプリング(10%)
- トレース: テールベースサンプリングで90%以上削減
- ROI計算でコスト削減の優先順位を判断する
32. マイクロサービスのオブザーバビリティ
この章で重視すること
- マイクロサービス固有のオブザーバビリティ課題
- サービス依存関係マップの構築
- 障害の連鎖伝播を検知する方法
- カナリアデプロイとオブザーバビリティ
- 実践的なインシデント調査フロー
32.1 マイクロサービスのオブザーバビリティ課題
モノリスとマイクロサービスのオブザーバビリティ比較:
モノリス:
- ログ: 1箇所を確認するだけ
- デバッグ: スタックトレースで原因特定
- デプロイ: 1つのサービスのみ
マイクロサービス(20サービス):
- ログ: 20サービスのログを横断的に検索
- デバッグ: どのサービスで問題が起きているか不明
- デプロイ: 複数サービスが同時にデプロイされる
必要なオブザーバビリティ:
① 分散トレーシング: リクエストの経路追跡
② 相関: メトリクス・ログ・トレースの紐付け
③ サービスマップ: 依存関係の自動可視化
④ デプロイ追跡: デプロイとメトリクス変化の相関
32.2 ECサイトの実践例
サービス構成
# ECサイトのマイクロサービス構成
services:
web-frontend: # Next.js フロントエンド
api-gateway: # Kong / Nginx
user-service: # ユーザー管理
product-service: # 商品カタログ
search-service: # Elasticsearch検索
cart-service: # カート(Redis)
order-service: # 注文管理
payment-service: # 決済(Stripe連携)
inventory-service: # 在庫管理
shipping-service: # 配送管理
notification-service: # メール/SMS通知
SLO設計
# ECサイトのSLO定義
slos:
- service: checkout-flow
description: "ユーザーが注文を完了できる"
sli: "成功した注文 / 全注文試行"
objective: 99.5% # 月次
window: 30d
- service: product-search
description: "商品検索が2秒以内に完了する"
sli: "2秒以内のレスポンス / 全検索リクエスト"
objective: 99.0%
window: 30d
- service: payment-processing
description: "決済が5秒以内に完了する"
sli: "5秒以内に完了した決済 / 全決済試行"
objective: 99.9% # 決済は特に高い目標
window: 30d
32.3 カナリアデプロイとオブザーバビリティ
# Argo Rollouts を使ったカナリアデプロイ
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: payment-service
spec:
replicas: 10
strategy:
canary:
steps:
- setWeight: 10 # 10%のトラフィックをカナリアに
- pause: {duration: 5m}
# ここでオブザーバビリティで自動検証
- analysis:
templates:
- templateName: payment-success-rate
args:
- name: service-name
value: payment-service
- setWeight: 50 # 50%に拡大
- pause: {duration: 10m}
- analysis:
templates:
- templateName: payment-latency-check
- setWeight: 100 # 100%(完全移行)
# Analysis Template(自動ロールバック条件)
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: payment-success-rate
spec:
args:
- name: service-name
metrics:
- name: success-rate
interval: 2m
failureLimit: 1
successCondition: "result[0] >= 0.999" # 99.9%以上の成功率
provider:
prometheus:
address: http://prometheus:9090
query: |
sum(rate(http_requests_total{
service="{{args.service-name}}",
version="canary",
status_code!~"5.."
}[5m]))
/
sum(rate(http_requests_total{
service="{{args.service-name}}",
version="canary"
}[5m]))
- name: p99-latency
interval: 2m
failureLimit: 1
successCondition: "result[0] < 0.5" # p99 < 500ms
provider:
prometheus:
address: http://prometheus:9090
query: |
histogram_quantile(0.99,
sum by (le) (
rate(http_request_duration_seconds_bucket{
service="{{args.service-name}}",
version="canary"
}[5m])
)
)
32.4 まとめ
- マイクロサービスでは分散トレーシングが必須
- サービスマップで依存関係と障害の伝播を把握する
- カナリアデプロイとオブザーバビリティを組み合わせて安全なリリースを実現
- ECサイトはチェックアウトフロー・検索・決済を中心にSLOを設計する
33. 具体的な障害パターンとデバッグ手順
この章で重視すること
- 典型的な障害パターンの分類
- 各パターンの検知方法
- 体系的なデバッグアプローチ
- 実際のインシデント事例
33.1 障害パターンのカタログ
パターン1: メモリリーク
症状:
- メモリ使用量が時間とともに増加し続ける
- GCが頻発する(Go/Java)
- OOMKillが発生
検知クエリ(Prometheus):
increase(process_resident_memory_bytes[6h]) > 500 * 1024 * 1024 # 6時間で500MB増加
対応手順:
1. Pyroscopeでメモリプロファイルを確認
→ "memory:inuse_space" でメモリを大量消費している関数を特定
2. go tool pprof / jmap でヒープダンプを取得
3. 原因となるオブジェクト・参照サイクルを特定
4. ホットフィックスのリリースまでの間、定期的にPodを再起動
事例(ECサイト):
注文履歴を表示するAPIで、ページネーションを実装し忘れ
全ユーザーの注文データをメモリにロードし続けていた
→ 12時間後にOOMKillが発生し503エラー
パターン2: カスケード障害
症状:
- 1つのサービスの障害が次々と他のサービスに伝播
- 「全サービスが同時にダウン」したように見える
検知方法:
- Tempoのサービスグラフで依存関係を確認
- 最初に問題が発生したサービスを特定(Trace Waterfall)
対応手順:
1. 「最初に失敗したサービス」を特定する
→ トレースで根本のSpanを確認
2. そのサービスをサーキットブレーカーで切り離す
3. 依存するサービスのタイムアウト・リトライ設定を確認
4. リトライが「Retry Storm」を引き起こしていないか確認
事例(決済システム):
外部決済APIが応答遅延
→ リトライが大量発生
→ スレッドプールが枯渇
→ order-serviceがタイムアウト
→ checkout-serviceもタイムアウト
→ ユーザーが再試行してさらに悪化
パターン3: スロークエリ
症状:
- p99レイテンシが急上昇
- DBのCPU/I/Oが高い
- 特定のエンドポイントのみ遅い
検知クエリ:
histogram_quantile(0.99,
sum by (endpoint, le) (
rate(http_request_duration_seconds_bucket[5m])
)
) > 1.0 # 1秒以上
対応手順:
1. Tempoでスロートレースを確認
→ どのSpanが長いか(DBクエリかAPIコールか)を特定
2. DBの場合: pg_stat_statements / slow query log を確認
3. 問題のSQLを特定してEXPLAIN ANALYZE を実行
4. インデックスの追加 or クエリの最適化
LogQLクエリ(PostgreSQLのスロークエリ):
{app="postgres"} |= "duration:"
| regexp `duration: (?P<duration>[0-9.]+) ms`
| duration > 1000
| line_format "Slow query ({{.duration}}ms)"
パターン4: リソース枯渇
症状:
- ディスクI/Oが高い
- ファイルディスクリプタが枯渇
- コネクションプールが枯渇
ファイルディスクリプタ枯渇の検知:
process_open_fds / process_max_fds > 0.85
DBコネクションプール枯渇:
(db_connections_max - db_connections_available) / db_connections_max > 0.90
対応手順(コネクションプール):
1. 現在のDB接続数を確認
SELECT count(*), state FROM pg_stat_activity GROUP BY state;
2. ロングランニングクエリを確認
SELECT pid, age(clock_timestamp(), query_start), query
FROM pg_stat_activity WHERE state != 'idle'
ORDER BY age desc LIMIT 20;
3. 必要に応じてpg_terminate_backend(pid)で切断
4. コネクションプールサイズの調整(HikariCP/pgBouncer等)
33.2 デバッグの体系的アプローチ
【5W1Hデバッグフレームワーク】
When いつ発生し始めたか?
→ メトリクスで開始時刻を特定
→ 直前のデプロイと相関確認
What 何が起きているか?
→ エラーレート・レイテンシ・可用性の変化
Where どのサービス・コンポーネントか?
→ 分散トレーシングで問題サービスを絞り込む
Who どのユーザー・リクエストが影響を受けているか?
→ 高カーディナリティ分析(ログ・トレースで)
Why なぜ問題が発生しているか?
→ ログでエラーの詳細を確認
→ プロファイルでコードレベルの問題を特定
How どのくらいの影響範囲か?
→ 影響ユーザー数・リクエスト数の推計
→ SLO消費率の確認
34. SRE視点のオブザーバビリティ実践
この章で重視すること
- SRE視点で必要な観測要件
- プロダクションレディネスレビューへの組み込み
- オブザーバビリティ文化の醸成
- DORAメトリクスとオブザーバビリティ
34.1 Production Readiness Review(PRR)チェックリスト
## オブザーバビリティ PRRチェックリスト
### メトリクス
- [ ] RED メトリクスが実装されている(Rate/Errors/Duration)
- [ ] 主要なビジネスイベントがメトリクス化されている
- [ ] SLI が定義され計測可能な状態になっている
- [ ] カスタムメトリクスのカーディナリティが 10,000以下
### ログ
- [ ] 構造化ログ(JSON)を使用している
- [ ] trace_id / span_id がすべてのログに含まれている
- [ ] センシティブデータがマスキングされている
- [ ] ログレベルが適切に設定されている(INFO主体)
### トレーシング
- [ ] OpenTelemetryで計装されている
- [ ] 全ての外部サービス呼び出しがSpanとして記録される
- [ ] DB クエリが Span として記録される
### アラート
- [ ] 少なくとも1つのSLOバーンレートアラートが設定されている
- [ ] すべてのアラートに Runbook URLが含まれている
- [ ] アラートが PagerDuty/incident.io に連携されている
- [ ] Critical アラートはテスト済み
### ダッシュボード
- [ ] サービス概要ダッシュボードが作成されている
- [ ] 可用性・レイテンシ・エラーレートが表示されている
- [ ] オンコールエンジニアが15分以内に状況把握できる
### オンコール
- [ ] Runbook が最新の状態に保たれている
- [ ] オンコールローテーションが設定されている
- [ ] エスカレーションポリシーが設定されている
34.2 DORAメトリクスとオブザーバビリティ
DORAメトリクス × オブザーバビリティ:
1. デプロイ頻度(Deployment Frequency)
計測: CIパイプラインのWebhook → Prometheus Counter
PromQL: increase(deployments_total[30d]) / 30 # 日次平均
Elite: 1日複数回
High: 1日〜1週間に1回
Medium: 1週間〜1月に1回
Low: 1月〜半年に1回
2. 変更リードタイム(Lead Time for Changes)
計測: PRのマージ時刻 → デプロイ完了時刻
deployment_lead_time_seconds = deploy_timestamp - merge_timestamp
Elite: < 1時間
High: 1日以内
Medium: 1週間以内
Low: 1ヶ月以上
3. 変更失敗率(Change Failure Rate)
計測: デプロイ後のロールバック数 / 全デプロイ数
PromQL:
increase(rollbacks_total[30d]) / increase(deployments_total[30d])
Elite: < 5%
High: 5〜10%
4. 復旧時間(MTTR: Mean Time to Recover)
計測: インシデント開始〜解決時刻
avg_over_time(incident_duration_seconds[30d])
Elite: < 1時間
High: < 1日
35. ツール選定ガイドと比較
35.1 オブザーバビリティツール全体マップ
【オブザーバビリティツールマップ】
メトリクス:
OSS: Prometheus + Thanos/Mimir
SaaS: Datadog, New Relic, Grafana Cloud
ログ:
OSS: Loki + Grafana
企業向け: Elasticsearch + Kibana
SaaS: Datadog Logs, Splunk, Papertrail
トレーシング:
OSS: Tempo + Grafana
企業向け: Datadog APM, New Relic APM
専門: Honeycomb, Lightstep
プロファイリング:
OSS: Pyroscope, Parca
SaaS: Datadog Continuous Profiler
統合プラットフォーム:
OSS: Grafana LGTM スタック
SaaS: Datadog, Dynatrace, New Relic
35.2 ツール選定マトリクス
| シナリオ | 推奨スタック | 理由 |
|---|---|---|
| スタートアップ・小規模 | Grafana Cloud(Free/Pro) | セットアップが速い、低コスト |
| 中規模・技術力高 | Grafana LGTM OSS | コスト最適、カスタマイズ自由 |
| 大規模エンタープライズ | Datadog or Dynatrace | サポート、コンプライアンス |
| Kubernetes中心 | Prometheus + LGTM | 業界標準、豊富なエコシステム |
| コスト最重視 | Prometheus + Loki + Tempo | 最低コスト |
| 開発スピード重視 | Datadog | セットアップが最速 |
付録A: よく使うコマンドリファレンス
Prometheus / PromQL
# Prometheus APIでメトリクスを確認
curl http://localhost:9090/api/v1/query?query=up
# ラベルの一覧を取得
curl http://localhost:9090/api/v1/labels
# 特定メトリクスのラベル値を取得
curl 'http://localhost:9090/api/v1/label/job/values'
# Prometheusの設定を再読み込み(Web API)
curl -X POST http://localhost:9090/-/reload
# Prometheusの状態確認
curl http://localhost:9090/-/healthy
curl http://localhost:9090/-/ready
# アラートの一覧確認
curl http://localhost:9090/api/v1/alerts
# Recording/Alerting Rulesの確認
curl http://localhost:9090/api/v1/rules
Alertmanager
# アラートの一覧
amtool alert query
amtool alert query --alertname=HighErrorRate
# サイレンス(一時抑制)の作成
amtool silence add alertname=TestAlert \
--duration=2h \
--comment="Testing new feature"
# サイレンスの一覧
amtool silence query
# サイレンスの削除
amtool silence expire <silence-id>
# Alertmanagerの設定確認
amtool config show
amtool config check --config.file=alertmanager.yml
Loki / LogCLI
# LogCLIのインストール
wget https://github.com/grafana/loki/releases/download/v3.0.0/logcli-linux-amd64.zip
unzip logcli-linux-amd64.zip
chmod +x logcli-linux-amd64
sudo mv logcli-linux-amd64 /usr/local/bin/logcli
# 環境変数設定
export LOKI_ADDR=http://localhost:3100
# ログの検索
logcli query '{app="payment-service", level="error"}' --limit=100
# 過去1時間のエラー
logcli query '{app="payment-service"}' --from="1h" --to="now" --limit=500
# ライブ表示
logcli query '{app="payment-service"}' --tail
# メトリクスクエリ
logcli query 'sum(rate({app="api"}[5m])) by (level)'
# ラベルの一覧
logcli labels
# ストリームの一覧
logcli series '{app="payment-service"}'
Tempo / TraceQL
# トレースの検索(Grafana Explore または curl)
curl "http://localhost:3200/api/search?q={.service.name=\"payment-service\"}&limit=20"
# トレースIDで取得
curl "http://localhost:3200/api/traces/<trace-id>"
# サービスグラフの確認
curl "http://localhost:3200/api/v1/search/tags"
付録B: アラートルールのベストプラクティス集
必須アラート(すべてのサービスに)
groups:
- name: universal-service-alerts
rules:
# サービスダウン(1分で発火)
- alert: ServiceDown
expr: up == 0
for: 1m
labels: {severity: critical}
annotations:
summary: "Service {{ $labels.job }} is down"
# エラーレート > 1%(5分継続)
- alert: HighErrorRate
expr: |
(sum by(job) (rate(http_requests_total{status_code=~"5.."}[5m])) /
sum by(job) (rate(http_requests_total[5m]))) > 0.01
for: 5m
labels: {severity: warning}
# p99レイテンシ > 1秒(10分継続)
- alert: HighLatency
expr: |
histogram_quantile(0.99, sum by(job, le)(rate(http_request_duration_seconds_bucket[5m]))) > 1.0
for: 10m
labels: {severity: warning}
# ディスク使用率 > 85%
- alert: DiskUsageHigh
expr: |
(1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) > 0.85
for: 5m
labels: {severity: warning}
# メモリ使用率 > 90%
- alert: MemoryUsageHigh
expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 0.90
for: 5m
labels: {severity: warning}
# SSL証明書が30日以内に期限切れ
- alert: SSLCertExpiryWarning
expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 30
for: 1h
labels: {severity: warning}
annotations:
summary: "SSL cert expires in {{ $value | humanizeDuration }}: {{ $labels.instance }}"
付録C: オブザーバビリティ成熟度チェックリスト
### レベル1(基本監視)
- [ ] Prometheusでインフラメトリクスを収集している
- [ ] Grafanaで基本的なダッシュボードがある
- [ ] Alertmanagerでアラートをメールに送っている
- [ ] アプリケーションのログをファイルに出力している
### レベル2(構造化)
- [ ] 構造化ログ(JSON)を実装している
- [ ] アプリケーションメトリクス(RED)を実装している
- [ ] Lokiでログを集約している
- [ ] PagerDutyなどのオンコールツールと連携している
- [ ] 全アラートにRunbook URLが含まれている
### レベル3(分散トレーシング)
- [ ] OpenTelemetryで全サービスを計装している
- [ ] Tempoで分散トレースを保存している
- [ ] Grafanaでメトリクス⇔ログ⇔トレースの相関が可能
- [ ] SLOを定義してエラーバジェットを管理している
- [ ] SLOバーンレートアラートを実装している
### レベル4(高度)
- [ ] Thanos/Mimirで長期保存を実現している
- [ ] Pyroscopeで継続的プロファイリングをしている
- [ ] カナリアデプロイで自動検証をしている
- [ ] コスト最適化(サンプリング・フィルタリング)を実装
- [ ] AIOpsで異常検知を補助している
### レベル5(エキスパート)
- [ ] eBPFで低レベルの観測を実装している
- [ ] 組織全体のオブザーバビリティ標準を策定している
- [ ] SLOレポートを自動生成し経営陣と共有している
- [ ] オブザーバビリティ文化がエンジニア全員に根付いている
まとめ
オブザーバビリティは、メトリクス、ログ、トレース、プロファイルを集めるだけでは成立しません。ユーザー影響、SLO、アラート、Runbook、インシデント対応までをつなげて、障害時に「何が起きているか」「どこから直すべきか」を短時間で判断できる状態を作ることが目的です。最初は少数の重要なサービスから始め、ノイズの少ないアラートと再利用できる調査手順を育てるのが現実的です。
参考文献
公式・標準
- OpenTelemetry Specification
- OpenTelemetry Documentation
- OpenTelemetry: Observability Primer
- OpenTelemetry: What is OpenTelemetry?
- W3C Trace Context