prom-clientでNode.jsアプリを監視する:リクエストレート、レイテンシ、カスタムビジネスメトリクスをリアルタイム追跡

Monitoring tutorial - IT technology blog
Monitoring tutorial - IT technology blog

夜中の2時にAPIが遅いという電話を受けて、原因がわからなかったことはありませんか?自分も経験があります——各サーバーにSSHしてログを確認し、topnetstatを実行しても問題が見つからない。prom-client + Grafanaを導入してから、状況が一変しました。ダッシュボードを開くだけで、リクエストレート、遅いエンドポイント、エラーレートがすぐに把握できます。

ブログにはPrometheus + GrafanaでサーバーをモニタリングするCPU、RAM、ディスクの記事があります。この記事はさらに一段深く掘り下げます:アプリケーションレベルの監視——つまりNode.jsのコード自体が何をしているか、どのエンドポイントがボトルネックになっているか、ビジネスロジックが正しく動いているかを確認します。

Node.jsの監視方法3選——選ぶ前に比較

prom-clientが常に正解とは限りません。以下の3つのアプローチはそれぞれ存在する理由があります。

方法1:ログのみ使用+手動分析

リクエスト/レスポンスのログをファイルに書き出し、grepawk、または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_idrequest_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に別記事があります;このダッシュボードと組み合わせることで、完全な監視サイクルが完成します。

Share: