DevOpsにおける包括的な可観測性:OpenTelemetryガイド

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

DevOpsにおける包括的な可観測性:OpenTelemetryガイド

午前2時。ポケットベルが鳴り響き、また本番環境でインシデントが発生したことを告げていました。ログは散乱し、メトリクスは漠然としたスパイクを示し、トレーシングは単なる後回しでした。聞き覚えがありますか?私も以前、目隠し状態でデバッグし、バラバラになったデータの山をふるいにかけていました。目標は、夜明けまでに根本原因を特定すること。このような状況が、より良い方法、特にOpenTelemetryを用いた包括的な可観測性を追求するきっかけとなりました。

可観測性とは、単にデータを収集することではありません。それは、予期していなかったシナリオであっても、システムにあらゆる質問をし、答えを得ることです。複雑なシステムで停電したときに手に取る懐中電灯のようなものだと考えてください。マイクロサービス、サーバーレス、クラウドネイティブアーキテクチャに取り組む多くの人々にとって、OpenTelemetryは急速に標準となりつつあります。それは混沌に必要な秩序をもたらします。

アプローチの比較:従来のモニタリング vs. 統合された可観測性

従来のモニタリング:症状ベースの苦労

長年、私たちはパッチワークのようなツールに頼ってきました。メトリクスにはPrometheus、ログにはELKスタック(Elasticsearch、Logstash、Kibana)、そしてJaegerやZipkinのような個別のトレーシングシステムを使っていたでしょう。

それぞれのツールは特定の仕事に優れていましたが、データの相関付けは悪夢でした。顧客が問題を報告すると、Grafanaダッシュボード(Prometheusメトリクス)からKibana(Elasticsearchログ)に飛び移っていました。タイムスタンプを一致させるのに苦労し、その後、ログに表示されるかもしれないトレースIDをJaegerで検索する、といった具合です。

  • 長所: 成熟しており、広く採用され、個々のコンポーネントに対する強力なコミュニティサポートがある。
  • 短所: サイロ化されたデータ、高いコンテキストスイッチングオーバーヘッド、異なるテレメトリー信号の相関付けが困難で、しばしば受動的。アラートは通常、既知の障害モードに対してのみ発生した。点と点をつなぐために必要な手作業のため、平均復旧時間(MTTR)が数時間に及ぶことがあった。

OpenTelemetry: 統合された未来

OpenTelemetry(OTel)は、OpenTracingとOpenCensusの統合から生まれました。トレース、メトリクス、ログといったテレメトリーデータを計測、生成、収集、エクスポートするための単一のAPI、SDK、ツールセットを作成しました。これはベンダーに依存しない標準です。つまり、アプリケーションを一度計測すれば、そのデータをオープンソースであれ商用であれ、互換性のある任意のバックエンドに送信できます。これにより、システムの内部状態を捕捉および管理する方法が根本的に簡素化されます。

  • 長所: 統合された計測、一貫したコンテキスト伝播(分散トレースに不可欠)、ベンダーロックインの軽減、活発なコミュニティ開発。OTelはクラウドネイティブ環境向けに設計されており、システムの動作の一貫したビューを提供することでMTTRの削減を目指している。
  • 短所: まだ進化中(特にログは急速に成熟しているが)、コンポーネントを理解するための初期学習曲線、膨大な量のテレメトリーデータの管理には慎重な計画が必要。

OpenTelemetryの柱:トレース、メトリクス、ログ

OpenTelemetryは、3つの基本的なテレメトリー信号の収集を標準化します。

分散トレーシング:リクエストの旅を追跡する

単一のユーザーリクエストが、多数のマイクロサービス、非同期キュー、データベース呼び出しを流れていく様子を想像してみてください。トレーシングがなければ、この旅はブラックボックスです。分散トレーシングは、そのリクエストの全パスを視覚化することを可能にします。

各ステップでのレイテンシーを表示し、ボトルネックを特定し、正確なサービス障害を特定します。各操作は『スパン』となり、関連するスパンの集まりが『トレース』を形成します。OTelは、ユニークなトレースIDとスパンIDがサービス境界を越えて伝播され、すべてがシームレスにリンクされることを保証します。


from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor

# トレーサープロバイダーをセットアップする
provider = TracerProvider()
processor = SimpleSpanProcessor(ConsoleSpanExporter())
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)

# アプリケーションのトレーサーを取得する
tracer = trace.get_tracer("my-app-tracer")

def process_order(order_id):
    with tracer.start_as_current_span("process_order") as span:
        span.set_attribute("order.id", order_id)
        print(f"注文を処理中: {order_id}")
        # 何らかの作業をシミュレートする
        do_inventory_check(order_id)
        do_payment_processing(order_id)

def do_inventory_check(order_id):
    with tracer.start_as_current_span("inventory_check"):
        print(f"  注文 {order_id} の在庫を確認中")
        # ... 実際の在庫ロジック ...

def do_payment_processing(order_id):
    with tracer.start_as_current_span("payment_processing"):
        print(f"  注文 {order_id} の支払いを処理中")
        # ... 実際の支払いロジック ...

process_order("12345")

メトリクス:一目でシステムの健全性を把握する

メトリクスは、システムの挙動に関する集計された定量的なデータを時系列で提供します。リクエストレート、エラー数、CPU使用率、メモリ使用量、カスタムビジネスメトリクスなどを考えてみてください。OTelは、カウンター(増加する値用)、ゲージ(現在の値用)、ヒストグラム(リクエストレイテンシーなどの値の統計的分布用)といった様々なメトリクス計測器を定義しています。これらはトレンドの発見、アラートの設定、システム全体の健全性監視に不可欠です。


from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader

# メータープロバイダーをセットアップする
reader = PeriodicExportingMetricReader(ConsoleMetricExporter())
provider = MeterProvider(metric_readers=[reader])
metrics.set_meter_provider(provider)

# アプリケーションのメーターを取得する
meter = metrics.get_meter("my-app-meter")

# カウンター計測器を作成する
requests_counter = meter.create_counter(
    "http.server.requests",
    description="HTTPリクエストの総数",
    unit="{requests}",
)

# リクエスト期間のヒストグラム計測器を作成する
request_duration_histogram = meter.create_histogram(
    "http.server.request.duration",
    description="HTTPリクエストの期間",
    unit="s",
)

def handle_request(path, duration):
    requests_counter.add(1, {"http.route": path, "http.method": "GET"})
    request_duration_histogram.record(duration, {"http.route": path, "http.method": "GET"})
    print(f"{path} へのリクエストを {duration}秒で処理しました")

handle_request("/api/users", 0.05)
handle_request("/api/products", 0.12)

ログ:詳細に潜む悪魔

ログは、詳細なイベントレベルの情報にとって依然として非常に重要です。OpenTelemetryは、トレースIDとスパンIDをログレコードに直接挿入することで、従来のロギングを強化します。この一見小さな変更が、状況を一変させます。午前2時のインシデント中にログエントリを詳しく調べているとき、それがどのトレースとスパンに属しているかを即座に知ることができます。これにより、ログメッセージからトレーシングUIの完全なトレースコンテキストに直接ジャンプできます。


import logging
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
from opentelemetry.sdk.logs import LogRecordsProcessor, LoggerProvider
from opentelemetry.sdk.logs.export import ConsoleLogExporter, SimpleLogRecordProcessor
from opentelemetry.instrumentation.logging import LoggingInstrumentor

# トレーサープロバイダーをセットアップする
resource = Resource.create({"service.name": "my-log-service"})
provider = TracerProvider(resource=resource)
span_processor = SimpleSpanProcessor(ConsoleSpanExporter())
provider.add_span_processor(span_processor)
trace.set_tracer_provider(provider)

# ロガープロバイダーをセットアップする
logger_provider = LoggerProvider(resource=resource)
log_processor = SimpleLogRecordProcessor(ConsoleLogExporter())
logger_provider.add_log_record_processor(log_processor)

# 標準のPythonロギングライブラリを計測する
LoggingInstrumentor().instrument(set_logging_format=True, log_level=logging.INFO, logger_provider=logger_provider)

# トレーサーとロガーを取得する
tracer = trace.get_tracer("my-log-service-tracer")
logger = logging.getLogger(__name__)

logger.info("アプリケーションが開始されました。")

with tracer.start_as_current_span("main_operation") as span:
    span.set_attribute("user.id", "testuser")
    logger.warning("潜在的に高い負荷が検出されました。")
    # 何らかのロジック
    with tracer.start_as_current_span("sub_operation"):
        logger.debug("サブオペレーションが進行中です。")
    logger.info("メインオペレーションが完了しました。")

logger.info("アプリケーションがシャットダウンされました。")

推奨セットアップ:本番環境に対応したOpenTelemetryスタック

DevOps環境でOpenTelemetryを真に活用するには、アプリケーションの計測だけでは不十分です。データ収集、処理、分析のための堅牢なパイプラインが必要です。典型的な本番環境に対応したスタックを以下に示します。

OpenTelemetryコレクター:テレメトリーゲートウェイ

OpenTelemetryコレクターは、強力なベンダーに依存しないプロキシです。テレメトリーデータを受信、処理、エクスポートできます。

中央ハブとして機能し、アプリケーションの計測とバックエンドシステムを分離します。これにより、データが高価な分析ツールに到達する前に、変換、フィルタリング、バッチ処理、さらにはサンプリングを行うことができます。コレクターは通常、ローカル収集のためにアプリケーションのサイドカー(KubernetesではサイドカーまたはDaemonSet)として、また集約とルーティングのためにゲートウェイ(専用デプロイメント)としてデプロイされます。

OTLP(OpenTelemetry Protocol)を受信し、Jaeger、Prometheus、Lokiにエクスポートする基本的なコレクター設定は次のようになります。


receivers:
  otlp:
    protocols:
      grpc:
      http:

exporters:
  jaeger:
    endpoint: jaeger:14250
    tls:
      insecure: true
  prometheus:
    endpoint: "0.0.0.0:8889"
  loki:
    endpoint: http://loki:3100/api/prom/push

processors:
  batch:

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [jaeger]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [loki]

バックエンドストレージと分析:データを理解する

コレクターを介してデータが流れたら、それを保存し、視覚化する場所が必要です。

  • トレース: JaegerとTempo(Grafana Labsエコシステムの一部)は、分散トレースの保存と視覚化に人気のある選択肢です。
  • メトリクス: Prometheusは、時系列メトリクスに関する事実上の標準です。Mimirは、スケーラブルなマルチテナントPrometheusストレージを提供します。
  • ログ: Loki(Grafana Labs)は、コスト効率が高く、Grafanaとの統合が容易になるように設計されたログ集約システムです。Elasticsearchも強力な選択肢であり、Kibanaと組み合わせて使用されることがよくあります。
  • ダッシュボードとアラート: Grafanaは普遍的なダッシュボードツールです。上記のすべてのバックエンドに接続し、システム全体の統一されたビューを提供します。

実装ガイド:実践的な作業

簡単な例を見てみましょう。Python FlaskアプリケーションでOpenTelemetryを機能させます。ここでの目標は、基本的な可観測性をどれだけ迅速に達成できるかを示すことです。

ステップ1:アプリケーションの計測(Python Flaskの例)

まず、Pythonアプリケーションに必要なOpenTelemetryパッケージをインストールします。


pip install opentelemetry-sdk opentelemetry-api opentelemetry-exporter-otlp opentelemetry-instrumentation-flask opentelemetry-instrumentation-requests opentelemetry-instrumentation-logging

次に、グローバル計測を設定するためのファイル(例:app_instrumentation.py)を作成します。その後、それをFlaskアプリで使用します。


# app_instrumentation.py

from opentelemetry import trace, metrics
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.instrumentation.logging import LoggingInstrumentor
from opentelemetry.sdk.logs import LoggerProvider, BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.grpc.log_exporter import OTLPLogExporter
import logging

def configure_opentelemetry(service_name):
    resource = Resource.create({"service.name": service_name})

    # トレーシングを設定する
    trace_provider = TracerProvider(resource=resource)
    span_exporter = OTLPSpanExporter(endpoint="localhost:4317") # OTLP gRPCエンドポイント
    trace_provider.add_span_processor(BatchSpanProcessor(span_exporter))
    trace.set_tracer_provider(trace_provider)

    # メトリクスを設定する
    metric_reader = PeriodicExportingMetricReader(
        OTLPMetricExporter(endpoint="localhost:4317")
    )
    metric_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
    metrics.set_meter_provider(metric_provider)

    # ログを設定する
    logger_provider = LoggerProvider(resource=resource)
    log_exporter = OTLPLogExporter(endpoint="localhost:4317")
    logger_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))
    LoggingInstrumentor().instrument(set_logging_format=True, log_level=logging.INFO, logger_provider=logger_provider)

    # ウェブフレームワークとHTTPクライアントを自動計測する
    FlaskInstrumentor().instrument()
    RequestsInstrumentor().instrument()

    print(f"サービス: {service_name} のOpenTelemetryが設定されました")

# app.py
from flask import Flask, request
import requests
import logging
from app_instrumentation import configure_opentelemetry

configure_opentelemetry("my-flask-app")
app = Flask(__name__)
logger = logging.getLogger(__name__)

@app.route('/')
def hello():
    logger.info("ルート / へのリクエストを受信しました。")
    # 分散トレーシングをデモンストレーションするために外部HTTPリクエストを作成する
    try:
        requests.get("http://example.com", timeout=1)
    except requests.exceptions.Timeout:
        logger.warning("example.com へのリクエストがタイムアウトしました。")
    except Exception as e:
        logger.error(f"example.com へのリクエストでエラーが発生しました: {e}")

    return "こんにちは、可観測性!"

@app.route('/slow')
def slow_route():
    logger.info("ルート /slow へのリクエストを受信しました。")
    import time
    time.sleep(0.1)
    logger.info("スロールートが完了しました。")
    return "遅い!"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

この設定は、トレース、メトリクス、ログをOTLP (gRPC) 経由で localhost:4317 に送信します。ここではOpenTelemetryコレクターがリッスンします。

ステップ2:OpenTelemetryコレクターのデプロイ

ローカル開発や簡単なセットアップの場合、OpenTelemetryコレクターをDocker Compose経由で実行できます。docker-compose.yamlcollector-config.yaml(上記の構成を使用)をアプリケーションと同じディレクトリに作成します。


# docker-compose.yaml
version: '3.8'
services:
  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    command: [--config=/etc/otel-collector-config.yaml]
    volumes:
      - ./collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - "4317:4317" # OTLP gRPCレシーバー
      - "4318:4318" # OTLP HTTPレシーバー (設定で有効になっている場合)
      - "8889:8889" # Prometheusメトリクスエクスポーター

  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686" # Jaeger UI
      - "14250:14250" # gRPCコレクター

  prometheus:
    image: prom/prometheus:latest
    command: --config.file=/etc/prometheus/prometheus.yml
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

  loki:
    image: grafana/loki:latest
    command: -config.file=/etc/loki/local-config.yaml
    volumes:
      - ./loki-local-config.yaml:/etc/loki/local-config.yaml
    ports:
      - "3100:3100"

  grafana:
    image: grafana/grafana:latest
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
      - GF_PATHS_PROVISIONING=/etc/grafana/provisioning
    volumes:
      - ./grafana-provisioning/:/etc/grafana/provisioning
    ports:
      - "3000:3000"

そして、コレクターをスクレイプするための基本的なprometheus.ymlです。


# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'otel-collector'
    static_configs:
      - targets: ['otel-collector:8889']

そしてloki-local-config.yaml


# loki-local-config.yaml
auth_enabled: false

server:
  http_listen_port: 3100

common:
  instance_path: /loki
  path_prefix: /tmp/loki
  ring:
    instance_addr: 127.0.0.1
    kvstore:
      store: inmemory
  replication_factor: 1

schema_config:
  configs:
    - from: 2020-10-27
      store: boltdb-shipper
      object_store: filesystem
      schema: v11
      period: 24h

compactor:
  compaction_interval: 10m

query_range:
  align_queries_with_step: true
  cache_results: true

chunks:
  max_chunk_age: 1h

memberlist:
  abort_if_cluster_join_fails: false

最後に、データソース用のGrafanaプロビジョニング(例:grafana-provisioning/datasources.yaml):


# grafana-provisioning/datasources.yaml
apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    url: http://prometheus:9090
    access: proxy
    isDefault: true
    version: 1

  - name: Jaeger
    type: jaeger
    url: http://jaeger:16686
    access: proxy
    version: 1

  - name: Loki
    type: loki
    url: http://loki:3100
    access: proxy
    version: 1

すべてをdocker-compose up -dで起動します。その後、Python Flaskアプリを実行します。

ステップ3:可観測性バックエンドのセットアップ

Docker Composeのセットアップにより、Jaeger、Prometheus、Loki、Grafanaが実行されます。http://localhost:3000でGrafanaにアクセスします。Prometheus、Jaeger、Lokiがデータソースとして構成されているはずです。これで、アプリケーションメトリクスを視覚化するためのダッシュボードを作成できます。Exploreタブを使用してログをクエリし、Jaeger UI(http://localhost:16686)またはGrafanaのExploreタブ(LokiがTempo経由でトレースに統合されている場合)でトレースを詳しく調べます。

ステップ4:検証と反復

Flaskアプリケーション(http://localhost:5000http://localhost:5000/slow)に対していくつかのリクエストを実行します。その後、Grafana、Jaeger、Lokiを確認します。以下が表示されるはずです。

  • Jaeger/Grafanaで、Flaskアプリを通るリクエストパスとexample.comへの外部呼び出しを示すトレース。
  • Prometheus/Grafanaで、http.server.requestshttp.server.request.durationのメトリクス。
  • Loki/Grafanaで、関連するトレースIDとスパンIDを含むログ。

この統合されたアプローチの真の力は、アラートが発生したときに発揮されます。推測する代わりに、メトリクスのスパイクを示すGrafanaダッシュボードから、関連するトレースとログに直接ジャンプできます。

これらはすべて同じコンテキストで強化されています。私はこのアプローチを本番環境で適用しており、その結果は常に安定していて影響力がありました。たとえば、特に困難なインシデントの際に、アップストリームサービスが断続的に障害を起こしていました。統合されたトレースは即座にボトルネックを特定し、私のチームは30分以内に問題を特定し、サーキットブレーカーを実装することができました。これは何時間も franticなログ調査に費やす可能性があったものを劇的に削減しました。

結論:可観測性を受け入れて正気を保つ

バラバラなモニタリングの時代は終わりました。今日の複雑な分散システムでは、可観測性への統一されたアプローチは単なる「あれば良いもの」ではありません。正気を保ち、インシデント対応時間を短縮し、システムがどのように動作するかを真に理解するために不可欠です。

OpenTelemetryは、これを実現するために必要な標準化を提供し、一度計測すればどこでも分析できる能力を与えます。小さく始めて、重要なサービスを計測し、徐々に拡大してください。すぐに、これなしでどうやって管理していたのだろうと思うことでしょう。

Share: