Agent-almanac instrument-distributed-tracing
git clone https://github.com/pjt222/agent-almanac
T=$(mktemp -d) && git clone --depth=1 https://github.com/pjt222/agent-almanac "$T" && mkdir -p ~/.claude/skills && cp -r "$T/i18n/ja/skills/instrument-distributed-tracing" ~/.claude/skills/pjt222-agent-almanac-instrument-distributed-tracing-d4153a && rm -rf "$T"
i18n/ja/skills/instrument-distributed-tracing/SKILL.md分散トレーシングのインストルメント
OpenTelemetryの分散トレーシングを実装して、マイクロサービス間のリクエストを追跡し、パフォーマンスのボトルネックを特定する。
使用タイミング
- 複数のサービスを持つ分散システムのレイテンシ問題をデバッグする場合
- マイクロサービス間のリクエストフローと依存関係を把握する場合
- トランザクション内の遅いデータベースクエリや外部API呼び出しを特定する場合
- 根本原因分析のためにトレースをログとメトリクスと相関させる場合
- ユーザーリクエストからレスポンスまでのエンドツーエンドのレイテンシを測定する場合
- レガシーのトレーシングシステム(Zipkin、Jaeger)からOpenTelemetryに移行する場合
- 詳細なレイテンシパーセンタイル追跡によるSLOコンプライアンスを確立する場合
入力
- 必須: インストルメントするサービスのリスト(言語とフレームワーク)
- 必須: トレーシングバックエンドの選択(Jaeger、Tempo、Zipkin、またはベンダーSaaS)
- 任意: 既存のインストルメンテーションライブラリ(OpenTracing、Zipkin)
- 任意: サンプリング戦略の要件(パーセンテージ、レート制限)
- 任意: ビジネス固有のメタデータ用のカスタムスパン属性
手順
完全な設定ファイルとテンプレートは拡張例を参照。
ステップ1: トレーシングバックエンドのセットアップ
JaegerまたはGrafana Tempoをデプロイしてトレースを受信・保存する。
オプションA: Jaegerオールインワン(開発/テスト):
# docker-compose.yml version: '3.8' services: jaeger: image: jaegertracing/all-in-one:1.51 ports: - "5775:5775/udp" # Zipkin compact thrift - "6831:6831/udp" # Jaeger compact thrift - "6832:6832/udp" # Jaeger binary thrift - "5778:5778" # Serve configs - "16686:16686" # Jaeger UI - "14268:14268" # Jaeger HTTP thrift - "14250:14250" # Jaeger GRPC - "9411:9411" # Zipkin compatible endpoint environment: - COLLECTOR_ZIPKIN_HOST_PORT=:9411 - COLLECTOR_OTLP_ENABLED=true restart: unless-stopped
オプションB: Grafana Tempo(本番、スケーラブル):
# docker-compose.yml version: '3.8' services: tempo: image: grafana/tempo:2.3.0 command: ["-config.file=/etc/tempo.yaml"] volumes: - ./tempo.yaml:/etc/tempo.yaml - tempo-data:/tmp/tempo ports: - "3200:3200" # Tempo HTTP - "4317:4317" # OTLP gRPC - "4318:4318" # OTLP HTTP - "9411:9411" # Zipkin restart: unless-stopped volumes: tempo-data:
Tempo設定(
tempo.yaml):
server: http_listen_port: 3200 distributor: receivers: jaeger: # ... (完全な設定はEXAMPLES.mdを参照)
S3ストレージを使用した本番環境:
storage: trace: backend: s3 s3: bucket: tempo-traces endpoint: s3.amazonaws.com region: us-east-1 wal: path: /tmp/tempo/wal pool: max_workers: 100 queue_depth: 10000
期待結果: トレーシングバックエンドがアクセス可能で、OTLPを介してトレースを受信する準備ができており、JaegerのUIまたはGrafanaが最初は「トレースなし」を表示する。
失敗時:
- ポートが使用中でないことを確認する:
netstat -tulpn | grep -E '(4317|16686|3200)' - コンテナのログを確認する:
またはdocker logs jaegerdocker logs tempo - OTLPエンドポイントをテストする:
curl http://localhost:4318/v1/traces -v - Tempoの場合:
で設定構文を検証するtempo -config.file=/etc/tempo.yaml -verify-config
ステップ2: アプリケーションのインストルメント(自動インストルメンテーション)
コード変更を最小限に抑えるために、一般的なフレームワーク向けのOpenTelemetry自動インストルメンテーションを使用する。
PythonとFlask:
pip install opentelemetry-distro opentelemetry-exporter-otlp 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.grpc.trace_exporter import OTLPSpanExporter # ... (完全な設定はEXAMPLES.mdを参照)
GoとGinフレームワーク:
go get go.opentelemetry.io/otel go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc go get go.opentelemetry.io/otel/sdk/trace go get go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin
package main import ( "context" "github.com/gin-gonic/gin" "go.opentelemetry.io/otel" # ... (完全な設定はEXAMPLES.mdを参照)
Node.jsとExpress:
npm install @opentelemetry/api \ @opentelemetry/sdk-node \ @opentelemetry/auto-instrumentations-node \ @opentelemetry/exporter-trace-otlp-grpc
// tracing.js const { NodeSDK } = require('@opentelemetry/sdk-node'); const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc'); const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); const { Resource } = require('@opentelemetry/resources'); const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions'); # ... (完全な設定はEXAMPLES.mdを参照)
期待結果: インストルメントされたサービスからのトレースがJaegerのUIまたはGrafanaに表示され、HTTPリクエストが自動的にスパンを作成する。
失敗時:
- エクスポーターのエンドポイントがアプリケーションから到達可能であることを確認する
- 環境変数を確認する:
OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4317 - デバッグログを有効にする:
(Python)、OTEL_LOG_LEVEL=debug
(Node.js)OTEL_LOG_LEVEL=DEBUG - 単純なスパンでテストする:手動でスパンを作成してエクスポートパイプラインを確認する
- OpenTelemetryパッケージ間のバージョン競合を確認する
ステップ3: 手動インストルメンテーションの追加
ビジネスロジック、データベースクエリ、外部呼び出し用のカスタムスパンを作成する。
Pythonの手動スパン:
from opentelemetry import trace tracer = trace.get_tracer(__name__) def process_order(order_id): # Create a span for the entire operation # ... (完全な設定はEXAMPLES.mdを参照)
Goの手動スパン:
import ( "context" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" # ... (完全な設定はEXAMPLES.mdを参照)
スパン属性のベストプラクティス:
- セマンティック規約を使用する:
、http.method
、http.status_code
、db.systemdb.statement - ビジネスコンテキストを追加する:
、user.id
、order.idproduct.category - リソース識別子を含める:
、instance.id
、regionavailability_zone - エラーを記録する:
とspan.RecordError(err)span.SetStatus(codes.Error, message) - 重要なマイルストーンにイベントを追加する:
span.AddEvent("cache_miss")
期待結果: カスタムスパンがトレースビューに表示され、親子関係が正しく、属性がスパン詳細に表示され、エラーが強調表示される。
失敗時:
- コンテキスト伝播を確認する:親スパンのコンテキストが子に渡されること
- スパン名が説明的で命名規則に従っていることを確認する
- スパンが終了していることを確認する(GoでのGo
、Pythonでのdefer span.End()
ブロック)with - 属性タイプを確認する:文字列、int、bool、floatのみ
- セマンティック規約を検証する:該当する場合は標準的な属性名を使用する
ステップ4: コンテキスト伝播の実装
サービス境界と非同期操作を越えてトレースコンテキストが流れることを確認する。
HTTP headers伝播(W3Cトレースコンテキスト):
# Client side (Python with requests) import requests from opentelemetry import trace from opentelemetry.propagate import inject tracer = trace.get_tracer(__name__) # ... (完全な設定はEXAMPLES.mdを参照)
// Server side (Go with Gin) import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/propagation" ) # ... (完全な設定はEXAMPLES.mdを参照)
メッセージキューの伝播(Kafka):
# Producer from opentelemetry.propagate import inject from kafka import KafkaProducer producer = KafkaProducer(bootstrap_servers=['kafka:9092']) # ... (完全な設定はEXAMPLES.mdを参照)
# Consumer from opentelemetry.propagate import extract def process_message(msg): # Extract trace context from Kafka headers headers = {k: v.decode('utf-8') for k, v in msg.headers} ctx = extract(headers) # Continue the trace with tracer.start_as_current_span("process_order_event", context=ctx): order_id = json.loads(msg.value)['order_id'] handle_order(order_id)
非同期操作(Python asyncio):
import asyncio from opentelemetry import trace, context async def async_operation(): # Capture current context token = context.attach(context.get_current()) try: with tracer.start_as_current_span("async_database_query"): await asyncio.sleep(0.1) # Simulated async work return "result" finally: context.detach(token)
期待結果: トレースが複数のサービスにまたがり、サービス境界を越えてトレースIDが一貫し、親子関係が保持される。
失敗時:
- W3Cトレースコンテキストプロパゲーターが設定されていることを確認する:
otel.propagation.set_global_textmap(TraceContextTextMapPropagator()) - HTTPリクエストにヘッダーが渡されていることを確認する
- Kafkaの場合: ブローカーバージョンがヘッダーをサポートしていることを確認する(v0.11以上)
- ヘッダーインスペクションでデバッグする:
ヘッダーの値をログに記録するtraceparent - トレースの可視化を使用して切断されたトレースリンクを特定する
ステップ5: サンプリング戦略の設定
可視性を維持しながらトレース量とコストを削減するためにサンプリングを実装する。
サンプリング戦略:
from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.sampling import ( ParentBased, TraceIdRatioBased, StaticSampler, Decision # ... (完全な設定はEXAMPLES.mdを参照)
Tempoを使用したテールベースサンプリング:
tempo.yamlでの設定:
overrides: defaults: metrics_generator: processors: [service-graphs, span-metrics] storage: path: /tmp/tempo/generator/wal remote_write: - url: http://prometheus:9090/api/v1/write send_exemplars: true # Tail sampling (requires tempo-query) ingestion_rate_limit_bytes: 5000000 ingestion_burst_size_bytes: 10000000
動的サンプリングにはGrafana TempoのTraceQLを使用する:
# Sample traces with errors { status = error } # Sample slow traces (>1s) { duration > 1s } # Sample specific services { resource.service.name = "checkout-service" }
期待結果: トレース量がターゲットのパーセンテージに削減され、エラートレースが常にサンプリングされ、サンプリング決定がトレースメタデータに表示される。
失敗時:
- サンプラーがトレーサープロバイダーの初期化前に適用されていることを確認する
- エクスポートされたスパンのサンプリング決定属性を確認する
- テールサンプリングの場合: 十分なバッファリングを確認する(
)ingestion_burst_size_bytes - ドロップされたトレースを監視する:
メトリクスotel_traces_dropped_total - サンプリングレートを検証するために合成的な高ボリュームトラフィックでテストする
ステップ6: トレースをメトリクスとログに相関させる
統合された可観測性のためにトレースをメトリクスとログにリンクする。
ログにトレースIDを追加する(Python):
import logging from opentelemetry import trace # Custom log formatter with trace context class TraceFormatter(logging.Formatter): def format(self, record): # ... (完全な設定はEXAMPLES.mdを参照)
トレースからメトリクスを生成する(Tempo):
# tempo.yaml metrics_generator: registry: external_labels: cluster: production storage: # ... (完全な設定はEXAMPLES.mdを参照)
これにより以下のPrometheusメトリクスが生成される:
- サービス間のリクエスト数traces_service_graph_request_total
- スパン時間のヒストグラムtraces_span_metrics_duration_seconds
- スパンの呼び出し数traces_spanmetrics_calls_total
メトリクスからトレースをクエリする(Grafana):
GrafanaのPrometheusデータソース設定にエグゼンプラーサポートを追加する:
datasources: - name: Prometheus type: prometheus url: http://prometheus:9090 jsonData: exemplarTraceIdDestinations: - name: trace_id datasourceName: Tempo
Grafanaダッシュボードでエグゼンプラーをenableにする:
{ "fieldConfig": { "defaults": { "custom": { "showExemplars": true } } } }
期待結果: メトリクスエグゼンプラーをクリックするとトレースが開き、ログにトレースIDが表示され、トレースがログにリンクし、シグナル間での統合されたデバッグが実現する。
失敗時:
- Prometheusでエグゼンプラーサポートが有効であることを確認する(v2.26以上が必要)
- トレースIDフォーマットが一致していることを確認する(32文字の16進数)
- Tempo設定でメトリクスジェネレーターが有効であることを確認する
- Tempoからリモートライトエンドポイントにアクセス可能であることを確認する
- エグゼンプラークエリをテストする:
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) and on() exemplar
バリデーション
- トレーシングバックエンドがすべてのインストルメントされたサービスからスパンを受信する
- トレースがサービス間で正しい親子関係を示す
- スパン属性にセマンティック規約とビジネスコンテキストが含まれている
- コンテキストがHTTP呼び出しとメッセージキューを越えて正しく伝播する
- サンプリング戦略がトレース量をターゲットのパーセンテージに削減する
- エラートレースが常にサンプリングされる(エラー認識サンプリングを使用する場合)
- トレースIDがアプリケーションログに正しいフォーマットで表示される
- Grafanaがエグゼンプラーを介してメトリクスにリンクされたトレースを表示する
- ログパネルにトレースビューアーへのデータリンクがある
- トレース保持が設定されたストレージポリシーに一致する
よくある落とし穴
- コンテキストが伝播されない: ダウンストリームの呼び出しに
を渡し忘れるとトレースが切断される。常にコンテキストを明示的に渡す。context - スパンが終了しない:
(Go)またはdefer span.End()
ブロック(Python)の欠如はスパンが開いたままになりメモリリークを引き起こす。with - 過剰なインストルメンテーション: すべての関数にスパンを作成するとトレースが膨大になる。サービス境界、データベース呼び出し、外部APIに焦点を当てる。
- エラーの記録漏れ:
を呼び出さないと貴重なデバッグ情報が失われる。常にスパンにエラーを記録する。span.RecordError() - 高カーディナリティ属性: 無制限の値(ユーザーID、リクエストボディ)をスパン属性として使用するとストレージの問題が発生する。サンプリングまたは集計ラベルを使用する。
- 不正なスパンの種類: 誤ったスパンの種類(CLIENT vs SERVER vs INTERNAL)を使用するとサービスグラフの生成に影響する。セマンティック規約に従う。
- コンテキスト前のサンプリング: サンプリング決定は親トレースコンテキストを尊重しなければならない。アップストリームのサンプリングを尊重するために
サンプラーを使用する。ParentBased
関連スキル
- トレースIDでリンクされたメトリクス、ログ、トレースを使用した統合デバッグcorrelate-observability-signals
- Tempoメトリクスジェネレーターを使用してトレースからメトリクスを生成するsetup-prometheus-monitoring
- 分散トレースとの相関のためにログにトレースIDを追加するconfigure-log-aggregation
- ダッシュボードでトレース由来のメトリクスとエグゼンプラーリンクを可視化するbuild-grafana-dashboards