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_ATTRIBUTES や OTEL_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を特定する
ログとトレースがつながると、障害調査がかなり楽になります。
ただし、ログにどのように traceId や spanId を出すかは、利用しているログ設定や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環境に育てていけます。
是非フォローしてください
最新の情報をお伝えします
