12
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NRI OpenStandiaAdvent Calendar 2024

Day 22

OpenTelemetryのSpring Boot starterを使ってトレースを見てみる

Last updated at Posted at 2024-12-21

はじめに

最近話題の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上でアプリを起動しています。

実装

さいころアプリ

さいころアプリのコードのほとんどは下記サンプルコードを拝借しました。

Controller
@RestController
public class RollDiceController {

    @Autowired
    private RollDiceService service;

    @GetMapping("/rolldice")
    public String index(@RequestParam("player") Optional<String> player) {
        return service.rolldice(player);
    }

}
Service
@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);
    }

}
Dao
@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秒毎にさいころアプリへリクエストを送信します。

Client
@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

build.gradle
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です。

docker-compose.yml
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にアクセスしてトレースを確認します。

{2086924B-7EBF-4765-89CD-77085B232498}.png

5秒毎にクライアントアプリからリクエストが送信されていることがわかります。トレースの中身は以下の通りです。

{B6D35391-907E-4665-8D44-DC3981422C3A}.png

今回作成したリクエストからは4つのスパンを取得することができました。スパンとは、トレースを構成する作業や操作の単位のことです。
TagsやProcessタブを開くと、各スパンが持つ情報を確認することができます。Spring Boot starterが作成したスパンには"telemetry.distro.name" : "opentelemetry-spring-boot-starter"という属性が付与されています。これら4つのスパンにもこの属性が確認できました。

アノテーションによるスパンの作成

続いて、アノテーションによるスパンの作成を行っていきます。以下の依存関係を追加します。(参考:Annotations | OpenTelemetry

build.gradle
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")

トレースを確認してみます。

{249798D7-D2C5-4CB8-97CB-0AC68A282994}.png

各メソッドに対応するスパンが追加されていることがわかります。アノテーションを付与するだけでトレースを充実させることができるのは楽で良いですね。
一方で、RollDiceServiceクラスのgetRandomNumber()メソッドに対応するスパンは作成されていません。これはSpring AOPの仕様によるものだそうです。アノテーションによるスパンの作成は、Spring AOPを利用して実装されているようです。

リソース属性の作成

アプリがスパンに持たせたいリソース属性を簡単に作成することができます。例として、example.key=valueという属性をスパンに付与してみます。設定は、Springのプロパティと同様にapplication.ymlまたはapplication.propertiesに記述することで行うことができます。(参考:SDK configuration | OpenTelemetry

application.yml
otel: 
  resource: 
    attributes:
      example:
        key: value

スパンを確認すると、作成した属性が反映されていることがわかります。

{8453C804-164B-407A-94CF-AF5975F05E94}.png

OpenTelemetry API/SDKによるスパンの作成

最後に、OpenTelemetryが提供するAPI/SDKを使用してスパンを作ってみます。Spring Boot starterの機能により、API/SDKを使用した手動計装が簡単に行えるようになっています。
今回はさいころアプリのRollDiceServiceクラスを手動計装したいと思います。まず、必要な依存関係を追加します。

build.gradle
implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
implementation("io.opentelemetry:opentelemetry-exporter-otlp")

1つ目の依存は、Spring Boot starterによって自動構成されたSDKを利用するためのものです。2つ目の依存は、手動計装によるスパンをOTLPでエクスポートするための依存です。

続いて、アプリの環境変数を設定します。

docker-compose.yml
- OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf

手動計装によるトレース情報の送信プロトコルはデフォルトだとgrpcですが、自動計装によるトレース情報はhttp/protobufが使われています(参考)。エンドポイントを複数設定することはできないので、自動計装と手動計装を併用するときは、手動計装で使用する送信プロトコルをhttp/protobufにしなければなりません

{93B92EDE-B411-4C78-9DB9-BF8AC4966E3C}.png
[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インスタンスを取得します。

RollDiceService.java
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);の行を以下のように修正します。

rolldiceメソッド
    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でトレースを確認してみます。

{E40FFFB5-3A36-4429-92E1-ED771AACC2C4}.png

自作のスパンを自動計装によるトレース内に反映させることができました。

おわりに

今回はOpenTelemetryのSpring Boot starterの機能を使用して、シンプルなSpring Bootアプリケーションを計装しました。アプリのコードをほとんど修正することなく充実したトレースを得ることができるのは凄いですね。手動で計装する場合でも、簡単にテレメトリを作るための機能が揃っていました。
OpenTelemetryについては最近知ったのですが、OpenTelemetryのAdvent Calendarがあるくらい流行っているようですね。今後サポートされるテレメトリや言語、バックエンドなども増えていくと思うので、これからもOpenTelemetryに注目していきたいと思います。
ここまで読んでくださった方、どうもありがとうございました。

12
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?