【2019年11月版】Quarkus で Hibernate-Panache の REST API 作ってネイティブ化し Jaeger でトレース

# Quarkus で REST API を作って Jaeger で見てみたい

先日の記事で Apollo Server に Opentracing 対応を入れて Jaeger でトレースログをチェックしてみましたが、Quarkus は MicroProfile 対応なので Opentracing 対応も非常に簡単なんですよね！？
…ということなので、本日は Quarkus での REST API 作成とネイティブ化、Jaegerでのトレースログ表示にチャレンジしてみたいと思います。

## 参考ページなど

Quarkus 本家の[マニュアルサイト](https://quarkus.io/guides/)は相当なボリュームがありますので、今回の手順で参考にしたページのみをご紹介いたします。

以下のページをベースに REST APIの作成を進めます。

- [QUARKUS - WRITING JSON REST SERVICES](https://quarkus.io/guides/rest-json-guide)

永続化については以下のページを参考に、Hibernate-Panache を使ってみます。

- [Quarkus - Simplified Hibernate ORM with Panache](https://quarkus.io/guides/hibernate-orm-panache-guide)

Hibernate-Panache で REST API を構築するサンプルは以下の公式リポジトリにありました。

- [hibernate-orm-panache-resteasy | github](https://github.com/quarkusio/quarkus-quickstarts/tree/master/hibernate-orm-panache-resteasy)

続いて Jaeger と連携する Opentracing 対応については以下のページを参考にします。

- [QUARKUS - USING OPENTRACING](https://quarkus.io/guides/opentracing-guide)

Swagger の対応については以下のページです。

- [QUARKUS - USING OPENAPI AND SWAGGER UI](https://quarkus.io/guides/openapi-swaggerui-guide)

最後に Native ビルドに関しては以下のページを参考にします。

- [QUARKUS - BUILDING A NATIVE EXECUTABLE](https://quarkus.io/guides/building-native-image-guide)

## 対象環境

OS: macOS Mojave 10.14.6
Dokcker: Docker Desktop 2.1.0.4

コンテナの中から親Hostを`docker.for.mac.host.internal`参照するために、Docker Desktop で手抜きをしてしまいました。すいません。。。
それと Windows や Mac で DockerDesktop をお使いの方は **メモリ 6G 以上** を割り当ててください。
ネイティブ化のビルドには想像以上のメモリを使います。

またローカルに JDK 8以降、Maven のご用意をお願いします。

それでは参ります！

## 1. Quarkus プロジェクトの作成

まず、以下のmavenコマンドで新規プロジェクトの作成を行います。

```shell
$ mvn io.quarkus:quarkus-maven-plugin:0.27.0:create
...
$ cd my-quarkus-project
```

で、いくつか質問されますが、REST APIとHelloResourceを生成するオプションを選択してください。あとは適当に答えておけばOKです。
続いて、

```shell
$ mvn quarkus:add-extension -Dextensions="quarkus-jdbc-postgresql,quarkus-smallrye-metrics,quarkus-smallrye-openapi,quarkus-smallrye-opentracing,quarkus-hibernate-orm-panache,quarkus-resteasy-jsonb"
```

でプラグインを追加します。今回の内容的にモリモリです。

## 2. Hibernate-Panache を使った サービスと API の作成

さて、実際のコーディングに入ってまいりましょう！

### 2-1. Hibernate の準備

プラグインを追加して早速ですが… pom.xmlの `<dependecies>` に以下の依存モジュールを追加します。

```maven:pom.xml
...
    <dependency>
      <groupId>com.fasterxml.jackson.datatype</groupId>
      <artifactId>jackson-datatype-jsr310</artifactId>
    </dependency>
    <dependency>
      <groupId>io.opentracing.contrib</groupId>
      <artifactId>opentracing-jdbc</artifactId>
    </dependency>
...
```
こういうところが VSCode だけじゃしんどくて、Eclipse などの IDE のサポートが欲しくなるところなんだよね。~~Java の良くないところだと思いますけど。。。~~

続いて hibernate の設定を以下のように追加します

```properties:src/main/resources/application.properties
quarkus.datasource.url = jdbc:tracing:postgresql://docker.for.mac.host.internal:5432/mydatabase
quarkus.datasource.driver=io.opentracing.contrib.jdbc.TracingDriver
quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQLDialect
quarkus.datasource.username = postgres
quarkus.datasource.password = postgres
# drop and create the database at startup (use `update` to only update the schema)
quarkus.hibernate-orm.database.generation = drop-and-create
```

後ほど、Quarkusをコンテナで動かすので、親Hostを見るために`docker.for.mac.host.internal`を使っています。WindowsやLinuxの方は適宜、修正をお願いいたします。

それと、接続設定にちょこっとおかしな設定が入っていますが・・・種明かしをしてしまうと Jaeger で SQL のトレースログを出力させるために、JDBC にトレース用のドライバを挟んでいます。そして通常、Hibernate はドライバーでDialectを自動判別しますが、トレース用のドライバ挟んでしまうために PostgreSQL かどうかの判別ができなくなってしまいますので、Dialectを直接、指定しています。

トレースログ用のドライバーがなければ下記のようにシンプルな設定でOKです。

```properties:src/main/resources/application.properties
quarkus.datasource.url = jdbc:postgresql://docker.for.mac.host.internal:5432/mydatabase
quarkus.datasource.driver = org.postgresql.Driver
quarkus.datasource.username = postgres
quarkus.datasource.password = postgres
```

また `quarkus.hibernate-orm.database.generation` はスキーマのマイグレーション方法の指定です。今回は試行錯誤多めで行くために`drop-and-create`を選択してます。

### 2-2. モデルクラスの作成

さて、いよいよ実装です。まずはモデルクラスから。

```java:src/main/java/org/acme/quarkus/sample/model/Person.java
import javax.persistence.Entity;
import java.time.LocalDate;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

import com.fasterxml.jackson.annotation.JsonFormat;

@Entity
public class Person extends PanacheEntity {
  public enum Status {
    Alive, DECEASED
  }
  
  public String name;
  @JsonFormat(pattern = "yyyy-MM-dd")
  public LocalDate birth;
  public Status status;
}
```
PanacheEntityを親クラスとしてモデルクラスを定義します。この Panache も以前、ご紹介した TypeORM 同様に "Active Record パターン" で簡単にDBアクセスできるようにするライブラリです。

今回は贅沢にも `Enum` と `LocalDate` をメンバーに使用してみました。
で、`LocalDate`のJSONシリアライズ・デシリアライズがデフォルトではできないのでJSONFormatアノテーションで書式を指示しています。

また、特に何も指定してない"Enum 型"の`Status`ですが、問題なく JSON に交換できるようです。。。とんでもない。

### 2-3. REST API インタフェース部分の実装

続いてのコーディングは REST APIサービスのインタフェース部分です。

```java:src/main/java/org/acme/quarkus/sample/PersonResource.java
package org.acme.quarkus.sample;

import javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
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.metrics.MetricUnits;
import org.eclipse.microprofile.metrics.annotation.Timed;
import org.eclipse.microprofile.metrics.annotation.Counted;

import org.acme.quarkus.sample.model.Person;

@Path("/person")
@Produces(MediaType.APPLICATION_JSON)
public class PersonResource {

    @POST
    @Transactional
    @Counted(name = "performed_create", description = "How many it have been called.")
    @Timed(name = "checksTimer_create", description = "A measure of how long it takes to perform creating person.", unit = MetricUnits.MILLISECONDS)
    public Person create(Person person) {
        person.persist();
        return person;
    }

    @GET
    @Path("/{id}")
    @Transactional
    @Counted(name = "performed_get", description = "How many it have been called.")
    @Timed(name = "checksTimer_get", description = "A measure of how long it takes to perform getting person.", unit = MetricUnits.MILLISECONDS)
    public Person get(@PathParam("id") Long id) {
        return Person.findById(id);
    }
}
```
とりあえず `create` と `get` だけのシンプルな動作確認用APIを作りましたが。。。

`create` の引数で `Person` を直接、受け取っています。JSON から `Person`の交換は自動でやってくれるということです。
で、JSONを変換しただけの `Person person` はまだDBに書き込まれていないので `person.persist()` で DBに保存します。ここでIDを自動発番して`person`に入れておいてくれます。そしてそれを返すだけ。楽チン。

`get` では `Person`の**クラスメソッド**となる`findById`を使用しています。Lombokもびっくりです。

ついでに見慣れない`@Timed`、`@Counted`アノテーションですが、これが Prometheus で記録するためのメトリクスを定義となります。

メソッド内のコードはあくまで純粋にビジネスロジックを記述することに集中でき、アノテーションで URLパスやURLパラメタとのマッピング、Content-Type、トランザクション、メトリクスなどの定義ができてしまいます。~~これが JavaEE だー！~~

### 2-4. Jaeger 設定の追加

さて、トレースログを採取するJaegerとの接続先とサンプラーの設定を行います。
ここでも接続先は親ホストである `docker.for.mac.host.internal` となります。

```properties:src/main/resources/application.properties
quarkus.jaeger.endpoint=http://docker.for.mac.host.internal:14268/api/traces
quarkus.jaeger.service-name=sample.quarkus
quarkus.jaeger.sampler-type=const
quarkus.jaeger.sampler-param=1
```
`service-name`はJaegerでの識別子となります。ここでは `sample.quarkus` とします。

`quarkus.jaeger.sampler-type` と `quarkus.jaeger.sampler-param`は適当です。

### 2-5. Quarkus サーバーのポート設定

最後に Quarkus が待ち受けるポートを以下の設定で指定します。

```properties:src/main/resources/application.properties
quarkus.http.port=8082
```

Quarkus 側の作業は、以上です！

## 3. Jager、PostgreSQL の準備

動作確認に必要な Jaegerや PostgreSQLをdocker-composeでサクッと用意してしまいます。

### 3-1. docker-compose.yml の作成

以下のような `docker-compose.yml` ファイルを作成します。

```yml:docker-compose.yml
version : "3"
services:
  db:
    image: postgres
    ports:
      - 5432:5432
    environment:
      - POSTGRES_DB=mydatabase
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
  adminer:
    image: adminer
    restart: always
    ports:
      - 8080:8080
  jaeger:
    image: jaegertracing/all-in-one
    environment:
      - COLLECTOR_ZIPKIN_HTTP_PORT=9411
    ports:
      - 5775:5775/udp
      - 6831:6831/udp
      - 6832:6832/udp
      - 5778:5778
      - 16686:16686
      - 14268:14268
      - 9411:9411
```

以下のコマンドで起動します。

```shell
$ docker-compose up -d
```

### 3-2. とりあえず Quarkus を動かしてみる

さて、ここまで来たところでとりあえず開発サーバー？を起動させてみましょう。以下のコマンドを叩きます。

```shell
$ mvn compile qurkus:dev
...
```

ブラウザで `http://localhost:8082` を開いてみましょう。Quarkus のスタートページが表示されればとりあえずOKです！

## 4. ネイティブ化

開発サーバーでの動作確認ができましたらいよいよネイティブ化をしてみます。

[Creating a container with a multi-stage Docker build](https://quarkus.io/guides/building-native-image-guide#creating-a-container-with-a-multi-stage-docker-build) の箇所を参考に、以下のようなネイティブコンパイル用のコンテナを作成いたします。

```Dockerfile
## Stage 1 : build with maven builder image with native capabilities
FROM quay.io/quarkus/centos-quarkus-maven:19.2.1 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
USER root
RUN chown -R quarkus /usr/src/app
USER quarkus
RUN mvn -f /usr/src/app/pom.xml -Pnative -Dmaven.test.skip=true clean package

## Stage 2 : create the docker final image
FROM registry.access.redhat.com/ubi8/ubi-minimal
WORKDIR /work/
COPY --from=build /usr/src/app/target/*-runner /work/application
RUN chmod 775 /work
EXPOSE 8080
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
```
そして、以下のコマンドでビルドします！

```shell
$ docker build -t quarkus-rest-demo .
...
```
ビルドが無事に終われば、`quarkus-rest-demo` イメージにネイティブ化されたバイナリが入っています！

このコンテナ単体で4GB程度のメモリを使いますのでメモリ不足や他のコンテナが起動している場合はビルドが失敗する可能性大です。(というか、かなりメモリに余裕がないとビルドに失敗します。)
これだけのコードでも環境によっては10分程度の時間がかかります。

## 5. 起動と動作確認

それでは、実際にコンテナを起動してましょう。PostgreSQLやJaegerのコンテナを止めている場合は
そちらを先に起動させておいてください。

### 5-1. ネイティブ Quarkus サービスの起動

以下のコマンドで起動します。

```shell
$ docker run -it quarkus-rest-demo
```

### 5-2. ターミナルで `POST`

別のターミナルを開き、以下のコマンドで`PersonResource#create`メソッドにJSONをPOSTしてみます。

```shell
$ curl -H 'Content-Type:application/json' -d '{"name":"Alice","birth":"2010-10-11","status":"Alive"}' http://localhost:8082/person

{"id":1,"birth":"2010-10-11","name":"Alice","status":"Alive"}
```
と、`id`が追加されてJSONが返却されたら成功です！

### 5-3. ブラウザで `GET`

ブラウザで `http://localhost:8082/person/1` を開いてみましょう。

こちらでも同様に `{"id":1,"birth":"2010-10-11","name":"Alice","status":"Alive"}`が取得できたでしょうか？

### 5-4. Adminer で確認

例によって Adminer で確認してみましょう。

ブラウザで `http://locahost:8080` を開き、Adminerにアクセスします。上記の設定で指定した`postgres/postgres/mydatabase`でログインし、 SQLコマンドにて`select * from person;` を投げてみると・・・

![SQLコマンド - db - Adminer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/5b1dbfe4-94cb-5653-b8bf-e713694d65fb.png)

(連打したので複数件の)レコードが取得できました！ちゃんと ID がインクリメントされているのがわかります。
このあたりの発番処理は Panache が自動でやってくれています。

### 5-5. Jaeger で確認

いよいよ本題の、Jaeger でのトレースログを確認してみましょう。

ブラウザで `http://localhost:16686` を開き、Jaeger のコンソールを開きます。
サービス名から`sample.quarkus`を選択し、`PersonResouce.create` のログをクリックしてみましょう。

![Jaeger UI native.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/9a5cd5e6-9f6e-01e7-dca9-dba5a36add7e.png)

このように、リクエスト全体で `27ms`、IDの発番に `11ms`、Insert文に `2ms` というのがわかってしまいます。
また発行されたSQL文も確認できるので、より一層ボトルネックのチェックが簡単ですね〜！

## 6. Prometheus + cAdvisor + Grafana でも見てみる

さて、[以前の記事](https://qiita.com/koinori/items/7505cebc57b68bc097ae)で使ってみた `cAdvisor` ですが今回もしっかりとチェックしてみましょう。

### 6-1. dockprom の準備

今回はちょと設定に手を加えたので、本家ではなく私のリポジトリから `clone` してきます。

```shell
$ git@github.com:Yoshinori-Koide-PRO/dockprom.git
...
$ cd dockprom
```

で、`prometheus/prometheus.yml` の設定で Quarkus 側(親ホスト)のアドレスを調整します。

```yml:prometheus/prometheus.yml
...
  - job_name: 'quarkus'
    scrape_interval: 5s
    static_configs:
      - targets: ['docker.for.mac.host.internal:8082']
...
```
今回は Docker Desktop を使っているので `docker.for.mac.host.internal` を使っていますが、linux の場合は、以下のような `.env` ファイルを作成し、

```
# ip -4 addr show docker0 | grep -Po 'inet \K[\d.]+' で取得できた IPを以下に記載
HOME_IP=172.17.0.1
```
`.env`で定義した`HOME_IP`をdocker-compose.ymlでコンテナの`extra_host`に渡します。

```yml:docker-compose.yml
...
  prometheus:
    image: prom/prometheus:v2.13.1
    container_name: prometheus
    volumes:
      - ./prometheus/:/etc/prometheus/
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'
      - '--storage.tsdb.retention.time=200h'
      - '--web.enable-lifecycle'
    restart: unless-stopped
    extra_hosts:
      - "docker_host:$HOME_IP"
    expose:
      - 9090
    networks:
      - monitor-net
    labels:
      org.label-schema.group: "monitoring"
...
```
これで親ホストのIPを`docker_host`というホスト名で解決できるようになります。
Prometheusの設定ファイルでは環境変数など使えないようなので、苦肉の作です。。。

で、起動は `docker-compose up -d` でOKです！

### 6-2. Prometheus での表示

まずは Prometheus です。

ブラウザで `http://localhost:9090` を開き、`admin/admin`で Prometheus にログインします。
`Expression`の欄に `personresource`と打つと候補がリスト表示されます。
![Prometheus Time Series Collection and Processing Server.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/ce5d5465-23f5-c465-ddf8-85b5ad095f7e.png)

適当に選択し、"Graph"タブをクリックすると、取得した値のグラフが表示されます！

![Prometheus Time Series Collection and Processing Server (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/1c51b5ea-dee9-894b-b52f-051ce0fb6890.png)

グラフ下の凡例？の文字列が Prometheus のクエリとなるようです。これをコピペすればこのグラフが Grafanaで表示できます。

### 6-3. cAdvisor + Grafana でチェック！

最後に Grafana でコンテナの状況を確認してみましょう。

ブラウザで `http://localhost:3000` を開き、`admin/admin` でログインします。
左上の`+`からメニューを開き`Docker Containers`を選択すると、立ち上がっているコンテナ群のダッシュボードが表示されます！
![Docker Containers - Grafana.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/010a3542-da82-7b36-010a-b8c1e17dd2d8.png)

上記のスクショで `recurcing_blackwall` と表示されているのが quarkus-rest-demo のコンテナです。(コンテナ名、指定しないから・・・)
`test_env_db_1` が PostgreSQL のコンテナです。

以下ではメモリ使用量のグラフです。

![Docker Containers - Grafana (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/2f7d7ff5-a75c-4615-1cdf-5f94b0ae1617.png)

まだコンテナを起動した直後ですが、PostgreSQLよりも圧倒的に少ないメモリ量で起動しているようです。。。
Javaで書いたアプリとは信じられません。。。
何と言っても単位が10MB単位っていう箇所ですね・・・。最近のJavaアプリケーションサーバーはGB単位でメモリ当てますよね？！
全体的なフットプリントが軽いのが安心感ありますね〜！

## 7. OpenAPIのエンドポイントの確認

さて、メトリクス系のエンドポイントについてはチェックができました。続いて OpenAPIのエンドポイントもチェックしてみましょう！

### 7-1. Swagger UI

Swagger UI は開発サーバーモードでなくては起動しません。こちらは `mvn quarkus:dev` で起動したのち、ブラウザで`http://localhost:8082/swagger-ui`を開いてみましょう。

![Swagger UI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/9c60e9e0-9c27-e3ff-e4ea-99f53630f423.png)

おなじみのSwagger UIですが、公開しているAPIの説明文と直接、叩けるコンソールが表示されます。
ネイティブ化した後でもSwagger UIを公開する場合は以下のオプションを設定するようです。

```properties:src/main/resources/application.properties
quarkus.swagger-ui.always-include=true
```

### 7-2. Open API

こちらはネイティブ化した後でも有効な(当然です)、OpenAPIのエンドポイントです。

ブラウザで `http://localhost:8082/openapi` を開くと以下のようなテキストファイルがダウンロードされます。

```txt
---
openapi: 3.0.1
info:
  title: Generated API
  version: "1.0"
paths:
  /hello:
    get:
      responses:
        200:
          description: OK
          content:
            text/plain:
              schema:
                type: string
  /person:
    post:
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Person'
  /person/{id}:
    get:
      parameters:
...
```

はい、OpenAPIバッチリですね！

## まとめ

Quarkus についてちょっとした設定とアノテーションの追加で非常に多くのエンドポイントとの連携ができるのが確認できました。
ネイティブ化しても動くかどうか疑問だったのですが、想像以上にサポートが厚くて驚きましたね。
特に Hibernateがらみは JDBC もあるし、ネイティブコンパイルとか大変なんじゃなかろうか？と疑っていましたが、バッチリ動いてしまいました。こりゃ参ったなぁ。。。

ネイティブビルドにはメモリ量とコンパイル時間という敷居はありますが、ここまで軽量なのいろいろできてしまうと・・・有り寄りの有り、なんじゃないでしょうか？！

上記の成果物は github にあげました。こちらをどうぞ〜

- [quarkus-examples/tree/hibernate-graphql| github.com](https://github.com/Yoshinori-Koide-PRO/quarkus-examples/tree/hibernate-graphql)

→ 起動方法がイマイチでしたので、構成を変更いたしました。詳しくは次の記事:"[【2019年度11月版】複数のdocker-composeをまたいでコンテナの名前解決をする](https://qiita.com/koinori/items/c318256e660cd68f3bf5)"をご覧ください！

本日は以上です！