CLOVER🍀

That was when it all began.

WildFlyのMicroProfile Telemetryサブシステムを使って、トレースを試す

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

WildFlyでOpenTelemetryを試してみたいということで、まずはMicroProfile Telemetryサブシステムを試してみたいと思います。

MicroProfile Telemetry(MicroProfile Telemetry Tracing)

MicroProfile Telemetryは現在バージョン1.0で、MicroProfile 6.0に含まれています。

MicroProfile 6.0 Release - MicroProfile

MicroProfile 5.0まではMicroProfile OpenTracingだったのですが、これが置き換えられました。

仕様はこちら。

MicroProfile Telemetry Tracing

GitHubリポジトリーはこちら。

GitHub - eclipse/microprofile-telemetry: microprofile telemetry

正確には、MicroProfile Telemetry Tracingですね。

こちらを見てみると、MicroProfile Telemetry TracingはOpenTelemetryによる分散トレーシングが有効になっている環境に、
MicroProfileアプリケーションが簡単に参加できるようにする仕様のようです。

This specification defines the behaviors that allow MicroProfile applications to easily participate in an environment where distributed tracing is enabled via OpenTelemetry (a merger between OpenTracing and OpenCensus).

MicroProfile Telemetry Tracing / Architecture

MicroProfile Telemetry Tracingのドキュメントおよび実装は、OpenTelemetry 1.13に準拠する必要があります。

This document and implementations MUST comply with the following OpenTelemetry 1.13 specifications:

MicroProfile Telemetry Tracing / Architecture

https://github.com/open-telemetry/opentelemetry-specification/blob/v1.13.0/specification/overview.md

また、OpenTelemetryのうちMicroProfile Telemetry Tracingが対象とするのはトレーシングのみで、メトリクスとロギングは対象外です。

それから、このエントリーを書いている時点でのOpenTelemetry仕様は1.25.0です。

OpenTelemetry Specification 1.25.0 | OpenTelemetry

今回は、この点は気にせず進めましょう。

MicroProfile Telemetry Tracingでは、自動Instrumentationと手動Instrumentation、エージェントによるInstrumentationの3つが
記載されています。

自動Instrumentationは、Jakarta RESTful Web Services(JAX-RS)のサーバー/クライアント、そしてMicroProfile REST Clientが
ソースコードを変更することなく分散トレーシングに参加できるようになるものです。

Jakarta RESTful Web Services (server and client), and MicroProfile REST Clients are automatically enlisted to participate in distributed tracing without code modification as specified in the Tracing API.

手動Instrumentationは、CDI管理Beanに対して@WithSpanアノテーションを付与する、もしくはSpanBuilderCDI管理されているSpan
使ってInstrumentationを明示的に行うものです。

エージェントによるInstrumentationは、OpenTelemetryのAutomatic Instrumentationを使用するものです。

Automatic Instrumentation | OpenTelemetry

Agent Configuration | OpenTelemetry

またMicroProfile Telemetry Tracingでは、以下のOpenTelemetryのAPICDI管理Beanとして提供する必要があることになっています。

  • io.opentelemetry.api.OpenTelemetry
  • io.opentelemetry.api.trace.Tracer
  • io.opentelemetry.api.trace.Span
  • io.opentelemetry.api.baggage.Baggage

OpenTelemetryのAPIを直接呼び出すSpan#currentBaggage#currentの実行結果は、CDI管理Beanとして取得するものと同じである
必要があります。

最後に重要なポイントとして、デフォルトではMicroProfile Telemetry Tracingは無効になっており、MicroProfile Config経由で
otel.sdk.disabled=falseを指定する必要があります。

MicroProfile Telemetry Tracing / Tracing Enablement

まずはこんなところですね。

WildFlyのMicroProfile Telemetryサブシステム

次にWildFlyに話を移しますが、MicroProfile Telemetry向けのサブシステムがあります。

WildFly Admin Guide / Subsystem configuration / MicroProfile Telemetry Subsystem Configuration

こちらはWildFly 28で追加されたようです。

WildFly 28 Beta1 is released!

この一方で、OpenTelemetryサブシステムもあります。

WildFly Admin Guide / Subsystem configuration / OpenTelemetry Subsystem Configuration

こちらはWildFly 25で、MicroProfile OpenTracingサブシステムを置き換える形でリリースされたようです。

WildFly 25 Beta1 is released!

最初、このドキュメントから入ったのでMicroProfile TelemetryサブシステムとOpenTelemetryサブシステムの位置づけの違いが
わからなかったのですが、

  • MicroProfile TelemetryサブシステムはMicroProfile Telemetry Tracingを使えるようにするもの
  • OpenTelemetryサブシステムは、OpenTelemetryライブラリーの設定(Exporter、Span Processor、Sampler)を行うもの

というもので、両者には依存関係がありMicroProfile TelemetryサブシステムはOpenTelemetryサブシステムが使えることが前提に
なっていますね。

実際のところ、MicroProfile Telemetryのレイヤー定義を見るとOpenTelemetryレイヤーに依存していることがわかります。

<?xml version="1.0" ?>
<layer-spec xmlns="urn:jboss:galleon:layer-spec:2.0" name="microprofile-telemetry">
    <props>
        <prop name="org.wildfly.rule.annotations" value="io.opentelemetry.instrumentation.annotations"/>
        <prop name="org.wildfly.rule.class" value="io.opentelemetry.api.*"/>
        <prop name="org.wildfly.rule.add-on-depends-on" value="only:cdi"/>
        <prop name="org.wildfly.rule.add-on" value="observability,microprofile-telemetry"/>
    </props>
    <dependencies>
        <layer name="cdi"/>
        <layer name="opentelemetry"/>
        <layer name="microprofile-config"/>
    </dependencies>
    <feature spec="subsystem.microprofile-telemetry"/>
</layer-spec>

https://github.com/wildfly/wildfly/blob/29.0.1.Final/galleon-pack/galleon-shared/src/main/resources/layers/standalone/microprofile-telemetry/layer-spec.xml

OpenTelemetryレイヤーはこちら。

https://github.com/wildfly/wildfly/blob/29.0.1.Final/galleon-pack/galleon-shared/src/main/resources/layers/standalone/opentelemetry/layer-spec.xml

ソースコードは、それぞれこのあたり。

SmallRye OpenTelemetry

ここまででMicroProfile Telemetry TracingとWildFlyのサブシステムを見てきましたが、WildFlyのMicroProfile Telemetry Tracingの実装は
なにか?というところが気になりますね。

SmallRye OpenTelemetryのようです。

GitHub - smallrye/smallrye-opentelemetry: SmallRye OpenTelemetry - A CDI and Jakarta REST implementation of OpenTelemetry Tracing

WildFly 29.0.1.Finalには、SmallRye OpenTelemetry 2.3.2が含まれています。

説明はこれくらいにして、実際に試していってみたいと思います。

お題

今回のお題は、以下のようにしましょう。

flowchart LR
    クライアント --> |curl/HTTP| A
    subgraph WildFly
    A[JAX-RS Server/API-A] --> |JAX-RS Client/HTTP| B[API-B]
    A --> |MicroProfile REST Client/HTTP| B[JAX-RS Server/API-B]
    end
    B --> |JDBC| D[(MySQL)]
    WildFly --> |テレメトリーデータ| J[Jaeger]

Webアプリケーションを2つ用意して、それぞれWildFlyにデプロイします(単純化して、ひとつのWildFlyに2つのWebアプリケーションを
デプロイします)。

ここでJAX-RSやMicroProfile REST Clientに対してトレーシングが有効になることを確認します。

なんとなくMySQLへのアクセスも置いていますが、こちらはMicroProfile Telemetry Tracingの対象外なのでアクセスだけCDI管理Beanで
トレースしたいと思います。

テレメトリーデータは、Jaegerで収集します。

2つのアプリケーションとデータベースを使うということで、テーブルのお題は書籍にします。

create table book(
    isbn varchar(15),
    title varchar(100),
    price int,
    primary key(isbn)
);

後述しますが、データベースにはMySQLを使います。

2つ目のアプリケーション(api-b)がMySQLにアクセスしますが、ひとつ目のアプリケーション(api-a)は単純なプロキシとして
振る舞う構成にします。

環境

今回の環境はこちら。

$ java --version
openjdk 17.0.8.1 2023-08-24
OpenJDK Runtime Environment (build 17.0.8.1+1-Ubuntu-0ubuntu122.04)
OpenJDK 64-Bit Server VM (build 17.0.8.1+1-Ubuntu-0ubuntu122.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.4 (dfbb324ad4a7c8fb0bf182e6d91b0ae20e3d2dd9)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.8.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.15.0-84-generic", arch: "amd64", family: "unix"

WildFlyは29.0.1.Finalを使い、172.17.0.2で動作しているものとします。

$ bin/standalone.sh --version
=========================================================================

  JBoss Bootstrap Environment

  JBOSS_HOME: /opt/wildfly

  JAVA: /opt/java/openjdk/bin/java

  JAVA_OPTS:  -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true  --add-exports=java.desktop/sun.awt=ALL-UNNAMED --add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-exports=java.naming/com.sun.jndi.url.ldap=ALL-UNNAMED --add-exports=java.naming/com.sun.jndi.url.ldaps=ALL-UNNAMED --add-exports=jdk.naming.dns/com.sun.jndi.dns=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED -Djava.security.manager=allow

=========================================================================

14:21:30,338 INFO  [org.jboss.modules] (main) JBoss Modules version 2.1.0.Final
WildFly Full 29.0.1.Final (WildFly Core 21.1.1.Final)

起動コマンド。

$ bin/standalone.sh \
    -Djboss.bind.address=0.0.0.0 \
    -Djboss.bind.address.management=0.0.0.0 \

JDBCドライバのデプロイと、DataSourceの作成は済んでいるものとします。

MySQLは、172.17.0.3で動作しているものとします。

 MySQL  localhost:3306 ssl  practice  SQL > select version();
+-----------+
| version() |
+-----------+
| 8.0.34    |
+-----------+
1 row in set (0.0007 sec)

Jaegerは、172.17.0.4で動作しているものとします。

$ ./jaeger-all-in-one version
2023/09/27 14:23:14 maxprocs: Leaving GOMAXPROCS=8: CPU quota undefined
2023/09/27 14:23:14 application version: git-commit=2d351c3f30072cae7f5755be20e34c2697b9e3b5, git-version=v1.49.0, build-date=2023-09-07T13:13:08Z
{"gitCommit":"2d351c3f30072cae7f5755be20e34c2697b9e3b5","gitVersion":"v1.49.0","buildDate":"2023-09-07T13:13:08Z"}

起動コマンド。

$ ./jaeger-all-in-one

WildFlyでMicroProfile Telemetryサブシステムを有効にする

まずはWildFlyのMicroProfile Telemetryサブシステムを有効にしましょう。

なのですが、MicroProfile TelemetryサブシステムはOpenTelemetryサブシステムが使えることが前提になっているので、最初に
OpenTelemetryサブシステムを有効にして、それからMicroProfile Telemetryサブシステムを有効にする必要があります。

WildFly Admin Guide / Subsystem configuration / MicroProfile Telemetry Subsystem Configuration

管理CLIで接続。

$ bin/jboss-cli.sh -c

OpenTelemetryのエクステンションおよびサブシステムを追加。

[standalone@localhost:9990 /] /extension=org.wildfly.extension.opentelemetry:add()
[standalone@localhost:9990 /] /subsystem=opentelemetry:add()

OpenTelemetryサブシステムの設定。

[standalone@localhost:9990 /] /subsystem=opentelemetry:write-attribute(name=exporter-type,value=otlp)
[standalone@localhost:9990 /] /subsystem=opentelemetry:write-attribute(name=endpoint,value=http://172.17.0.4:4317)

今回は、JaegerにOTLPプロトコルテレメトリーデータを送るようにします。

WildFly Admin Guide / Subsystem configuration / OpenTelemetry Subsystem Configuration / Configuration

続いて、MicroProfile Telemetryのエクステンションとサブシステムを追加。

[standalone@localhost:9990 /] /extension=org.wildfly.extension.microprofile.telemetry:add()
[standalone@localhost:9990 /] /subsystem=microprofile-telemetry:add()

ここまでやったら、WildFlyを再起動します。

[standalone@localhost:9990 /] reload

ちょっとハマったのは、以下でしたね。

[standalone@localhost:9990 /] /subsystem=opentelemetry:write-attribute(name=endpoint,value=http://172.17.0.4:4317)

プロトコルはJaegerとOTLPのどちらかを選ぶことになるのですが、otlp(OpenTelemetry protocol)を選んでも、Jaegerの
gRPCのポートを選ぶ必要があります(gRPCは4317、HTTPは4318)

endpoint: The URL via which OpenTelemetry will push traces. The default is Jaeger’s gRPC-based endpoint, http://localhost:14250

14250は、Jaegerエージェントが使うgRPCでのポートです。

最初はHTTPポートを指定していたら、あとでこんなエラーに悩まされることになりました…。

22:51:53,695 SEVERE [io.opentelemetry.exporter.internal.grpc.OkHttpGrpcExporter] (OkHttp http://172.17.0.4:4318/...) Failed to export spans. The request could not be executed. Full error message: FRAME_SIZE_ERROR: 4740180

また、今回はデフォルトのstandalone.xmlを使っているのでOpenTelemetryサブシステムやMicroProfile Telemetryサブシステムを
それぞれ有効化していますが、standalone-microprofile.xmlを使ってWildFlyを実行すれば最初から有効になっているみたいです。
もっとも、テレメトリーデータの送信先の設定などは必要ですが。

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

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

api-bを作成する

最初に、api-aの裏にいるapi-bから作成していきます。

Maven依存関係など。

    <groupId>org.littlewings</groupId>
    <artifactId>api-b</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.opentelemetry</groupId>
                <artifactId>opentelemetry-bom</artifactId>
                <version>1.20.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>io.opentelemetry.instrumentation</groupId>
                <artifactId>opentelemetry-instrumentation-bom</artifactId>
                <version>1.20.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.eclipse.microprofile</groupId>
                <artifactId>microprofile</artifactId>
                <version>6.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-web-api</artifactId>
            <version>10.0.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry.instrumentation</groupId>
            <artifactId>opentelemetry-instrumentation-annotations</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.eclipse.microprofile.config</groupId>
            <artifactId>microprofile-config-api</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>api-b</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.4.0</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
        </plugins>
    </build>

OpenTelemetry SDKおよびMicroProfile Configへの依存関係が入っているところがポイントです。api-bに関しては、明示的に
MicroProfile Configを追加しなくてもよいのですが。

Jakarta Persistence(JPA)のエンティティ。

src/main/java/org/littlewings/wildfly/telemetry/b/Book.java

package org.littlewings.wildfly.telemetry.b;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "book")
public class Book {
    @Id
    private String isbn;
    private String title;
    private Integer price;

    // getter/setterは省略
}

JPAEntityManagerを使ってデータベースにアクセスを行うクラス。

src/main/java/org/littlewings/wildfly/telemetry/b/BookRepository.java

package org.littlewings.wildfly.telemetry.b;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;

import java.util.List;

@ApplicationScoped
public class BookRepository {
    @PersistenceContext
    private EntityManager entityManager;

    @Inject
    private Tracer tracer;

    @WithSpan
    public Book findByIsbn(String isbn) {
        return entityManager.find(Book.class, isbn);
    }

    @WithSpan
    public List<Book> findAll() {
        return entityManager
                .createQuery("select b from Book b order by b.price desc", Book.class)
                .getResultList();
    }

    public Book save(Book book) {
        Span span = tracer.spanBuilder("BookRepository.save")
                .setSpanKind(SpanKind.INTERNAL)
                .setParent(Context.current().with(Span.current()))
                .startSpan();

        try {
            if (entityManager.find(Book.class, book.getIsbn()) != null) {
                entityManager.merge(book);
            } else {
                entityManager.persist(book);
            }

            return book;
        } finally {
            span.end();
        }
    }
}

このクラスでは、OpenTelemetry InstrumentationのAPIを明示的に使っています。

CDI管理Beanでは、@WithSpanアノテーションを付与するだけでこのメソッドのトレースを記録してくれます。

また、手動で記録する場合は自分でSpanを開始すればOKです。

    public Book save(Book book) {
        Span span = tracer.spanBuilder("BookRepository.save")
                .setSpanKind(SpanKind.INTERNAL)
                .setParent(Context.current().with(Span.current()))
                .startSpan();

        try {
            if (entityManager.find(Book.class, book.getIsbn()) != null) {
                entityManager.merge(book);
            } else {
                entityManager.persist(book);
            }

            return book;
        } finally {
            span.end();
        }

MicroProfile Telemetryサブシステムを有効にしているので、OpenTelemetry関係のクラスのインジェクションができます。

    @Inject
    private Tracer tracer;

永続化ユニットの設定。

src/main/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
             version="3.0">
    <persistence-unit name="main.pu" transaction-type="JTA">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <jta-data-source>java:jboss/datasources/MySqlDs</jta-data-source>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

Jakarta RESTful Web Services(JAX-RS)まわりのクラス。

src/main/java/org/littlewings/wildfly/telemetry/b/JaxrsActivator.java

package org.littlewings.wildfly.telemetry.b;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("")
public class JaxrsActivator extends Application {
}

リクエストとレスポンスはレコードとして定義しました。

src/main/java/org/littlewings/wildfly/telemetry/b/BookRequest.java

package org.littlewings.wildfly.telemetry.b;

public record BookRequest(String title, Integer price) {
}

src/main/java/org/littlewings/wildfly/telemetry/b/BookResponse.java

package org.littlewings.wildfly.telemetry.b;

public record BookResponse(String isbn, String title, Integer price) {
}

JAX-RSリソースクラス。

src/main/java/org/littlewings/wildfly/telemetry/b/BookResource.java

package org.littlewings.wildfly.telemetry.b;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;

import java.util.List;

@Transactional
@ApplicationScoped
@Path("books")
public class BookResource {
    @Inject
    private BookRepository bookRepository;

    @GET
    @Path("{isbn}")
    @Produces(MediaType.APPLICATION_JSON)
    public BookResponse findByIsbn(@PathParam("isbn") String isbn) {
        Book book = bookRepository.findByIsbn(isbn);

        if (book == null) {
            throw new NotFoundException(String.format("book[%s] not found", isbn));
        }

        return new BookResponse(book.getIsbn(), book.getTitle(), book.getPrice());
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<BookResponse> findAll() {
        return bookRepository
                .findAll()
                .stream()
                .map(book -> new BookResponse(book.getIsbn(), book.getTitle(), book.getPrice()))
                .toList();
    }

    @PUT
    @Path("{isbn}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public void register(@PathParam("isbn") String isbn, BookRequest bookRequest) {
        Book book = new Book();
        book.setIsbn(isbn);
        book.setTitle(bookRequest.title());
        book.setPrice(bookRequest.price());

        bookRepository.save(book);
    }
}

最後に、MicroProfile Configで扱うプロパティファイルを用意。

src/main/resources/META-INF/microprofile-config.properties

## OpenTelemetry SDK Configuration
otel.sdk.disabled=false
otel.service.name=api-b

otel.sdk.disabledが重要です。

デフォルトではMicroProfile Telemetry Tracingは無効になっており、MicroProfile Config経由でotel.sdk.disabled=falseを指定する必要がある
という話でした。

MicroProfile Telemetry Tracing / Tracing Enablement

これをすっかり忘れていて、めちゃくちゃハマりました…。

ところで、このotel.sdk.disabledはなんの設定だろう?と思ったのですが、これはたぶんOpenTelemetry SDKの設定ですね。

Environment Variable Specification | OpenTelemetry

Java向けのOpenTelemetry SDKでは、環境変数だけではなくてシステムプロパティでも設定が可能です。
※OpenTelemetryの仕様としては環境変数とファイルでの指定が可能です

Agent Configuration / Configuring the agent

よって、otel.service.nameというのもOpenTelemetry SDKのプロパティです。

https://github.com/smallrye/smallrye-opentelemetry/blob/2.3.2/implementation/config/src/main/java/io/smallrye/opentelemetry/implementation/config/OpenTelemetryConfigProducer.java#L27-L30

あとはパッケージングして

$ mvn package

WildFlyにデプロイしておきます。

api-aを作成する

次は、api-bの前段にあるapi-aを作成します。

Maven依存関係など。

    <groupId>org.littlewings</groupId>
    <artifactId>api-a</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.opentelemetry</groupId>
                <artifactId>opentelemetry-bom</artifactId>
                <version>1.20.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>io.opentelemetry.instrumentation</groupId>
                <artifactId>opentelemetry-instrumentation-bom</artifactId>
                <version>1.20.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.eclipse.microprofile</groupId>
                <artifactId>microprofile</artifactId>
                <version>6.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-web-api</artifactId>
            <version>10.0.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry.instrumentation</groupId>
            <artifactId>opentelemetry-instrumentation-annotations</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.eclipse.microprofile.rest.client</groupId>
            <artifactId>microprofile-rest-client-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.microprofile.config</groupId>
            <artifactId>microprofile-config-api</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>api-a</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.4.0</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
        </plugins>
    </build>

api-bと少し違うのは、MicroProfile Rest Clientを依存関係に加えていることですね。

        <dependency>
            <groupId>org.eclipse.microprofile.rest.client</groupId>
            <artifactId>microprofile-rest-client-api</artifactId>
            <scope>provided</scope>
        </dependency>

api-aからapi-bへは、MicroProfile Rest ClientおよびJAX-RSのクライアントからアクセスします。

api-bに対する、リクエストおよびレスポンス。

src/main/java/org/littlewings/wildfly/telemetry/b/BookRequest.java

package org.littlewings.wildfly.telemetry.b;

public record BookRequest(String title, Integer price) {
}

src/main/java/org/littlewings/wildfly/telemetry/b/BookResponse.java

package org.littlewings.wildfly.telemetry.b;

public record BookResponse(String isbn, String title, Integer price) {
}

こちらは、先ほどのapi-bの定義と同じです。

次に、MicroProfile Rest Clientで使うインターフェースを定義します。api-bのリソースクラスの定義をなぞったものです。

src/main/java/org/littlewings/wildfly/telemetry/b/BookResourceClient.java

package org.littlewings.wildfly.telemetry.b;

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import java.util.List;

@Path("books")
@RegisterRestClient
public interface BookResourceClient {
    @GET
    @Path("{isbn}")
    @Produces(MediaType.APPLICATION_JSON)
    BookResponse findByIsbn(@PathParam("isbn") String isbn);

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    List<BookResponse> findAll();

    @PUT
    @Path("{isbn}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    void register(@PathParam("isbn") String isbn, BookRequest bookRequest);
}

こちらのインターフェース定義と、@RegisterRestClientアノテーションの付与でBookResourceClientから作成された
MicroProfile Rest Clientの実体がインジェクションできるようになります。

@Path("books")
@RegisterRestClient
public interface BookResourceClient {

ここまでは、api-bにアクセスするためのソースコードでした。

続いては、api-a自身が定義するJAX-RSリソースです。

JAX-RSの有効化。

src/main/java/org/littlewings/wildfly/telemetry/a/JaxrsActivator.java

package org.littlewings.wildfly.telemetry.a;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("")
public class JaxrsActivator extends Application {
}

api-aへのアクセスをプロキシするJAX-RSリソース。

src/main/java/org/littlewings/wildfly/telemetry/a/ProxyResource.java

package org.littlewings.wildfly.telemetry.a;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.core.GenericType;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriBuilder;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.littlewings.wildfly.telemetry.b.BookRequest;
import org.littlewings.wildfly.telemetry.b.BookResourceClient;
import org.littlewings.wildfly.telemetry.b.BookResponse;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

@Path("proxy")
@ApplicationScoped
public class ProxyResource {
    @Inject
    @ConfigProperty(name = "api_b.endpoint.base")
    private String endpointBase;

    @Inject
    private BookResourceClient bookResourceClient;

    @GET
    @Path("{isbn}")
    @Produces(MediaType.APPLICATION_JSON)
    public Map<String, Object> findByIsbn(@PathParam("isbn") String isbn) {
        try (Client client = ClientBuilder.newClient()) {
            BookResponse bookResponse =
                    client
                            .target(UriBuilder.fromUri(endpointBase + "/books/{isbn}").build(isbn))
                            .request()
                            .get(BookResponse.class);

            return Map.of(
                    "isbn", bookResponse.isbn(),
                    "title", bookResponse.title(),
                    "price", bookResponse.price()
            );
        }
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Map<String, Object>> findAll() {
        try (Client client = ClientBuilder.newClient()) {
            List<BookResponse> bookResponses =
                    client
                            .target(UriBuilder.fromUri(endpointBase + "/books"))
                            .request()
                            .get(new GenericType<List<BookResponse>>() {
                            });

            return bookResponses
                    .stream()
                    .map(bookResponse ->
                            Map.<String, Object>of(
                                    "isbn", bookResponse.isbn(),
                                    "title", bookResponse.title(),
                                    "price", bookResponse.price()
                            )
                    )
                    .toList();
        }
    }

    @PUT
    @Path("{isbn}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public void register(@PathParam("isbn") String isbn, Map<String, Object> request) {
        bookResourceClient.register(
                isbn,
                new BookRequest((String) request.get("title"), ((BigDecimal) request.get("price")).intValue())
        );
    }
}

MicroProfile TelemetryがJAX-RSクライアントとMicroProfile Rest Clientの両方に作用していることが確認できるように、参照系の
定義はJAX-RSクライアントで、

    @GET
    @Path("{isbn}")
    @Produces(MediaType.APPLICATION_JSON)
    public Map<String, Object> findByIsbn(@PathParam("isbn") String isbn) {
        try (Client client = ClientBuilder.newClient()) {
            BookResponse bookResponse =
                    client
                            .target(UriBuilder.fromUri(endpointBase + "/books/{isbn}").build(isbn))
                            .request()
                            .get(BookResponse.class);

            return Map.of(
                    "isbn", bookResponse.isbn(),
                    "title", bookResponse.title(),
                    "price", bookResponse.price()
            );
        }
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Map<String, Object>> findAll() {
        try (Client client = ClientBuilder.newClient()) {
            List<BookResponse> bookResponses =
                    client
                            .target(UriBuilder.fromUri(endpointBase + "/books"))
                            .request()
                            .get(new GenericType<List<BookResponse>>() {
                            });

            return bookResponses
                    .stream()
                    .map(bookResponse ->
                            Map.<String, Object>of(
                                    "isbn", bookResponse.isbn(),
                                    "title", bookResponse.title(),
                                    "price", bookResponse.price()
                            )
                    )
                    .toList();
        }
    }

更新処理はMicroProfile Rest Clientで行うことにしました。

    @PUT
    @Path("{isbn}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public void register(@PathParam("isbn") String isbn, Map<String, Object> request) {
        bookResourceClient.register(
                isbn,
                new BookRequest((String) request.get("title"), ((BigDecimal) request.get("price")).intValue())
        );
    }

先ほど定義したapi-bへアクセスするためのインターフェースに関して生成されるインスタンスは、@Injectでインジェクションします。

    @Inject
    private BookResourceClient bookResourceClient;

あとは設定です。

src/main/resources/META-INF/microprofile-config.properties

## OpenTelemetry SDK Configuration
otel.sdk.disabled=false
otel.service.name=api-a

api_b.endpoint.base=http://localhost:8080/api-b
org.littlewings.wildfly.telemetry.b.BookResourceClient/mp-rest/url=${api_b.endpoint.base}

こちらもotel.sdk.disabled=falseが必要です。otel.service.nameapi-aですね。

また、こちらはMicroProfile Rest Clientで使う、呼び出し先のWebサービスのベースのURLです。

org.littlewings.wildfly.telemetry.b.BookResourceClient/mp-rest/url=${api_b.endpoint.base}

ここより先は、インターフェース定義の@Pathに従います。

また、JAX-RSクライアントで使うアクセス先の定義とも共通化しました。

api_b.endpoint.base=http://localhost:8080/api-b
org.littlewings.wildfly.telemetry.b.BookResourceClient/mp-rest/url=${api_b.endpoint.base}

今回は、このベースの値をMicroProfile Configの@ConfigPropertyで取得し、

    @Inject
    @ConfigProperty(name = "api_b.endpoint.base")
    private String endpointBase;

JAX-RSクライアントの接続先を決定する処理で使っています。

    @GET
    @Path("{isbn}")
    @Produces(MediaType.APPLICATION_JSON)
    public Map<String, Object> findByIsbn(@PathParam("isbn") String isbn) {
        try (Client client = ClientBuilder.newClient()) {
            BookResponse bookResponse =
                    client
                            .target(UriBuilder.fromUri(endpointBase + "/books/{isbn}").build(isbn))
                            .request()
                            .get(BookResponse.class);

            return Map.of(
                    "isbn", bookResponse.isbn(),
                    "title", bookResponse.title(),
                    "price", bookResponse.price()
            );
        }
    }

これで準備はできました。

これで、パッケージングして

$ mvn package

WildFlyにデプロイしておきます。

ところで、このアプリケーションをビルドすると、以下のような警告が表示されます。

[WARNING] 不明な列挙型定数ですorg.osgi.annotation.bundle.Requirement.Resolution.OPTIONAL
  理由: org.osgi.annotation.bundle.Requirement$Resolutionのクラス・ファイルが見つかりません

これは、MicroProfile Configの問題のようなので、今回は置いておきます。

Remove OSGi annotations from the API · Issue #716 · eclipse/microprofile-config · GitHub

確認する

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

まずはデータの登録。2つ登録してみます。

$ curl -XPOST -H 'Content-Type: application/json' 172.17.0.2:8080/api-a/proxy/978-1484280782 -d '{ "title": "Java EE to Jakarta EE 10 Recipes: A Problem-Solution Approach for Enterprise Java", "price": 7163 }'


$ curl -XPUT -H 'Content-Type: application/json' 172.17.0.2:8080/api-a/proxy/978-1484282137 -d '{ "title": "Pro Jakarta EE 10: Open Source Enterprise Java-based Cloud-native Applications Development", "price": 8373 }'

この時点で、http://[Jaegerが動作しているホスト]:16686/にアクセスして、Web UIを確認してみます。

トレースが記録されていますね。

中を見てみると、JAX-RSサーバー、MicroProfile Rest Client、CDI管理Beanのトレースが確認できます。

タグのspan.kindを見ると、サーバーやクライアント、

それ以外など、どの種類のコンポーネントでトレースが記録されたのかを確認できます。

CDI管理Beanで記録したものは、この経路だと手動instrumentationしたものですね。

    public Book save(Book book) {
        Span span = tracer.spanBuilder("BookRepository.save")
                .setSpanKind(SpanKind.INTERNAL)
                .setParent(Context.current().with(Span.current()))
                .startSpan();

        try {
            if (entityManager.find(Book.class, book.getIsbn()) != null) {
                entityManager.merge(book);
            } else {
                entityManager.persist(book);
            }

            return book;
        } finally {
            span.end();
        }
    }

JAX-RSサーバーやMicroProfile Rest Clientの方は、特にソースコード上でOpenTelemetry InstrumentationのAPIは使っていません。

次は、参照系も見てみましょう。

$ curl 172.17.0.2:8080/api-a/proxy
[{"price":8373,"isbn":"978-1484282137","title":"Pro Jakarta EE 10: Open Source Enterprise Java-based Cloud-native Applications Development"},{"price":7163,"isbn":"978-1484280782","title":"Java EE to Jakarta EE 10 Recipes: A Problem-Solution Approach for Enterprise Java"}]


$ curl 172.17.0.2:8080/api-a/proxy/978-1484282137
{"price":8373,"isbn":"978-1484282137","title":"Pro Jakarta EE 10: Open Source Enterprise Java-based Cloud-native Applications Development"}

参照系のトレースも記録されました。

こちらも、JAX-RSサーバー、JAX-RSクライアント、CDI管理Beanまで記録されています。

こちらのCDI管理Beanのメソッドは、@WithSpanアノテーションで記録したものでした。

    @WithSpan
    public Book findByIsbn(String isbn) {
        return entityManager.find(Book.class, isbn);
    }

なんとか通して確認できました。今回はこんなところでしょうか。

JDBCは?

ところで、分散トレーシングをやっていて、構成要素にデータベースが入ってる割にはデータベースアクセスがトレースに入っていないのが
やっぱり気になります。

OpenTelemetry InstrumentationのJDBCの内容を見ると、組み込み方は以下の2種類があるようです。

  • OpenTelemetryDataSourceを使って、オリジナルのDataSourceをラップする方法
  • OpenTelemetryDriverJDBC URL(jdbc:otel:)を使って、JDBCConnectionをラップする方法

https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/v1.20.0/instrumentation/jdbc/library

WildFlyで行う場合は、どうしたらいいんでしょうね…?

また別の機会に見てみましょうか…。

おわりに

WildFlyのMicroProfile Telemetryサブシステムを使って、アプリケーションのトレースを行ってみました。

OpenTelemetryサブシステムとJaegerの組み合わせにハマったり、MicroProfile TelemetryでOpenTelemetry SDKを有効化するのを
忘れたりとだいぶハマりましたが、最終的にはなんとか通せました…。

あとはJDBCに踏み込めると、トレースとしてはまとまるかなと思うので、また確認したいですね。