Jersey (JAX-RS2) アプリケーションに Zipkin を導入してみる


## したいこと

[Jersey] で作った JAX-RS2 Java アプリケーションを [Zipkin] でトレーシングします。

* お試しなので、Javaアプリケーションも [Zipkin] サーバーも同じマシン上に立てます
* [Zipkin] サーバーは [docker-zipkin] で立てます
* JAX-RSサーバー実装 ==> [jersey-server 2.x]
* JAX-RSクライアント実装 ==> [jersey-client 2.x]
* Java HTTP サーバー実装に [jersey-container-jdk-http] を使います
* [brave-instrumentation-jaxrs2] ライブラリがトレーシングデータを作ります
* [zipkin-reporter] ライブラリが [Zipkin] サーバーにトレーシングデータを送ります

![jersey-zipkin.png](https://qiita-image-store.s3.amazonaws.com/0/60131/d0db83b0-3e99-6125-6391-0cf8bdcb6e3d.png "jersey-zipkin.png")

## Zipkin サーバーを立てる

[docker] をインストールします.

* [Mac](https://store.docker.com/editions/community/docker-ce-desktop-mac) / [Windows](https://store.docker.com/editions/community/docker-ce-desktop-windows) / Linuxはパッケージ管理で入れる

Linux は [docker-compose] を追加でインストールします. Mac, Windows は Docker installer に同梱されているので不要です

```
sudo curl -L https://github.com/docker/compose/releases/download/1.18.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
```

github から [docker-zipkin] をクローンして [docker] で [Zipkin] サーバーを動かします

```
git clone https://github.com/openzipkin/docker-zipkin.git
cd docker-zipkin
docker-compose up -d
```

次のURLで [Zipkin] の Web UI にアクセスできます

* http://localhost:9411

## アプリケーションを実装する

アプリケーションは表題の通り [Jersey] で作ります. 完成したものを github に上げました.

* https://github.com/megmogmog1965/jersey-zipkin

### 依存ライブラリ (pom.xml)

サーバーサイド JAX-RS2 参照実装である Jersey 2 を利用します.

> Jerseyでググると良く出る、groupId=``com.sun.jersey`` は Jersey 1系で互換性がないので注意.

```xml
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.core/jersey-server -->
<dependency>
  <groupId>org.glassfish.jersey.core</groupId>
  <artifactId>jersey-server</artifactId>
  <version>2.26</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.inject/jersey-hk2 -->
<dependency>
  <groupId>org.glassfish.jersey.inject</groupId>
  <artifactId>jersey-hk2</artifactId>
  <version>2.26</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.containers/jersey-container-servlet -->
<dependency>
  <groupId>org.glassfish.jersey.containers</groupId>
  <artifactId>jersey-container-servlet</artifactId>
  <version>2.26</version>
</dependency>
```

クライアントサイド JAX-RS2 も参照実装である Jersey 2 を利用します.

```xml
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.core/jersey-client -->
<dependency>
  <groupId>org.glassfish.jersey.core</groupId>
  <artifactId>jersey-client</artifactId>
  <version>2.26</version>
</dependency>
```

Javaで書いた Web Application は普段は [Tomcat], [Glassfish], [Jetty] 等で動かしますが、今回は簡単の為 ``jersey-container-jdk-http`` を利用してHttpサーバー実装をアプリケーションに直接埋め込みます.

```xml
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.containers/jersey-container-jdk-http -->
<dependency>
  <groupId>org.glassfish.jersey.containers</groupId>
  <artifactId>jersey-container-jdk-http</artifactId>
  <version>2.26</version>
</dependency>
```

JAX-RS2 Javaアプリケーション用の Zipkin Instrumentation ライブラリです. これを使って JAX-RSサーバー/クライアントでトレーシングが出来るようになります.

```xml
<!-- https://mvnrepository.com/artifact/io.zipkin.brave/brave-instrumentation-jaxrs2 -->
<dependency>
  <groupId>io.zipkin.brave</groupId>
  <artifactId>brave-instrumentation-jaxrs2</artifactId>
  <version>4.3.1</version>
</dependency>
```

収集したトレーシングデータは [Zipkin] サーバーに送信しなければなりません. このライブラリが任意の宛先に送信してくれます.

```xml
<!-- https://mvnrepository.com/artifact/io.zipkin.reporter/zipkin-sender-urlconnection -->
<dependency>
  <groupId>io.zipkin.reporter</groupId>
  <artifactId>zipkin-sender-urlconnection</artifactId>
  <version>1.1.2</version>
</dependency>
```

### Java コード

JAX-RS2 では ``javax.ws.rs.core.Feature`` IFを実装した外部ライブラリをプラグインできます. 

Javaアプリケーション向けの [Zipkin] ライブラリである [Brave] は様々なJavaフレームワークに対応しています. 今回は JAX-RS2 用の [brave-instrumentation-jaxrs2] を使用します.

* ``brave.jaxrs2.TracingFeature`` は JAX-RS2 用のトレーシング実装 (Trace Instrumentation) で、[トレーシングデータを作成](https://zipkin.io/pages/architecture.html) します
* ``zipkin.reporter.AsyncReporter`` はアプリケーション上に集めたトレーシングデータを [Zipkin] サーバーに送信します

```java
package zipkin;

import brave.Tracing;
import brave.jaxrs2.TracingFeature;
import brave.sampler.Sampler;
import zipkin.reporter.AsyncReporter;
import zipkin.reporter.urlconnection.URLConnectionSender;
import javax.ws.rs.core.Feature;

public class ZipkinFeature {

    /**
     * @param localServiceName Zipkin Web UI に表示されるサービス名
     */
    public static Feature create(String localServiceName) {
        // create a zipkin reporter.
        AsyncReporter<Span> asyncReporter = AsyncReporter
                .builder(URLConnectionSender.create("http://localhost:9411/api/v1/spans"))
                .build();

        // create a zipkin tracing.
        Tracing tracing = Tracing.newBuilder()
                .reporter(asyncReporter)
                .localServiceName(localServiceName)
                .sampler(Sampler.ALWAYS_SAMPLE)
                .build();

        // create a JAX-RS feature.
        Feature tracingFeature = TracingFeature.create(tracing);

        return tracingFeature;
    }
}
```

サーバーサイド JAX-RS2 のエントリポイントである ``javax.ws.rs.core.Application`` IFを実装したクラスを作ります. ここで先程生成したトレーシング機能 ``brave.jaxrs2.TracingFeature`` をサーバーサイド JAX-RS2 に登録します.

```java
package app;

import zipkin.ZipkinFeature;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Feature;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;

@ApplicationPath("")
public class MyApplication extends Application {

    @Override
    public Set<Object> getSingletons() {
        // create a JAX-RS feature.
        Feature tracingFeature = ZipkinFeature.create("server");

        return new LinkedHashSet<>(Arrays.asList(tracingFeature));
    }
}
```

サーバーサイド JAX-RS2 のリソースクラス (API定義) を作ります. 本来は複数のサービスをデプロイして [Zipkin] の動作を確認したいのですが、簡単化の為に 3 API を定義し、クライアント JAX-RS2 で順に呼んでいます.
ここでも先程生成したトレーシング機能 ``brave.jaxrs2.TracingFeature`` をクライアント JAX-RS2 に登録しています.

* /front --> /mid --> /back

```java
package resource;

import zipkin.ZipkinFeature;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.Feature;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("")
public class MyResource {

    /**
     * /front --> /mid --> /back
     *
     * @return String that will be returned as a text/plain response.
     */
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("front")
    public Response front() {
        // create a JAX-RS feature.
        Feature tracingFeature = ZipkinFeature.create("client");

        // http client: GET /mid
        Response r = ClientBuilder.newClient()
                .register(tracingFeature)
                .target("http://localhost:8080/myapp/mid")
                .request()
                .get();

        return Response.fromResponse(r).build();
    }

    /**
     * /mid --> /back
     *
     * @return String that will be returned as a text/plain response.
     */
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("mid")
    public Response mid() {
        // create a JAX-RS feature.
        Feature tracingFeature = ZipkinFeature.create("client");

        // http client: GET /back
        Response r = ClientBuilder.newClient()
                .register(tracingFeature)
                .target("http://localhost:8080/myapp/back")
                .request()
                .get();

        return Response.fromResponse(r).build();
    }

    /**
     * /back
     *
     * @return String that will be returned as a text/plain response.
     */
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("back")
    public String back() {
        return "hello";
    }
}
```

簡単に動かしたいので、Httpサーバー実装には [jersey-container-jdk-http] を使用します。

```java
package main;

import app.MyApplication;
import com.sun.net.httpserver.HttpServer;
import org.glassfish.jersey.jdkhttp.JdkHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;
import java.net.URI;

public class Main {

    public static void main(String[] args) {
        // register JAX-RS Application class.
        ResourceConfig rc = ResourceConfig.forApplicationClass(MyApplication.class);

        // register a path of REST resources.
        rc.packages(true, "resource");

        // run a jersey server.
        URI uri = URI.create("http://localhost:8080/myapp/");
        HttpServer httpServer = JdkHttpServerFactory.createHttpServer(uri, rc);
        Runtime.getRuntime().addShutdownHook(new Thread(() -> httpServer.stop(0)));
    }
}
```

アプリケーションを実行します. 以下のURLでアクセスできます.

* http://localhost:8080/myapp/front

Zipkin Web UI にアクセスしましょう. 先程アクセスした際のトレーシングデータが見れる筈です.

* http://localhost:9411

![zipkin.png](https://qiita-image-store.s3.amazonaws.com/0/60131/2afa0af4-066b-f2f2-f1cf-89e925dacb2c.png "zipkin.png")


[Jersey]:https://jersey.github.io/
[Zipkin]:https://zipkin.io/
[docker-zipkin]:https://github.com/openzipkin/docker-zipkin
[docker]:https://www.docker.com/
[docker-compose]:https://docs.docker.com/compose/
[IntelliJ IDEA]:https://www.jetbrains.com/idea/
[NetBeans]:https://ja.netbeans.org/

[Tomcat]:http://tomcat.apache.org/
[Glassfish]:https://javaee.github.io/glassfish/
[Jetty]:https://www.eclipse.org/jetty/

[jersey-server 2.x]:https://mvnrepository.com/artifact/org.glassfish.jersey.core/jersey-server/2.26
[jersey-client 2.x]:https://mvnrepository.com/artifact/org.glassfish.jersey.core/jersey-client/2.26
[jersey-container-jdk-http]:https://mvnrepository.com/artifact/org.glassfish.jersey.containers/jersey-container-jdk-http/2.26
[Brave]:https://github.com/openzipkin/brave
[brave-instrumentation-jaxrs2]:https://github.com/openzipkin/brave/tree/master/instrumentation/jaxrs2
[zipkin-reporter]:https://mvnrepository.com/artifact/io.zipkin.reporter/zipkin-reporter/1.1.2