NodeJS 速習チュートリアル

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

1. オブザーバビリティ(Observability)の導入

Node.js アプリケーションにおけるオブザーバビリティ(可観測性)とは、メトリクスやログを収集・分析することで、システムの挙動を深く理解することを指します。

オブザーバビリティの 3 つの柱: メトリクス(Metrics)、ログ(Logs)、トレース(Traces)は、システムの健全性とパフォーマンスについて、それぞれ異なる、かつ補完的な視点を提供します。

2. アプリケーションメトリクスの収集

2.1 Prometheus クライアントの使用

Prometheus は、時系列データを収集するための標準的なオープンソースツールです。

2.2 基本的なメトリクス収集

const express = require('express');
const client = require('prom-client');

// メトリクスを登録するためのレジストリを作成
const register = new client.Registry();

// すべてのメトリクスに付与されるデフォルトラベルを追加
register.setDefaultLabels({
  app: 'nodejs-monitoring-demo'
});

// デフォルトのメトリクス(CPU, メモリ等)の収集を有効化
client.collectDefaultMetrics({ register });

// カスタムメトリクス(ヒストグラム)の作成
const httpRequestDurationMicroseconds = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP リクエストの所要時間(秒)',
  labelNames: ['method', 'route', 'code'],
  buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10] // レスポンスタイムのバケット設定
});

const app = express();

// リクエスト時間を追跡するためのカスタムミドルウェア
app.use((req, res, next) => {
  const end = httpRequestDurationMicroseconds.startTimer();
  res.on('finish', () => {
    end({ method: req.method, route: req.path, code: res.statusCode });
  });
  next();
});

// メトリクス公開用エンドポイントの露出
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

// サンプルルート
app.get('/', (req, res) => {
  res.send('こんにちは、オブザーバビリティ!');
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`サーバーがポート ${PORT} で起動しました`);
});

3. 監視すべき主要なメトリクス

3.1 システムメトリクス

  • CPU 使用率 (CPU Usage)
  • メモリ使用率 (Heap & RSS)
  • イベントループの遅延 (Event Loop Lag)
  • ガベージコレクション (Garbage Collection)
  • アクティブなハンドル/リクエスト数 (Active Handles/Requests)

3.2 アプリケーションメトリクス

  • リクエストレートと所要時間 (Request Rate & Duration)
  • エラー率 (Error Rates)
  • データベースクエリのパフォーマンス (Database Query Performance)
  • キャッシュのヒット/ミス率 (Cache Hit/Miss Ratios)
  • キューの長さ (Queue Lengths)

4. 分散トレーシング (Distributed Tracing)

分散トレーシングは、マイクロサービスアーキテクチャにおいて、複数のサービスをまたいで流れるリクエストを追跡するのに役立ちます。

4.1 OpenTelemetry のセットアップ

// 必要なパッケージをインストール
// npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-http
// npm install @opentelemetry/exporter-trace-otlp-http

const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');

const sdk = new NodeSDK({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'my-service',
    [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
  }),
  traceExporter: new OTLPTraceExporter({
    url: 'http://collector:4318/v1/traces',
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start()
  .then(() => console.log('トレーシングが初期化されました'))
  .catch((error) => console.log('トレーシングの初期化エラー', error));

5. ロギングのベストプラクティス

5.1 Pino による構造化ロギング

const pino = require('pino');
const express = require('express');
const pinoHttp = require('pino-http');

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => ({ level: label.toUpperCase() }),
  },
});

const app = express();

// HTTP リクエストロギングミドルウェア
app.use(pinoHttp({
  logger,
  customLogLevel: function (res, err) {
    if (res.statusCode >= 400 && res.statusCode < 500) {
      return 'warn';
    } else if (res.statusCode >= 500 || err) {
      return 'error';
    }
    return 'info';
  },
}));

app.get('/', (req, res) => {
  req.log.info('リクエストを処理中');
  res.json({ status: 'ok' });
});

app.listen(3000, () => {
  logger.info('サーバーがポート 3000 で起動しました');
});

5.2 ログのエンリッチメント(コンテキストの付与)

// ログにコンテキストを追加
app.use((req, res, next) => {
  const childLogger = logger.child({
    requestId: req.id,
    userId: req.user?.id || 'anonymous',
    path: req.path,
    method: req.method
  });
  req.log = childLogger;
  next();
});

6. アラート設定と可視化

6.1 Grafana ダッシュボードの例

Grafana ダッシュボードでメトリクスを可視化します。一般的なメトリクスのクエリ例は以下の通りです:

Node.js メモリ使用量 (RSS, MB 単位)

process_resident_memory_bytes{job="nodejs"} / 1024 / 1024

リクエスト所要時間 (p99, ミリ秒単位)

histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le)) * 1000

エラー率

sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))

6.2 アラートルール (Prometheus)

groups:
- name: nodejs
  rules:
  - alert: HighErrorRate
    expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.05
    for: 10m
    labels:
      severity: critical
    annotations:
      summary: "インスタンス {{ $labels.instance }} で高いエラー率を検出"

7. プロダクション環境用モニタリングツール

7.1 オープンソース (Open Source)

  • Prometheus + Grafana: 定番の組み合わせ
  • Elasticsearch + Fluentd + Kibana (EFK): ログ管理スタック
  • Jaeger: 分散トレーシング
  • Loki: 軽量なログ集約システム

7.2 商用サービス (Commercial)

  • Datadog: 包括的なモニタリングプラットフォーム
  • New Relic: アプリケーションパフォーマンス管理 (APM)
  • Dynatrace: AI 駆動のモニタリング
  • AppDynamics: エンタープライズ向け APM

7.3 クラウドネイティブ (Cloud Native)

  • AWS CloudWatch
  • Google Cloud Operations (Stackdriver)
  • Azure Monitor
  • OpenTelemetry Collector

8. ベストプラクティス

8.1 推奨事項 (Do's)

  • 一貫したフォーマットの構造化ロギングを使用する
  • システムメトリクスとアプリケーションメトリクスの両方を監視する
  • SLO (Service Level Objectives) に基づいたアラートを設定する
  • マイクロサービスには分散トレーシングを導入する

8.2 禁止事項 (Don'ts)

  • 機密情報をログに記録しない
  • メトリクスに高カーディナリティ (High-cardinality) なラベルを使用しない
  • デバッグをログだけに頼らない
  • アラート疲れ (Alert Fatigue) を避ける。対応が必要なアラートに集中する

9. まとめ

モダンな Node.js デプロイメントにおいて、オブザーバビリティは単なる「監視」以上の意味を持ちます。

  • メトリクスで「何が起きているか」を知る
  • ログで「なぜ起きたか」を詳しく調査する
  • 分散トレーシングで「どこで問題が発生したか」を特定する

これらを適切に組み合わせることで、システムのレジリエンス(回復力)を高め、ユーザーに対して高品質なサービスを提供し続けることが可能になります。