Grafana Tempo:Distributed Tracingのセットアップ、LokiとPrometheusを一つのGrafanaで統合する

Monitoring tutorial - IT technology blog
Monitoring tutorial - IT technology blog

ログとメトリクスだけではproductionのデバッグに不十分なとき

マイクロサービスシステムで遅いリクエストのデバッグに3時間費やしたことがあります——Lokiからのログも、Prometheusからのメトリクスも十分あったのに、それでもどのサービスがボトルネックなのかわかりませんでした。ログは「何かがおかしい」と示し、メトリクスは「latencyが上昇している」と示します。しかし、それらをつなぐものが何もない:リクエストはどのサービスを通ったのか?各ステップでどれだけ時間がかかったのか?どこで失敗したのか?

そこでDistributed TracingとGrafana Tempoを調べ始めました。この記事ではセットアップに直接入ります:TempoをゼロからインストールしてLokiとPrometheusに接続し、一つのtraceから関連するlogsとそのサービスのmetricsへ直接ジャンプできるよう設定します——すべて一つのGrafana内で。

Distributed Tracingとは何か、なぜTempoを選ぶのか

最もわかりやすいイメージは、各リクエストにGPSをつけることです。リクエストがservice-auth、service-order、service-paymentを通過するとき——各ステップで「span」が生成されます。すべてのspanをつなぎ合わせると一つの「trace」となり、リクエスト全体の経路が見えてきます:どこを通り、どこで滞留し、どこで失敗したのか。

Grafana Tempoはtraceをオブジェクトストレージ(local、S3、GCS)にインデックスなしで直接保存します——つまり、Jaegerのようにエラスティクサーチやカサンドラをメンテナンスする必要がありません。決め手となったポイント:TempoはLokiとPrometheusと一緒に一つのGrafana上で動くよう構築されているため、traceからlogsへ、metricsへのジャンプはネイティブにサポートされており、プラグインの追加インストールや手動リンクの作成は不要です。

Docker ComposeでTempoをインストールする

ディレクトリ構成の準備

mkdir -p tempo-stack/{tempo,grafana/provisioning/datasources}
cd tempo-stack

Tempoの設定ファイル

tempo/tempo.yamlを作成します:

stream_over_http_enabled: true

server:
  http_listen_port: 3200

distributor:
  receivers:
    otlp:
      protocols:
        http:
          endpoint: 0.0.0.0:4318
        grpc:
          endpoint: 0.0.0.0:4317
    jaeger:
      protocols:
        thrift_http:
          endpoint: 0.0.0.0:14268

ingester:
  max_block_duration: 5m

compactor:
  compaction:
    block_retention: 24h  # productionでは168hに増やす

storage:
  trace:
    backend: local
    local:
      path: /tmp/tempo/blocks
    wal:
      path: /tmp/tempo/wal

metrics_generator:
  registry:
    external_labels:
      source: tempo
      cluster: docker-compose
  storage:
    path: /tmp/tempo/generator/wal
    remote_write:
      - url: http://prometheus:9090/api/v1/write
        send_exemplars: true

overrides:
  defaults:
    metrics_generator:
      processors: [service-graphs, span-metrics]
      generate_native_histograms: both

Docker Compose

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

version: "3.8"

services:
  tempo:
    image: grafana/tempo:latest
    command: ["-config.file=/etc/tempo.yaml"]
    volumes:
      - ./tempo/tempo.yaml:/etc/tempo.yaml
      - tempo-data:/tmp/tempo
    ports:
      - "3200:3200"   # Tempo HTTP API
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP
      - "14268:14268" # Jaeger HTTP
    restart: unless-stopped

  prometheus:
    image: prom/prometheus:latest
    command:
      - "--config.file=/etc/prometheus/prometheus.yml"
      - "--enable-feature=remote-write-receiver"  # TempoがメトリクスをプッシュするためTempo側で必須
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    ports:
      - "9090:9090"
    restart: unless-stopped

  loki:
    image: grafana/loki:latest
    ports:
      - "3100:3100"
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning
      - grafana-data:/var/lib/grafana
    ports:
      - "3000:3000"
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
    restart: unless-stopped

volumes:
  tempo-data:
  prometheus-data:
  grafana-data:

重要な注意点:Prometheusはmetricsジェネレーターからmetricsを受信するために、--enable-feature=remote-write-receiverフラグを必ず有効化する必要があります。最初にこれを忘れてしまい、service-graphがGrafanaに何も表示されない原因を特定するのにかなり時間がかかりました。

Correlationを機能させるためのDatasource設定

Tempoをインストールした後に多くの人が見落とすステップです——そして、traceをクリックしてもlogsに飛べない原因を不思議に思います。grafana/provisioning/datasources/datasources.yamlを作成します:

apiVersion: 1

datasources:
  - name: Tempo
    type: tempo
    uid: tempo
    url: http://tempo:3200
    jsonData:
      httpMethod: GET
      tracesToLogsV2:
        datasourceUid: loki
        spanStartTimeShift: "-1m"
        spanEndTimeShift: "1m"
        filterByTraceID: true
        filterBySpanID: false
      tracesToMetrics:
        datasourceUid: prometheus
        spanStartTimeShift: "-1m"
        spanEndTimeShift: "1m"
        tags:
          - key: service.name
            value: service
        queries:
          - name: Request Rate
            query: rate(traces_spanmetrics_calls_total{$$__tags}[5m])
          - name: Error Rate
            query: rate(traces_spanmetrics_calls_total{$$__tags,status_code="STATUS_CODE_ERROR"}[5m])
          - name: Duration P95
            query: histogram_quantile(0.95, sum(rate(traces_spanmetrics_duration_milliseconds_bucket{$$__tags}[5m])) by (le))
      serviceMap:
        datasourceUid: prometheus
      nodeGraph:
        enabled: true
      lokiSearch:
        datasourceUid: loki

  - name: Loki
    type: loki
    uid: loki
    url: http://loki:3100
    jsonData:
      derivedFields:
        - datasourceUid: tempo
          matcherRegex: '"trace_id":"(\w+)"'
          name: TraceID
          url: "$${__value.raw}"
          urlDisplayLabel: "View Trace in Tempo"

  - name: Prometheus
    type: prometheus
    uid: prometheus
    url: http://prometheus:9090
    isDefault: true

この設定は主に3つのことを行います:

  • tracesToLogsV2:Tempo内の一つのspanから、そのspanの時間範囲のログをLokiから自動的にクエリします——タイムスタンプを手動で指定したりフィルタリングしたりする必要はありません
  • tracesToMetrics:traceからPrometheusにジャンプして、そのサービスのrequest rate、error rate、P95 latencyを確認します
  • Loki derivedFieldstrace_idを含むログエントリから、Tempo内の対応するtraceへの直接リンクを生成します

アプリケーションからTempoにtraceを送信する

TempoはJaeger、Zipkin、OTLPなど複数のプロトコルでtraceを受け取ります。OTLPはオープンスタンダードであり、ほとんどのモダンフレームワークが標準でサポートしているため、最良の選択です。Pythonでの例:

pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc opentelemetry-instrumentation-fastapi
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
from opentelemetry.sdk.resources import Resource

resource = Resource.create({
    "service.name": "my-api",
    "service.version": "1.0.0",
})

provider = TracerProvider(resource=resource)
otlp_exporter = OTLPSpanExporter(
    endpoint="http://localhost:4317",
    insecure=True,
)
provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
trace.set_tracer_provider(provider)

tracer = trace.get_tracer(__name__)

# コード内での使用例:
with tracer.start_as_current_span("process-order") as span:
    span.set_attribute("order.id", "12345")
    span.set_attribute("user.id", "user-abc")
    # ... ビジネスロジックをここに記述

Traces → Logsのcorrelationを機能させるには、アプリケーションのログにtrace_idを含める必要があります。このフィルターをloggerに追加します:

import logging
from opentelemetry import trace

class TraceIDFilter(logging.Filter):
    def filter(self, record):
        span = trace.get_current_span()
        ctx = span.get_span_context()
        record.trace_id = format(ctx.trace_id, "032x") if ctx.is_valid else ""
        return True

# LokiがパースしてGrafanaがtrace_idを抽出できるようJSON形式で
handler = logging.StreamHandler()
handler.addFilter(TraceIDFilter())
handler.setFormatter(logging.Formatter(
    '{"time": "%(asctime)s", "level": "%(levelname)s", "msg": "%(message)s", "trace_id": "%(trace_id)s"}'
))
logging.getLogger().addHandler(handler)

動作確認と実際の使い方

スタックの起動と動作確認

docker compose up -d

# Tempoが起動していることを確認
curl http://localhost:3200/status

# 受信したtraceを確認(アプリがデータを送信した後)
curl "http://localhost:3200/api/search?limit=5"

TraceQLでクエリを実行する

TraceQLはTempoに固有のクエリ言語です——PromQLに似た構文ですが、metricsではなくtraceに対して動作します。Grafana → Explore → datasourceでTempoを選択して試してみましょう:

# 特定のサービスのtraceを検索
{ .service.name = "my-api" }

# エラーのあるtraceを検索
{ status = error }

# 1秒より遅いtraceを検索
{ duration > 1s }

# 組み合わせ:my-apiのエラーリクエストで500ms超
{ .service.name = "my-api" && status = error && duration > 500ms }

実際のデバッグワークフロー

以前は、latencyのアラートが来ると、3〜4タブを並べて開いていました:Prometheusでmetricを確認し、Lokiでログをgrepして、タイムスタンプを手動で合わせて影響を受けたリクエストを特定する。このスタックをセットアップしてから、残るフローはこれだけです:

  1. AlertmanagerからLatency増加のアラートを受信
  2. Grafana Explore → Tempoで{ duration > 2s }をフィルタリングして、遅いリクエストをすぐに特定
  3. traceをクリックして、最も時間を占めているspanを確認
  4. 「Logs for this span」をクリック → Lokiがそのspanの時間範囲に対応するログを自動でフィルタリング
  5. 「Metrics」をクリック → Prometheusがその時点でのサービスのrequest rateとリソース使用量を表示

調査にかかる時間は30分から5分に短縮され、チームはアラートを無視するのではなく信頼するようになりました。

Productionで運用する際のポイント

  • Sampling:100%のtraceを送信しないでください。ほとんどのシステムでは10〜20%のHead-based samplingで十分です。エラーのあるtraceを100%保持したい場合はTail-based samplingを使用します。
  • Object storage:コンテナの再起動でデータを失わないよう、backend: localをS3またはGCSに変更してください
  • Retention:開発環境では24時間、Productionではコンプライアンス要件に応じて7〜30日間
# Production: localの代わりにS3を使用
storage:
  trace:
    backend: s3
    s3:
      bucket: my-tempo-traces
      endpoint: s3.amazonaws.com
      region: ap-northeast-1
      access_key: ${S3_ACCESS_KEY}
      secret_key: ${S3_SECRET_KEY}

このセットアップがあれば、次にlatencyのアラートが来ても、私のように3時間も原因を探し回る必要はなくなります。一つのTrace IDから関連するlogsとmetricsへのジャンプはわずか数秒——インシデントのすべてのコンテキストが一つのGrafana画面に収まっています。

Share: