オブザーバビリティとモニタリング

目次

概要

まず、この章の中心構造を図で確認します。細部に入る前に、どの概念がどこへつながるかをつかむための地図です。

flowchart LR A["アプリケーション"] --> B["メトリクス"] A --> C["ログ"] A --> D["トレース"] A --> E["プロファイル"] B --> F["収集基盤"] C --> F D --> F E --> F F --> G["ダッシュボード"] F --> H["アラート"] G --> I["原因調査"] H --> I I --> J["改善"] J --> A
コード例の読み方

コード例は、そのまま写すためだけのものではありません。直前の本文で「何を確かめる例か」を押さえ、直後の説明で「どの性質が見えるか」を確認してください。実務では、ここに入力の境界、失敗時の挙動、依存する実行環境を足して読むと判断しやすくなります。

要点

オブザーバビリティは、メトリクス・ログ・トレース・プロファイルなどのテレメトリから、システム内部の状態を調査できるようにする設計です。

モニタリングは既知の異常を検知するための仕組みであり、オブザーバビリティは未知の問題を調査できる状態を作るための考え方です。現代の分散システムでは、単一の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_httpcheck_pingcheck_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が革新的だった理由:

  1. Pullモデル: PrometheusがHTTPエンドポイントをスクレイピング(能動的取得)
  2. 多次元データモデル: メトリクス名 + ラベルの組み合わせ
  3. PromQL: 強力なクエリ言語
  4. サービスディスカバリ: Kubernetes、Consul、EC2等と自動統合
  5. アラートマネージャー: アラートのルーティング・グループ化・抑制
┌─────────────────────────────────────────────────────────┐
│                  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本柱(メトリクス・ログ・トレース・プロファイル)

この章で重視すること


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のトレースに自動ジャンプできる

相関のワークフロー:

  1. Grafanaダッシュボードでエラーレートスパイクを発見
  2. そのスパイク期間のExemplarをクリック → Tempoのトレースが開く
  3. 問題のSpanをクリック → Lokiの関連ログが開く
  4. パフォーマンス問題があれば → 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 ExporterPostgreSQL 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)があれば11マッチング(デフォルト)
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_leftgroup_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 ログとは何か

ログはシステムが生成するイベント記録の集合体だ。ソフトウェアエンジニアリングにおけるログの価値は:

  1. デバッグ: 問題が発生したときの詳細な状況記録
  2. 監査: セキュリティ・コンプライアンス上の記録
  3. 分析: ビジネスイベントやユーザー行動の分析
  4. パフォーマンス: スロークエリやボトルネックの特定

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)は、ElasticsearchLogstashKibanaの頭文字を取ったログ管理スタックだ。近年は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戦略:

  1. 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戦略:

  1. 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%保存
  1. 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%保存
  1. 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 パターン

flowchart LR A["アプリケーション"] -->|OTLP| B["Collector Agent"] B --> C["Collector Gateway"] C --> D["Processor"] D --> E{"シグナル種別"} E -->|Traces| F["Tempo・Jaeger"] E -->|Metrics| G["Prometheus・Mimir"] E -->|Logs| H["Loki・Elasticsearch"] D --> I["サンプリング・変換・マスキング"] I --> E

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構成の限界
  • ThanosMimirの違いと選択基準
  • 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、インシデント対応までをつなげて、障害時に「何が起きているか」「どこから直すべきか」を短時間で判断できる状態を作ることが目的です。最初は少数の重要なサービスから始め、ノイズの少ないアラートと再利用できる調査手順を育てるのが現実的です。

参考文献

公式・標準

書籍

解説・補助