CLOVER🍀

That was when it all began.

Jaerger/OpenTracing API/JAX-RS/MySQLで、Distributed Tracing

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

先日、Quarkusを使ってOpenTracing Extensionを試してみました。

QuarkusのOpenTracing Extensionを試す - CLOVER🍀

今度は、Quarkusを介さず、JaegerやOpenTracingそのものを使って遊んでみようかと。

お題

今回は、JaegerとOpenTracing ContribのJAX-RSJDBC向けの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、JaegerのIPアドレスは、それぞれ

  • 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-RSJDBCを使ったアプリケーションを作成します。お題で「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-RSJDBCに対する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: Jaeger Bindings for Java OpenTracing API

あと、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から取得するようなので、あとで設定しましょう。

https://github.com/opentracing-contrib/java-jdbc/blob/release-0.1.3/src/main/java/io/opentracing/contrib/jdbc/TracingDriver.java#L174-L179

一応、Driverのインスタンスを取得することができればTracerを設定することはできるみたいですが…。

https://github.com/opentracing-contrib/java-jdbc/blob/release-0.1.3/src/main/java/io/opentracing/contrib/jdbc/TracingDriver.java#L126-L128

今回のソースコードでは、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つがあるようです。

Custom configuration

あと、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);

Custom configuration

最初、これを見落としていて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」のトレースを検索してみます。

f:id:Kazuhira:20190622214721p:plain

@Traceアノテーションを付与していないJAX-RSリソースクラスのメソッドについてもトレースされていますし、operationNameを明示的に
設定したメソッドについては、その名前が利用されています。

f:id:Kazuhira:20190622214826p:plain

詳細を見てみましょう。データ登録(PUT)の方。

f:id:Kazuhira:20190622215216p:plain

1件検索の方。

f:id:Kazuhira:20190622215305p:plain

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();

Tracing client requests

ドキュメントにあるように、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」で検索。

f:id:Kazuhira:20190622222909p:plain

詳細を見ると、api-frontend - api-backend - MySQLまでがトレースできていることが確認できます。

f:id:Kazuhira:20190622222959p:plain

f:id:Kazuhira:20190622223124p:plain

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」というのは、トレースを伝播させるためのキーですね。

Propagation Format

Trace/Span Identity

https://github.com/jaegertracing/jaeger-client-java/blob/v0.35.5/jaeger-core/src/main/java/io/jaegertracing/internal/propagation/TextMapCodec.java#L42

値は、「{trace-id}:{span-id}:{parent-span-id}:{flags}」で構成されるようです。

で、クライアント側がHTTPヘッダーで送り

https://github.com/opentracing-contrib/java-jaxrs/blob/release-0.5.0/opentracing-jaxrs2/src/main/java/io/opentracing/contrib/jaxrs2/client/ClientTracingFilter.java#L80

サーバー側で取り出して伝播させるわけですね。

https://github.com/opentracing-contrib/java-jaxrs/blob/release-0.5.0/opentracing-jaxrs2/src/main/java/io/opentracing/contrib/jaxrs2/server/ServerTracingFilter.java#L107-L110

なるほどー。

JAX-RS Instrumental Auto Discovery

あと、ちょっとオマケ的に。

今回はServerTracingDynamicFeatureのインスタンスを自分でリソースとして登録し、SpanFinishingFilterも自分で登録しましたが、
「opentracing-jaxrs2-discovery」を使うとこのあたりを自動で行うクラスを提供してくれます。

Auto discovery

https://github.com/opentracing-contrib/java-jaxrs/blob/release-0.5.0/opentracing-jaxrs2-discovery/src/main/java/io/opentracing/contrib/jaxrs2/discovery/DiscoverableServerTracingFeature.java

https://github.com/opentracing-contrib/java-jaxrs/blob/release-0.5.0/opentracing-jaxrs2-discovery/src/main/java/io/opentracing/contrib/jaxrs2/discovery/DiscoverableSpanFinishingFilter.java

まとめ

JaegerとOpenTracing APIを使って、JAX-RSを使ったアプリケーションからMySQLへのアクセスまでをDistributed Tracingしてみました。

最初は勝手がわからなかったりハマったりしましたが、まあなんとかなりました…。

Propagationまわりは、もうちょっと考えたり情報を集めたりした方がよさそうな気がします。

まあ、気長に頑張りましょう。