これは、なにをしたくて書いたもの?
先日、Quarkusを使ってOpenTracing Extensionを試してみました。
QuarkusのOpenTracing Extensionを試す - CLOVER🍀
今度は、Quarkusを介さず、JaegerやOpenTracingそのものを使って遊んでみようかと。
お題
今回は、JaegerとOpenTracing ContribのJAX-RS、JDBC向けのInstrumentationを使って遊んでみましょう。
GitHub - opentracing-contrib/java-jaxrs: OpenTracing Java JAX-RS instrumentation
GitHub - opentracing-contrib/java-jdbc: OpenTracing Instrumentation for JDBC
データベースは、MySQL 8.0.16を使うことにします。またJaegerは1.12.0をDockerイメージで利用。
$ docker container run -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 jaegertracing/all-in-one:1.12.0
- MySQL … 172.17.0.2
- Jaeger … 172.17.0.3
とします。
これらを使って、以下のようなアプリケーションを作ってDistributed Tracingを試してみたいと思います。
api-frontend[JAX-RS Server - JAX-RS Client] → api-backend[JAX-RS Server - Doma 2] → MySQL
JAX-RSの実装には、RESTEasy(+Undertow)を使用します。
テーブルは、書籍をお題に。
create table book(isbn varchar(15), title varchar(100), price int, primary key(isbn));
環境
今回の環境は、こちらです。
$ java -version openjdk version "11.0.3" 2019-04-16 OpenJDK Runtime Environment (build 11.0.3+7-Ubuntu-1ubuntu218.04.1) OpenJDK 64-Bit Server VM (build 11.0.3+7-Ubuntu-1ubuntu218.04.1, mixed mode, sharing) $ mvn -version Apache Maven 3.6.1 (d66c9c0b3152b2e69ee9bac180bb8fcc8e6af555; 2019-04-05T04:00:29+09:00) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 11.0.3, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-11-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "4.15.0-52-generic", arch: "amd64", family: "unix"
api-backend
最初に、JAX-RSとJDBCを使ったアプリケーションを作成します。お題で「api-backend」と表記していたものです。
Maven依存関係は、こちら。
<dependencies> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-undertow</artifactId> <version>4.0.0.Final</version> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jackson2-provider</artifactId> <version>4.0.0.Final</version> </dependency> <dependency> <groupId>org.seasar.doma</groupId> <artifactId>doma</artifactId> <version>2.24.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.16</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jaegertracing</groupId> <artifactId>jaeger-client</artifactId> <version>0.35.5</version> </dependency> <dependency> <groupId>io.opentracing.contrib</groupId> <artifactId>opentracing-jaxrs2</artifactId> <version>0.5.0</version> </dependency> <dependency> <groupId>io.opentracing.contrib</groupId> <artifactId>opentracing-jdbc</artifactId> <version>0.1.3</version> </dependency> </dependencies>
RESTEasy、Undertow、Domaについては省略します。
JAX-RSとJDBCに対するOpenTracingのInstrumentalを使うために、以下の2つのライブラリを利用します。
GitHub - opentracing-contrib/java-jaxrs: OpenTracing Java JAX-RS instrumentation
GitHub - opentracing-contrib/java-jdbc: OpenTracing Instrumentation for JDBC
OpenTracing APIの実装としては、Jaegerのクライアントライブラリを使用。
GitHub - jaegertracing/jaeger-client-java: 🛑 This library is DEPRECATED!
あと、Uber JARを作るのにSpring Boot Maven Pluginを使うことにしました。
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.1.5.RELEASE</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
Doma 2を使ったクラスの作成
では、最初にEntityクラスを作成。
src/main/java/org/littlewings/jaeger/backend/Book.java
package org.littlewings.jaeger.backend; import org.seasar.doma.Column; import org.seasar.doma.Entity; import org.seasar.doma.Id; @Entity public class Book { @Id private String isbn; @Column private String title; @Column private Integer price; // getter/setterは省略 }
続いて、Dao。
src/main/java/org/littlewings/jaeger/backend/BookDao.java
package org.littlewings.jaeger.backend; import java.util.List; import org.seasar.doma.Dao; import org.seasar.doma.Insert; import org.seasar.doma.Select; import org.seasar.doma.experimental.Sql; @Dao(config = AppConfig.class) 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); }
Configクラス。
src/main/java/org/littlewings/jaeger/backend/AppConfig.java
package org.littlewings.jaeger.backend; import javax.sql.DataSource; import org.seasar.doma.SingletonConfig; 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; @SingletonConfig public class AppConfig implements Config { private static final AppConfig CONFIG = new AppConfig(); Dialect dialect; LocalTransactionDataSource dataSource; TransactionManager transactionManager; public static AppConfig singleton() { return CONFIG; } private AppConfig() { dialect = new MysqlDialect(); dataSource = new LocalTransactionDataSource( "jdbc:tracing:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8&useSSL=false&allowPublicKeyRetrieval=true", "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.LENIENT_SNAKE_LOWER_CASE; } }
今回のポイントは、OpenTracing JDBC Instrumentalを使っていることです。
dataSource = new LocalTransactionDataSource( "jdbc:tracing:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8&useSSL=false&allowPublicKeyRetrieval=true", "kazuhira", "password");
JDBC URLのScheme部分に「tracing」が入っているところがポイントです。
jdbc:tracing:mysql
Tracerは、GlobalTracerから取得するようなので、あとで設定しましょう。
一応、Driverのインスタンスを取得することができればTracerを設定することはできるみたいですが…。
今回のソースコードでは、JAX-RSのApplicationクラスのサブクラスを作成する時に、JDBC用のTracerをGlobalTracerに登録することにします。
JAX-RSリソースクラスの作成
今度は、JAX-RSリソースクラスを作成します。 src/main/java/org/littlewings/jaeger/backend/BookResource.java
package org.littlewings.jaeger.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.eclipse.microprofile.opentracing.Traced; import org.seasar.doma.jdbc.tx.TransactionManager; @Path("book") public class BookResource { TransactionManager tm = AppConfig.singleton().getTransactionManager(); BookDao bookDao = new BookDaoImpl(); @GET @Produces(MediaType.APPLICATION_JSON) @Traced public List<Book> findAll() { return tm.required(() -> bookDao.findAll()); } @GET @Path("{isbn}") @Produces(MediaType.APPLICATION_JSON) @Traced(operationName = "findByIsbn") 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)); } }
メソッドに、@Tracedアノテーションを付けたり付けていなかったりしています。
実は付けていてもいなくてもよくて(@Tracedのvalue属性でトレースを無効にできたりしますが)、operationNameを指定することで
トレース時にどのような名前を付与するかを設定することができます。
今回は、こんな感じですね。
@Traced public List<Book> findAll() { @Traced(operationName = "findByIsbn") public Book findByIsbn(@PathParam("isbn") String isbn) { public void register(@PathParam("isbn") String isbn, Book book) {
また、@Traceアノテーションはクラスにも付与することができます。
そして、JAX-RSのApplicationクラスのサブクラスを作成。
src/main/java/org/littlewings/jaeger/backend/JaxrsActivator.java
package org.littlewings.jaeger.backend; import java.util.HashSet; import java.util.Set; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; import io.jaegertracing.Configuration; import io.opentracing.Tracer; import io.opentracing.contrib.jaxrs2.server.ServerTracingDynamicFeature; import io.opentracing.util.GlobalTracer; @ApplicationPath("") public class JaxrsActivator extends Application { Set<Object> singleResources = new HashSet<>(); public JaxrsActivator() { String jaegerEndpoint = "http://172.17.0.3:14268/api/traces"; Tracer jaxrsTracer = new Configuration("api-backend") .withSampler(new Configuration.SamplerConfiguration() .withType("const") .withParam(1)) .withReporter( new Configuration.ReporterConfiguration() .withSender(new Configuration.SenderConfiguration().withEndpoint(jaegerEndpoint))) .getTracerBuilder() .build(); Tracer jdbcTracer = new Configuration("mysql") .withSampler(new Configuration.SamplerConfiguration() .withType("const") .withParam(1)) .withReporter( new Configuration.ReporterConfiguration() .withSender(new Configuration.SenderConfiguration().withEndpoint(jaegerEndpoint))) .getTracerBuilder() .withScopeManager(jaxrsTracer.scopeManager()) .build(); GlobalTracer.registerIfAbsent(jdbcTracer); ServerTracingDynamicFeature serverTracingDynamicFeature = new ServerTracingDynamicFeature.Builder(jaxrsTracer) .withTraceSerialization(false) .build(); singleResources.add(serverTracingDynamicFeature); singleResources.add(new BookResource()); } @Override public Set<Object> getSingletons() { return singleResources; } }
まず、JAX-RS用のTracerを作成。
String jaegerEndpoint = "http://172.17.0.3:14268/api/traces"; Tracer jaxrsTracer = new Configuration("api-backend") .withSampler(new Configuration.SamplerConfiguration() .withType("const") .withParam(1)) .withReporter( new Configuration.ReporterConfiguration() .withSender(new Configuration.SenderConfiguration().withEndpoint(jaegerEndpoint))) .getTracerBuilder() .build();
Samplerは簡単のためにConstantとし、さらにParamは1にして全部の操作をトレース対象としました。
Sampling — Jaeger documentation
Samplerは、Constant以外にもProbabilistic、Rate Limiting、Remote(これがデフォルト)があるんですねぇ。
このTracerをServerTracingDynamicFeatureに登録して、JAX-RSリソースのひとつとして登録します。
ServerTracingDynamicFeature serverTracingDynamicFeature = new ServerTracingDynamicFeature.Builder(jaxrsTracer) .withTraceSerialization(false) .build(); singleResources.add(serverTracingDynamicFeature); singleResources.add(new BookResource());
withTraceSerializationは、デフォルトがtrueで、そのままにしておくとJAX-RSのデータのシリアライズ・デシリアライズもトレース対象に
含まれるのですが、今回は外しておきました。
DynamicFeatureの登録方法には、こうやってSignletonなりソースのひとつとして登録する方法と@Providerとして登録する方法の
2つがあるようです。
あと、JAX-RSとは直接関係がありませんが、OpenTracing JDBC Instrumentation向けにTracerを作成して、GlobalTracerに登録しておきます。
Tracer jdbcTracer = new Configuration("mysql") .withSampler(new Configuration.SamplerConfiguration() .withType("const") .withParam(1)) .withReporter( new Configuration.ReporterConfiguration() .withSender(new Configuration.SenderConfiguration().withEndpoint(jaegerEndpoint))) .getTracerBuilder() .withScopeManager(jaxrsTracer.scopeManager()) .build(); GlobalTracer.registerIfAbsent(jdbcTracer);
ScopeManagerは、JAX-RS用のTracerのものを使用しています。
.withScopeManager(jaxrsTracer.scopeManager())
起動クラス
最後に、mainメソッドを持った起動クラスを作成。
src/main/java/org/littlewings/jaeger/backend/App.java
package org.littlewings.jaeger.backend; import java.util.EnumSet; import javax.servlet.DispatcherType; import io.opentracing.contrib.jaxrs2.server.SpanFinishingFilter; import io.undertow.Undertow; import io.undertow.servlet.api.DeploymentInfo; import io.undertow.servlet.api.FilterInfo; import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer; public class App { public static void main(String... args) { UndertowJaxrsServer server = new UndertowJaxrsServer() .start(Undertow.builder().addHttpListener(9080, "localhost")); DeploymentInfo di = server.undertowDeployment(JaxrsActivator.class); di.setContextPath(""); di.setDeploymentName("api-backend"); di.addFilter( new FilterInfo("tracingFilter", SpanFinishingFilter.class) .setAsyncSupported(true) ); di.addFilterUrlMapping("tracingFilter", "*", DispatcherType.REQUEST); server.deploy(di); // server.stop(); } }
8080ポートはapi-frontendに渡そうと思うので、こちらは9080ポートを使用することにします。
UndertowJaxrsServer server = new UndertowJaxrsServer() .start(Undertow.builder().addHttpListener(9080, "localhost"));
あとは、これまでに作成したJAX-RS Applicationを登録するわけですが、この時に一緒にOpenTracing JAX-RS Instrumentationの
ServletFilterも設定します。
DeploymentInfo di = server.undertowDeployment(JaxrsActivator.class); di.setContextPath(""); di.setDeploymentName("api-backend"); di.addFilter( new FilterInfo("tracingFilter", SpanFinishingFilter.class) .setAsyncSupported(true) ); di.addFilterUrlMapping("tracingFilter", "*", DispatcherType.REQUEST);
最初、これを見落としていてDynamicFeatureだけ登録して動かしてみたら、全然トレースが記録されなくてハマりました…。
確認
それでは、パッケージングして確認してみます。
$ mvn package
起動。
$ java -jar target/api-backend-0.0.1-SNAPSHOT.jar
データ登録。
$ curl -i -XPUT -H 'Content-Type: application/json' localhost:9080/book/978-4774182179 -d '{"isbn": "978-4774182179", "title": "[改訂新版]Spring入門 ――Javaフレームワーク ・より良い設計とアーキテクチャ", "price": 4104}' HTTP/1.1 204 No Content Date: Sat, 22 Jun 2019 12:42:34 GMT $ curl -i -XPUT -H 'Content-Type: application/json' localhost:9080/book/978-4798142470 -d '{"isbn": "978-4798142470", "title": "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", "price": 4320}' HTTP/1.1 204 No Content Date: Sat, 22 Jun 2019 12:42:44 GMT $ curl -i -XPUT -H 'Content-Type: application/json' localhost:9080/book/978-4777519699 -d '{"isbn": "978-4777519699", "title": "はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発", "price": 2700}' HTTP/1.1 204 No Content Date: Sat, 22 Jun 2019 12:42:53 GMT
全件取得。
$ curl localhost:9080/book [{"isbn":"978-4774182179","title":"[改訂新版]Spring入門 ――Javaフレームワーク ・より良い設計とアーキテクチャ","price":4104},{"isbn":"978-4777519699","title":"はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発","price":2700},{"isbn":"978-4798142470","title":"Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発","price":4320}]
1件取得。
$ curl localhost:9080/book/978-4774182179 {"isbn":"978-4774182179","title":"[改訂新版]Spring入門 ――Javaフレームワーク ・より良い設計とアーキテクチャ","price":4104}
Jaegerで確認してみましょう。「http://[Jaegerが動作しているホスト]:16686」にアクセスして、「api-backend」のトレースを検索してみます。
@Traceアノテーションを付与していないJAX-RSリソースクラスのメソッドについてもトレースされていますし、operationNameを明示的に
設定したメソッドについては、その名前が利用されています。
詳細を見てみましょう。データ登録(PUT)の方。
1件検索の方。
MySQLへのアクセスを含めて、トレースできていることが確認できましたね。
これでapi-backendの確認ができたので、1度データを削除。
mysql> truncate table book; Query OK, 0 rows affected (0.25 sec)
Jaegerも再起動しておきます。
api-frontend
次は、先ほど作成したapi-backendにアクセスするJAX-RS Clientを含んだ、JAX-RSアプリケーションを作成します。
Maven依存関係は、こちら。
<dependencies> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-undertow</artifactId> <version>4.0.0.Final</version> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jackson2-provider</artifactId> <version>4.0.0.Final</version> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-client</artifactId> <version>4.0.0.Final</version> </dependency> <dependency> <groupId>io.jaegertracing</groupId> <artifactId>jaeger-client</artifactId> <version>0.35.5</version> </dependency> <dependency> <groupId>io.opentracing.contrib</groupId> <artifactId>opentracing-jaxrs2</artifactId> <version>0.5.0</version> </dependency> </dependencies>
先ほどのapi-backendでの依存関係から、OpenTracing JDBC InstrumentalやDoma 2を削り、RESTEasy Clientを足したものです。
Uber JAR用に、Spring Boot Maven Pluginを使用するのは同じです。
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.1.5.RELEASE</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
api-backendで使ったBookクラスの定義は、Doma 2のアノテーションを削って使いまわします。 j src/main/java/org/littlewings/jaeger/front/Book.java
package org.littlewings.jaeger.front; public class Book { private String isbn; private String title; private Integer price; // getter/setterは省略 }
JAX-RSリソースクラスの作成
では、JAX-RSリソースクラスの作成をします。JAX-RS Clientのコードも含むので、ちょっと長いです…。
api-backendへのアクセスを行う、JAX-RSリソースクラス。まあ、プロキシみたいなものですね…。
src/main/java/org/littlewings/jaeger/front/BookResource.java
package org.littlewings.jaeger.front; 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 io.jaegertracing.Configuration; import io.opentracing.Tracer; import io.opentracing.contrib.jaxrs2.client.ClientTracingFeature; import io.opentracing.util.GlobalTracer; import org.eclipse.microprofile.opentracing.Traced; @Path("book") public class BookResource { Client client; public BookResource() { String jaegerEndpoint = "http://172.17.0.3:14268/api/traces"; Tracer jaxrsServerTracer = GlobalTracer.get(); Tracer jaxrsClientTracer = new Configuration("api-client") .withSampler(new Configuration.SamplerConfiguration() .withType("const") .withParam(1)) .withReporter( new Configuration.ReporterConfiguration() .withSender(new Configuration.SenderConfiguration().withEndpoint(jaegerEndpoint))) .getTracerBuilder() .withScopeManager(jaxrsServerTracer.scopeManager()) .build(); client = ClientBuilder .newBuilder() .register(new ClientTracingFeature.Builder(jaxrsClientTracer).build()) .build(); } @GET @Produces(MediaType.APPLICATION_JSON) public List<Book> findAll() { Response response = null; try { response = client .target("http://localhost: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://localhost: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://localhost:9080/book/{isbn}").build(isbn)) .request() .put(Entity.entity(book, MediaType.APPLICATION_JSON_TYPE)); } finally { Optional.ofNullable(response).ifPresent(Response::close); } } }
こちらは、@Tracedアノテーションは使わないことにしました。
JAX-RS Clientですが、ClientTracingFeatureを使ってTracerを組み込むようにします。
String jaegerEndpoint = "http://172.17.0.3:14268/api/traces"; Tracer jaxrsServerTracer = GlobalTracer.get(); Tracer jaxrsClientTracer = new Configuration("api-client") .withSampler(new Configuration.SamplerConfiguration() .withType("const") .withParam(1)) .withReporter( new Configuration.ReporterConfiguration() .withSender(new Configuration.SenderConfiguration().withEndpoint(jaegerEndpoint))) .getTracerBuilder() .withScopeManager(jaxrsServerTracer.scopeManager()) .build(); client = ClientBuilder .newBuilder() .register(new ClientTracingFeature.Builder(jaxrsClientTracer).build()) .build();
ドキュメントにあるように、ClientTracingFeatureのClassクラスを渡してもいいのですが、それだとGlobalTracerを使ってしまうので、
今回はインスタンスを登録。
Client client = ClientBuilder.newBuilder()
.reqister(ClientTracingFeature.class)
.build();
また、ScopeManagerを合わせるために、GlobalTracerにあらかじめJAX-RSリソースクラス用に登録しておいたTracerを使いました…。
Tracer jaxrsServerTracer = GlobalTracer.get(); Tracer jaxrsClientTracer = new Configuration("api-client") .withSampler(new Configuration.SamplerConfiguration() .withType("const") .withParam(1)) .withReporter( new Configuration.ReporterConfiguration() .withSender(new Configuration.SenderConfiguration().withEndpoint(jaegerEndpoint))) .getTracerBuilder() .withScopeManager(jaxrsServerTracer.scopeManager()) .build();
JAX-RSリソースクラス用のTracerは、api-backendと同様にJAX-RS Applicationのサブクラス内で作成。
src/main/java/org/littlewings/jaeger/front/JaxrsActivator.java
package org.littlewings.jaeger.front; import java.util.HashSet; import java.util.Set; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; import io.jaegertracing.Configuration; import io.opentracing.Tracer; import io.opentracing.contrib.jaxrs2.server.ServerTracingDynamicFeature; import io.opentracing.util.GlobalTracer; @ApplicationPath("") public class JaxrsActivator extends Application { Set<Object> singleResources = new HashSet<>(); public JaxrsActivator() { String jaegerEndpoint = "http://172.17.0.3:14268/api/traces"; Tracer jaxrsServerTracer = new Configuration("api-frontend") .withSampler(new Configuration.SamplerConfiguration() .withType("const") .withParam(1)) .withReporter( new Configuration.ReporterConfiguration() .withSender(new Configuration.SenderConfiguration().withEndpoint(jaegerEndpoint))) .getTracerBuilder() .build(); GlobalTracer.registerIfAbsent(jaxrsServerTracer); ServerTracingDynamicFeature serverTracingDynamicFeature = new ServerTracingDynamicFeature.Builder(jaxrsServerTracer) .withTraceSerialization(false) .build(); singleResources.add(serverTracingDynamicFeature); singleResources.add(new BookResource()); } @Override public Set<Object> getSingletons() { return singleResources; } }
api-backendの時からは、OpenTracing JDBC Instrumental用のTracerがなくなったことと、GlobalTracerとしてJAX-RSリソースクラス用の
Tracerを登録したことくらいしか差がないので、詳細は省略。
起動クラス
mainメソッドを持った起動クラスは、こちら。
src/main/java/org/littlewings/jaeger/front/App.java
package org.littlewings.jaeger.front; import javax.servlet.DispatcherType; import io.opentracing.contrib.jaxrs2.server.SpanFinishingFilter; import io.undertow.Undertow; import io.undertow.servlet.api.DeploymentInfo; import io.undertow.servlet.api.FilterInfo; import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer; public class App { public static void main(String... args) { UndertowJaxrsServer server = new UndertowJaxrsServer() .start(Undertow.builder().addHttpListener(8080, "localhost")); DeploymentInfo di = server.undertowDeployment(JaxrsActivator.class); di.setContextPath(""); di.setDeploymentName("api-frontend"); di.addFilter( new FilterInfo("tracingFilter", SpanFinishingFilter.class) .setAsyncSupported(true) ); di.addFilterUrlMapping("tracingFilter", "*", DispatcherType.REQUEST); server.deploy(di); // server.stop(); } }
利用するポートが8080以外は、api-backendとほとんど変わらないので、こちらも詳細は割愛。
確認
それでは、api-frontend、api-backendまで通して確認してみましょう。
api-backendは先ほど起動したままなので、api-frontendをパッケージングして
$ mvn package
起動。
$ java -jar target/api-frontend-0.0.1-SNAPSHOT.jar
データ登録。
$ curl -i -XPUT -H 'Content-Type: application/json' localhost:8080/book/978-4774182179 -d '{"isbn": "978-4774182179", "title": "[改訂新版]Spring入門 ――Javaフレームワーク ・より良い設計とアーキテクチャ", "price": 4104}' HTTP/1.1 204 No Content Date: Sat, 22 Jun 2019 13:26:09 GMT $ curl -i -XPUT -H 'Content-Type: application/json' localhost:8080/book/978-4798142470 -d '{"isbn": "978-4798142470", "title": "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", "price": 4320}' HTTP/1.1 204 No Content Date: Sat, 22 Jun 2019 13:26:22 GMT $ curl -i -XPUT -H 'Content-Type: application/json' localhost:8080/book/978-4777519699 -d '{"isbn": "978-4777519699", "title": "はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発", "price": 2700}' HTTP/1.1 204 No Content Date: Sat, 22 Jun 2019 13:26:32 GMT
全件取得。
$ curl localhost:8080/book [{"isbn":"978-4774182179","title":"[改訂新版]Spring入門 ――Javaフレームワーク ・より良い設計とアーキテクチャ","price":4104},{"isbn":"978-4777519699","title":"はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発","price":2700},{"isbn":"978-4798142470","title":"Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発","price":4320}]
1件取得。
$ curl localhost:8080/book/978-4774182179 {"isbn":"978-4774182179","title":"[改訂新版]Spring入門 ――Javaフレームワーク ・より良い設計とアーキテクチャ","price":4104}
JaegerのWeb UIで確認してみます。「api-frontend」で検索。
詳細を見ると、api-frontend - api-backend - MySQLまでがトレースできていることが確認できます。
OKみたいですね。
api-frontendとapi-backendのつなぎは?
ところで、api-frontendとapi-backendのトレースはあっさりとつながりましたが、どうなっているんでしょう?
HTTPヘッダーを見てみましょう。
$ sudo tcpdump -i lo tcp port 9080 -A
アクセス。
$ curl localhost:8080/book/978-4774182179 {"isbn":"978-4774182179","title":"[改訂新版]Spring入門 ――Javaフレームワーク ・より良い設計とアーキテクチャ","price":4104}
この時、JAX-RSクライアントからのリクエストに含まれるHTTPヘッダーに「uber-trace-id」というものが含まれています。
22:33:51.794795 IP localhost.52670 > localhost.9080: Flags [P.], seq 1:210, ack 1, win 342, options [nop,nop,TS val 2152851073 ecr 2152851073], length 209 E...^.@.@.."..........#x.;.V. .....V....... .Q...Q..GET /book/978-4774182179 HTTP/1.1 uber-trace-id: f3844bee7a377a5b%3A2ee771b9d61714c5%3Af3844bee7a377a5b%3A1 Host: localhost:9080 Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.4 (Java/11.0.3)
「uber-trace-id」というのは、トレースを伝播させるためのキーですね。
値は、「{trace-id}:{span-id}:{parent-span-id}:{flags}」で構成されるようです。
で、クライアント側がHTTPヘッダーで送り
サーバー側で取り出して伝播させるわけですね。
なるほどー。
JAX-RS Instrumental Auto Discovery
あと、ちょっとオマケ的に。
今回はServerTracingDynamicFeatureのインスタンスを自分でリソースとして登録し、SpanFinishingFilterも自分で登録しましたが、
「opentracing-jaxrs2-discovery」を使うとこのあたりを自動で行うクラスを提供してくれます。
まとめ
JaegerとOpenTracing APIを使って、JAX-RSを使ったアプリケーションからMySQLへのアクセスまでをDistributed Tracingしてみました。
最初は勝手がわからなかったりハマったりしましたが、まあなんとかなりました…。
Propagationまわりは、もうちょっと考えたり情報を集めたりした方がよさそうな気がします。
まあ、気長に頑張りましょう。