はじめに
最近話題のOpenTelemetryについて勉強しました。アプリケーションのコードをほぼ修正することなくテレメトリ情報を取得できるようです。とても便利に感じたのでご紹介したいと思います。
本記事では、今年の9月にstableとなったSpring Boot starterを使用して、Spring BootのアプリケーションにOpenTelemetryを導入してみます。今回は主にトレースについて見ていきたいと思います。
OpenTelemetry
概要
公式サイトでは、OpenTelemetryについて以下のように説明されていました。
OpenTelemetryはオブザーバビリティフレームワークであり、トレース、メトリクス、ログのようなテレメトリーデータを作成・管理するためにデザインされたツールキットです。
各用語の説明は以下の通りです。
- オブザーバビリティ
オブザーバビリティとは、システムの出力を調べることによって、システムの内部状態を理解する能力のことです
- トレース
一般的にトレースとして知られている分散トレースは、マイクロサービスやサーバーレスアプリケーションのようなマルチサービスアーキテクチャを伝播するリクエスト(アプリケーションまたはエンドユーザーによって行われる)が辿った経路を記録します。
- メトリクス
メトリクス とは、インフラやアプリケーションに関する数値データを一定期間にわたって集計したものです。 たとえば、システムエラー率、CPU使用率、あるサービスのリクエスト率などです。
- ログ
ログは、サービスや他のコンポーネントが発するタイムスタンプ付きのメッセージです。
- テレメトリ
テレメトリー とは、システムやその動作から送出されるデータのことです。 データはトレース、メトリクス、ログなどの形式で得られます。
つまり、OpenTelemetryを使うと多くのテレメトリを送出することができるので、システムの内部状態を理解しやすくなりデバッグや障害解析が容易になるということです。
計装
システムからテレメトリが送出されるようにすることを計装(instrumentation)と言います。計装には2つの種類があります。
- 自動計装(Automatic instrumentation)
- アプリケーションのコードを変更せずに、システムからテレメトリを送出する。
- 手動計装(Manual instrumentation)
- APIやSDKなどの計装ライブラリを使用して、テレメトリを送出するためのコードをアプリケーションに追加する。
以降では、次節で紹介する簡単なベースアプリに計装を施していきたいと思います。
ベースアプリ
概要
ベースアプリの概要は以下の通りです。トレースを充実させるためにアプリのレイヤは多めです。
- さいころアプリ(サーバアプリ)
- GETリクエストを送ると1から6までの数字がランダムに1つ返却される
- 出た目の数をDBに書き込む
- DB
- 出た目の数の書き込み先
- クライアントアプリ
- さいころアプリにリクエストを送る
構成
- Spring Boot 3.4.0
- JDK 17.0.13
- Gradle 8.11.1
- H2DB
- JDBC
Docker上でアプリを起動しています。
実装
さいころアプリ
さいころアプリのコードのほとんどは下記サンプルコードを拝借しました。
@RestController
public class RollDiceController {
@Autowired
private RollDiceService service;
@GetMapping("/rolldice")
public String index(@RequestParam("player") Optional<String> player) {
return service.rolldice(player);
}
}
@Service
public class RollDiceService {
private static final Logger logger = LoggerFactory.getLogger(RollDiceService.class);
@Autowired
private RollDiceDao dao;
public String rolldice(Optional<String> player) {
int result = this.getRandomNumber(1, 6);
if (player.isPresent()) {
logger.info("{} is rolling the dice: {}", player.get(), result);
} else {
logger.info("Anonymous player is rolling the dice: {}", result);
}
dao.insert(result, player);
return Integer.toString(result);
}
public int getRandomNumber(int min, int max) {
return ThreadLocalRandom.current().nextInt(min, max + 1);
}
}
@Repository
public class RollDiceDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public void insert(int result, Optional<String> player) {
String playerName;
if (player.isPresent()) {
playerName = player.get();
} else {
playerName = "Anonymous player";
}
jdbcTemplate.update("INSERT INTO dice(player_name, val) VALUES (?, ?)",
playerName,
result
);
}
}
クライアントアプリ
5秒毎にさいころアプリへリクエストを送信します。
@Component
public class Client {
private RestClient restClient;
public Client(RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder.build();
}
@Scheduled(fixedRate = 5000)
public void callRollDice() {
restClient.get()
.uri("http://rolldice-app:8080/rolldice")
.retrieve()
.toBodilessEntity();
}
}
Spring Boot starter
Spring Bootアプリケーションを自動計装する手段は2つあります。1つはOpenTelemetry Java agentを使用する方法で、もう1つがSpring Boot starterを使用する方法です。両者の使い分けについては上記サイトを参照してください。今回はSpring Boot starterを使用します。
準備
Spring Boot starterはSpring Boot 2.6+または3.1+で使用できます。サーバとクライアントのアプリに以下の依存を追加します。(参考:Getting started | OpenTelemetry)
implementation(platform(SpringBootPlugin.BOM_COORDINATES))
implementation(platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:2.10.0"))
implementation("io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter")
続いて、トレースの送信先としてJaegerを用意します。Jaegerは、トレースデータを分析・可視化することができるOSSです。
services:
jaeger:
image: jaegertracing/all-in-one
container_name: jaeger
ports:
- "16686:16686"
- "4318:4318"
サーバとクライアントのアプリにSpring Boot starter用の環境変数を設定します。今回はアプリをJaegerと同じネットワーク内のコンテナで起動しているので、エンドポイントはhttp://jaeger:4318
としています。
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318
- OTEL_SERVICE_NAME=rolldice-app
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318
- OTEL_SERVICE_NAME=client-app
トレースの確認
http://localhost:16686/jaeger/ui/search
にアクセスしてトレースを確認します。
5秒毎にクライアントアプリからリクエストが送信されていることがわかります。トレースの中身は以下の通りです。
今回作成したリクエストからは4つのスパンを取得することができました。スパンとは、トレースを構成する作業や操作の単位のことです。
TagsやProcessタブを開くと、各スパンが持つ情報を確認することができます。Spring Boot starterが作成したスパンには"telemetry.distro.name" : "opentelemetry-spring-boot-starter"
という属性が付与されています。これら4つのスパンにもこの属性が確認できました。
アノテーションによるスパンの作成
続いて、アノテーションによるスパンの作成を行っていきます。以下の依存関係を追加します。(参考:Annotations | OpenTelemetry)
implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations")
implementation("org.springframework.boot:spring-boot-starter-aop")
アノテーションを各メソッドに付与します。
//// rolldice-app
// RollDiceController
@WithSpan("index")
// RollDiceService
@WithSpan("rolldice")
@WithSpan("getRandomNumber")
// RollDiceDao
@WithSpan("insert")
//// client-app
// Client
@WithSpan("callRollDice")
トレースを確認してみます。
各メソッドに対応するスパンが追加されていることがわかります。アノテーションを付与するだけでトレースを充実させることができるのは楽で良いですね。
一方で、RollDiceService
クラスのgetRandomNumber()
メソッドに対応するスパンは作成されていません。これはSpring AOPの仕様によるものだそうです。アノテーションによるスパンの作成は、Spring AOPを利用して実装されているようです。
リソース属性の作成
アプリがスパンに持たせたいリソース属性を簡単に作成することができます。例として、example.key=value
という属性をスパンに付与してみます。設定は、Springのプロパティと同様にapplication.yml
またはapplication.properties
に記述することで行うことができます。(参考:SDK configuration | OpenTelemetry)
otel:
resource:
attributes:
example:
key: value
スパンを確認すると、作成した属性が反映されていることがわかります。
OpenTelemetry API/SDKによるスパンの作成
最後に、OpenTelemetryが提供するAPI/SDKを使用してスパンを作ってみます。Spring Boot starterの機能により、API/SDKを使用した手動計装が簡単に行えるようになっています。
今回はさいころアプリのRollDiceService
クラスを手動計装したいと思います。まず、必要な依存関係を追加します。
implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
1つ目の依存は、Spring Boot starterによって自動構成されたSDKを利用するためのものです。2つ目の依存は、手動計装によるスパンをOTLPでエクスポートするための依存です。
続いて、アプリの環境変数を設定します。
- OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
手動計装によるトレース情報の送信プロトコルはデフォルトだとgrpc
ですが、自動計装によるトレース情報はhttp/protobuf
が使われています(参考)。エンドポイントを複数設定することはできないので、自動計装と手動計装を併用するときは、手動計装で使用する送信プロトコルをhttp/protobuf
にしなければなりません。
[1]: OpenTelemetry Java agent 2.x and the OpenTelemetry Spring Boot starter use http/protobuf by default.
引用元:https://opentelemetry.io/docs/languages/java/configuration/#properties-exporters
自動計装と手動計装を併用するときは送信プロトコルを明示的に指定する
次に、スパンの作成に必要なTracer
インスタンスを取得します。
public static final String INSTRUMENTATION_SCOPE = "com.example.app.service";
private final Tracer tracer;
public RollDiceService() {
OpenTelemetry sdk = AutoConfiguredOpenTelemetrySdk.initialize().getOpenTelemetrySdk();
this.tracer = sdk.getTracer(INSTRUMENTATION_SCOPE);
}
Spring Boot starterにより、手動計装に必要なSDKが自動構成されています(参考:Zero-code SDK autoconfigure)。自動構成されたOpenTelemetry
インスタンスからtracer
を取得することができます。
続いて、取得したtracer
を使用してスパンを作成していきます。rolldice
メソッドのint result = this.getRandomNumber(1, 6);
の行を以下のように修正します。
int result = 0;
Span span = tracer.spanBuilder("getRandomNumber-span").setSpanKind(SpanKind.INTERNAL).startSpan();
try (Scope ignored = span.makeCurrent()) {
result = this.getRandomNumber(1, 6);
} catch (Exception e) {
span.setStatus(StatusCode.ERROR, "error message");
} finally {
span.end();
}
tracer
でスパンを作成し、span.makeCurrent()
によりトレースに反映させています。処理が終了したら、span.end()
でスパンを閉じます。
それでは、Jaegerでトレースを確認してみます。
自作のスパンを自動計装によるトレース内に反映させることができました。
おわりに
今回はOpenTelemetryのSpring Boot starterの機能を使用して、シンプルなSpring Bootアプリケーションを計装しました。アプリのコードをほとんど修正することなく充実したトレースを得ることができるのは凄いですね。手動で計装する場合でも、簡単にテレメトリを作るための機能が揃っていました。
OpenTelemetryについては最近知ったのですが、OpenTelemetryのAdvent Calendarがあるくらい流行っているようですね。今後サポートされるテレメトリや言語、バックエンドなども増えていくと思うので、これからもOpenTelemetryに注目していきたいと思います。
ここまで読んでくださった方、どうもありがとうございました。