これは、なにをしたくて書いたもの?
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 - 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つが
記載されています。
- MicroProfile Telemetry Tracing / Architecture / Automatic Instrumentation
- MicroProfile Telemetry Tracing / Architecture / Manual Instrumentation
- MicroProfile Telemetry Tracing / Architecture / Agent Instrumentation
自動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
アノテーションを付与する、もしくはSpanBuilder
やCDI管理されているSpan
を
使ってInstrumentationを明示的に行うものです。
エージェントによるInstrumentationは、OpenTelemetryのAutomatic Instrumentationを使用するものです。
Automatic Instrumentation | OpenTelemetry
Agent Configuration | OpenTelemetry
またMicroProfile Telemetry Tracingでは、以下のOpenTelemetryのAPIをCDI管理Beanとして提供する必要があることになっています。
io.opentelemetry.api.OpenTelemetry
io.opentelemetry.api.trace.Tracer
io.opentelemetry.api.trace.Span
io.opentelemetry.api.baggage.Baggage
OpenTelemetryのAPIを直接呼び出すSpan#current
とBaggage#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で追加されたようです。
この一方で、OpenTelemetryサブシステムもあります。
WildFly Admin Guide / Subsystem configuration / OpenTelemetry Subsystem Configuration
こちらはWildFly 25で、MicroProfile OpenTracingサブシステムを置き換える形でリリースされたようです。
最初、このドキュメントから入ったので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>
OpenTelemetryレイヤーはこちら。
ソースコードは、それぞれこのあたり。
- https://github.com/wildfly/wildfly/tree/29.0.1.Final/microprofile/telemetry-smallrye
- https://github.com/wildfly/wildfly/tree/29.0.1.Final/observability/opentelemetry
SmallRye OpenTelemetry
ここまででMicroProfile Telemetry TracingとWildFlyのサブシステムを見てきましたが、WildFlyのMicroProfile Telemetry Tracingの実装は
なにか?というところが気になりますね。
SmallRye OpenTelemetryのようです。
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プロトコルでテレメトリーデータを送るようにします。
続いて、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は省略 }
JPAのEntityManager
を使ってデータベースにアクセスを行うクラス。
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のプロパティです。
あとはパッケージングして
$ mvn package
WildFlyにデプロイしておきます。
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のクライアントからアクセスします。
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 { }
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.name
はapi-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
をラップする方法OpenTelemetryDriver
とJDBC URL(jdbc:otel:
)を使って、JDBCのConnection
をラップする方法
WildFlyで行う場合は、どうしたらいいんでしょうね…?
また別の機会に見てみましょうか…。
おわりに
WildFlyのMicroProfile Telemetryサブシステムを使って、アプリケーションのトレースを行ってみました。
OpenTelemetryサブシステムとJaegerの組み合わせにハマったり、MicroProfile TelemetryでOpenTelemetry SDKを有効化するのを
忘れたりとだいぶハマりましたが、最終的にはなんとか通せました…。
あとはJDBCに踏み込めると、トレースとしてはまとまるかなと思うので、また確認したいですね。