OpenTelemetry実践 - 分散トレーシングとメトリクス計装

中級 | 15分 で読める | 2026.04.24

公式ドキュメント

この記事の要点

OpenTelemetryでトレース・メトリクス・ログを統合的に計装
• 自動計装と手動計装を組み合わせてアプリケーションの可視性を向上
Collectorで複数バックエンド(Jaeger、Tempo、Prometheus)にエクスポート

OpenTelemetryとは

OpenTelemetry(OTel)は、ベンダー中立の観測性フレームワークです。トレース・メトリクス・ログを統一的な方法で計装・収集・エクスポートできます。

主要コンポーネント

コンポーネント役割
API計装コードが使うインターフェース
SDKAPI実装とデータ処理
Instrumentation自動計装ライブラリ(HTTP、DB、フレームワーク)
Collectorデータ収集・変換・エクスポート
Exportersバックエンド(Jaeger、Tempo、Prometheus)への送信

ポイント: OpenTelemetryは計装レイヤーに特化し、ストレージやUIは外部バックエンドに任せます。

自動計装(Node.js)

# 依存関係のインストール
npm install @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http \
  @opentelemetry/exporter-metrics-otlp-http
// tracing.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'my-app',
    [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
    [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development',
  }),
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
  }),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter({
      url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/metrics',
    }),
    exportIntervalMillis: 60000, // 1分ごと
  }),
  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-http': {
        enabled: true,
      },
      '@opentelemetry/instrumentation-express': {
        enabled: true,
      },
      '@opentelemetry/instrumentation-pg': {
        enabled: true,
      },
      '@opentelemetry/instrumentation-redis': {
        enabled: true,
      },
    }),
  ],
});

sdk.start();

// シャットダウン処理
process.on('SIGTERM', () => {
  sdk
    .shutdown()
    .then(() => console.log('Tracing terminated'))
    .catch((error) => console.error('Error terminating tracing', error))
    .finally(() => process.exit(0));
});

export default sdk;
// app.ts
import './tracing'; // 最初にインポート
import express from 'express';

const app = express();

app.get('/api/users/:id', async (req, res) => {
  const user = await fetchUser(req.params.id);
  res.json(user);
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

実践メモ: 自動計装は、HTTP・Express・PostgreSQL・Redisなどのライブラリを自動的にトレースします。

手動計装(Node.js)

// instrumentation.ts
import { trace, context, SpanStatusCode } from '@opentelemetry/api';
import { metrics } from '@opentelemetry/api';

// Tracerの取得
const tracer = trace.getTracer('my-app', '1.0.0');

// Meterの取得
const meter = metrics.getMeter('my-app', '1.0.0');

// カスタムメトリクス
const requestCounter = meter.createCounter('http_requests_total', {
  description: 'Total number of HTTP requests',
});

const requestDuration = meter.createHistogram('http_request_duration_seconds', {
  description: 'HTTP request duration',
  unit: 'seconds',
});

export async function fetchUser(userId: string) {
  // Spanの作成
  return tracer.startActiveSpan('fetchUser', async (span) => {
    try {
      span.setAttributes({
        'user.id': userId,
        'db.system': 'postgresql',
      });

      const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);

      if (!user) {
        span.setStatus({ code: SpanStatusCode.ERROR, message: 'User not found' });
        span.recordException(new Error('User not found'));
        throw new Error('User not found');
      }

      span.setStatus({ code: SpanStatusCode.OK });
      return user;
    } catch (error) {
      span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
      span.recordException(error);
      throw error;
    } finally {
      span.end();
    }
  });
}

export function recordHttpRequest(method: string, path: string, statusCode: number, duration: number) {
  // メトリクスの記録
  requestCounter.add(1, {
    method,
    path,
    status: statusCode.toString(),
  });

  requestDuration.record(duration / 1000, {
    method,
    path,
    status: statusCode.toString(),
  });
}
// middleware.ts
import { trace, context } from '@opentelemetry/api';
import { recordHttpRequest } from './instrumentation';

export function tracingMiddleware(req, res, next) {
  const tracer = trace.getTracer('my-app');
  const span = tracer.startSpan(`${req.method} ${req.path}`);

  const start = Date.now();

  // レスポンス完了時
  res.on('finish', () => {
    const duration = Date.now() - start;
    
    span.setAttributes({
      'http.method': req.method,
      'http.url': req.url,
      'http.status_code': res.statusCode,
      'http.route': req.route?.path,
    });

    span.setStatus({ 
      code: res.statusCode >= 500 ? SpanStatusCode.ERROR : SpanStatusCode.OK 
    });

    recordHttpRequest(req.method, req.path, res.statusCode, duration);
    span.end();
  });

  // Contextを伝播
  context.with(trace.setSpan(context.active(), span), () => {
    next();
  });
}

注意: span.end()を必ず呼び出してください。呼び忘れるとメモリリークが発生します。

Python自動計装

# インストール
pip install opentelemetry-distro \
  opentelemetry-exporter-otlp \
  opentelemetry-instrumentation-flask \
  opentelemetry-instrumentation-requests \
  opentelemetry-instrumentation-sqlalchemy

# 自動計装ライブラリのインストール
opentelemetry-bootstrap -a install
# app.py
from flask import Flask
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor

# リソースの設定
resource = Resource.create({
    "service.name": "my-python-app",
    "service.version": "1.0.0",
    "deployment.environment": "production",
})

# TracerProviderの設定
provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(
    OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")
)
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)

# Flaskアプリ
app = Flask(__name__)

# 自動計装
FlaskInstrumentor().instrument_app(app)
RequestsInstrumentor().instrument()

@app.route('/api/data')
def get_data():
    tracer = trace.get_tracer(__name__)
    
    with tracer.start_as_current_span("process_data") as span:
        span.set_attribute("user.id", "123")
        # ビジネスロジック
        data = {"result": "success"}
        return data

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
# 環境変数で起動
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_SERVICE_NAME=my-python-app

opentelemetry-instrument python app.py

OpenTelemetry Collector

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 10s
    send_batch_size: 1024
  
  memory_limiter:
    check_interval: 1s
    limit_mib: 512
  
  resource:
    attributes:
      - key: cluster.name
        value: production
        action: insert

exporters:
  # Jaegerへのエクスポート
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true
  
  # Tempoへのエクスポート
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true
  
  # Prometheusへのエクスポート
  prometheus:
    endpoint: 0.0.0.0:8889
  
  # ログ出力(デバッグ用)
  logging:
    loglevel: debug

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch, resource]
      exporters: [otlp/jaeger, otlp/tempo, logging]
    
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch, resource]
      exporters: [prometheus, logging]
# docker-compose.yml
services:
  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.96.0
    command: ["--config=/etc/otel-collector-config.yaml"]
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP
      - "8889:8889"   # Prometheus metrics
    networks:
      - monitoring

  jaeger:
    image: jaegertracing/all-in-one:1.55
    ports:
      - "16686:16686"  # Jaeger UI
      - "4317:4317"    # OTLP gRPC
    environment:
      - COLLECTOR_OTLP_ENABLED=true
    networks:
      - monitoring

  tempo:
    image: grafana/tempo:2.4.0
    command: ["-config.file=/etc/tempo.yaml"]
    volumes:
      - ./tempo.yaml:/etc/tempo.yaml
      - tempo-data:/tmp/tempo
    ports:
      - "3200:3200"   # Tempo
      - "4317"        # OTLP gRPC
    networks:
      - monitoring

networks:
  monitoring:

volumes:
  tempo-data:

ポイント: Collectorを使うと、アプリケーションはCollectorにのみ送信し、複数バックエンドへの配信はCollectorが担当します。

Context Propagation(コンテキスト伝播)

// frontend.ts (Next.js)
import { trace, context, propagation } from '@opentelemetry/api';

export async function fetchData() {
  const tracer = trace.getTracer('frontend');
  
  return tracer.startActiveSpan('fetchData', async (span) => {
    const headers = {};
    
    // トレースコンテキストをHTTPヘッダーに注入
    propagation.inject(context.active(), headers);
    
    const response = await fetch('http://backend:3000/api/data', {
      headers,
    });
    
    const data = await response.json();
    span.end();
    return data;
  });
}
// backend.ts (Express)
import { propagation, context, trace } from '@opentelemetry/api';

app.use((req, res, next) => {
  // HTTPヘッダーからトレースコンテキストを抽出
  const extractedContext = propagation.extract(context.active(), req.headers);
  
  context.with(extractedContext, () => {
    const tracer = trace.getTracer('backend');
    const span = tracer.startSpan('handleRequest');
    
    // スパン情報をリクエストに保存
    req.span = span;
    
    res.on('finish', () => {
      span.end();
    });
    
    next();
  });
});

実践メモ: W3C Trace Contextがデフォルトの伝播フォーマットです。フロントエンド→バックエンド→データベースまで一貫してトレースできます。

Semantic Conventions

// セマンティック属性の使用
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';

span.setAttributes({
  // HTTP
  [SemanticAttributes.HTTP_METHOD]: 'GET',
  [SemanticAttributes.HTTP_URL]: 'https://api.example.com/users',
  [SemanticAttributes.HTTP_STATUS_CODE]: 200,
  
  // データベース
  [SemanticAttributes.DB_SYSTEM]: 'postgresql',
  [SemanticAttributes.DB_NAME]: 'myapp',
  [SemanticAttributes.DB_STATEMENT]: 'SELECT * FROM users WHERE id = $1',
  [SemanticAttributes.DB_USER]: 'app_user',
  
  // RPC
  [SemanticAttributes.RPC_SERVICE]: 'UserService',
  [SemanticAttributes.RPC_METHOD]: 'GetUser',
  
  // メッセージング
  [SemanticAttributes.MESSAGING_SYSTEM]: 'kafka',
  [SemanticAttributes.MESSAGING_DESTINATION]: 'user-events',
  [SemanticAttributes.MESSAGING_OPERATION]: 'publish',
});

注意: Semantic Conventionsに従うと、異なるサービス間でも一貫した属性名が使われ、クエリが簡単になります。

ログとトレースの統合

// logger.ts
import pino from 'pino';
import { trace, context } from '@opentelemetry/api';

const logger = pino({
  mixin() {
    const span = trace.getSpan(context.active());
    if (!span) return {};
    
    const spanContext = span.spanContext();
    return {
      trace_id: spanContext.traceId,
      span_id: spanContext.spanId,
      trace_flags: spanContext.traceFlags,
    };
  },
});

export default logger;
// 使用例
import logger from './logger';

app.get('/api/users/:id', async (req, res) => {
  logger.info({ userId: req.params.id }, 'Fetching user');
  
  try {
    const user = await fetchUser(req.params.id);
    res.json(user);
  } catch (error) {
    logger.error({ error, userId: req.params.id }, 'Failed to fetch user');
    res.status(500).json({ error: 'Internal Server Error' });
  }
});

カスタムメトリクス

// metrics.ts
import { metrics } from '@opentelemetry/api';

const meter = metrics.getMeter('my-app', '1.0.0');

// Counter(単調増加)
export const httpRequestsTotal = meter.createCounter('http_requests_total', {
  description: 'Total number of HTTP requests',
});

// Histogram(分布)
export const httpRequestDuration = meter.createHistogram('http_request_duration_seconds', {
  description: 'HTTP request duration in seconds',
  unit: 'seconds',
});

// UpDownCounter(増減可能)
export const activeConnections = meter.createUpDownCounter('active_connections', {
  description: 'Number of active connections',
});

// Gauge(非同期観測)
meter.createObservableGauge('process_memory_usage_bytes', {
  description: 'Process memory usage',
  unit: 'bytes',
}).addCallback((observableResult) => {
  const memUsage = process.memoryUsage();
  observableResult.observe(memUsage.heapUsed, { type: 'heap' });
  observableResult.observe(memUsage.rss, { type: 'rss' });
});
// 使用例
import { httpRequestsTotal, httpRequestDuration } from './metrics';

app.use((req, res, next) => {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    
    httpRequestsTotal.add(1, {
      method: req.method,
      path: req.route?.path || req.path,
      status: res.statusCode.toString(),
    });
    
    httpRequestDuration.record(duration, {
      method: req.method,
      path: req.route?.path || req.path,
    });
  });
  
  next();
});

サンプリング設定

// sampling.ts
import { ParentBasedSampler, TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-base';

// 10%のトレースをサンプリング
const sampler = new ParentBasedSampler({
  root: new TraceIdRatioBasedSampler(0.1),
});

const sdk = new NodeSDK({
  sampler,
  // その他の設定
});
// カスタムサンプラー
import { Sampler, SamplingDecision } from '@opentelemetry/sdk-trace-base';

class ErrorSampler implements Sampler {
  shouldSample(context, traceId, spanName, spanKind, attributes) {
    // エラーの場合は必ずサンプリング
    if (attributes['http.status_code'] >= 500) {
      return { decision: SamplingDecision.RECORD_AND_SAMPLED };
    }
    
    // それ以外は10%
    return Math.random() < 0.1
      ? { decision: SamplingDecision.RECORD_AND_SAMPLED }
      : { decision: SamplingDecision.NOT_RECORD };
  }
}

ポイント: 本番環境ではサンプリングを使ってデータ量とコストを削減します。エラーは常にサンプリングするのがベストプラクティスです。

関連記事

この技術を体系的に学びたいですか?

未来学では東証プライム上場企業のITエンジニアが24時間サポート。月額24,800円から、退会金0円のオンラインIT塾です。

メールで無料相談する
← 一覧に戻る