Node.jsへのOpenTelemetry導入ガイド:マイクロサービス向けTracing、Metrics、Logsの実装手法

Development tutorial - IT technology blog
Development tutorial - IT technology blog

背景とマイクロサービスにおいてOpenTelemetryが必要な理由

Node.jsアプリケーションがモノリス構成で動作しているうちは、エラーのデバッグやパフォーマンス計測は比較的シンプルでした。集約されたログファイルを確認するか、基本的なAPMエージェントを導入するだけでコールスタックを即座に把握できたからです。しかし、システムがDockerコンテナやKubernetesポッドで稼働する5〜10個のマイクロサービスへと分割されると、状況は一変します。

よくあるシナリオを思い浮かべてみてください。ユーザーが「決済」ボタンをクリックします。このリクエストはAPI Gatewayを経由し、JWT認証のためにUser Serviceへ送られ(15ms所要)、Order Serviceで注文を作成し、Payment Serviceを呼び出してクレジットカード決済を実行後、Kafka経由でNotification Serviceへイベントを発行します。その時、どこか1つの処理で2.5秒の遅延が発生するか、あるいは500エラーが返ってきたとします。

この場合、どこから追跡を始めればよいでしょうか?各ポッドにSSHでログインし、タイムスタンプを頼りにログファイルをgrepするのでしょうか?この方法では何時間も浪費してしまう上に、非同期プロセス間のコンテキストを見落とすリスクが極めて高くなります。

本質的な問題は、コンテキスト伝播(Context Propagation)の欠如にあります。各サービスがバラバラなフォーマットで個別にログを出力しており、リクエストの一連の処理経路全体を横断する統一されたtrace_idが存在しないのです。

システムが肥大化して数十のサービスが毎秒2,000〜5,000リクエストを処理するようになると、統合された監視ソリューションがなければ根本原因(Root Cause)の特定は絶望的になります。そこで業界標準として台頭したのがOpenTelemetry(OTel)です。CNCF(Cloud Native Computing Foundation)がホストするOTelは、オブザーバビリティ(Observability)の3大ピラーであるTraces、Metrics、Logsを包括的に収集するための統一APIおよびSDKを提供します。最大の利点は、特定のベンダーロックインを気にすることなく、バックエンド(Jaeger、Grafana Tempo、Datadogなど)を自由に変更できる点です。

Node.js向けOpenTelemetryパッケージのインストール

Node.js向けのOpenTelemetryエコシステムは高度にモジュール化されており、非常に柔軟です。Expressの各ルートやPostgreSQLのクエリごとに手動で計測コードを差し込む必要はありません。auto-instrumentationパッケージが、Express、Fastify、HTTP、pg、Redis、Mongooseといった主要ライブラリに自動で「フック」してくれます。

Node.jsプロジェクトのルートディレクトリでターミナルを開き、コアライブラリと標準のOTLP(OpenTelemetry Protocol)Exporterをインストールします。

npm install @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http \
  @opentelemetry/exporter-metrics-otlp-http \
  @opentelemetry/resources \
  @opentelemetry/semantic-conventions

各パッケージの具体的な役割は以下の通りです。

  • @opentelemetry/sdk-node: Node.jsプロセス内のデータ収集ライフサイクルと設定を管理するメインSDK。
  • @opentelemetry/auto-instrumentations-node: アプリケーションのビジネスロジックを変更することなく、主要ライブラリのトレースを自動収集するプラグインバンドル。
  • @opentelemetry/exporter-*-otlp-http: HTTPプロトコルを介してトレースおよびメトリクスデータをOTel Collectorやストレージバックエンドに転送するモジュール。
  • @opentelemetry/resources & semantic-conventions: サービス名、バージョン、実行環境(production/staging)などのサービス識別メタデータを付与するモジュール。

詳細な設定手順:Tracing、Metrics、Logs

ここで厳守すべき鉄則があります。それは、トレースの初期化ファイルはアプリケーションコード全体の実行前にロードされなければならないということです。SDKがメモリに展開される前にExpressやMongooseをrequireしてしまうと、モンキーパッチによる自動計装が正しく機能しません。

1. Distributed Tracingのセットアップ(tracer.js)

プロジェクトのルートディレクトリにtracer.jsを作成します。

// tracer.js
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 { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } = require('@opentelemetry/semantic-conventions');

const traceExporter = new OTLPTraceExporter({
  url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
});

const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: process.env.SERVICE_NAME || 'order-service',
    [ATTR_SERVICE_VERSION]: '1.0.0',
  }),
  traceExporter,
  instrumentations: [
    getNodeAutoInstrumentations({
      // 不要なスパムスパンの発生を抑え、RAM使用量を節約するためにfsの計装を無効化
      '@opentelemetry/instrumentation-fs': { enabled: false },
    }),
  ],
});

sdk.start();

// コンテナやプロセスの停止時にグレースフルシャットダウンを実行
process.on('SIGTERM', () => {
  sdk.shutdown()
    .then(() => console.log('OpenTelemetry SDKは正常にシャットダウンしました'))
    .catch((err) => console.error('OTel SDKの停止中にエラーが発生しました', err))
    .finally(() => process.exit(0));
});

2. システムパフォーマンス計測用Metricsの追加

Tracesが個々のリクエストの詳細を掘り下げるのに対し、Metricsはスループット(RPS)、5xxエラー率、P95/P99レイテンシといったシステム全体の健全性を俯瞰するために用います。設定は、NodeSDKのオプションにMetricReaderを追加するだけです。

const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http');

const metricExporter = new OTLPMetricExporter({
  url: process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT || 'http://localhost:4318/v1/metrics',
});

const metricReader = new PeriodicExportingMetricReader({
  exporter: metricExporter,
  exportIntervalMillis: 15000, // 15秒ごとに定期的にデータを送信
});

// tracer.js内のNodeSDKのオプションにmetricReaderを追加:
// const sdk = new NodeSDK({
//   ...
//   metricReader,
// });

注文完了数のカウントといった独自のビジネスメトリクスを計測する場合は、Metrics APIを直接呼び出します。

const { metrics } = require('@opentelemetry/api');

const meter = metrics.getMeter('order-meter');
const orderCounter = meter.createCounter('orders_completed_total', {
  description: '完了した注文の合計数',
});

// 注文処理が成功した際にカウンタをインクリメント
orderCounter.add(1, { payment_method: 'credit_card', status: 'success' });

3. Trace IDと同期したログの標準化

データを相互連携させる最大のメリットは、トレースとログの相関(Trace-Log Correlation)にあります。エラーログを確認する際、ログに含まれるtrace_idをコピーしてGrafanaやJaegerに貼り付けるだけで、該当リクエストのウォーターフォールチャート全体を瞬時に可視化できます。

ロギングライブラリのwinstonを使用し、各ログ行に現在のアクティブなスパンコンテキストを埋め込みます。

// logger.js
const { trace, context } = require('@opentelemetry/api');
const winston = require('winston');

const otelFormat = winston.format((info) => {
  const currentSpan = trace.getSpan(context.active());
  if (currentSpan) {
    const spanContext = currentSpan.spanContext();
    info.trace_id = spanContext.traceId;
    info.span_id = spanContext.spanId;
  }
  return info;
});

const logger = winston.createLogger({
  format: winston.format.combine(
    otelFormat(),
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [new winston.transports.Console()],
});

module.exports = logger;

ターミナルに出力されるJSON構造には、以下のように統合されたトレース識別子が含まれます。

{
  "level": "info",
  "message": "注文が正常に作成されました",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "timestamp": "2026-10-30T10:15:30.123Z"
}

動作検証と実践的なモニタリング

ローカル環境で最も手軽に動作検証を行うには、Docker Composeを使用してJaeger all-in-oneコンテナを起動します。

docker-compose.ymlを作成します。

version: '3.8'
services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686" # Jaeger UI Webインターフェース
      - "4318:4318"   # OTLP HTTPレシーバーポート

Jaegerコンテナを起動します。

docker compose up -d

OTel設定ファイルを最優先でロードするため、--requireフラグを指定してNode.jsアプリケーションを起動します。

node --require ./tracer.js server.js

cURLやPostmanからテストリクエストをいくつか送信し、ブラウザでhttp://localhost:16686にアクセスします。

  1. Serviceドロップダウンからorder-serviceを選択します。
  2. Find Tracesをクリックし、記録されたリクエスト一覧を読み込みます。
  3. トレースをクリックすると、各ミドルウェアのレイテンシ、データベースクエリの待機時間、外部へのHTTP呼び出しの詳細がJaeger上に表示されます。

毎秒数千リクエスト規模の高トラフィックな本番環境では、TraceIdRatioBasedSamplerを使用してサンプリング(例:リクエストの10%を抽出)を有効化することを推奨します。これにより、CPUオーバーヘッドを2%未満に抑えつつ、ネットワーク帯域の逼迫を防ぎ、バックエンドのストレージコストを削減できます。

Share: