Pythonで学ぶprometheus-client入門:PrometheusとGrafanaに向けたカスタムメトリクスの収集と公開

Python tutorial - IT technology blog
Python tutorial - IT technology blog

導入背景とメトリクスの必要性

本番環境でスクリプトやバックエンドサービスを運用していて最も厄介なのは、エラーログが一切出力されないままシステムが徐々に遅くなっていく現象です。ログは特定の時点において「何が起きたか」を記録するものに過ぎず、リアルタイムのスループットやp99レイテンシ、データベース接続プールの枯渇状況を把握したい場合には力不足です。

私の以前のプロジェクトでも、深夜に5万件のレコードを同期するcronジョブが突然フリーズする問題に悩まされました。2GBを超える巨大なログファイルをgrepでいくら追っても、外部APIの読み込みで詰まっているのか、DB書き込みで止まっているのか特定できませんでした。しかし、コードにprometheus-clientを組み込んだ途端、原因は一目瞭然となりました。処理速度が120レコード/秒から0に急落し、DB接続プールのGaugeが上限に張り付いていたのです。勘や手探りの調査とはこれでお別れです。

押さえておくべき4つの主要メトリクス型

  • Counter: 単調増加のみを行う値で、アプリ再起動時にのみ0にリセットされます。リクエスト総数、決済成功数、失敗したジョブ数などのカウントに適しています。
  • Gauge: 上下動する任意の数値を表します。現在のCPU/メモリ使用量、稼働中のワーカー数、接続プールのアクティブコネクション数などの測定によく使われます。
  • Histogram: データの分布(レスポンスタイム、ペイロードサイズなど)を計測し、定義済みのバケット(区間)に集計します。Prometheusサーバー側で分位値(p50、p95、p99など)を柔軟に算出できます。
  • Summary: Histogramと同様に分布を計測しますが、クライアント側で分位値を直接計算します。デメリットとして、アプリの負荷が高くなる点や、複数インスタンス間での合算(集計)ができない点が挙げられます。バックエンドでは基本的にHistogramの利用が推奨されます。

環境構築

PyPIから公式ライブラリをインストールします:

pip install prometheus-client

ローカル環境でPrometheusとGrafanaを含む一連のパイプラインを手軽に検証するには、Docker Composeを利用するのが最も早いです。docker-compose.ymlを作成します:

version: '3.8'
services:
  prometheus:
    image: prom/prometheus:v2.51.0
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    extra_hosts:
      - "host.docker.internal:host-gateway"

  grafana:
    image: grafana/grafana:10.4.0
    ports:
      - "3000:3000"
    depends_on:
      - prometheus

注意:上記のextra_hosts設定により、Linux上のPrometheusコンテナからホストマシン上で動作するPythonスクリプトへhost.docker.internalというホスト名でアクセスできるようになります。

Pythonコードでの実装

1. メトリクスの初期化と更新

start_http_server関数は独立したデーモンスレッドを起動してHTTPサーバーを立ち上げ、Prometheusが定期的にデータを取得(スクレイプ)するための/metricsエンドポイントを提供します。

import time
import random
from prometheus_client import start_http_server, Counter, Gauge, Histogram

# メトリクスの定義
REQUEST_COUNT = Counter(
    'app_http_requests_total',
    '受信したHTTPリクエストの総数',
    ['method', 'endpoint', 'status']
)

ACTIVE_WORKERS = Gauge(
    'app_active_workers',
    '同時に処理を実行しているアクティブワーカー数'
)

REQUEST_LATENCY = Histogram(
    'app_request_latency_seconds',
    'リクエスト処理時間(秒)',
    ['endpoint'],
    buckets=[0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
)

def process_request():
    endpoint = random.choice(['/api/v1/orders', '/api/v1/users', '/api/v1/checkout'])
    method = 'POST' if endpoint == '/api/v1/checkout' else 'GET'
    
    ACTIVE_WORKERS.inc()
    start_time = time.time()
    
    try:
        # 20ms〜600msのレイテンシをシミュレート
        duration = random.uniform(0.02, 0.6)
        time.sleep(duration)
        status = '200' if random.random() > 0.1 else '500'
    finally:
        elapsed = time.time() - start_time
        REQUEST_LATENCY.labels(endpoint=endpoint).observe(elapsed)
        REQUEST_COUNT.labels(method=method, endpoint=endpoint, status=status).inc()
        ACTIVE_WORKERS.dec()

if __name__ == '__main__':
    start_http_server(8000)
    print("メトリクスエンドポイントの準備が完了しました: http://localhost:8000/metrics")
    
    while True:
        process_request()
        time.sleep(0.05)

2. Prometheusのダウンを防ぐ実践上の注意点(ハイカーディナリティ問題)

  • ラベル爆発(ハイカーディナリティ)の回避:ユーザーID、メールアドレス、UUID、注文IDなどをラベルに含めることは絶対に避けてください。例えば10万人のユーザーが存在する場合、1つのメトリクスにつき10万本の独立した時系列データ(タイムシリーズ)が生成されてしまいます。その結果、Prometheusのメモリが枯渇し、わずか数時間でOOMKilledに至ります。ラベルにはHTTPステータスコード、APIルート、サービス名、HTTPメソッドなどの有限な値のみを設定すべきです。
  • 標準的な命名規約:メトリクス名は時間やサイズの単位を末尾に付け、Counterには複数形を使用するのがベストプラクティスです。例:http_request_duration_seconds(Histogram)、http_requests_total(Counter)、db_connection_pool_active(Gauge)。
  • 実態に即したバケット設計:Histogramのデフォルトバケットは5ms〜10sです。もし200ms〜30s程度かかるElasticsearchのクエリを計測する場合、100ms未満の小さなバケットは完全に無駄となり、メモリを浪費します。
  • デコレータの活用:try/finallyを手動で記述する代わりに、@REQUEST_LATENCY.labels(endpoint='/api/pay').time()や@ACTIVE_WORKERS.track_inprogress()といった簡潔なデコレータ構文を計測対象の関数に付与できます。

3. Prometheusのスクレイプジョブ設定

Docker Composeファイルと同じ階層にprometheus.ymlを作成し、Prometheusが5秒ごとにメトリクスを取得するように設定します:

global:
  scrape_interval: 5s
  evaluation_interval: 5s

scrape_configs:
  - job_name: 'python-app'
    static_configs:
      - targets: ['host.docker.internal:8000']

動作確認と可視化

1. curlによる生メトリクスの確認

アプリケーションが標準のPrometheusテキストフォーマットで正しく出力しているか、リクエストを送信して確認します:

curl -s http://localhost:8000/metrics | grep -E "app_http_requests_total|app_active_workers"

レスポンスとして、次のようにラベル構造が明確なデータが返されます:

# TYPE app_http_requests_total counter
app_http_requests_total{endpoint="/api/v1/orders",method="GET",status="200"} 482.0
app_http_requests_total{endpoint="/api/v1/checkout",method="POST",status="500"} 53.0
# TYPE app_active_workers gauge
app_active_workers 1.0

2. 基本的なPromQLクエリの実行

ブラウザでhttp://localhost:9090/graphを開き、以下のクエリを試してみましょう:

  • 1秒あたりのリクエスト数(スループット / RPS):
    sum(rate(app_http_requests_total[1m])) by (endpoint)
  • 5xxエラー率(エラー率 %):
    (sum(rate(app_http_requests_total{status=~"5.."}[5m])) / sum(rate(app_http_requests_total[5m]))) * 100
  • エンドポイント別の95パーセンタイルレイテンシ(p95レイテンシ):
    histogram_quantile(0.95, sum(rate(app_request_latency_seconds_bucket[5m])) by (le, endpoint))

3. Grafanaでのダッシュボード構築

  1. http://localhost:3000にアクセスし、デフォルトのユーザー名とパスワード(admin/admin)でログインします。
  2. Connections > Data Sourcesに移動し、Prometheusを選択してServer URLにhttp://prometheus:9090を入力後、Save & Testをクリックします。
  3. 新規ダッシュボードを作成し、主要な3つのパネルを追加します:
    • Stat: app_active_workersをクエリし、ワーカー数が5を超えたら黄色、10を超えたら赤色になるようしきい値を設定します。
    • Time Series: RPSとp95レイテンシの時系列グラフを描画し、トラフィック急増の検知に役立てます。
    • Pie ChartまたはBar Gauge: ステータス200と500の割合を集計し、APIの安定性を一目で把握できるようにします。

カスタムメトリクスの導入に必要なコーディングはわずか15分程度ですが、従来のログ解析と比べて障害調査やボトルネック特定の効率を劇的に向上させてくれます。

Share: