Spring Boot + OpenTelemetry入門:トレース・メトリクスを可視化する

はじめに

Spring BootでWebアプリケーションを運用していると、次のような問題にぶつかることがあります。

・APIが遅いが、どの処理が遅いのかわからない
・DBなのか外部APIなのか、原因の切り分けに時間がかかる
・複数サービスをまたいだリクエストの流れを追えない
・ログはあるが、1つのリクエスト単位で追跡しづらい
・本番障害時に「どこで詰まっているか」をすぐ確認できない

こうした課題を解決するために使われるのが OpenTelemetry です。

OpenTelemetryは、アプリケーションから トレース、メトリクス、ログ などのテレメトリデータを収集し、特定の監視サービスに依存しない形で外部へ送信するための仕組みです。MicrometerのOTLPドキュメントでも、OTLPはOpenTelemetry対応バックエンドへデータを送るためのベンダーニュートラルなプロトコルとして説明されています。

Spring Bootでは、ActuatorとMicrometerを中心にObservability機能が整備されています。Spring Boot公式ドキュメントでも、OpenTelemetryを使う方法として、OpenTelemetry Java AgentやOpenTelemetry Spring Boot Starterのようなコミュニティサポートの選択肢に加えて、Springチームが公式にサポートする方法として Micrometer + OTLP exporter を使う構成が説明されています。

この記事では、Spring BootアプリケーションにOpenTelemetryを導入し、ローカルでトレースとメトリクスを確認するための基本を解説します。

この記事でわかること

・OpenTelemetryとは何か
・Spring BootでOpenTelemetryを使う考え方
・Micrometer / Actuator / OTLPの関係
・Gradleの依存関係
・application.ymlの設定例
・ローカルでJaegerを使ってトレースを見る方法
・独自処理にトレースを追加する方法
・よくあるエラーと対処

この記事では、Spring Boot + Gradle + application.yml を前提にします。

OpenTelemetryとは?

OpenTelemetryは、アプリケーションの状態を外部から観測するための標準的な仕組みです。

監視というと、以前は以下のように分かれて考えられることが多くありました。

・ログ:アプリケーションが出力する文字情報
・メトリクス:CPU、メモリ、リクエスト数、レスポンスタイムなどの数値
・トレース:1つのリクエストがどの処理を通ったかの流れ

OpenTelemetryは、これらをできるだけ統一的に扱うための仕様・ライブラリ群です。

特にWeb API開発で重要なのが 分散トレーシング です。

例えば、以下のような構成があるとします。

ユーザー
  ↓
API Gateway
  ↓
Spring Boot API
  ↓
外部決済API
  ↓
MySQL

通常のログだけだと、「どのリクエストが、どのサービスを通り、どこで遅くなったのか」を追うのは大変です。

OpenTelemetryを使うと、1つのリクエストに対して traceId が付き、サービスをまたいだ処理の流れを追跡しやすくなります。

Spring Bootでは何を使うのか?

Spring BootでOpenTelemetryを使う方法はいくつかあります。

代表的には以下です。

1. OpenTelemetry Java Agentを使う
2. OpenTelemetry Spring Boot Starterを使う
3. Spring Boot Actuator + Micrometer + OTLP Exporterを使う

OpenTelemetry公式ドキュメントでは、Javaのゼロコード計装としてJava Agent、Spring Boot Starter、Quarkus OpenTelemetry Extensionなどが選択肢として説明されています。

一方、Spring Boot公式ドキュメントでは、Springチームが公式にサポートするOpenTelemetry連携として、MicrometerとOTLP exporterを使う構成が説明されています。また、Spring BootではOpenTelemetry APIを直接使うよりも、Micrometer Observation / Tracing APIを使うことが推奨されています。

この記事では、Spring Bootらしく管理しやすい Spring Boot Actuator + Micrometer + OpenTelemetry OTLP の構成で進めます。

全体構成

今回の構成は次のようになります。

Spring Boot App
  ↓ traces / metrics
OTLP Exporter
  ↓
Jaeger / OpenTelemetry Collector / 監視基盤

ローカルでは、まず Jaeger を使ってトレースを確認します。

実務では、以下のようなバックエンドへ送信することもあります。

・Grafana Tempo
・Jaeger
・Zipkin
・Datadog
・New Relic
・Google Cloud Trace
・AWS X-Ray
・OpenTelemetry Collector

ポイントは、アプリケーション側はOTLPでデータを送るだけにしておくことです。
そうすると、将来的にバックエンドを変える場合でも、アプリケーションコードへの影響を減らせます。

トレース・メトリクス・ログの違い

OpenTelemetryを理解するうえで、まずは3つの違いを押さえておくと楽です。

トレース:
  1つのリクエストが、どの処理をどの順番で通ったかを見る

メトリクス:
  リクエスト数、レスポンスタイム、エラー数、JVMメモリなどを数値で見る

ログ:
  アプリケーションが出力したイベントやエラー内容を見る

例えば、APIが遅いときは以下のように使い分けます。

1. メトリクスで「どのAPIが遅いか」を見る
2. トレースで「そのAPIのどこが遅いか」を見る
3. ログで「なぜ失敗したか」を詳しく見る

OpenTelemetryを入れる目的は、単にデータを送ることではありません。
障害調査や性能改善で、原因に早くたどり着けるようにすることです。

build.gradleの設定

Spring Boot 4系では、OpenTelemetry連携用のスターターとして spring-boot-starter-opentelemetry が用意されています。Spring Boot公式ドキュメントでも、OpenTelemetryでOTLPへトレースを送る場合の依存関係として org.springframework.boot:spring-boot-starter-opentelemetry が示されています。

Gradleでは以下のように設定します。

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-actuator")

    // OpenTelemetry / OTLP tracing
    implementation("org.springframework.boot:spring-boot-starter-opentelemetry")

    // MetricsをOTLPで送信する場合
    runtimeOnly("io.micrometer:micrometer-registry-otlp")

    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

Spring Boot 3系で spring-boot-starter-opentelemetry が使えない場合は、以下のように個別依存を追加する構成もあります。

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-actuator")

    // Micrometer Observation APIをOpenTelemetryへ橋渡しする
    implementation("io.micrometer:micrometer-tracing-bridge-otel")

    // OTLP exporter
    implementation("io.opentelemetry:opentelemetry-exporter-otlp")

    // MetricsをOTLPで送信する場合
    runtimeOnly("io.micrometer:micrometer-registry-otlp")

    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

Spring Bootのバージョンによって依存関係の書き方が少し変わるため、まずは利用しているSpring Bootの公式ドキュメントを確認してください。

application.ymlの設定

次に、application.yml を設定します。

以下は、トレースとメトリクスをローカルのOTLPエンドポイントへ送信する例です。

spring:
  application:
    name: spring-boot-otel-sample

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus

  tracing:
    sampling:
      # ローカル確認では100%送信。本番では下げることを検討する
      probability: 1.0
    export:
      otlp:
        enabled: true

  opentelemetry:
    resource-attributes:
      service.name: spring-boot-otel-sample
      deployment.environment: local
    tracing:
      export:
        otlp:
          endpoint: http://localhost:4318/v1/traces
          transport: http/protobuf

  otlp:
    metrics:
      export:
        enabled: true
        url: http://localhost:4318/v1/metrics
        step: 10s

重要なのは以下です。

management.tracing.sampling.probability:
  トレースをどの割合で送るか

management.opentelemetry.resource-attributes:
  service.nameや環境名など、送信データに付ける属性

management.opentelemetry.tracing.export.otlp.endpoint:
  トレース送信先のOTLPエンドポイント

management.otlp.metrics.export.url:
  メトリクス送信先のOTLPエンドポイント

Spring Boot公式ドキュメントでは、デフォルトではリクエストの一部だけがサンプリングされるため、全リクエストをトレースバックエンドへ送るには management.tracing.sampling.probability: 1.0 のように設定する例が示されています。

また、OpenTelemetryのリソース属性は management.opentelemetry.resource-attributes で設定でき、OTEL_RESOURCE_ATTRIBUTESOTEL_SERVICE_NAME などの環境変数とも統合されると説明されています。

ローカルでJaegerを起動する

トレースを確認するために、ローカルでJaegerを起動します。

compose.yml を作成します。

services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686" # Jaeger UI
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP
    environment:
      COLLECTOR_OTLP_ENABLED: "true"

起動します。

docker compose up -d

Jaeger UIは以下で確認できます。

http://localhost:16686

Spring Bootアプリを起動してAPIを何度か呼び出すと、Jaeger上でサービス名 spring-boot-otel-sample のトレースが見えるようになります。

動作確認用のControllerを作る

まずは簡単なControllerを作ります。

package com.example.demo.api;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/sample")
public class SampleController {

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

    private final SampleService sampleService;

    public SampleController(SampleService sampleService) {
        this.sampleService = sampleService;
    }

    @GetMapping("/{id}")
    public SampleResponse getSample(@PathVariable String id) {
        log.info("sample api called. id={}", id);

        String message = sampleService.findMessage(id);

        return new SampleResponse(id, message);
    }

    public record SampleResponse(String id, String message) {
    }
}

Serviceクラスも作ります。

package com.example.demo.api;

import org.springframework.stereotype.Service;

@Service
public class SampleService {

    public String findMessage(String id) {
        // 実際はDBアクセスや外部API呼び出しが入るイメージ
        simulateSlowProcess();

        return "Hello OpenTelemetry: " + id;
    }

    private void simulateSlowProcess() {
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException("interrupted", e);
        }
    }
}

アプリを起動します。

./gradlew bootRun

Windowsの場合です。

gradlew.bat bootRun

APIを呼び出します。

curl http://localhost:8080/api/sample/100

レスポンス例です。

{
  "id": "100",
  "message": "Hello OpenTelemetry: 100"
}

Jaeger UIを開き、サービス名 spring-boot-otel-sample を選択してトレースを検索します。
APIリクエストごとにトレースが表示され、どの処理にどのくらい時間がかかったかを確認できます。

独自処理にトレースを追加する

自動計測だけでも、HTTPリクエストなどの基本的なトレースは取得できます。

ただし、実務では以下のような処理も追跡したくなります。

・注文作成処理
・在庫チェック処理
・外部API呼び出し
・ファイル生成処理
・S3アップロード処理
・バッチ処理

Spring Bootでは、OpenTelemetry APIを直接使うより、Micrometer Observation APIを使うのがおすすめです。Spring Boot公式ドキュメントでも、トレースではOpenTelemetry APIを直接使うより、Micrometer Observation または Tracing APIを使うことが強く推奨されています。

ObservationRegistry を使う例です。

package com.example.demo.api;

import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final ObservationRegistry observationRegistry;

    public OrderService(ObservationRegistry observationRegistry) {
        this.observationRegistry = observationRegistry;
    }

    public String createOrder(String itemCode) {
        return Observation
                .createNotStarted("order.create", observationRegistry)
                .lowCardinalityKeyValue("feature", "order")
                .lowCardinalityKeyValue("item.code", itemCode)
                .observe(() -> {
                    // ここに実際の注文処理を書く
                    simulateProcess();
                    return "ORDER_CREATED";
                });
    }

    private void simulateProcess() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException("interrupted", e);
        }
    }
}

このようにすると、order.create という名前の観測データが作られ、トレース上でも処理時間を追いやすくなります。

@Observedを使う方法

メソッド単位で簡単に観測したい場合は、@Observed を使う方法もあります。

ただし、アノテーションのスキャンを有効にするには設定が必要です。Spring Boot公式ドキュメントでは、@Observed@Timed などのアノテーションを有効化するには management.observations.annotations.enabled=true を設定し、さらに spring-boot-starter-aspectj に含まれるAspectJ Weaverへの依存が必要と説明されています。

Gradleに追加します。

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-aspectj")
}

application.yml に追加します。

management:
  observations:
    annotations:
      enabled: true

Serviceに @Observed を付けます。

package com.example.demo.api;

import io.micrometer.observation.annotation.Observed;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    @Observed(
            name = "payment.authorize",
            contextualName = "authorize payment"
    )
    public String authorize(String orderId) {
        simulateExternalApi();
        return "AUTHORIZED";
    }

    private void simulateExternalApi() {
        try {
            Thread.sleep(700);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException("interrupted", e);
        }
    }
}

注意点として、すでにSpring MVC ControllerやSpring Data Repositoryなどで自動計測されている箇所に @Observed を重ねると、観測データが重複する場合があります。公式ドキュメントでも、すでに自動計測されているクラスやメソッドにアノテーションを付けると重複する可能性があると説明されています。

そのため、まずは重要な業務処理に絞って付けるのがおすすめです。

メトリクスを確認する

Spring Boot Actuatorを入れると、メトリクスエンドポイントも利用できます。

curl http://localhost:8080/actuator/metrics

例えば、HTTPサーバーリクエストのメトリクスを見る場合です。

curl http://localhost:8080/actuator/metrics/http.server.requests

OTLPでメトリクスを送る場合は、io.micrometer:micrometer-registry-otlp を追加し、management.otlp.metrics.export.* を設定します。Micrometerのドキュメントでも、Spring Bootでは management.otlp.metrics.export で始まるプロパティがOTLPメトリクス設定にバインドされると説明されています。

management:
  otlp:
    metrics:
      export:
        enabled: true
        url: http://localhost:4318/v1/metrics
        step: 10s

なお、Spring Boot公式ドキュメントでは、SpringのメトリクスはOpenTelemetryの SdkMeterProvider ではなくMicrometerを使う方針であり、MicrometerメトリクスをOTLPでOpenTelemetry対応バックエンドへ送信できると説明されています。

ログとtraceIdの関係

トレースを導入すると、ログ調査でも traceId が重要になります。

例えば、障害時に以下のような流れで調査できます。

1. エラーログから traceId を見つける
2. JaegerやTempoで同じ traceId を検索する
3. そのリクエストがどの処理を通ったか確認する
4. 遅い処理や失敗した外部APIを特定する

ログとトレースがつながると、障害調査がかなり楽になります。

ただし、ログにどのように traceIdspanId を出すかは、利用しているログ設定やSpring Bootのバージョンによって変わります。まずはトレースが正しく送信されていることを確認し、その後にログパターンや構造化ログを整えるのが安全です。

Java Agentとの違い

OpenTelemetry Java Agentを使うと、アプリケーションコードをあまり変更せずに自動計測できます。

起動例です。

java -javaagent:opentelemetry-javaagent.jar \
  -Dotel.service.name=spring-boot-otel-sample \
  -Dotel.exporter.otlp.endpoint=http://localhost:4318 \
  -jar build/libs/app.jar

Java Agentは導入が簡単で、既存アプリケーションに後付けしやすいのがメリットです。

一方で、Spring Boot側の設定・依存関係・Micrometer Observation APIと統合して管理したい場合は、今回紹介しているSpring Boot Actuator + Micrometer構成のほうが扱いやすい場面もあります。

ざっくり使い分けるなら以下です。

Java Agent:
  既存アプリにできるだけコード変更なしで導入したい

Spring Boot + Micrometer:
  Spring Bootの設定、Actuator、Observation APIと合わせて管理したい

OpenTelemetry Spring Boot Starter:
  OpenTelemetryコミュニティのSpring Boot向けスターターを使いたい

OpenTelemetry公式ドキュメントでも、Java AgentやSpring Boot Starterなど複数の導入方法が用意されています。

よくあるエラーと対処

1. Jaegerにトレースが表示されない

確認ポイントです。

・Jaegerが起動しているか
・4318ポートが開いているか
・OTLP endpointが http://localhost:4318/v1/traces になっているか
・management.tracing.sampling.probability が 1.0 になっているか
・依存関係にOpenTelemetry / OTLP exporterが入っているか

ローカル確認では、まずサンプリングを100%にしましょう。

management:
  tracing:
    sampling:
      probability: 1.0

本番ではデータ量が増えすぎる可能性があるため、必要に応じて値を下げます。

2. メトリクスがOTLPへ送信されない

確認ポイントです。

・io.micrometer:micrometer-registry-otlp が入っているか
・management.otlp.metrics.export.enabled が true になっているか
・url が http://localhost:4318/v1/metrics になっているか
・送信間隔 step が長すぎないか

設定例です。

management:
  otlp:
    metrics:
      export:
        enabled: true
        url: http://localhost:4318/v1/metrics
        step: 10s

3. トレースが多すぎる

ローカルでは sampling.probability: 1.0 で問題ありませんが、本番ではすべてのリクエストを送るとデータ量が増えます。

management:
  tracing:
    sampling:
      probability: 0.1

例えば 0.1 なら、おおよそ10%のリクエストをサンプリングするイメージです。

4. @Observedが効かない

確認ポイントです。

・management.observations.annotations.enabled が true か
・spring-boot-starter-aspectj を追加しているか
・対象クラスがSpring Beanになっているか
・privateメソッドに付けていないか

設定例です。

management:
  observations:
    annotations:
      enabled: true

依存関係です。

implementation("org.springframework.boot:spring-boot-starter-aspectj")

5. OpenTelemetry APIを直接使うべきか迷う

Spring Bootアプリケーションでは、まずはMicrometer Observation APIを使うのがおすすめです。

理由は、Spring BootのActuatorやMicrometerと自然に連携できるためです。Spring Boot公式ドキュメントでも、トレースではOpenTelemetry APIを直接使うより、Micrometer Observation / Tracing APIを使うことが推奨されています。

OpenTelemetry APIを直接使うのは、以下のようなケースに絞るとよいです。

・既に社内標準がOpenTelemetry APIに寄っている
・ライブラリ側がOpenTelemetry APIを要求している
・Spring Boot以外のアプリケーションと同じ計装コードにしたい

導入チェックリスト

Spring BootにOpenTelemetryを導入するときは、以下を確認しましょう。

□ spring-boot-starter-actuator を追加したか
□ OpenTelemetry / OTLP exporter 関連の依存関係を追加したか
□ application.yml に service.name を設定したか
□ トレース送信先 endpoint を設定したか
□ ローカル確認用に sampling.probability を 1.0 にしたか
□ JaegerやOpenTelemetry Collectorを起動したか
□ APIを実行してトレースが出るか確認したか
□ 必要なら micrometer-registry-otlp を追加したか
□ メトリクス送信先 url を設定したか
□ 重要な業務処理に Observation / @Observed を追加したか
□ 本番でのサンプリング率を検討したか
□ ログに traceId を出す方針を決めたか

まとめ

Spring Boot + OpenTelemetryを使うと、APIの処理時間やサービス間の呼び出しを可視化しやすくなります。

重要なポイントは以下です。

・Spring BootではActuatorとMicrometerを中心にObservabilityを扱う
・OpenTelemetryへはOTLPでトレースやメトリクスを送信できる
・ローカルではJaegerを使うとトレースを確認しやすい
・application.ymlでservice.nameやOTLP endpointを設定する
・独自処理はMicrometer Observation APIで計測する
・@Observedを使う場合はアノテーション有効化とAspectJが必要
・本番ではサンプリング率やデータ量に注意する

最初からすべてを完璧に計測する必要はありません。

まずは、Spring BootアプリにActuatorとOpenTelemetry連携を追加し、1つのAPIリクエストがJaeger上で見えるところまで進めるのがおすすめです。そこから、外部API呼び出し、DBアクセス、バッチ処理、重要な業務処理へ少しずつ観測ポイントを増やしていくと、実務でも使えるObservability環境に育てていけます。

是非フォローしてください

最新の情報をお伝えします

類似投稿