CLOVER🍀

That was when it all began.

OpenTelemetry/Jaeger/JAX-RS/MySQLで、Distributed Tracing

これは、なにをしたくて書いたもの?

OpenTelemetryについて、1度見ておきたいと思いまして。

OpenTelemetryの前身のひとつであるOpenTracingを使ったエントリーを過去に書いていたので、こちらをOpenTelemetryに置き換える形で
進めていくことにします。

Jaeger/OpenTracing API/JAX-RS/MySQLで、Distributed Tracing - CLOVER🍀

OpenTracingとOpenCensus

OpenTelemetryは、OpenTracingとOpenCensusが統合されたものです。概要は、InfoQの記事を見るのが良いと思います。

Merging OpenTracing and OpenCensus into a Single Distributed Tracing Framework

OpenTelemetryがメトリック仕様のロードマップを発表

OpenTracingはトレースAPIを提供し、OpenCensusはトレースAPIとメトリクスAPIを提供するものでした。

現在は両プロジェクトとも、OpenTelemetryを使うことを勧めています。

The OpenTracing project is deprecated. Please use OpenTelemetry, which provides backwards compatibility with OpenTracing.

The OpenTracing project

OpenCensus and OpenTracing have merged to form OpenTelemetry, which serves as the next major version of OpenCensus and OpenTracing. OpenTelemetry will offer backwards compatibility with existing OpenCensus integrations, and we will continue to make security patches to existing OpenCensus libraries for two years.

OpenCensus

OpenTelemetry

では、OpenTelemetryについて見ていきましょう。

OpenTelemetry

Webサイトの説明を見ると、OpenTelemetryとはツール、APISDKのコレクションであり、テレメトリーデータ(メトリクス、ログ、トレース)を
計測、生成、収集およびエクスポートして、ソフトウェアのパフォーマンスと動作の分析に役立てるものだということが書かれています。

OpenTelemetry is a collection of tools, APIs, and SDKs. Use it to instrument, generate, collect, and export telemetry data (metrics, logs, and traces) to help you analyze your software’s performance and behavior.

ドキュメントはこちら。

Documentation | OpenTelemetry

まずは、ざっくりOpenTelemetryの概要を見ていきます。

コンセプトからたどっていきましょう。

OpenTelemetry Concepts | OpenTelemetry

What is OpenTelemetry? | OpenTelemetry

OpenTelemetryは、以下のコンポーネントで構成されているそうです。

  • Cross-Languageな仕様
  • テレメトリーデータを収集、変換、エクスポートするためのツール(コレクター)
  • 言語ごとのSDK
  • 自動instrumentationとcontribパッケージ

Components | OpenTelemetry

コンポーネント(というか仕様)は、以下の要素で定義されています。

  • API … トレース、メトリクス、ログデータを生成、関連付けるためのデータ型とオペレーションを定義する
  • SDKAPIの言語固有の実装要件を定義する
  • データ(プロトコル) … OpenTelemetryバックエンドがサポートできるOTLP(OpenTelemetry Line Protocol)を定義する

コンポーネントのステータスは、以下にまとめられています。

Status | OpenTelemetry

コレクター、言語SDK、自動instrumentationについてもう少し掘り下げられているので、見てみましょう。

  • コレクター
    • テレメトリーデータを受信、処理およびエクスポートできるプロキシ
    • 複数の形式(OTLP、Jaeger、Prometheus、その他商用、独自のツールなど)でのテレメトリーデータの受信と、ひとつ以上のバックエンドへのデータの送信をサポートする
    • エクスポートされる前のデータの処理やフィルタリングも可能
    • 詳細は Components | OpenTelemetry
  • 言語SDK
  • 自動instrumentation

それぞれの「詳細」ページも、少し見ていきましょうか。

データソース

データソースに関するページは、こちら。

Signals | OpenTelemetry

データソースは、以下のようなものが定義されています。

  • トレース
    • 単一のリクエストを追跡する単位
    • トレース内は、「スパン」という単位で構成され、トレースはスパンのツリーとして表現される
    • OpenTelemetryではスパンの作成や管理に使う、tracerインターフェースを提供する
    • トレースの仕様は Specification / Tracing Signal を参照
  • メトリクス
    • サービスの実行中にキャプチャされた測定値
    • 測定値自体だけではなく、キャプチャされた時間と関連するメタデータで構成される(メトリクスイベント)
    • counter、measure、observerの3つの計測方法を定義している
    • メトリクスの仕様は Specification / Metric Signal を参照
  • ログ
    • メタデータを使用した、構造化(こちらが推奨)または非構造化のタイムスタンプ付きテキストレコード
    • スパンに添付することが可能
    • OpenTelemetryでは、分散トレーシングやメトリクスの一部ではないデータを、すべてログとして扱う
    • ログの仕様は Specification / Log Signal を参照
  • Baggage
    • 名前と値のペアのこと
    • 同じトランザクションにおいて、前のサービスにより提供される属性を使用して、イベントに対してインデックス付けする目的としている
    • Baggageの値はメトリクスの追加のディメンションや、ログやトレースの追加のコンテキストとして利用される
        • あるWebサービスで、どのサービスからリクエストが送信されたのかをコンテキストに含める
        • SaaSプロバイダーで、リクエストを行ったAPIユーザーやトークンに関する情報をコンテキストに含める
        • ブラウザの情報をコンテキストに含めることで、特定のブラウザで問題が起こっていることを確認する
    • Baggageの仕様は Specification / Baggage Signal を参照
Instrumenting

Instrumentingには、主にSDKに関する話が書かれています。

Instrumentation | OpenTelemetry

提供形態は言語ごとに差があるようですが、たとえばJavaの場合は以下になります。

  • コア … アプリケーションに手動で組み込むためのAPIおよびSDK
  • instrumentation … コア機能に加えて、様々なライブラリやフレームワークへ自動でinstrumentationを行う機能
  • contrib … JMXメトリクス収集など、オプションのコンポーネント

あとは、instrumentationを自動または手動で行うための手順のイメージが書かれています。

自動instrumentationについてはさらに別ページで解説があり、APIおよびSDKを使ったライブラリの作成方法(APIの使い方)が書かれている
感じですね。

Libraries | OpenTelemetry

データ収集

最後に、データ収集について

Components | OpenTelemetry

データ収集にはOpenTelemetry Collectorという、テレメトリーデータを受信、処理およびエクスポートする方法を提供するものです。

OpenTelemetry Collectorには、以下の2つの実行形態があります。

また、OpenTelemetry Collectorは以下のコンポーネントで構成されます。

  • レシーバー … データをコレクターに取り込む。これは、PushまたはPullで行うことができる
  • プロセッサー … 受信したデータに処理を行う
  • エクスポーター … 受信したデータを送信する。これは、PushまたはPullで行うことができる
用語集

いろいろ言葉が出てきましたが…用語集も見るとよいでしょう。

Glossary | OpenTelemetry

と、説明ばかりが続いたので、そろそろ本題に戻ってOpenTelemetryを使ってみましょう。

OpenTelemetry Java instrumentation

今回はJavaを使うので、Java用のinstrumentationのドキュメントを見てみます。

Java | OpenTelemetry

importするBOMについても書いてあります。
※自動instrumentationのみを使用する場合は、特にBOMやAPIへの依存関係は必要ありません

Javaの自動instrumentationについてのドキュメントは、こちら。

Automatic Instrumentation | OpenTelemetry

自動instrumentationで、サポートされているライブラリやフレームワーク等のリストは、こちら。

opentelemetry-java-instrumentation/docs/supported-libraries.md at main · open-telemetry/opentelemetry-java-instrumentation · GitHub

自動instrumentationが適用できない場合は、手動での適用になります。

Manual Instrumentation | OpenTelemetry

今回は、JAX-RSJDBCを選択して自動instrumentationを適用します。

お題と環境

最初に書いたとおり、こちらのエントリーをOpenTelemetryに置き換える形で書いていきます。

Jaeger/OpenTracing API/JAX-RS/MySQLで、Distributed Tracing - CLOVER🍀

今回扱うのは、OpenTelemetryの"トレーシング"のみです。メトリクスなども含めようかと思いましたが、トレーシングだけでだいぶ
苦労したのでここで区切ります…。

構成としては、2つのAPIサーバーを用意して間をHTTPでつなぎます。また、背後のAPIサーバーの後ろにはMySQLを配置してアクセスします。

api-frontend[JAX-RS Server - JAX-RS Client] → api-backend[JAX-RS Server - Doma 2] → MySQL

APIサーバーにはOpenTelemetry Collectorのエージェントを組み込み、自動instrumentationを使用してJAX-RSのクライアントおよびサーバー、
JDBCをトレーシングします。

一方でテレメトリーデータは、以下のように流れていくようにします。OpenTelemetryのエージェントがOpenTelemetry Collector Gateway
データを送り、さらにJaegerにエクスポートする(JaegerがOpenTelemetry Collector Gatewayのバックエンドとなる)形ですね。

OpenTeletry Collector Agent[api-frontendおよびapi-backend] → OpenTelemetry Collector Gateway → Jaeger

OpenTelemetry Collector GatewayとJaegerの位置関係が最初よくわからなくなっていたのですが、こちらの図を眺めつつコレクターの説明を
見返すとなんとかなりました。

Collector | OpenTelemetry

JAX-RSの実装にはRESTEasy(+Vert.x)を使用します。

また、環境についてはこちら。

$ java --version
openjdk 17.0.1 2021-10-19
OpenJDK Runtime Environment (build 17.0.1+12-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.1+12-Ubuntu-120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.4 (9b656c72d54e5bacbed989b64718c159fe39b537)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.1, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-99-generic", arch: "amd64", family: "unix"

MySQL

$ mysql --version
mysql  Ver 8.0.28 for Linux on x86_64 (MySQL Community Server - GPL)

OpenTelemetry Collector Gatewayは、こちらから取得します。

GitHub - open-telemetry/opentelemetry-collector-releases: OpenTelemetry Collector Official Releases

$ curl -OL https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.43.0/otelcol_0.43.0_linux_amd64.tar.gz
$ tar xf otelcol_0.43.0_linux_amd64.tar.gz
$ ./otelcol --version
otelcol version 0.43.0

Jaeger。

$ curl -OL https://github.com/jaegertracing/jaeger/releases/download/v1.31.0/jaeger-1.31.0-linux-amd64.tar.gz
$ tar xf jaeger-1.31.0-linux-amd64.tar.gz
$ cd jaeger-1.31.0-linux-amd64
$  ./jaeger-all-in-one version 
2022/02/09 15:08:48 maxprocs: Leaving GOMAXPROCS=8: CPU quota undefined
{"gitCommit":"845d73de7250b7ee703c35853412ca8dd53f8c2d","gitVersion":"v1.31.0","buildDate":"2022-02-04T20:49:21Z"}

Jaegerは、jaeger-all-in-oneで起動しておきます。

$ ./jaeger-all-in-one

各サーバーのIPアドレスは、以下のようになっているものとします。

  • api-frontend … 172.17.0.2
  • api-backend … 172.17.0.3
  • MySQL … 172.17.0.4
  • OpenTelemetry Collector Gateway … 172.17.0.5
  • Jaeger … 172.17.0.6

アプリケーションを作成する

では、アプリケーションを作成していきます。

テーブル定義

まずは、MySQLに定義するテーブルから。お題は書籍とします。

create table book (
  isbn varchar(14),
  title varchar(255),
  price int,
  primary key(isbn)
);
api-backend

アプリケーション側。api-backendから。

Maven依存関係とプラグインの設定。

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.jboss.resteasy</groupId>
                <artifactId>resteasy-bom</artifactId>
                <version>5.0.2.Final</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-vertx</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-jackson2-provider</artifactId>
        </dependency>
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-core</artifactId>
            <version>4.1.8</version>
        </dependency>
        <dependency>
            <groupId>org.seasar.doma</groupId>
            <artifactId>doma-core</artifactId>
            <version>2.51.0</version>
        </dependency>
        <dependency>
            <groupId>org.seasar.doma</groupId>
            <artifactId>doma-processor</artifactId>
            <version>2.51.0</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.6.3</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

RESTEasy+Vert.x Container、MySQLへのアクセスにはDoma 2を使用します。Spring BootのMaven Pluginは、Uber JARの作成に
利用しているだけです。

こちらを見ているとRESTEasyは5.0.xにしておいた方が良さそうだったので、5.0.2.Finalを選びました(現時点での最新版は6.0.0.Final)。

https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/v1.10.1/instrumentation/jaxrs/jaxrs-2.0/jaxrs-2.0-resteasy-3.1/javaagent/build.gradle.kts

ソースコードは、さらっと書きます。

Doma 2のEntityクラス。

src/main/java/org/littlewings/opentelemetry/backend/Book.java

package org.littlewings.opentelemetry.backend;

import org.seasar.doma.Column;
import org.seasar.doma.Entity;
import org.seasar.doma.Id;

@Entity
public class Book {
    @Id
    String isbn;

    @Column
    String title;

    @Column
    Integer price;

    // getter/setterは省略
}

Doma 2のDaoインターフェース。

src/main/java/org/littlewings/opentelemetry/backend/BookDao.java

package org.littlewings.opentelemetry.backend;

import java.util.List;

import org.seasar.doma.Dao;
import org.seasar.doma.Insert;
import org.seasar.doma.Select;
import org.seasar.doma.Sql;

@Dao
public interface BookDao {
    @Insert
    public int insert(Book book);

    @Sql("select /*%expand*/* from book")
    @Select
    public List<Book> findAll();

    @Sql("select /*%expand*/* from book where isbn = /* isbn */'1'")
    @Select
    public Book findByIsbn(String isbn);
}

Doma 2のConfigクラス。

src/main/java/org/littlewings/opentelemetry/backend/DomaConfig.java

package org.littlewings.opentelemetry.backend;

import javax.sql.DataSource;

import org.seasar.doma.jdbc.Config;
import org.seasar.doma.jdbc.Naming;
import org.seasar.doma.jdbc.dialect.Dialect;
import org.seasar.doma.jdbc.dialect.MysqlDialect;
import org.seasar.doma.jdbc.tx.LocalTransactionDataSource;
import org.seasar.doma.jdbc.tx.LocalTransactionManager;
import org.seasar.doma.jdbc.tx.TransactionManager;

public class DomaConfig implements Config {
    private static final DomaConfig CONFIG = new DomaConfig();

    Dialect dialect;

    LocalTransactionDataSource dataSource;

    TransactionManager transactionManager;

    public static DomaConfig singleton() {
        return CONFIG;
    }

    private DomaConfig() {
        dialect = new MysqlDialect();
        dataSource = new LocalTransactionDataSource(
                "jdbc:mysql://172.17.0.4:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_0900_bin",
                "kazuhira",
                "password"
        );
        transactionManager = new LocalTransactionManager(dataSource.getLocalTransaction(getJdbcLogger()));
    }

    @Override
    public DataSource getDataSource() {
        return dataSource;
    }

    @Override
    public Dialect getDialect() {
        return dialect;
    }

    @Override
    public TransactionManager getTransactionManager() {
        return transactionManager;
    }

    @Override
    public Naming getNaming() {
        return Naming.SNAKE_LOWER_CASE;
    }
}

JAX-RSリソースクラス。リクエストやレスポンスには、Entityクラスをそのまま使います。

src/main/java/org/littlewings/opentelemetry/backend/BookResource.java

package org.littlewings.opentelemetry.backend;

import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.seasar.doma.jdbc.tx.TransactionManager;

@Path("book")
public class BookResource {
    TransactionManager tm = DomaConfig.singleton().getTransactionManager();
    BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Book> findAll() {
        return tm.required(() -> bookDao.findAll());
    }

    @GET
    @Path("{isbn}")
    @Produces(MediaType.APPLICATION_JSON)
    public Book findByIsbn(@PathParam("isbn") String isbn) {
        return tm.required(() -> bookDao.findByIsbn(isbn));
    }

    @PUT
    @Path("{isbn}")
    @Consumes(MediaType.APPLICATION_JSON)
    public void register(@PathParam("isbn") String isbn, Book book) {
        tm.required(() -> bookDao.insert(book));
    }
}

JAX-RS Applicationのサブクラス。

src/main/java/org/littlewings/opentelemetry/backend/JaxrsActivator.java

package org.littlewings.opentelemetry.backend;

import java.util.Set;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("")
public class JaxrsActivator extends Application {
    @Override
    public Set<Object> getSingletons() {
        return Set.of(new BookResource());
    }
}

mainクラス。

src/main/java/org/littlewings/opentelemetry/backend/App.java

package org.littlewings.opentelemetry.backend;

import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import org.jboss.logging.Logger;
import org.jboss.resteasy.plugins.server.vertx.VertxRequestHandler;
import org.jboss.resteasy.plugins.server.vertx.VertxResteasyDeployment;
import org.jboss.resteasy.spi.ResteasyDeployment;

public class App {
    public static void main(String... args) {
        Logger logger = Logger.getLogger(App.class);

        Vertx vertx = Vertx.vertx();
        HttpServer server = vertx.createHttpServer();

        ResteasyDeployment deployment = new VertxResteasyDeployment();
        deployment.setApplication(new JaxrsActivator());
        deployment.start();

        server.requestHandler(new VertxRequestHandler(vertx, deployment));

        server.listen(9080);

        logger.infof("start server.");
    }
}

api-backendは、9080ポートでリッスンすることにしました。

ここまで、特にOpenTelemetryに関するキーワードは登場しません。あくまで、JAX-RSJDBCを使ったアプリケーションです。

パッケージング。

$ mvn package

ここで、通常なら以下のようにしてアプリケーションを起動するわけですが。

$ java -jar target/api-backend-0.0.1-SNAPSHOT.jar

今回はOpenTelemetry Collector Agentとしての設定を行います。

こちらのドキュメントに沿って、OpenTelemetry Collector AgentのJARファイルを取得します。

Automatic Instrumentation | OpenTelemetry

$ curl -OL https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.10.1/opentelemetry-javaagent.jar

こちらのJARファイルを、-javaagent:path/to/opentelemetry-javaagent.jarという形で実行時に指定します。

エージェントの設定は、環境変数システムプロパティ、設定ファイルの選択肢から行えるのですが、今回は設定ファイルで行うことに
します。

エージェントで設定できる項目は、こちらを参照します。

https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/v1.10.1/docs/agent-config.md

https://github.com/open-telemetry/opentelemetry-java/blob/v1.10.1/sdk-extensions/autoconfigure/README.md

https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/v1.10.1/docs/config/common.md

opentelemetry-java-instrumentation/docs/suppressing-instrumentation.md at v1.10.1 · open-telemetry/opentelemetry-java-instrumentation · GitHub

設定は、こんな感じにしました。最低限の設定ですね。

otel-agent.properties

otel.service.name=api-backend
otel.traces.exporter=otlp
otel.exporter.otlp.endpoint=http://172.17.0.5:4317

otel.service.nameは論理的なサービス名、otel.traces.exporterはトレーシングで使用するエクスポーター、otel.exporter.otlp.endpoint
OTLP(OpenTelemetry Line Protorol)のエンドポイントを指定します。

OTLPのエンドポイントは、OpenTelemetry Collector Gatewayを指します。
otel.traces.exporterotlpを指定しているのは、OpenTelemetry Collector Gatewayの設定を見る必要があります。

OpenTelemetry Collector Agentとその設定ファイルを組み込んでの起動方法は、以下のようになります。

$ java -javaagent:opentelemetry-javaagent.jar \
    -Dotel.javaagent.configuration-file=otel-agent.properties \
    -jar target/api-backend-0.0.1-SNAPSHOT.jar
api-frontend

最後のアプリケーションは、api-frontendです。

Maven依存関係とプラグインの設定。

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.jboss.resteasy</groupId>
                <artifactId>resteasy-bom</artifactId>
                <version>5.0.2.Final</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-vertx</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-jackson2-provider</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-client-vertx</artifactId>
        </dependency>
        <!-- vertx-core 4.1.0がresteasy-client-vertxの推移的依存関係に含まれる -->
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-core</artifactId>
            <version>4.1.8</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.6.3</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

今度は、JAX-RSのクライアントも入ります。vertx-coreresteasy-client-vertxから推移的に依存関係に入るのですが、今回は
api-backendとバージョンを合わせるために明示しました。resteasy-vertxの方は、vertx-coreprovidedになっているんですよね。

ソースコードは、こちらもさらっと。

リクエストやレスポンスで使うクラス。

src/main/java/org/littlewings/opentelemetry/frontend/Book.java

package org.littlewings.opentelemetry.frontend;

public class Book {
    String isbn;

    String title;

    Integer price;

    // getter/setterは省略
}

JAX-RSリソースクラス。api-backendを呼び出すJAX-RSクライアントも含みます。

src/main/java/org/littlewings/opentelemetry/frontend/BookResource.java

package org.littlewings.opentelemetry.frontend;

import java.util.List;
import java.util.Optional;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;

import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.client.jaxrs.engines.vertx.VertxClientHttpEngine;

@Path("book")
public class BookResource {
    Client client;

    public BookResource() {
        VertxClientHttpEngine engine = new VertxClientHttpEngine();
        client = ((ResteasyClientBuilder) ClientBuilder.newBuilder()).httpEngine(engine).build();
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @SuppressWarnings("unchecked")
    public List<Book> findAll() {
        Response response = null;

        try {
            response = client
                    .target("http://172.17.0.3:9080/book")
                    .request()
                    .get();

            return response.readEntity(List.class);
        } finally {
            Optional.ofNullable(response).ifPresent(Response::close);
        }
    }

    @GET
    @Path("{isbn}")
    @Produces(MediaType.APPLICATION_JSON)
    public Book findByIsbn(@PathParam("isbn") String isbn) {
        Response response = null;

        try {
            response = client
                    .target(UriBuilder.fromUri("http://172.17.0.3:9080/book/{isbn}").build(isbn))
                    .request()
                    .get();

            return response.readEntity(Book.class);

        } finally {
            Optional.ofNullable(response).ifPresent(Response::close);
        }
    }

    @PUT
    @Path("{isbn}")
    @Consumes(MediaType.APPLICATION_JSON)
    public void register(@PathParam("isbn") String isbn, Book book) {
        Response response = null;

        try {
            response = client
                    .target(UriBuilder.fromUri("http://172.17.0.3:9080/book/{isbn}").build(isbn))
                    .request()
                    .put(Entity.entity(book, MediaType.APPLICATION_JSON_TYPE));
        } finally {
            Optional.ofNullable(response).ifPresent(Response::close);
        }
    }
}

JAX-RS Applicationのサブクラス。

src/main/java/org/littlewings/opentelemetry/frontend/JaxrsActivator.java

package org.littlewings.opentelemetry.frontend;

import java.util.Set;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("")
public class JaxrsActivator extends Application {
    @Override
    public Set<Object> getSingletons() {
        return Set.of(new BookResource());
    }
}

mainクラス。

src/main/java/org/littlewings/opentelemetry/frontend/App.java

package org.littlewings.opentelemetry.frontend;

import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import org.jboss.logging.Logger;
import org.jboss.resteasy.plugins.server.vertx.VertxRequestHandler;
import org.jboss.resteasy.plugins.server.vertx.VertxResteasyDeployment;
import org.jboss.resteasy.spi.ResteasyDeployment;

public class App {
    public static void main(String... args) {
        Logger logger = Logger.getLogger(App.class);

        Vertx vertx = Vertx.vertx();
        HttpServer server = vertx.createHttpServer();

        ResteasyDeployment deployment = new VertxResteasyDeployment();
        deployment.setApplication(new JaxrsActivator());
        deployment.start();

        server.requestHandler(new VertxRequestHandler(vertx, deployment));

        server.listen(8080);

        logger.infof("start server.");
    }
}

api-frontendは、8080ポートでリッスンすることにします。

OpenTelemetry Collector Agentの設定ファイルは、こちら。

otel-agent.properties

otel.service.name=api-frontend
otel.traces.exporter=otlp
otel.exporter.otlp.endpoint=http://172.17.0.5:4317

api-backendの時とは、otel.service.nameが違うだけです。

パッケージングして

$ mvn package

OpenTelemetry Collector Agentを組み込んで起動。

$ java -javaagent:opentelemetry-javaagent.jar \
    -Dotel.javaagent.configuration-file=otel-agent.properties \
    -jar target/api-frontend-0.0.1-SNAPSHOT.jar

ここまでで、アプリケーション側の準備は完了です。

Jaeger

Jaegerは、単純に起動しているわけです。

$ ./jaeger-all-in-one

OpenTelemetry Collector Gatewayを設定する

最後は、OpenTelemetry Collector Gatewayです。

GitHub - open-telemetry/opentelemetry-collector-releases: OpenTelemetry Collector Official Releases

ダウンロードは、こんな感じで行っていました。

$ curl -OL https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.43.0/otelcol_0.43.0_linux_amd64.tar.gz
$ tar xf otelcol_0.43.0_linux_amd64.tar.gz
$ ./otelcol --version
otelcol version 0.43.0

OpenTelemetry Collector Gatewayには、設定が必要になります。

オフィシャルなDockerイメージに含まれているデフォルトの設定ファイルは、以下のようです。

https://github.com/open-telemetry/opentelemetry-collector-releases/blob/v0.43.0/configs/otelcol.yaml

ですが、この設定を見てもよくわからないので…。

OpenTelemetry Collector Gatewayの設定方法は、こちらを参照します。

Configuration | OpenTelemetry

設定ファイルはYAMLで作成し、まずは以下の要素が基本になります。

  • receivers … OpenTelemetry Collector Gatewayがデータを取り込む方法(pullおよびpush)をレシーバーを定義する
  • processors … 取り込んだデータを受信してから、エクスポートするまでにデータを処理するプロセッサーを定義する
    • 使用可能なProcessorsは こちら
    • 説明は こちら も見た方がよいでしょう
  • exporters … 1つ以上のバックエンドに、データを送信する(pushまたはpull)エクスポーターを定義する

また、extensionsという要素もあり、テレメトリーデータを扱わないタスクで使用できます。たとえば、ヘルスチェックやサービスディスカバリー
などです。

これらの要素(receivers、processors、exporters、extensions)は定義しただけでは有効にならず、servicesという要素で有効にする必要が
あります。

otel-collector-config.yaml

receivers:
  otlp:
    protocols:
      grpc:
      http:
#  jaeger:
#    protocols:
#      grpc:
#      thrift_binary:
#      thrift_compact:
#      thrift_http:

processors:
  batch:

exporters:
  logging:
    loglevel: debug
  jaeger:
    endpoint: 172.17.0.6:14250
    tls:
      insecure: true

extensions:
  health_check:
  pprof:
  zpages:
  memory_ballast:
    size_mib: 512

service:
  extensions: [health_check, pprof, zpages,memory_ballast]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [logging,jaeger]
#    metrics:
#      receivers: [otlp]
#      processors: [batch]
#      exporters: [logging]

使っていない部分はコメントアウトしていますが、こんな感じの設定です。

  • receivers
    • otlp … OTLP(OpenTelemetry Line Protocol)でテレメトリーデータを受け取る
  • processors
    • batch … データをバッチ処理し、データの送信に必要な接続回数を減らす
  • exporters
    • logging … データをコンソールにログ出力する(デバッグ用途)
    • jaeger … データをJaegerに送信する
  • extensions
    • とりあえず、ドキュメントに記載のあるextensionを並べておきました
  • services
    • 定義したextensionをすべて有効に
    • pipelines
      • トレース用として、receiver(OTLP) → processor(batch) → exporter(logging、Jaeger)となるようにパイプラインを構成
        • 2つのエクスポーターを利用
      • メトリクス用のパイプラインは例示のみでコメントアウト

今回はコメントアウトしていますが、レシーバーとしてJaegerのプロトコルで受け付けることもできます。

receivers:
  otlp:
    protocols:
      grpc:
      http:
#  jaeger:
#    protocols:
#      grpc:
#      thrift_binary:
#      thrift_compact:
#      thrift_http:

有効にするかどうかは、servicesでのパイプライン定義次第です。

ここで、OpenTelemetry Collector Agentの設定に立ち戻ると、トレース用のテレメトリーデータの送信先にOTLPを選択していることが繋がります。

otel-agent.properties

otel.service.name=api-backend
otel.traces.exporter=otlp
otel.exporter.otlp.endpoint=http://172.17.0.5:4317

あとは、設定ファイルを使用してOpenTelemetry Collector Gatewayを起動します。

$ ./otelcol --config otel-collector-config.yaml

動作確認

では、動作確認してみます。

api-frontendに向けて、データを登録してみます。

$ curl -XPUT -H 'Content-Type: application/json' \
     172.17.0.2:8080/book/978-4774182179 \
     -d '{"isbn": "978-4774182179", "title": "[改訂新版]Spring入門 ――Javaフレームワーク ・より良い設計とアーキテクチャ", "price": 4104}'


$ curl -XPUT -H 'Content-Type: application/json' \
     172.17.0.2:8080/book/978-4798142470 \
     -d '{"isbn": "978-4798142470", "title": "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", "price": 4320}'


$ curl -XPUT -H 'Content-Type: application/json' \
     172.17.0.2:8080/book/978-4777519699 \
     -d '{"isbn": "978-4777519699", "title": "はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発", "price": 2700}'

この時、OpenTelemetry Collector Gatewayには、exportersで設定したlogging exporterにより標準出力にログが書き出されます。

2022-02-10T15:31:59.919Z        INFO    loggingexporter/logging_exporter.go:40  TracesExporter  {"#spans": 3}
2022-02-10T15:31:59.919Z        DEBUG   loggingexporter/logging_exporter.go:49  ResourceSpans #0
Resource SchemaURL: https://opentelemetry.io/schemas/1.8.0
Resource labels:
     -> container.id: STRING(1b081521d1fb7ec1973065518fd8f58cb89a7f547c258305dd33474bd936c15d)
     -> host.arch: STRING(amd64)
     -> host.name: STRING(api-backend)
     -> os.description: STRING(Linux 5.4.0-99-generic)
     -> os.type: STRING(linux)
     -> process.command_line: STRING(/opt/java/openjdk:bin:java -javaagent:opentelemetry-javaagent.jar -Dotel.javaagent.configuration-file=otel-agent.properties)
     -> process.executable.path: STRING(/opt/java/openjdk:bin:java)
     -> process.pid: INT(10)
     -> process.runtime.description: STRING(Eclipse Adoptium OpenJDK 64-Bit Server VM 17.0.2+8)
     -> process.runtime.name: STRING(OpenJDK Runtime Environment)
     -> process.runtime.version: STRING(17.0.2+8)
     -> service.name: STRING(api-backend)
     -> telemetry.auto.version: STRING(1.10.1)
     -> telemetry.sdk.language: STRING(java)
     -> telemetry.sdk.name: STRING(opentelemetry)
     -> telemetry.sdk.version: STRING(1.10.1)
InstrumentationLibrarySpans #0
InstrumentationLibraryMetrics SchemaURL:
InstrumentationLibrary io.opentelemetry.jaxrs-2.0-common 1.10.1
Span #0
    Trace ID       : 7764aa073ebbd4015ebc1955cff85959
    Parent ID      : 45d4f51a90086df5
    ID             : b5a29f5f12419436
    Name           : BookResource.register
    Kind           : SPAN_KIND_INTERNAL
    Start time     : 2022-02-10 15:31:58.899855 +0000 UTC
    End time       : 2022-02-10 15:31:58.9657288 +0000 UTC
    Status code    : STATUS_CODE_UNSET
    Status message :
Attributes:
     -> thread.name: STRING(vert.x-eventloop-thread-0)
     -> thread.id: INT(22)
     -> code.function: STRING(register)
     -> code.namespace: STRING(org.littlewings.opentelemetry.backend.BookResource)
InstrumentationLibrarySpans #1
InstrumentationLibraryMetrics SchemaURL:
InstrumentationLibrary io.opentelemetry.netty-4.1 1.10.1
Span #0
    Trace ID       : 7764aa073ebbd4015ebc1955cff85959
    Parent ID      : 738bf23b4280535f
    ID             : 45d4f51a90086df5
    Name           : /book/{isbn}
    Kind           : SPAN_KIND_SERVER
    Start time     : 2022-02-10 15:31:58.898511038 +0000 UTC
    End time       : 2022-02-10 15:31:58.966471625 +0000 UTC
    Status code    : STATUS_CODE_UNSET
    Status message :
Attributes:
     -> net.peer.ip: STRING(172.17.0.2)
     -> thread.name: STRING(vert.x-eventloop-thread-0)
     -> http.method: STRING(PUT)
     -> thread.id: INT(22)
     -> http.scheme: STRING(http)
     -> http.host: STRING(172.17.0.3:9080)
     -> http.target: STRING(/book/978-4774182179)
     -> http.user_agent: STRING(Vertx)
     -> http.flavor: STRING(1.1)
     -> net.peer.port: INT(55674)
     -> http.status_code: INT(204)
InstrumentationLibrarySpans #2
InstrumentationLibraryMetrics SchemaURL:
InstrumentationLibrary io.opentelemetry.jdbc 1.10.1
Span #0
    Trace ID       : 7764aa073ebbd4015ebc1955cff85959
    Parent ID      : b5a29f5f12419436
    ID             : 8c10a2ac662effd8
    Name           : INSERT practice.book
    Kind           : SPAN_KIND_CLIENT
    Start time     : 2022-02-10 15:31:58.933422769 +0000 UTC
    End time       : 2022-02-10 15:31:58.937315169 +0000 UTC
    Status code    : STATUS_CODE_UNSET
    Status message :
Attributes:
     -> thread.name: STRING(vert.x-eventloop-thread-0)
     -> thread.id: INT(22)
     -> db.name: STRING(practice)
     -> db.sql.table: STRING(book)
     -> db.operation: STRING(INSERT)
     -> db.statement: STRING(insert into book (isbn, title, price) values (?, ?, ?))
     -> db.system: STRING(mysql)
     -> net.peer.port: INT(3306)
     -> db.connection_string: STRING(mysql://172.17.0.4:3306)
     -> net.peer.name: STRING(172.17.0.4)
     -> db.user: STRING(kazuhira)

2022-02-10T15:32:00.097Z        info    jaegerexporter@v0.43.0/exporter.go:186  State of the connection with the Jaeger Collector backend       {"kind": "exporter", "name": "jaeger", "state": "READY"}

データの確認。

$ curl 172.17.0.2:8080/book
[{"isbn":"978-4774182179","title":"[改訂新版]Spring入門 ――Javaフレームワーク ・より良い設計とアーキテクチャ","price":4104},{"isbn":"978-4798142470","title":"Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発","price":4320}]


$ curl 172.17.0.2:8080/book/978-4774182179
{"isbn":"978-4774182179","title":"[改訂新版]Spring入門 ――Javaフレームワーク ・より良い設計とアーキテクチャ","price":4104}


$ curl 172.17.0.2:8080/book/978-4798142470
{"isbn":"978-4798142470","title":"Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発","price":4320}

では、Jaegerに送信されたデータを見てみましょう。

http://[Jaegerが動作しているホスト]:16686/にアクセスしてWeb UIを表示すると、2つのサービスが認識されています。

api-frontendを選んで「Find Traces」ボタンを押してみます。

こんな感じで、トレースデータが表示されます。

トレースデータを選択すると(今回はPUTを選択)、

各スパンの中身を表示できます。

TagsやProcessを開くと、詳細な情報も確認できます。

全件取得の例。

依存関係の表示。

トレースデータのみですか、今回はこんなところでしょうか。

まとめ

OpenTelemetry、Jaegerを使って、Distributed Tracing…というかトレースデータを収集、表示してみました。

OpenTelemetryを扱うのは初めてだったので、各構成要素の役割がなかなか押さえられず苦戦しましたが、とりあえずなんとかなりました。

今回はボリュームの関係でメトリクスやロギングは外しトレースのみにしましたが、それはまたそのうち…。

OpenTracingとOpenCensusから、OpenTelemetryになったあたりの情報も見ておいて良かったと思います。