OpenTelemetry: trace、log、metricの統一規格

1. 概要

マイクロサービス化すると、1つのリクエストが5個、10個の異なるプロセスを経由する可能性があります。エラーが発生したとき、リクエストがどこを通ったのか、どのステップが遅いのか、どのログがそのリクエストに属しているのかを把握する必要があります。OpenTelemetry(OTel)はこの問題を解決するためのオープン標準です。trace、metric、logの収集方法を統一し、それらをあらゆるバックエンドに送信することができます。

この記事を読めば、OTelの主要概念と注目される理由を理解できます。また Spring Boot 3アプリケーションにOTelを組み込む方法、Collectorを実行する方法、instrumentation部分のテストを書く方法、そして使うべきでない場合を認識する方法も習得できます。

2. OpenTelemetryとは何か?

OpenTelemetryはCNCFに属するオープンソースプロジェクトです。これはテレメトリデータの作成、説明、および送信方法を定義します。OTelはデータを保存または表示する場所ではありません。その役割はJaeger、Prometheus、Grafana Tempo/Lokiなどのバックエンドまたは商用サービスが担当します。

2.1. 3つのシグナルタイプ

  • Trace: リクエストの流れ。各traceは複数のspanで構成され、各spanは1つの作業単位です。例えば「PaymentServiceを呼ぶ」または「ordersテーブルをクエリする」といったものです。spanには開始時刻、終了時刻、および属性があります。
  • Metric: 時系列の集計値。例えば秒単位のリクエスト数、p99レイテンシー、失敗した注文数などです。
  • Log: 離散的なイベント記録。OTelはtraceIdとspanIdをログに追加するため、特定のログからそのtraceに直接アクセスできます。

最大の価値は相互の結びつきにあります。3つのシグナルはすべて共通のコンテキストと共通のリソース説明(例:service.name)を使用します。

2.2. 主要なコンポーネント

コンポーネント 役割
API codeがspanとmetricを作成するためのインターフェース。ベンダーに依存しない
SDK APIの実装:サンプリング、バッチ処理、エクスポート
OTLP データ送信プロトコル(gRPCまたはHTTP/protobuf)
Collector テレメトリを受け取り、処理し、転送する独立したプロセス
Semantic conventions http.request.method、db.systemなどの属性名の命名規約
Instrumentation 人気のあるフレームワーク用のテレメトリを自動生成するライブラリまたはagent

2.3. Context propagation

複数のサービスのspanが1つのtraceにまとめられるようにするには、コンテキストがリクエストとともに伝播する必要があります。HTTPの場合、OTelはデフォルトではW3C Trace Context標準を使用し、traceparentヘッダーを通じて伝播します:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

このヘッダーは4つの部分から構成されます:バージョン、traceId、親spanのspanId、およびサンプリングフラグです。サービスがリクエストを受け取ると、このヘッダーを読み取り、同じtraceに属する子spanを作成します。

3. OpenTelemetryが注目されている理由

  • ベンダーニュートラル: コードを一度instrumentします。バックエンドを変更する場合は、exporter設定またはCollectorの設定を変更するだけで、コードを変更する必要はありません。
  • 従来の標準を統合: OTelはOpenTracingとOpenCensusを統合することで生まれました。コミュニティはもう2つの方向の間で選択する必要がありません。
  • 3つのシグナルに対する1つの標準: 以前は、各データ型は通常独自のagentと形式を持っていました。
  • 広いエコシステム: 多くのフレームワーク、クラウドプロバイダー、observability toolsはOTLPをサポートしています。Spring Boot 3もMicrometerを通じて統合されています。
  • Collectorの柔軟性: インフラストラクチャレイヤーでデータをフィルタリング、サンプリング、複数のバックエンドにルーティングでき、アプリケーションを変更する必要がありません。

4. Spring Boot 3へのOpenTelemetryの適用

Spring Boot 3はMicrometer Observationを抽象層として使用します。1つのobservationを作成すると、Micrometerはmetric(timer)とspanの両方を生成します。OTelへのブリッジがエクスポート部分を担当します。

4.1. 依存関係

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- Micrometer Tracing -> OpenTelemetryのブリッジ -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-tracing-bridge-otel</artifactId>
    </dependency>
    <!-- OTLPでtraceをエクスポート -->
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-otlp</artifactId>
    </dependency>
    <!-- OTLPでmetricをエクスポート -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-otlp</artifactId>
    </dependency>
    <!-- テスト用 -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-observation-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

バージョンを指定する必要はありません。Spring BootはMicrometerとOpenTelemetryライブラリのバージョンをBOM経由で管理しています。

4.2. 設定

spring:
  application:
    name: order-service

management:
  tracing:
    sampling:
      probability: 1.0   # 開発環境: 100%サンプリング、本番環境では減らすべき
  otlp:
    tracing:
      endpoint: http://localhost:4318/v1/traces
    metrics:
      export:
        url: http://localhost:4318/v1/metrics
        step: 30s

logging:
  pattern:
    level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"

各設定グループの意味:

  • spring.application.nameはservice.nameとして使用されます。これはバックエンドがサービスを区別するための最も重要な属性です。
  • management.tracing.sampling.probabilityは記録されるtraceの割合を決定します。
  • 2つのotlpエンドポイントはOTLPを介してCollectorを指しており、デフォルトポートは4318です。
  • logging.pattern.levelはMDCからtraceIdとspanIdを各ログ行に挿入します。これがログとtraceを関連付ける最も簡単な方法です。

4.3. ObservationAPIを使用したビジネスロジックのinstrumentation

コントローラーへのHTTPリクエストとRestClient/WebClientを通じた呼び出しは、Springによってすでに自動的にinstrumentされています。自分で追加する必要があるのは、重要なビジネスステップです:

public record Order(String id, String paymentMethod, long amount) {}
@FunctionalInterface
public interface PaymentService {
    String charge(Order order);
}
@Service
public class CheckoutService {

    private static final Logger log = LoggerFactory.getLogger(CheckoutService.class);

    private final ObservationRegistry registry;
    private final PaymentService paymentService;

    public CheckoutService(ObservationRegistry registry, PaymentService paymentService) {
        this.registry = registry;
        this.paymentService = paymentService;
    }

    public String checkout(Order order) {
        return Observation.createNotStarted("order.checkout", registry)
                .lowCardinalityKeyValue("payment.method", order.paymentMethod())
                .highCardinalityKeyValue("order.id", order.id())
                .observe(() -> {
                    log.info("注文 {} を処理中", order.id());
                    return paymentService.charge(order);
                });
    }
}

上記のコード内の注意点:

  • Observation.createNotStarted("order.checkout", registry)はorder.checkoutという名前のobservationを作成します。
  • lowCardinalityKeyValueは少ない値を持つ属性を追加します(CARD、CASH、WALLET)。この属性はmetricとspanの両方に表示されます。
  • highCardinalityKeyValueは非常に多くの値を持つ属性(注文IDなど)を追加します。これはspanのみに関連付けられ、metricを膨らませません。
  • observe(...)はspanを開き、時間を測定し、例外がある場合は記録し、supplierが値を返した後でspanを閉じます。

実行時、ログは次のようになります:

 INFO [order-service,4bf92f3577b34da6a3ce929d0e0e4736,a1b2c3d4e5f60718] ... 注文 A-1001 を処理中

バックエンド側は2つのものを受け取ります:

  • リクエストのHTTP span下にあるspan order.checkout。
  • payment.methodで分割されるtimer metric order.checkout。このmetricからコール数、総時間、最大時間を計算できます。

注意: Spring内でコードを書く場合、直接io.opentelemetry.api.trace.Tracerを呼び出す代わりに、ObservationAPIを優先してください。コードは特定のtracing libraryに依存しなくなります。1回の測定でmetricとtraceの両方が得られます。

4.4. 代替案:Java agent

コードを変更したくない場合、またはdependencyを追加したくない場合は、OpenTelemetry Java agentを使用できます。agentはJDBC、HTTPクライアント、Kafkaなど、非常に多くの一般的なライブラリを自動的にinstrumentします:

export OTEL_SERVICE_NAME=order-service
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1

java -javaagent:./opentelemetry-javaagent.jar -jar order-service.jar

agentは古いアプリケーションまたは運用チームがinstrumentationを一元管理したい場合に適しています。トレードオフとしては、より詳細な制御が難しくなり、起動時間が増加し、agentはbytecodeに干渉する「ブラックボックス」として機能します。

注意: 同じシグナルタイプに対してJava agentとMicrometerブリッジを同時に実行しないでください。重複したspanまたはずれたcontextを受け取ります。

5. OpenTelemetry Collectorの実行

アプリケーションはバックエンドに直接送信するのではなく、Collectorに送信する必要があります。Collectorはデータをバッチ処理、フィルタリング、ルーティングする場所です。以下は最小限の設定です:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch: {}
  attributes/strip-pii:
    actions:
      - key: customer.email
        action: delete

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [attributes/strip-pii, batch]
      exporters: [debug]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]

この設定は4つの部分で構成されています:

  • receivers.otlpはgRPC(4317)とHTTP(4318)の両方でデータを受け取ります。
  • attributes/strip-piiはデータがインフラストラクチャを離れる前にcustomer.email属性を削除します。
  • batchはデータを集約してからコールを削減して送信します。
  • exporter debugはコンソールにデータを出力して確認します。実際の環境では、使用しているバックエンドのexporterに置き換えます。

Dockerでcollectorを起動します:

docker run --rm -p 4317:4317 -p 4318:4318 \
  -v "$(pwd)/otel-collector.yaml:/etc/otelcol-contrib/config.yaml" \
  otel/opentelemetry-collector-contrib

checkout APIを呼び出してから、Collectorのコンソールを確認してください。次のようなspanが表示されます:

Span #0
    Trace ID       : 4bf92f3577b34da6a3ce929d0e0e4736
    Parent ID      : 9a8b7c6d5e4f3a2b
    Name           : order.checkout
    Attributes:
         -> payment.method: Str(CARD)
         -> order.id: Str(A-1001)

6. Instrumentation部分のテスト

Instrumentationもコードなので、テストが必要です。TestObservationRegistryを使用すると、Spring contextやCollectorを起動しなくてもobservationをチェックできます:

import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class CheckoutServiceTest {

    @Test
    void whenCheckout_thenObservationIsRecordedWithPaymentMethod() {
        TestObservationRegistry registry = TestObservationRegistry.create();
        PaymentService paymentService = order -> "PAID-" + order.id();
        CheckoutService service = new CheckoutService(registry, paymentService);

        String result = service.checkout(new Order("A-1001", "CARD", 250_000));

        assertThat(result).isEqualTo("PAID-A-1001");
        TestObservationRegistryAssert.assertThat(registry)
                .hasObservationWithNameEqualTo("order.checkout")
                .that()
                .hasLowCardinalityKeyValue("payment.method", "CARD")
                .hasHighCardinalityKeyValue("order.id", "A-1001")
                .hasBeenStarted()
                .hasBeenStopped();
    }
}

このテストは3つのことを確認します:

  • ビジネスロジック結果が正しい。
  • Observation order.checkoutが作成された、正しい属性付きで。
  • Observationが開かれて閉じられた。開かれたままのspanは本番環境で非常に発見しにくいエラーです。

7. よくあるエラー

  • Cardinality爆発: userId、orderId、またはパラメータを含むURLをmetricタグに入れるとmetricの文字列数が増加し、ストレージコストが増加します。これらの値はspanにのみ関連付けるべきです。
  • 本番環境で100%サンプリング: データ量とコストがトラフィックに比例して増加します。probabilityを減らすか、Collectorでtail samplingを使用してエラーまたは遅いtraceを保持するようにします。
  • 非同期実行時にcontextが失われる: @Async、CompletableFuture、または自分で作成したthread poolでは、contextは自動的に新しいthreadに移動しません。context propagation機構でexecutorをラップする必要があります。またはSpring管理のexecutorを使用します。
  • 機密データのリーク: email、カード番号、tokenは属性またはログに混入する可能性があります。コード内でフィルタリングし、Collectorに追加の層を追加してください。
  • 恣意的な命名: semantic conventionsに従わないと、dashboard及びalertがサービス間で再利用できなくなります。
  • Collectorをスキップ: アプリケーションからバックエンドに直接送信すると、バックエンドを変更するたびにすべてのサービスを再デプロイする必要があります。

8. 使うべきでない場合(またはまだ必要ではない場合)

  • 小さなアプリケーション、1つのインスタンス、依存関係が少ない: 構造化ログとActuatorからいくつかのmetricはしばしば十分です。分散traceは「distributed」なものがない場合、ほぼ価値をもたらしません。
  • TeamがまだバックエンドとOperatorを持っていない: OTelはパイプラインに過ぎません。データを見る人がいない場合、CPU、ネットワーク、ストレージコストを追加するだけです。
  • システムが遅延またはリソースに非常に敏感: 例えば組み込みアプリケーションや、マイクロ秒単位で計測するホットパス。instrumentationを広く有効にする前に、オーバーヘッドを測定してください。
  • 1回のOTelですべての既存ツールを置き換えることを期待: 各シグナルの各言語での成熟度は異なる場合があります。使用する部分のサポート状態を確認し、traceから始めて段階的に移行してください。

9. 結論

OpenTelemetryは共通のAPI、OTLPプロトコル、Collectorを通じてtrace、metric、logの作成と送信方法を統一しています。そのため、コード内のinstrumentationをバックエンド選択から分離できます。

Spring Boot 3では、最も簡潔なパスが4つのステップで構成されています。まず、Micrometer Observationを使用して1回の測定でmetricとspanの両方を取得します。次に、OTLPを通じてCollectorにエクスポートします。3番目に、traceIdをログに挿入します。4番目に、重要なobservationのテストを作成します。同時に、cardinality、sampling、機密データを最初から制御してください。

システムがまだ小さい場合、急ぐ必要はありません。サービスの数が増え、トラブルシューティングに数時間かかるようになったとき、OTelは投資する価値のあるプラットフォームです。

opentelemetryobservabilityspring-boottracingtech-trends