Agent-almanac correlate-observability-signals
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/correlate-observability-signals" ~/.claude/skills/pjt222-agent-almanac-correlate-observability-signals-cb45d7 && rm -rf "$T"
i18n/ja/skills/correlate-observability-signals/SKILL.mdオブザーバビリティシグナルの相関
オブザーバビリティの三本柱にわたって、メトリクス、ログ、トレースを統合デバッグ用に接続する。
使用タイミング
- 複数システムにまたがる複雑なインシデントの調査時
- MTTR(平均修復時間)の短縮時
- 統合オブザーバビリティダッシュボードの構築時
- 分散トレーシングの実装時
- サイロ化されたツールから統合オブザーバビリティへの移行時
入力
- 必須: Prometheus(メトリクス)
- 必須: ログ集約システム(Loki、Elasticsearch、CloudWatch)
- 必須: 分散トレーシングバックエンド(Tempo、Jaeger、Zipkin)
- 任意: 統合可視化用のGrafana
- 任意: OpenTelemetryインストルメンテーション
手順
完全な設定ファイルとテンプレートについては拡張例を参照。
ステップ1: トレースコンテキスト伝播の実装
OpenTelemetryを使用してすべてのログとメトリクスにトレースIDを追加する:
// Go example: Propagate trace context to logs package main import ( "context" "log" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" ) func handleRequest(ctx context.Context, userID string) { // Extract trace context span := trace.SpanFromContext(ctx) traceID := span.SpanContext().TraceID().String() // Include trace ID in structured logs log.Printf("trace_id=%s user_id=%s action=process_request", traceID, userID) // Business logic here processData(ctx, userID) } func processData(ctx context.Context, userID string) { tracer := otel.Tracer("my-service") ctx, span := tracer.Start(ctx, "processData") defer span.End() traceID := span.SpanContext().TraceID().String() log.Printf("trace_id=%s user_id=%s action=process_data", traceID, userID) // More work }
Pythonの例:
# Python: Flask with OpenTelemetry from flask import Flask, request from opentelemetry import trace from opentelemetry.instrumentation.flask import FlaskInstrumentor import logging app = Flask(__name__) FlaskInstrumentor().instrument_app(app) logging.basicConfig( format='%(asctime)s trace_id=%(otelTraceID)s span_id=%(otelSpanID)s %(message)s', level=logging.INFO ) @app.route('/api/users/<user_id>') def get_user(user_id): span = trace.get_current_span() trace_id = format(span.get_span_context().trace_id, '032x') logging.info(f"Fetching user {user_id}", extra={ 'otelTraceID': trace_id, 'otelSpanID': format(span.get_span_context().span_id, '016x') }) # Business logic return {"user_id": user_id}
期待結果: すべてのログに
trace_idフィールドが含まれ、ログからトレースへの相関が可能になる。
失敗時: トレースIDが欠落している場合、OpenTelemetry SDKの初期化とコンテキスト伝播を確認する。
ステップ2: Prometheusでのエグザンプラー設定
エグザンプラーはメトリクスをトレースにリンクする:
# prometheus.yml global: scrape_interval: 15s # Enable exemplar storage exemplars: max_exemplars: 100000 # Per TSDB block scrape_configs: - job_name: 'api-service' static_configs: - targets: ['api-service:8080'] # Scrape exemplars metric_relabel_configs: - source_labels: [__name__] regex: 'http_request_duration_seconds.*' action: keep
エグザンプラーを発行するようにアプリケーションをインストルメント:
// Go: Emit exemplars with Prometheus histogram package main import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "go.opentelemetry.io/otel/trace" ) var httpDuration = promauto.NewHistogramVec( prometheus.HistogramOpts{ Name: "http_request_duration_seconds", Help: "HTTP request duration", Buckets: prometheus.DefBuckets, }, []string{"method", "endpoint", "status"}, ) func recordRequest(ctx context.Context, method, endpoint, status string, duration float64) { // Get trace ID for exemplar span := trace.SpanFromContext(ctx) traceID := span.SpanContext().TraceID().String() // Record metric with exemplar observer := httpDuration.WithLabelValues(method, endpoint, status) observer.(prometheus.ExemplarObserver).ObserveWithExemplar( duration, prometheus.Labels{"trace_id": traceID}, ) }
Prometheusでエグザンプラーをクエリする:
# Histogram with exemplars histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
Grafanaでは、エグザンプラーがヒストグラムグラフ上にドットとして表示され、トレースにリンクする。
期待結果: Grafanaのメトリクスグラフにエグザンプラーが表示され、クリックすると対応するトレースが開く。
失敗時: Prometheusバージョンが2.26以上(エグザンプラーサポート)であることを確認し、Grafanaデータソース設定でエグザンプラーが有効になっているか確認する。
ステップ3: REDメソッドによる統合ダッシュボードの構築
REDメソッド: Rate(レート)、Errors(エラー)、Duration(期間)(サービス向け)
{ "dashboard": { "title": "API Service - RED Dashboard", "panels": [ { "title": "Request Rate (req/s)", "type": "graph", "targets": [ { "expr": "sum(rate(http_requests_total{job=\"api-service\"}[5m])) by (endpoint)", "legendFormat": "{{ endpoint }}" } ], "exemplars": true }, { "title": "Error Rate (%)", "type": "graph", "targets": [ { "expr": "sum(rate(http_requests_total{job=\"api-service\", status=~\"5..\"}[5m])) / sum(rate(http_requests_total{job=\"api-service\"}[5m])) * 100", "legendFormat": "Error %" } ], "exemplars": true }, { "title": "Request Duration (p50, p95, p99)", "type": "graph", "targets": [ { "expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket{job=\"api-service\"}[5m]))", "legendFormat": "p50" }, { "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{job=\"api-service\"}[5m]))", "legendFormat": "p95" }, { "expr": "histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{job=\"api-service\"}[5m]))", "legendFormat": "p99" } ], "exemplars": true }, { "title": "Correlated Logs", "type": "logs", "datasource": "Loki", "targets": [ { "expr": "{job=\"api-service\"} |= \"error\"" } ], "options": { "showTime": true, "enableLogDetails": true } } ] } }
期待結果: レート、エラー、期間+相関ログを表示する単一ダッシュボード。
失敗時: パネルが「No Data」を表示する場合、メトリクス名がインストルメンテーションと一致しているか確認する。
ステップ4: リソース用のUSEメソッドの実装
USEメソッド: Utilization(使用率)、Saturation(飽和度)、Errors(エラー)(CPU、メモリ、ディスクなどのリソース向け)
{ "dashboard": { "title": "Node Resources - USE Dashboard", "panels": [ { "title": "CPU Utilization (%)", "type": "graph", "targets": [ { "expr": "100 - (avg(rate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)", "legendFormat": "CPU Usage %" } ] }, { "title": "CPU Saturation (Load Average)", "type": "graph", "targets": [ { "expr": "node_load1", "legendFormat": "1min load" }, { "expr": "node_load5", "legendFormat": "5min load" }, { "expr": "count(node_cpu_seconds_total{mode=\"idle\"})", "legendFormat": "CPU cores (threshold)" } ] }, { "title": "Memory Utilization (%)", "type": "graph", "targets": [ { "expr": "(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes * 100", "legendFormat": "Memory Usage %" } ] }, { "title": "Memory Saturation (Page Faults)", "type": "graph", "targets": [ { "expr": "rate(node_vmstat_pgmajfault[5m])", "legendFormat": "Major page faults/s" } ] }, { "title": "Disk Utilization (%)", "type": "graph", "targets": [ { "expr": "(node_filesystem_size_bytes - node_filesystem_free_bytes) / node_filesystem_size_bytes * 100", "legendFormat": "{{ device }}" } ] }, { "title": "Disk Saturation (IO Wait %)", "type": "graph", "targets": [ { "expr": "rate(node_cpu_seconds_total{mode=\"iowait\"}[5m]) * 100", "legendFormat": "IO Wait %" } ] } ] } }
期待結果: すべてのUSEディメンションにわたるリソースの健全性を表示するダッシュボード。
失敗時: node_exporterが実行中でシステムメトリクスをスクレイピングしていることを確認する。
ステップ5: Lokiでのログからトレースへのリンク
トレースIDを抽出するようにLokiを設定する:
# loki-config.yml schema_config: configs: - from: 2024-01-01 store: boltdb-shipper object_store: s3 schema: v11 index: prefix: index_ period: 24h # Derived fields for trace linking query_config: derived_fields: - name: TraceID source: trace_id url: 'https://tempo.company.com/trace/${__value.raw}' urlDisplayLabel: 'View Trace'
GrafanaでLokiデータソースを設定する:
{ "name": "Loki", "type": "loki", "url": "http://loki:3100", "jsonData": { "derivedFields": [ { "datasourceUid": "tempo-uid", "matcherRegex": "trace_id=(\\w+)", "name": "TraceID", "url": "$${__value.raw}" } ] } }
期待結果: LokiログのトレースIDをクリックするとTempoの対応するトレースが開く。
失敗時: 正規表現がログフォーマットに一致しているか確認し、TempoデータソースUIDを確認する。
ステップ6: 統合インシデントビューの作成
すべてのシグナルをまとめるダッシュボードを構築する:
{ "dashboard": { "title": "Incident Investigation", "templating": { "list": [ { # ... (完全な設定はEXAMPLES.mdを参照)
インシデント中のワークフロー:
- 高エラー率のアラートが発報
- オンコールエンジニアがGrafanaダッシュボードを開く
- 特定の時間帯のエラー率のスパイクを特定
- 期間ヒストグラムのエグザンプラードットをクリック→トレースが開く
- トレースが遅いデータベースクエリを表示
- スパンの「View Logs」をクリック→そのトレースのログが開く
- ログがタイムアウトの原因となっている特定のSQLクエリを明らかにする
- 2分以内に根本原因を特定
期待結果: メトリクス/ログ/トレース間を行き来するデバッグ用の単一ペイン。
失敗時: リンクが機能しない場合、データソース設定とトレースID伝播を確認する。
バリデーション
- すべてのアプリケーションログにトレースIDが含まれている
- Prometheusがエグザンプラーをスクレイピングしている
- Grafanaダッシュボードのヒストグラムにエグザンプラードットが表示される
- エグザンプラーをクリックするとTempo/Jaegerの対応するトレースが開く
- Lokiログに機能する「View Trace」リンクがある
- 主要サービスのREDダッシュボードが作成されている
- インフラストラクチャのUSEダッシュボードが作成されている
- 統合インシデントダッシュボードがGameDayでテスト済み
よくある落とし穴
- 一貫性のないトレースIDフォーマット: OpenTelemetryは32文字hex、Jaegerは16文字を使用。1つを選ぶ
- コンテキスト伝播の欠落: トレースIDがサービス間で流れない場合、分散トレーシングが壊れる。OpenTelemetryの自動インストルメンテーションを使用する
- エグザンプラーの過負荷: エグザンプラーが多すぎると(>100k)Prometheusが遅くなる可能性がある。高ボリュームメトリクスをサンプリングする
- 時計のズレ: トレースは複数サービスにまたがる。NTPが設定されていることを確認する。クロックドリフトがトレースの順序の問題を引き起こす
- データ保持期間の不一致: トレースがメトリクスより先に期限切れになると相関が壊れる。保持ポリシーを揃える
関連スキル
-- 相関のためのメトリクス基盤setup-prometheus-monitoring
-- 相関のためのログ基盤configure-log-aggregation
-- 相関のためのトレース基盤instrument-distributed-tracing
-- 統合可視化レイヤーbuild-grafana-dashboards