夜中の2時にAPIが遅いという電話を受けて、原因がわからなかったことはありませんか?自分も経験があります——各サーバーにSSHしてログを確認し、topやnetstatを実行しても問題が見つからない。prom-client + Grafanaを導入してから、状況が一変しました。ダッシュボードを開くだけで、リクエストレート、遅いエンドポイント、エラーレートがすぐに把握できます。
ブログにはPrometheus + GrafanaでサーバーをモニタリングするCPU、RAM、ディスクの記事があります。この記事はさらに一段深く掘り下げます:アプリケーションレベルの監視——つまりNode.jsのコード自体が何をしているか、どのエンドポイントがボトルネックになっているか、ビジネスロジックが正しく動いているかを確認します。
Node.jsの監視方法3選——選ぶ前に比較
prom-clientが常に正解とは限りません。以下の3つのアプローチはそれぞれ存在する理由があります。
方法1:ログのみ使用+手動分析
リクエスト/レスポンスのログをファイルに書き出し、grep、awk、またはGraylogを使って後から分析します。
- メリット:追加設定不要、ログはすでにある、特定のバグのデバッグが容易
- デメリット:時間軸でのトレンドが見えない、リアルタイムアラートがない、手動分析に時間がかかる——特に午前3時にインシデントが発生した場合
方法2:商用APM(Datadog、New Relic、Dynatrace)
エージェントをインストールするだけで自動的にすべてをトレースし、最初からきれいなダッシュボードが利用できます。
- メリット:セットアップが非常に簡単、分散トレーシングや異常検知が利用可能、インフラ管理不要
- デメリット:コストが高い(Datadogはホストあたり月$15〜、データ取り込みは$0.10/GB別途)、ベンダーロックイン、独自のビジネスロジックに合わせたメトリクス定義が困難
方法3:prom-client + Prometheus + Grafana(セルフホスト)
コードからメトリクスを自分で公開し、Prometheusが定期的にスクレイプし、Grafanaで可視化してアラートを設定します。
- メリット:完全無料、フルコントロール、任意のメトリクスを自由に定義可能、大きなコミュニティ、Kubernetesとの優れた統合
- デメリット:クエリにPromQLの知識が必要、Prometheus + Grafanaのインフラを自己管理する必要がある
prom-clientを選ぶ理由
プロジェクトにすでにPrometheusがある(または導入予定の)場合、prom-clientは最も自然な選択肢です。商用APMは高いバジェットを持つ大規模チームや複雑な分散トレーシングが必要なケースに適しています。スタートアップ、サイドプロジェクト、あるいは独自のビジネスメトリクスを正確に追跡したい場合——prom-client + Grafanaで十分であり、完全無料です。
prom-clientには4種類のメトリクスがあります。Counterは増加のみ——リクエストやエラーのカウントに使用。Gaugeは増減可能——アクティブ接続数やメモリに使用。Histogramは分布を計測——リクエスト時間に使用。Summaryはクライアント側でパーセンタイルを計算。Web APIでは、CounterとHistogramが最も頻繁に使用します。
Express.jsへのprom-client統合——ステップバイステップ
ステップ1:パッケージのインストール
npm install prom-client
ステップ2:専用ファイルでメトリクスを初期化
監視ロジックをmetrics.jsという専用ファイルに分離して、ビジネスコードと混在しないようにします:
// metrics.js
const client = require('prom-client');
const register = new client.Registry();
// Node.jsのデフォルトメトリクスを収集(メモリヒープ、イベントループラグ、GCなど)
client.collectDefaultMetrics({ register });
// Counter: HTTPリクエストの総数をカウント
const httpRequestsTotal = new client.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code'],
registers: [register],
});
// Histogram: リクエスト処理時間の分布(レイテンシ)
const httpRequestDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
registers: [register],
});
// ビジネスメトリクス: 作成された注文数をカウント(例)
const ordersCreatedTotal = new client.Counter({
name: 'orders_created_total',
help: 'Total number of orders created',
labelNames: ['status', 'payment_method'],
registers: [register],
});
// Gauge: 現在アクティブなユーザー数(増減可能)
const activeUsers = new client.Gauge({
name: 'active_users_current',
help: 'Number of currently active users',
registers: [register],
});
module.exports = { register, httpRequestsTotal, httpRequestDuration, ordersCreatedTotal, activeUsers };
ステップ3:すべてのHTTPリクエストを自動追跡するミドルウェア
// middleware/metricsMiddleware.js
const { httpRequestsTotal, httpRequestDuration } = require('../metrics');
function metricsMiddleware(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
// req.route.pathは /api/users/123 ではなく /api/users/:id のようなパターンを返す
const route = req.route ? req.route.path : req.path;
const labels = { method: req.method, route, status_code: res.statusCode };
httpRequestsTotal.inc(labels);
httpRequestDuration.observe(labels, duration);
});
next();
}
module.exports = metricsMiddleware;
ステップ4:ミドルウェアの登録と/metricsの公開
// app.js
const express = require('express');
const { register } = require('./metrics');
const metricsMiddleware = require('./middleware/metricsMiddleware');
const app = express();
app.use(express.json());
app.use(metricsMiddleware);
// Prometheusスクレイプエンドポイント——公開厳禁、後述の注意事項を参照
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
app.get('/api/orders', (req, res) => {
res.json({ orders: [] });
});
app.listen(3000, () => console.log('Server :3000 | Metrics: :3000/metrics'));
ステップ5:ルートハンドラーでカスタムビジネスメトリクスを追跡
ここがprom-clientが汎用的な監視と比べて輝く部分です——独自のビジネスロジックを正確に追跡できます:
// routes/orders.js
const { ordersCreatedTotal, activeUsers } = require('../metrics');
app.post('/api/orders', async (req, res) => {
try {
const order = await createOrder(req.body);
ordersCreatedTotal.inc({ status: 'success', payment_method: order.paymentMethod });
res.json({ success: true, orderId: order.id });
} catch (err) {
ordersCreatedTotal.inc({ status: 'failed', payment_method: req.body.paymentMethod || 'unknown' });
res.status(500).json({ error: err.message });
}
});
app.post('/api/login', async (req, res) => {
// ... 認証ロジック
activeUsers.inc();
res.json({ token: '...' });
});
app.post('/api/logout', (req, res) => {
activeUsers.dec();
res.json({ success: true });
});
Node.jsアプリのPrometheusスクレイプ設定
prometheus.ymlファイルを開き、既存のnode-exporterジョブと並行して新しいジョブを追加します:
scrape_configs:
- job_name: 'node-exporter'
static_configs:
- targets: ['localhost:9100']
# Node.jsアプリケーション用の新しいジョブ
- job_name: 'nodejs-app'
static_configs:
- targets: ['localhost:3000']
metrics_path: '/metrics'
scrape_interval: 15s
Prometheusの設定をリロードします(再起動不要):
curl -X POST http://localhost:9090/-/reload
GrafanaダッシュボードのPromQLクエリ
Prometheusのスクレイプが完了したら、ダッシュボードを構築します。以下の4つのパネルでAPIの健全性を全体的に把握できます:
リクエストレート(requests/秒)
sum(rate(http_requests_total[5m])) by (route, method)
P95レイテンシ——最重要メトリクス
histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, route))
P95とは95%のリクエストがX秒以内に完了することを意味します。平均よりもはるかに実態を反映します——平均は速いリクエストに引っ張られ、本当に遅いリクエストを隠してしまいます。
エラーレート(5xxエラーの割合)
rate(http_requests_total{status_code=~"5.."}[5m]) / rate(http_requests_total[5m]) * 100
ビジネスメトリクス:注文成功率
rate(orders_created_total{status="success"}[5m]) / rate(orders_created_total[5m]) * 100
Grafanaに接続する前のクイック確認
# アプリを起動
node app.js
# テストリクエストを送信
curl http://localhost:3000/api/orders
curl -X POST http://localhost:3000/api/orders \
-H "Content-Type: application/json" \
-d '{"item":"product-1","paymentMethod":"card"}'
# 生のメトリクス出力を確認
curl http://localhost:3000/metrics
このような出力が表示されれば問題ありません:
# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",route="/api/orders",status_code="200"} 3
http_requests_total{method="POST",route="/api/orders",status_code="200"} 1
# HELP http_request_duration_seconds Duration of HTTP requests in seconds
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.005",...} 2
http_request_duration_seconds_bucket{le="0.01",...} 4
実践からの注意点
- user_idをラベルに使わない:ラベルはカーディナリティが低い必要があります。
user_idやrequest_idをラベルに使うと数百万のタイムシリーズが生成され、Prometheusはすぐにメモリ不足になります。method、route、status_codeは安全です。 - /metricsエンドポイントを保護する:公開しないでください。Basic Auth、内部IPホワイトリスト、または内部Prometheusのみがアクセスできる別ポートへのバインディングを使用してください。このエンドポイントはインフラに関する多くの情報を露出します。
- scrape_intervalは15sで十分:具体的な理由がない限り5s以下に設定しないでください——アプリとPrometheus両方に不要な負荷をかけます。
- ルート正規化のテスト:ネストされたExpressルーターでは、
req.route.pathが相対パスを返す場合があります。/api/users/:idが/api/users/123と混同されないよう十分にテストしてください。
Grafanaにメトリクスが表示できたら、次のステップはアラートの設定です——P95レイテンシが500msを超えた場合や、エラーレートが5分間継続して5%を超えた場合の警告が典型例です。AlertmanagerについてはブログEに別記事があります;このダッシュボードと組み合わせることで、完全な監視サイクルが完成します。
