あなたのシステムは動いている——でも、本当に何をしているか把握できていますか?
これは PLG Stack シリーズ全2回の 第1部 です。各コンポーネントがどのように動作し、何のために使うのかを詳しく解説します。第2部 では、Docker Compose・Kubernetes・Alerting・本番デバッグを実践します。
目次
- Observabilityとは何か、なぜ必要か
- PLG Stackの概要
- Prometheus — メトリクス収集システム
- Loki — ログ管理システム
- Grafana — 可視化・アラートプラットフォーム
1. Observabilityとは何か?
ツールの話をする前に、私たちが解決しようとしている問題 を理解しましょう。
リアルなシナリオ
金曜日の深夜11時、顧客からメッセージが届きます:「アプリが壊れてる、ログインできない」。サーバーにSSH接続して top を実行すると、CPUが90%——でも理由がわからない。ログを読むと、エラーが何千行も出ているが、どれが重要かわからない。問題がいつから始まったのか、何人のユーザーが影響を受けているのか、根本原因は何なのか——何もわからない。
これが Observabilityのないシステム です。
Observabilityの3本柱
Observabilityとは、外部シグナルからシステムの内部状態を理解する能力です。3種類のデータで構成されます:
┌─────────────────────────────────────────────────────────┐
│ OBSERVABILITY │
│ │
│ 📊 METRICS 📝 LOGS 🔍 TRACES │
│ 「どのくらい?」 「何が起きた?」 「なぜ?」 │
│ │
│ - CPU: 85% - ERROR: DB - リクエストが │
│ - RAM: 12GB connection Service A │
│ - 500 req/s failed → B → C → DB │
│ - P99: 250ms - User #123 Cで2.3秒かかった │
│ logged in │
└─────────────────────────────────────────────────────────┘
Metrics が答える質問:どのくらい?速い?エラーが出ている?
例:CPUが85%、毎秒500リクエスト、エラー率0.1%。
Logs が答える質問:何が起きた?いつ?誰に?
例:2025-05-20 02:13:41 ERROR user=123 action=login msg="DB connection timeout"
Traces が答える質問:リクエストはどこを通った?どのステップが遅い?
(本記事ではMetricsとLogsに集中します。TracesはTempo/Jaegerの別トピックです)
PLG StackはMetrics(Prometheus)とLogs(Loki)を処理し、Grafanaで表示します。
2. PLG Stackの概要
PLG Stackの構成
| 文字 | ツール | 役割 | 何をするか |
|---|---|---|---|
| P | Prometheus | Metricsバックエンド | システム数値の収集・保存・クエリ |
| L | Loki | Logバックエンド | ログの収集・保存・検索 |
| G | Grafana | フロントエンド | ダッシュボード・アラート・データ探索 |
サポートコンポーネントも存在します:
| ツール | 役割 |
|---|---|
| node_exporter | Linuxホストのメトリクスを収集するエージェント |
| cAdvisor | コンテナのメトリクスを収集するエージェント |
| Promtail / Alloy | ログを読み取ってLokiに送信するエージェント |
| Alertmanager | Prometheusからのアラートを処理・送信 |
なぜ他の選択肢ではなくPLGを選ぶのか?
| PLG Stack | ELK Stack | Datadog | |
|---|---|---|---|
| コスト | 無料 | 無料 | ~$15-23/host |
| 必要RAM | 低い (~2GB) | 高い (~8GB+) | 軽量エージェント |
| Logインデックス | ラベルのみ | フルテキスト | フルテキスト |
| Metrics | 非常に強力 | 弱い | 非常に強力 |
| セットアップ | 中程度 | 難しい | 簡単 |
| セルフホスト | あり | あり | なし |
| ベンダーロックイン | なし | なし | あり |
PLGを使うべき場面:
- セルフホストが必要、SaaSにお金を払いたくない
- 多数のコンテナ/マイクロサービスを監視する必要がある
- データを完全に管理したい
- DevOps/SREが運用できるチームがある
他の選択肢を検討すべき場面:
- 小規模チームでインフラ運用担当者がいない → Grafana Cloud(マネージド)
- 超高速なフルテキストログ検索が必要 → ELK
- 自己管理したくない → Datadog
データフロー全体像
3. Prometheus
Prometheusは何のために使うのか?
Prometheusは 時系列データベース で、時間とともに変化するメトリクスを保存するために特別に設計されています:
- CPUが何%使われているか
- 処理中のリクエストが何件あるか
- データベースクエリにどのくらいかかっているか
- ディスクの空き容量が何GB残っているか
- 直近1分間でHTTP 500エラーが何件発生したか
重要な違い:Prometheusはデータを プッシュで受け取らず、HTTPエンドポイント /metrics からターゲットのデータを能動的に スクレイプ(プル) します。このプルモデルには多くの利点があります:Prometheusはどのターゲットが生きているか/死んでいるかを正確に把握でき、アプリケーションからPrometheusへのファイアウォール開放も不要で、デバッグも簡単です。
動作の詳細
ステップ1:ターゲットがHTTP経由でメトリクスを公開する
→ http://your-app:8080/metrics にアクセスすると、テキスト形式で表示:
# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",status="200"} 12453
http_requests_total{method="POST",status="500"} 47
# HELP http_request_duration_seconds Request latency
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.05"} 8543
http_request_duration_seconds_bucket{le="0.1"} 11234
http_request_duration_seconds_bucket{le="+Inf"} 12453
ステップ2:Prometheusが定期的にスクレイプ(デフォルト15秒)
→ HTTP GET http://your-app:8080/metrics を実行
→ テキスト形式をパース
→ 現在のタイムスタンプとともにTSDBに保存
ステップ3:データをPromQLでクエリ
→ rate(http_requests_total[5m]) → 5分間のreq/s
→ histogram_quantile(0.99, ...) → P99レイテンシ
メトリクスの種類 — 必ず押さえておくこと
Prometheusには4種類のメトリクスがあり、それぞれ異なる目的で使います:
Counter — 増えるだけで減らない。「何回発生したかを数える」用途:
http_requests_total{status="200"} = 15234 # アプリ起動からの総リクエスト数
errors_total = 47 # 総エラー数
bytes_sent_total = 1048576000 # 送信済み総バイト数
→ 使用時:意味を持たせるには常に rate() または increase() でラップする。カウンターを直接クエリしない
Gauge — 増減どちらもあり得る。「現在の状態」用途:
memory_usage_bytes = 2147483648 # 現在使用中のRAM
active_connections = 42 # 現在開いているコネクション数
queue_size = 150 # キューのタスク数
→ 使用時:関数でラップせずそのままクエリ可能
Histogram — 値をバケットに分類する。「時間・サイズの計測」用途:
http_duration_seconds_bucket{le="0.1"} = 9000 # 100ms未満のリクエスト9000件
http_duration_seconds_bucket{le="0.5"} = 11000 # 500ms未満のリクエスト11000件
http_duration_seconds_bucket{le="+Inf"} = 12000 # 合計12000リクエスト
http_duration_seconds_sum = 1250.3 # 合計時間(秒)
http_duration_seconds_count = 12000 # 観測の総数
→ 使用時:histogram_quantile() でP50/P95/P99を計算
Summary — Histogramに似ているが、クライアント側でパーセンタイルを計算。複数インスタンスの集計ができないため、あまり使われない。
Prometheusの設定
# prometheus.yml
global:
scrape_interval: 15s # 15秒ごとにスクレイプ
evaluation_interval: 15s # 15秒ごとにアラートルールを評価
external_labels:
environment: 'production'
region: 'asia-southeast1'
rule_files:
- "/etc/prometheus/rules/*.yml"
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
scrape_configs:
# Prometheus自身を監視
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Linuxホストのメトリクスを収集
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
# Dockerコンテナのメトリクスを収集
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
# 自作アプリのメトリクスを収集
- job_name: 'my-web-app'
static_configs:
- targets:
- 'web-app-1:8080'
- 'web-app-2:8080'
metrics_path: '/metrics'
scrape_interval: 10s # グローバル設定を上書き、重要なアプリは頻度を上げる
# Docker Swarmによるサービスディスカバリ
- job_name: 'docker-services'
dockerswarm_sd_configs:
- host: unix:///var/run/docker.sock
role: tasks
relabel_configs:
# "prometheus.io/scrape=true" ラベルを持つコンテナのみスクレイプ
- source_labels: [__meta_dockerswarm_service_label_prometheus_io_scrape]
action: keep
regex: true
PromQL — Prometheusのクエリ言語
PromQLは最も難しい部分ですが、最も重要でもあります。PromQLを理解する = システムに正しい質問を投げる方法がわかる、ということです。
基本:
# セレクタ — 現在値を取得
http_requests_total
http_requests_total{job="my-app"}
http_requests_total{status=~"5.."} # 正規表現: 500, 501, 502...
http_requests_total{status!="200"} # not equal
# レンジベクタ — 期間内のデータを取得
http_requests_total[5m] # 直近5分間
# rate() — 秒あたりの増加速度(カウンターに使用)
rate(http_requests_total[5m])
# → 5分間の平均リクエスト数/秒
# increase() — 期間内の総増加数
increase(http_requests_total[1h])
# → 直近1時間の総リクエスト数
# 集計
sum(rate(http_requests_total[5m])) # 全体の合計
sum by (status) (rate(http_requests_total[5m])) # ステータスコード別の合計
avg by (instance) (node_cpu_usage_percent) # ホスト別の平均
topk(5, rate(http_requests_total[5m])) # 上位5件
よく使う実践的なクエリ:
# ===== CPU =====
# ホスト別のCPU使用率%
100 - (avg by (instance) (
rate(node_cpu_seconds_total{mode="idle"}[5m])
) * 100)
# ===== メモリ =====
# RAM使用率%
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100
# ===== ディスク =====
# 残ディスク容量%
(node_filesystem_avail_bytes{mountpoint="/"} /
node_filesystem_size_bytes{mountpoint="/"}) * 100
# ===== HTTPアプリケーション =====
# リクエストレート (req/s)
sum(rate(http_requests_total[5m]))
# エラーレート(リクエストのエラー率%)
sum(rate(http_requests_total{status=~"5.."}[5m])) /
sum(rate(http_requests_total[5m])) * 100
# P50、P95、P99レイテンシ
histogram_quantile(0.99,
sum by (le) (rate(http_request_duration_seconds_bucket[5m]))
)
# ===== データベース =====
# アクティブなPostgreSQLコネクション
pg_stat_activity_count{state="active"}
# Redisキャッシュヒット率
rate(redis_keyspace_hits_total[5m]) /
(rate(redis_keyspace_hits_total[5m]) + rate(redis_keyspace_misses_total[5m]))
アプリケーションからメトリクスを公開する
主要な言語には、Prometheusクライアントライブラリが用意されています:
Go:
import "github.com/prometheus/client_golang/prometheus"
var requestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "status"},
)
func init() {
prometheus.MustRegister(requestsTotal)
}
func handler(w http.ResponseWriter, r *http.Request) {
// ... リクエスト処理 ...
requestsTotal.WithLabelValues("GET", "200").Inc()
}
// エンドポイントを公開
http.Handle("/metrics", promhttp.Handler())
Python(FastAPI):
from prometheus_client import Counter, Histogram, Response, generate_latest
import time
REQUEST_COUNT = Counter(
'http_requests_total', 'Total HTTP requests',
['method', 'endpoint', 'status']
)
REQUEST_LATENCY = Histogram(
'http_request_duration_seconds', 'Request latency',
buckets=[.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5]
)
@app.middleware("http")
async def metrics_middleware(request, call_next):
start = time.time()
response = await call_next(request)
duration = time.time() - start
REQUEST_COUNT.labels(request.method, request.url.path, response.status_code).inc()
REQUEST_LATENCY.observe(duration)
return response
@app.get("/metrics")
def metrics():
return Response(generate_latest(), media_type="text/plain")
Node.js:
const promClient = require('prom-client');
const requestCount = new promClient.Counter({
name: 'http_requests_total',
help: 'Total HTTP requests',
labelNames: ['method', 'status']
});
const requestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'Request latency',
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5]
});
app.use((req, res, next) => {
const end = requestDuration.startTimer();
res.on('finish', () => {
requestCount.inc({ method: req.method, status: res.statusCode });
end();
});
next();
});
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(await promClient.register.metrics());
});
知っておくべき主要なExporter
自作アプリを書かない場合は、既製のExporterを使います:
| Exporter | 監視対象 |
|---|---|
node_exporter |
LinuxホストのCPU、RAM、ディスク、ネットワーク |
cAdvisor |
Dockerコンテナのメトリクス |
kube-state-metrics |
Kubernetesオブジェクト(Pod、Deploymentなど) |
blackbox_exporter |
外部からのHTTP/TCPエンドポイントのヘルスチェック |
postgres_exporter |
PostgreSQLデータベースのメトリクス |
redis_exporter |
Redisのメトリクス |
nginx_exporter |
Nginxのコネクション、リクエスト |
4. Loki
Lokiは何のために使うのか?
Lokiは ログの保存・検索システム です。アプリケーションが何が起きているかを記録するために出力するテキスト行を扱います:
2025-05-20T14:23:41Z INFO user=alice action=purchase product_id=789 amount=299000
2025-05-20T14:23:41Z ERROR user=bob action=login error="invalid password" attempt=3
2025-05-20T14:23:42Z WARN worker=5 queue=emails msg="queue is filling up" size=8420
ログがなければ、エラーが起きたときに「エラーがある」とはわかっても「どこで、誰が影響を受けて、なぜ」はわかりません。Prometheusはエラーが 何件 あるかを教えてくれます——Lokiは 具体的な各エラー を読み返せます。
なぜElasticsearchではなくLokiを使うのか?
これは多くの人が混乱するポイントです。Elasticsearchは各ログ行の 全内容をインデックス化 します——すべての単語がインデックスに入ります。これにより検索は非常に高速になりますが、RAMとディスクを大量に消費します。
Lokiは ラベル(メタデータ)のみをインデックス化 します。ログの内容は圧縮チャンクとして保存されます。クエリ時は、まずラベルで絞り込み(高速)、その後テキストをgrepします(やや遅いが許容範囲)。
Elasticsearch:
各ログ行 → 全単語をインデックス → "ERROR"検索が超速 → RAM/ディスク消費大
Loki:
各ログ行 → ラベルのみインデックス(job, host, container)→ "ERROR"検索はやや遅い → 大幅に節約
1日50GBのログの場合:
ELKに必要なストレージ: ~200GB(インデックス後)
Lokiに必要なストレージ: ~20-30GB(圧縮後)
Lokiのラベル — 最も重要なこと
ラベルは各ログストリームに付与するメタデータです。ラベルはLokiの パフォーマンスを決定します。
# Lokiのログエントリの例(ラベルあり):
ラベル:
job="my-app"
environment="prod"
host="server-01"
container="api"
タイムスタンプ: 2025-05-20T14:23:41Z
ログ行: "ERROR user=123 msg=login_failed"
ラベルの黄金ルール — カーディナリティは低く保つ:
# ✅ 正しい — 固定値の数が少ない
{job="api", environment="prod", host="web-01"}
# ❌ 間違い — user_idは何百万種類もある
{job="api", user_id="12345"}
# ❌ 間違い — request_idはリクエストごとに一意 → 何百万ものストリームが生成されOOM
{job="api", request_id="abc-xyz-123"}
user_id や request_id は ログの内容 に含め、ラベルには入れないでください。Lokiにプッシュした後、LogQLでパースします。
Promtailの設定
Promtailは各サーバーで動くエージェントで、ログを読み取ってLokiに送信します:
# promtail-config.yml
server:
http_listen_port: 9080
positions:
filename: /tmp/positions.yaml # 再起動後に再読み込みしないよう読取位置を記憶
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
# システムログを読み取る
- job_name: system-logs
static_configs:
- targets: [localhost]
labels:
job: varlogs
__path__: /var/log/{syslog,auth.log,kern.log}
# 全Dockerコンテナのログを読み取る
- job_name: docker-containers
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
# コンテナ名をラベルとして使用
- source_labels: [__meta_docker_container_name]
regex: '/(.*)'
target_label: container
# docker-composeのサービス名を使用
- source_labels: [__meta_docker_container_label_com_docker_compose_service]
target_label: service
pipeline_stages:
# ログがJSONなら各フィールドをパース
- json:
expressions:
level: level
message: message
# 素早くフィルタできるよう "level" フィールドをラベルとして使用
- labels:
level:
# ストレージ節約のためdebugログは破棄
- drop:
source: level
expression: "debug"
# nginxのログファイルを読み取る
- job_name: nginx
static_configs:
- targets: [localhost]
labels:
job: nginx
__path__: /var/log/nginx/access.log
pipeline_stages:
# 正規表現でnginxアクセスログ形式をパース
- regex:
expression: '^(?P<remote_addr>\S+) .* "(?P<method>\S+) (?P<path>\S+) \S+" (?P<status>\d+) (?P<bytes_sent>\d+)'
# HTTPステータスで素早くフィルタできるようラベルとして使用
- labels:
status:
method:
LogQL — Lokiのクエリ言語
# ===== 基本 =====
# あるジョブの全ログを取得
{job="my-app"}
# 複数ラベルで絞り込む
{job="my-app", environment="prod"}
# フルテキスト検索
{job="my-app"} |= "ERROR" # "ERROR"を含む
{job="my-app"} != "DEBUG" # "DEBUG"を含まない
{job="my-app"} |~ "user=[0-9]+" # 正規表現にマッチ
# ===== ログのパース =====
# JSONログをパース
{job="my-app"} | json
# パース後、フィールドで絞り込む
{job="my-app"} | json | level="error"
{job="my-app"} | json | status_code >= 500
{job="my-app"} | json | duration > 1000 # duration > 1000ms
# 正規表現でカスタム形式のログをパース
{job="nginx"} | pattern `<ip> - - [<_>] "<method> <path> <_>" <status> <size>`
| status >= 400
# Logfmt(key=value形式)
{job="my-app"} | logfmt | level="error" | user != ""
# ===== ログからメトリクスを生成 =====
# ログ行数/秒を計算
rate({job="my-app"}[1m])
# エラー数/秒を計算
rate({job="my-app"} |= "ERROR" [1m])
# パース後にフィールド別で集計
sum by (status) (
rate({job="nginx"} | pattern `<_> "<_> <_> <_>" <status> <_>` [1m])
)
# ブルートフォース検知 — 5分間に10回以上ログイン失敗したユーザー
sum by (user) (
count_over_time(
{job="auth-service"} | json | action="login" | status="failed" [5m]
)
) > 10
5. Grafana
Grafanaは何のために使うのか?
GrafanaはPrometheusとLokiの上に乗る プレゼンテーション層 です。主に3つのことを行います:
- 可視化 — 生データをグラフ、表、読みやすい数値に変換
- アラート — しきい値を監視し、超えたときに通知を送信
- 探索 — インシデントのデバッグや調査のためのアドホッククエリ
Prometheusがメトリクス収集エンジン、Lokiがログ倉庫だとすると、Grafanaは 毎日実際に作業する場所 です。
パネルの種類と使い分け
Time series — 時間とともに変化するメトリクスに:
使う場面: CPU使用率、メモリ、リクエストレート、時間別エラーレート
クエリ例: rate(http_requests_total[5m])
Stat — トレンド付きの大きな単一数値に:
使う場面: 稼働率%、今日の総リクエスト数、エラー数
クエリ例: sum(rate(http_requests_total{status=~"5.."}[24h]))
Gauge — しきい値に応じた色付きでmin/max有りの数値に:
使う場面: CPU%、ディスク%、メモリ% — 0-100の上限がある値
しきい値: 0-70% → 緑、70-90% → 黄、90-100% → 赤
Table — 複数列のテーブル形式データに:
使う場面: 遅いエンドポイントTop10、サービスと状態の一覧
クエリ例: topk(10, sum by (endpoint) (rate(http_requests_total{status="500"}[1h])))
Logs — ダッシュボードにLokiのログを直接表示:
使う場面: メトリクスの隣で特定サービスのエラーログを見る
クエリ例: {job="my-app", environment="prod"} | json | level="error"
Heatmap — 時間ごとの値の分布に:
使う場面: リクエストレイテンシの分布 — 遅いリクエストが何件あるかを一目で把握
クエリ例: sum by (le) (rate(http_request_duration_seconds_bucket[5m]))
REDメソッドとUSEメソッド
何を監視すべきかを知るための2つの有名なフレームワーク:
REDメソッド — サービス/マイクロサービス向け:
- Rate — 毎秒何リクエストあるか
- Error — リクエストのエラー率
- Duration — リクエストにかかる時間(P50/P95/P99)
USEメソッド — インフラ向け:
- Utilization — リソースが何%使われているか(CPU%、ディスク%)
- Saturation — キュー/バックログが詰まっていないか
- Errors — ハードウェア/カーネルエラーが出ていないか
プロビジョニングによるデータソースの追加(コードで管理)
# grafana/provisioning/datasources/datasources.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
jsonData:
timeInterval: "15s" # scrape_intervalと一致させる
- name: Loki
type: loki
access: proxy
url: http://loki:3100
jsonData:
# LokiとPrometheusを連携してCorrelationを有効にする
derivedFields:
- datasourceUid: prometheus
matcherRegex: "request_id=(\\w+)"
name: RequestID
url: '${__value.raw}'
すぐにインポートすべきダッシュボードテンプレート
一から作らず、Grafana Labsの既製ダッシュボードをインポートしましょう(Dashboards → Import → IDを入力):
| ID | 内容 |
|---|---|
1860 |
Node Exporter Full — CPU、RAM、ディスク、ネットワークの詳細 |
13978 |
Node Exporter Quickstart — よりシンプル、スタートに最適 |
3662 |
Prometheus 2.0 Overview — Prometheus自身を監視 |
15141 |
Kubernetes Cluster Monitoring |
14055 |
Loki Dashboard — Loki自身を監視 |
9628 |
PostgreSQL Database |
11835 |
Redis Exporter |
第1部のまとめ
ここまでで各コンポーネントをしっかり理解できたはずです:
- Prometheus — ターゲットからメトリクスをプル、時系列として保存、PromQLでクエリ
- Loki — Promtailからログを受信、フルテキストではなくラベルをインデックス化、LogQLでクエリ
- Grafana — 両方のデータを可視化、しきい値超過時にアラート、デバッグ時に探索
第2部 では実践に直行します:Docker Composeでスタック全体をデプロイ、AlertmanagerでSlack通知を設定、実践的なアラートルールの構築、よくある本番デバッグシナリオを扱います。
