Docker BuildKitを使う
はじめに
Dockerの 18.09 以降にはBuildKitという機能が存在しますが、今までなんとなく使ってただけなのでちゃんとまとめてある程度理解しておきたいなと
Docker BuildKitとは
そもそもBuildKitそのものはDocker Engineとは別でMoby Projectで開発されていたものです。 キャッシュや処理の並列化、アーキテクチャの見直しによってソースコードをビルドアーティファクトへ変換する処理をより効率よく行なうことができるようになったり、Dockerfileの拡張文法を提供していたりします。Dockerの 18.09 以降はDocker Engineに拡張として取り込まれ、特に特別なインストールなどはなしに利用することができます。
何ができるん?
つまるところ、BuidKitはどういうことを可能にしてくれるのかということを以下にまとめます。
- ビルドの高速化される
- ビルドの標準出力に各ステップの実行時間が出力される
- Dockerfileの拡張文法が利用可能になる(後述)
使ってみる
環境
実行環境は以下の通り
$ docker version Client: Docker Engine - Community Version: 19.03.12 API version: 1.40 Go version: go1.13.10 Git commit: 48a66213fe Built: Mon Jun 22 15:45:36 2020 OS/Arch: linux/amd64 Experimental: false Server: Docker Engine - Community Engine: Version: 19.03.12 API version: 1.40 (minimum version 1.12) Go version: go1.13.10 Git commit: 48a66213fe Built: Mon Jun 22 15:44:07 2020 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.2.13 GitCommit: 7ad184331fa3e55e52b890ea95e65ba581ae3429 runc: Version: 1.0.0-rc10 GitCommit: dc9208a3303feef5b3839f4323d9beb36df0a9dd docker-init: Version: 0.18.0 GitCommit: fec3683 $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.1 LTS Release: 20.04 Codename: focal $ uname -srvmpio Linux 5.4.0-53-generic #59-Ubuntu SMP Wed Oct 21 09:38:44 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
Dockerfileを用意しておく
Docker BuildKitの機能を試すためにいかのDockerfileをベースにします。
特に明示してなければ以下のDcokerfileを利用してビルドを行っています。
FROM centos:7 RUN yum install -y java
BuildKit有効化
環境変数での有効化
BuildKitは環境変数にDOCKER_BUILDKIT=1
と設定しておくと有効化されます。例えば単発のビルド時に有効化したい場合は以下のようにします。
$DOCKER_BUILDKIT=1 docker build .
デフォルトでの有効化
デフォルトで有効化するためには/etc/docker/daemon.json
のfeature
の項目をtrue
に設定します。
{ "features": { "buildkit": true } }
今回はデフォルトとの対比を行いたいのでこちらは使わず環境変数の方で指定して、BuildKitを使いたいと思います。
出力結果のが変わる
まずは普通にdocker build
した場合は以下のような出力になります。
$ docker build -t java-centos . Sending build context to Docker daemon 2.048kB Step 1/2 : FROM centos:7 ---> 7e6257c9f8d8 Step 2/2 : RUN yum install -y java ---> Using cache ---> 68dff4f570e9 Successfully built 68dff4f570e9 Successfully tagged java-centos:latest
次にBuildKitを有効化してDockerBuildを実行します。
$ DOCKER_BUILDKIT=1 docker build -t java-centos . [+] Building 0.0s (6/6) FINISHED => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 37B 0.0s => [internal] load metadata for docker.io/library/centos:7 0.0s => [1/2] FROM docker.io/library/centos:7 0.0s => CACHED [2/2] RUN yum install -y java 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:fc5361ee029af85ff2d83403afe003214d2dfcb539216b3fcfa1349852ed614a 0.0s => => naming to docker.io/library/java-centos
出力する項目がすこし変わっているのと、各工程にどのくらいの時間がかかったのかの計測結果が出力されるようになります。
また、--progress=plain
を付け加えることで出力形式をプレーン?なものに変えることもできるみたいです。
$ DOCKER_BUILDKIT=1 docker build --progress=plain -t java-centos . #2 [internal] load build definition from Dockerfile #2 transferring dockerfile: 37B done #2 DONE 0.0s #1 [internal] load .dockerignore #1 transferring context: 2B done #1 DONE 0.0s #3 [internal] load metadata for docker.io/library/centos:7 #3 DONE 0.0s #4 [1/2] FROM docker.io/library/centos:7 #4 DONE 0.0s #5 [2/2] RUN yum install -y java #5 CACHED #6 exporting to image #6 exporting layers done #6 writing image sha256:fc5361ee029af85ff2d83403afe003214d2dfcb539216b3fcfa1349852ed614a done #6 naming to docker.io/library/java-centos done #6 DONE 0.0s
(いまのところこいつの利点があんまりわかってないが、余計な=>
がなくなってるだけのかな...?)
BuildKitのフロンドエンドの置き換え
ここで言うフロントエンドとはBuildKitの中で動くビルドディフィニションをLLB(BuildKitが生成する中間バイナリフォーマット)に変換するコンポーネントのことを指します。Docker目線でいえばDockerfileをバイナリに変換するためのコンポーネントのことを指します。
Dcokerfileの先頭行に以下のような形式のコメントを追加することでこのフロントエンドを置き換えることができます。
# syntax = <frontend image>
現状Dockerfileのフロントエンドのイメージは以下の2つが用意されているようです。
後述しますが、フロントエンドを置き換えることによってDockerfile内でエクスペリメンタルな文法を利用することが可能となります。
フロントエンドを置き換えてエクスペリメンタルの機能を利用する(実験機能)
BuildKitを使ったDockerfileのビルドにおいて別フロントエンドを使うことで拡張の機能を利用することができます。
フロントエンドの置き換え
フロントエンドの置き換えに関しては前述していますが、今回使いたい機能を利用するために、Dockerfileの先頭にコメントを追加します。
# syntax=docker/dockerfile:experimental FROM centos:7 RUN yum install -y java
これで拡張文法とその機能を利用することが可能です。
拡張文法
置き換えたフロントエンドで便利そうな機能をいくつかまとめます。
RUN --mount=type=bind
--mount=type=bind
はビルドコンテクスト(Dockerfileが置かれているワーキングディレクトリ)、もしくは、ビルドされるイメージコンテナのディレクトリをバインドすることができます。バインドされるディレクトリはデフォルトはリードオンリーです。
下記のような項目を,
でつないで利用することができます。
項目 | 説明 |
---|---|
target(必須) | Dockerコンテナ内のマウント先のパス |
from | ビルドストレージ、もしくはソースのルート。デフォルトはビルドのコンテクストにバインドされます |
source | from の中のソースパス、デフォルトのソースパスはfrom のルートディレクトリ |
rw,readwrite | マウントされたディレクトリに書き込むことを許可します。ただし、書き込んだデータは後に削除されます |
例えば下記のDockerfileではビルドコンテキストにあるDockerfileの内容をtext.txt
に書き写しています。
# syntax=docker/dockerfile:experimental FROM busybox RUN --mount=type=bind,target=tmp cat tmp/Dockerfile > text.txt
下記のように実行するとビルドコンテキストがDokcerコンテナ内の/tmp
にバインディングされ、Dockerfileの内容がtext.txt
に書き移すことができます。
$ DOCKER_BUILDKIT=1 docker build -t busybox-tmp . [+] Building 2.1s (9/9) FINISHED => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 37B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => resolve image config for docker.io/docker/dockerfile:experimental 1.8s => CACHED docker-image://docker.io/docker/dockerfile:experimental@sha256:de85b2f3a3e8a2f7fe48e8e84a65f6fdd5cd5183afa6412fff9caa6871649c44 0.0s => [internal] load metadata for docker.io/library/busybox:latest 0.0s => [internal] load build context 0.0s => => transferring context: 31B 0.0s => [1/2] FROM docker.io/library/busybox 0.0s => CACHED [2/2] RUN --mount=type=bind,target=tmp cat tmp/Dockerfile > text.txt 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:d75bb6670538d7af739b6f201d170a5fa38128e7cbbcf56ef9448682695d0b36 0.0s => => naming to docker.io/library/busybox-tmp $ docker run busybox-tmp cat text.txt # syntax=docker/dockerfile:experimental FROM busybox RUN --mount=type=bind,target=tmp cat tmp/Dockerfile > text.txt
この機能のユースケースとしてはDockerコンテナ内に必要は無いけど実行はしたいスクリプトがあるディレクトリをバインドしたりすることが考えられます。
ちなみにですが、target
にコンテナ内に元来存在しないディレクトリを指定すると勝手にディレクトリを作成しそこに対してビルドコンテキスト(や指定されたビルドストレージ、もしくはイメージのディレクトリ)をバインドしてくれます。
RUN --mount=type=cache
コンパイラーやパッケージマネージャーのキャッシュディレクトリをバインドすることができます。
下記のような項目を,
でつないで利用することができます。
項目 | 説明 |
---|---|
id | キャッシュを識別するためのID |
target(必須) | Dockerコンテナ内のマウント先のパス |
ro,readonly | セットされていればキャッシュがリードオンリーになります |
from | キャッシュマウントを行なうビルドストレージ。デフォルトは空のディレクトリ |
source | マウントを行なうfrom の中のサブパス。デフォルトはfrom のルート |
mode | 新しいキャッシュディレクトリのファイルモード、デフォルトは0755 |
uid | 新しいキャッシュディレクトリのUserID。デフォルトは0 |
gid | 新しいキャッシュディレクトリのグループID。デフォルトは0 |
例えば下記のDockerfileはmavenのm2ディレクトリをキャッシュして、2回目以降は.m2のキャッシュが/root/.m2
にマウントされるようになります。
# syntax = docker/dockerfile:1.1-experimental FROM maven:3.6.2-jdk-8-slim AS build-env ADD . /javasoerceroot WORKDIR /javasorceroot RUN --mount=type=cache,target=/root/.m2 mvn clean package
aptパッケージをキャッシュする場合以下のようにします。
# syntax = docker/dockerfile:experimental FROM ubuntu RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \ apt update && apt-get --no-install-recommends install -y gcc
RUN --mount=type=tmpfs
tmpfsをコンテナへバインドすることができます。
下記のような項目を,
でつないで利用することができます。
項目 | 説明 |
---|---|
target(必須) | Dockerコンテナ内のマウント先のパス |
RUN --mount=type=secret
秘密鍵のファイルようなクレデンシャルファイルに対して、をイメージの中に残すこと無くバインドすることができます。
項目 | 説明 |
---|---|
id | シークレットのID。ターゲットパスのデフォルトベース名として利用されます。 |
target | Dockerコンテナ内のマウント先のパス。デフォルトでは/run/secrets/ + id となります。 |
required | true でセットすると、シークレットが有効で無かった場合に命令がエラーとして取り扱われるようになります。デフォルトはfalse です |
mode | シークレットのファイルモードデフォルトは0400 |
uid | シークレットのユーザID。デフォルトは0 |
gid | シークレットのグループID。デフォルトは0 |
例えばAWSのS3へアクセスしたいようなユースケースでは以下のようにします。
# syntax = docker/dockerfile:experimental FROM python:3 RUN pip install awscli RUN --mount=type=secret,id=aws,target=/root/.aws/credentials aws s3 cp s3://... ...
docker build時に--secret
フラグを用いてバインドするクレデンシャルなファイルを渡します。
$ docker build --secret id=aws,src=$HOME/.aws/credentials .
その他機能はこちらをご覧ください。
参考資料
QuarkusアプリのメトリクスをMicrometer+Prometeus+Grafanaで可視化する
はじめに
Quarkus 1.9のリリースブログをなんとなく眺めていると以下のようなことが書かれていました。
Micrometer extension maturing
1.8 introduced a new Micrometer extension. During the 1.9 development cycle, it matured a lot and is now the recommended way to collect metrics in Quarkus.
原文リンク
どうやら1.9以降はMicrometerがメトリクスの収集方法としておすすめされるようなので、使ってみてPromethus+Grafanaで可視化するのをやってみようかと思います。
PrometeusとGrafanaはDocker使って立ち上げて、ホストにポートフォワードします。
あと、ついでにQuarkusで設定できるMicrometerの設定をまとめておこうと思います。
やってみる
環境
動作環境は以下の通り
$ java --version openjdk 11.0.8 2020-07-14 OpenJDK Runtime Environment 18.9 (build 11.0.8+10) OpenJDK 64-Bit Server VM 18.9 (build 11.0.8+10, mixed mode) $ mvn --version Apache Maven 3.6.3 Maven home: /usr/share/maven Java version: 11.0.8, vendor: N/A, runtime: /home/someone/.sdkman/candidates/java/11.0.8-open Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-52-generic", arch: "amd64", family: "unix" $ uname -srvmpio Linux 5.4.0-52-generic #57-Ubuntu SMP Thu Oct 15 10:57:00 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.1 LTS Release: 20.04 Codename: focal $ docker version Client: Docker Engine - Community Version: 19.03.12 API version: 1.40 Go version: go1.13.10 Git commit: 48a66213fe Built: Mon Jun 22 15:45:36 2020 OS/Arch: linux/amd64 Experimental: false Server: Docker Engine - Community Engine: Version: 19.03.12 API version: 1.40 (minimum version 1.12) Go version: go1.13.10 Git commit: 48a66213fe Built: Mon Jun 22 15:44:07 2020 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.2.13 GitCommit: 7ad184331fa3e55e52b890ea95e65ba581ae3429 runc: Version: 1.0.0-rc10 GitCommit: dc9208a3303feef5b3839f4323d9beb36df0a9dd docker-init: Version: 0.18.0
プロジェクトを作成する
Quarkus - Start coding with code.quarkus.ioでプロジェクトを作成します。
設定は以下の通り。
ブログではMicroProfile Metricsじゃなくて、Micrometerの方を押してたきがしますがここではExperimentalってなってますね。
多分まだ修正されてないだけなんでしょうか。
pomにPrometheusの依存を追加しておく
MicromerterでPrometheusの形式の情報を取得するために以下の依存を追加しておく必要があります。
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency>
cURLでメトリクスを取得する
この状態ですでにメトリクスは取得できます。
アプリを起動して、cURLでlocalhost:8080/metrics/
を叩いてみます。
$ ./mvnw clean compile quarkus:dev $ curl localhost:8080/metrics/ # HELP jvm_threads_states_threads The current number of threads having NEW state # TYPE jvm_threads_states_threads gauge jvm_threads_states_threads{state="runnable",} 20.0 jvm_threads_states_threads{state="blocked",} 0.0 jvm_threads_states_threads{state="waiting",} 10.0 jvm_threads_states_threads{state="timed-waiting",} 5.0 jvm_threads_states_threads{state="new",} 0.0 jvm_threads_states_threads{state="terminated",} 0.0 # HELP jvm_gc_memory_promoted_bytes_total Count of positive increases in the size of the old generation memory pool before GC to after GC # TYPE jvm_gc_memory_promoted_bytes_total counter jvm_gc_memory_promoted_bytes_total 0.0 # HELP jvm_gc_memory_allocated_bytes_total Incremented for an increase in the size of the young generation memory pool after one GC to before the next # TYPE jvm_gc_memory_allocated_bytes_total counter jvm_gc_memory_allocated_bytes_total 5.8720256E7 # HELP jvm_gc_overhead_percent An approximation of the percent of CPU time used by GC activities over the last lookback period or since monitoring began, whichever is shorter, in the range [0..1] # TYPE jvm_gc_overhead_percent gauge jvm_gc_overhead_percent 0.0014644940855901103 # HELP process_cpu_usage The "recent cpu usage" for the Java Virtual Machine process # TYPE process_cpu_usage gauge process_cpu_usage 1.0287248248851947E-4 # HELP system_cpu_usage The "recent cpu usage" for the whole system # TYPE system_cpu_usage gauge system_cpu_usage 0.13014392884149495 # HELP process_uptime_seconds The uptime of the Java virtual machine # TYPE process_uptime_seconds gauge process_uptime_seconds 7.019 # HELP process_start_time_seconds Start time of the process since unix epoch. # TYPE process_start_time_seconds gauge process_start_time_seconds 1.604492368312E9 # HELP jvm_info_total JVM version info # TYPE jvm_info_total counter jvm_info_total{runtime="OpenJDK Runtime Environment",vendor="Oracle Corporation",version="11.0.8+10",} 1.0 # HELP jvm_buffer_count_buffers An estimate of the number of buffers in the pool # TYPE jvm_buffer_count_buffers gauge jvm_buffer_count_buffers{id="mapped",} 0.0 jvm_buffer_count_buffers{id="direct",} 11.0 # HELP jvm_buffer_memory_used_bytes An estimate of the memory that the Java virtual machine is using for this buffer pool # TYPE jvm_buffer_memory_used_bytes gauge jvm_buffer_memory_used_bytes{id="mapped",} 0.0 jvm_buffer_memory_used_bytes{id="direct",} 21370.0 # HELP process_files_max_files The maximum file descriptor count # TYPE process_files_max_files gauge process_files_max_files 1048576.0 # HELP jvm_gc_pause_seconds Time spent in GC pause # TYPE jvm_gc_pause_seconds summary jvm_gc_pause_seconds_count{action="end of minor GC",cause="Metadata GC Threshold",} 1.0 jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Metadata GC Threshold",} 0.007 # HELP jvm_gc_pause_seconds_max Time spent in GC pause # TYPE jvm_gc_pause_seconds_max gauge jvm_gc_pause_seconds_max{action="end of minor GC",cause="Metadata GC Threshold",} 0.007 # HELP jvm_memory_committed_bytes The amount of memory in bytes that is committed for the Java virtual machine to use # TYPE jvm_memory_committed_bytes gauge jvm_memory_committed_bytes{area="heap",id="G1 Survivor Space",} 8388608.0 jvm_memory_committed_bytes{area="heap",id="G1 Old Gen",} 2.07618048E8 jvm_memory_committed_bytes{area="nonheap",id="Metaspace",} 3.9010304E7 jvm_memory_committed_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 2555904.0 jvm_memory_committed_bytes{area="heap",id="G1 Eden Space",} 3.0408704E8 jvm_memory_committed_bytes{area="nonheap",id="Compressed Class Space",} 4980736.0 jvm_memory_committed_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 8716288.0 # HELP system_load_average_1m The sum of the number of runnable entities queued to available processors and the number of runnable entities running on the available processors averaged over a period of time # TYPE system_load_average_1m gauge system_load_average_1m 1.11 # HELP jvm_gc_live_data_size_bytes Size of old generation memory pool after a full GC # TYPE jvm_gc_live_data_size_bytes gauge jvm_gc_live_data_size_bytes 1.2500864E7 # HELP jvm_memory_used_bytes The amount of used memory # TYPE jvm_memory_used_bytes gauge jvm_memory_used_bytes{area="heap",id="G1 Survivor Space",} 8388608.0 jvm_memory_used_bytes{area="heap",id="G1 Old Gen",} 1.0403712E7 jvm_memory_used_bytes{area="nonheap",id="Metaspace",} 3.7437216E7 jvm_memory_used_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 1258624.0 jvm_memory_used_bytes{area="heap",id="G1 Eden Space",} 6291456.0 jvm_memory_used_bytes{area="nonheap",id="Compressed Class Space",} 4416816.0 jvm_memory_used_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 8708736.0 # HELP jvm_classes_loaded_classes The number of classes that are currently loaded in the Java virtual machine # TYPE jvm_classes_loaded_classes gauge jvm_classes_loaded_classes 6538.0 # HELP jvm_classes_unloaded_classes_total The total number of classes unloaded since the Java virtual machine has started execution # TYPE jvm_classes_unloaded_classes_total counter jvm_classes_unloaded_classes_total 0.0 # HELP http_server_connections_seconds_max # TYPE http_server_connections_seconds_max gauge http_server_connections_seconds_max 0.016392673 # HELP http_server_connections_seconds # TYPE http_server_connections_seconds summary http_server_connections_seconds_active_count 1.0 http_server_connections_seconds_duration_sum 0.016127428 # HELP jvm_gc_max_data_size_bytes Max size of old generation memory pool # TYPE jvm_gc_max_data_size_bytes gauge jvm_gc_max_data_size_bytes 8.321499136E9 # HELP jvm_buffer_total_capacity_bytes An estimate of the total capacity of the buffers in this pool # TYPE jvm_buffer_total_capacity_bytes gauge jvm_buffer_total_capacity_bytes{id="mapped",} 0.0 jvm_buffer_total_capacity_bytes{id="direct",} 21368.0 # HELP jvm_memory_max_bytes The maximum amount of memory in bytes that can be used for memory management # TYPE jvm_memory_max_bytes gauge jvm_memory_max_bytes{area="heap",id="G1 Survivor Space",} -1.0 jvm_memory_max_bytes{area="heap",id="G1 Old Gen",} 8.321499136E9 jvm_memory_max_bytes{area="nonheap",id="Metaspace",} -1.0 jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 7553024.0 jvm_memory_max_bytes{area="heap",id="G1 Eden Space",} -1.0 jvm_memory_max_bytes{area="nonheap",id="Compressed Class Space",} 1.073741824E9 jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 2.44105216E8 # HELP process_files_open_files The open file descriptor count # TYPE process_files_open_files gauge process_files_open_files 212.0 # HELP jvm_threads_daemon_threads The current number of live daemon threads # TYPE jvm_threads_daemon_threads gauge jvm_threads_daemon_threads 13.0 # HELP jvm_threads_peak_threads The peak live thread count since the Java virtual machine started or peak was reset # TYPE jvm_threads_peak_threads gauge jvm_threads_peak_threads 42.0 # HELP jvm_threads_live_threads The current number of live threads including both daemon and non-daemon threads # TYPE jvm_threads_live_threads gauge jvm_threads_live_threads 35.0 # HELP http_server_bytes_read # TYPE http_server_bytes_read summary http_server_bytes_read_count 1.0 http_server_bytes_read_sum 0.0 # HELP http_server_bytes_read_max # TYPE http_server_bytes_read_max gauge http_server_bytes_read_max 0.0 # HELP jvm_memory_usage_after_gc_percent The percentage of old gen heap used after the last GC event, in the range [0..1] # TYPE jvm_memory_usage_after_gc_percent gauge jvm_memory_usage_after_gc_percent{area="heap",generation="old",} 0.0015022370123094128 # HELP system_cpu_count The number of processors available to the Java virtual machine # TYPE system_cpu_count gauge system_cpu_count 8.0
Prometeusでデータを取得する
Prometeus Pull型のモニタリングとアラートを行なうためのツールキットです。 Dockerで準備して、アプリのメトリクスを取得してみます。
prometheus.ymlを準備しておく
ここを参考にPrometheusの設定を記述します。
Dockerでボリュームマウントできれば、設定はどこでも良いのですが一旦/tmp/etc/prometheus.yaml
に作成しました。
global: scrape_interval: 15s evaluation_interval: 15s rule_files: scrape_configs: - job_name: prometheus static_configs: - targets: ['${IP_OF_YOUR_APP}:8080']
注目すべきは-targets
のところで、ここにQuarkusあぷりのホストとポートを指定します。
アプリは/metrics
で情報の公開を行っていますが、Prometheusはデフォルトで、/metrics
に情報を取得しに行くためPathなどの指定は必要ありません。
docker runでPrometeusを起動する
以下のコマンドでPrometeusを起動します。
ボリュームのマウントはホスト側のyamlファイルを置いた場所に指定します。
docker run \ -p 9090:9090 \ -v /tmp/etc/prometheus.yaml:/etc/prometheus/prometheus.yml \ prom/prometheus
PrometheusのUIを確認
起動してブラウザーから9090にアクセスしてみるとデータが取得できているのが確認できます。
Grafanaで可視化する
Grafanaはメトリクスの可視化ツールです。
Prometheusなどで収集したデータに対してよりリッチなUIを提供したりできます。
こちらもDockerでさくっと立ち上げてみます。
docker run -d -p 3000:3000 grafana/grafana
http://localhost:3000/
にアクセスすると、Login画面にリダイレクトされます。
初回パスワード/ユーザ名はadmin/admin
です。
Prometeusに接続する
Prometeusからメトリクスを収集するようにします。
右のメニューバーの歯車を選択し、add data source
を選択すると以下のような画面が表示されます。
ここでは迷うこと無くPrometheusを選択します。
すると以下の設定画面に遷移します。
いろいろ設定項目がありますが、今回はただ動かしたいだけなので、Prometheusが動いているIPとポートをURL
のとろこに設定します。同じやり方をされていればホストマシンのIPをここにかけば大丈夫です。
ダッシュボードを作成する
最後にダッシュボードを作成してみます。
また、右のメニューバーから+のボタンを押して、Dashbord
を選択すると以下のような画面に遷移します。
あとはお好きなようにメトリクスをしたり、クエリを書いたりしてお好みのダッシュボードを作っていく感じです。
画像では単にMetrics
のところでjvm_gc_max_data_size_bytes
を選択しただけです。
設定項目一覧
上記までで可視化は完了しましたが、Quarkusで設定できるMicrometerのプロパティをいくつかまとめておこうと思います。
すべてのプロパティを見たい方はこちらを参照ください(気が向いたら追記していくかもです)
プロパティ | 説明 | 型(デフォルト値) |
---|---|---|
quarkus.micrometer.enabled | Micromerterのサポートを有効化するフラグ | boolean(true) |
quarkus.micrometer.registry-enabled-default | クラスパス内にMeterRegistryの実装を見つけた場合に自動的に認識するか否かを決めるためのフラグ。デフォルトでは自動的に認識する。 | boolean(true) |
quarkus.micrometer.binder-enabled-default | クラスパス内にMeterBinderの実装を見つけた場合に自動的に認識するか否かを決めるためのフラグ。デフォルトでは自動的に認識する。 | boolean(true) |
quarkus.micrometer.binder.vertx.enabled | Vert.xのメトリクスサポートの有効化フラグマイクロメータのサポートが有効化されている場合、Vert.xのMetricsOpitongaクラスパスにある場合、このプロパティにtrueが指定されている場合、quarkus.micrometer.binder-enabled-default がtrueの場合にtrueになる |
boolean |
quarkus.micrometer.binder.mp-metrics.enabled | MicroProfile Metricsのサポート有効化フラグ、 | boolean |
quarkus.micrometer.binder.jvm | jvmのメトリクスサポートの有効化フラグ | boolean(true) |
quarkus.micrometer.binder.system | システムのメトリクスサポート有効化フラグ | boolean |
quarkus.micrometer.export.prometheus.path | メトリクスを公開するパスを指定するためのプロパティ | string(/metrics) |
quarkus.micrometer.export.json.enabled | Jsonフォーマットでのメトリクス公開を行なうか否かのフラグ | boolean(false) |
参考資料
Spring REST DocsとSpring Cloud Contractを連携して使ってみる
はじめに
Spring REST DocsはSpringのRestFullアプリケーションにおけるWebAPIのインターフェースをドキュメント化する際のサポートをしてくれます。Spring MVC Testをもとにスニペットを自動生成し、Asciidoctorの形式で出力してくれます。
また、Spring Cloud ContractはConsumer Driven Contractのサポートを提供しているプロジェクトです。Spring Cloud Contractに関しては以前にブログを書いたので良ければこちらをご覧ください。
この2つは組み合わせて利用することが可能なようなので、試してみたいと思います。
組み合わせることでなにが変わるのか?
端的に言うと、Spring Cloud Contractで書いていた契約のYamlファイルを書く必要がなくなるみたいです。その分テストを自分で書くことになるのでその面だけで考えると一長一短みたいなところはあるかもしれないです。ただ、全体として考えると、テストを記述することでCDCのサポートを受けれるようになり、かつAPIがヒューマンリーダブルな形でドキュメント化されるようになります。
やってみる
実行環境
実行環境は以下の通り
$ java --version openjdk 15.0.1 2020-10-20 OpenJDK Runtime Environment (build 15.0.1+9-18) OpenJDK 64-Bit Server VM (build 15.0.1+9-18, mixed mode, sharing) $ mvn --version Apache Maven 3.6.3 Maven home: /usr/share/maven Java version: 15.0.1, vendor: Oracle Corporation, runtime: /home/someone/.sdkman/candidates/java/15.0.1-open Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-52-generic", arch: "amd64", family: "unix"
プロジェクトはSpring Iniializrを使って、以下の設定で作成します
また、CDCのConsumer側はSpring Cloud Contractの使い方と変わらないため、Spring Cloud ContractのProducer側のみを作成します。
pomを軽く眺めてみる
作成されたプロジェクトのpomを軽く眺めてみると必要な依存が入っているのといくつかのプラグインが設定されています。
<plugins> <plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>2.2.4.RELEASE</version> <extensions>true</extensions> <configuration> <testFramework>JUNIT5</testFramework> </configuration> </plugin> <plugin> <groupId>org.asciidoctor</groupId> <artifactId>asciidoctor-maven-plugin</artifactId> <version>1.5.8</version> <executions> <execution> <id>generate-docs</id> <phase>prepare-package</phase> <goals> <goal>process-asciidoc</goal> </goals> <configuration> <backend>html</backend> <doctype>book</doctype> </configuration> </execution> </executions> <dependencies> <dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-asciidoctor</artifactId> <version>${spring-restdocs.version}</version> </dependency> </dependencies> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins>
asciidocとspring−cloud-contractのプラグインが入れられているのが確認できます。
Pom等に設定を追加する
2つを連携されるためにいくつか設定を行なう必要があります。
PomにContractのテストをスキップする設定を追加する
前述の通り、REST DocとCloud Contractを連携される場合、Yamlなどの契約を書く必要がありません。つまり、Cloud Contractが自動生成するテスト実行する必要がないので、スキップする設定を追加してやる必要があります。
ドキュメントによると、ユーザープロパティにspring.cloud.contract.verifier.skip
をtrueで設定してやることでテストをスキップできるようなので設定を記述します。
<properties> <java.version>15</java.version> <spring-cloud.version>Hoxton.SR8</spring-cloud.version> <spring.cloud.contract.verifier.skip>true</spring.cloud.contract.verifier.skip> </properties>
Spring Cloud Contract Pluginを削除する(追記)
このブログを投稿した際に以下のようなアドバイスをSpring Cloud Contract のAutherであるMarcin Grzejszczakさんからいただきました。
Thank you for telling me this, Mr. Marcin Grzejszczak!!
Nice article! BTW if you're using Spring Cloud Contract with Rest Docs you don't need to add the maven contract plugin cause you will not generate the tests nor will you package the stubs automatically.
— Marcin Grzejszczak (@MGrzejszczak) November 2, 2020
Contract とREST Docsを連携させる場合はテストもスタブも自動生成するわけで無いので、このプラグインは必要ないとのことです。なので消してしまって大丈夫です。
Stubの作成のための設定を追加する
StubのJarを作成するためにassembly pluginを入れて設定を追加してやる必要があります。まず、以下のプラグインをPomに追加します。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <executions> <execution> <id>stub</id> <phase>prepare-package</phase> <goals> <goal>single</goal> </goals> <inherited>false</inherited> <configuration> <attach>true</attach> <descriptors>./src/assembly/stub.xml</descriptors> </configuration> </execution> </executions> </plugin>
次にサンプルを参考にstub.xml
を作成し${project_root}/src/assembly/
配下に配置します。
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 https://maven.apache.org/xsd/assembly-1.1.3.xsd"> <id>stubs</id> <formats> <format>jar</format> </formats> <includeBaseDirectory>false</includeBaseDirectory> <fileSets> <fileSet> <directory>src/main/java</directory> <outputDirectory>/</outputDirectory> <includes> <include>**dev/hirooka/model/*.*</include> </includes> </fileSet> <fileSet> <directory>${project.build.directory}/classes</directory> <outputDirectory>/</outputDirectory> <includes> <include>**dev/hirooka/model/*.*</include> </includes> </fileSet> <fileSet> <directory>${project.build.directory}/snippets/stubs</directory> <outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/mappings</outputDirectory> <includes> <include>**/*</include> </includes> </fileSet> <fileSet> <directory>./target/generated-snippets/contracts</directory> <outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/contracts</outputDirectory> <includes> <include>**/*.groovy</include> </includes> </fileSet> </fileSets> </assembly>
ここで、ポイントは<directory>./target/generated-snippets/contracts</directory>
のところで、これは自動生成される契約のDSLを指定してやる必要があります。
デフォルトではtarget配下にcontract
ディレクトリが作成されそこにgroovyが出力されるのでそこを指定します。
<plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>2.2.4.RELEASE</version> <extensions>true</extensions> <configuration> <testFramework>JUNIT5</testFramework> <packageWithBaseClasses>dev.hirooka</packageWithBaseClasses> </configuration> </plugin>
spring-cloud-contract-wiremockの依存を追加する
Spring Cloud Contract Rest Docsのインテグレーションを使うためにはpomに以下の依存を追加する必要があります。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-wiremock</artifactId> <scope>test</scope> </dependency>
こいつを追加することで、Rest Docからスタブを生成することができるようになります。
ベースとなるテストと実装を書く
実装に入る前に先にテストを記述しておきます。
テストはJunit5とMockMVC
を使って記述します。Spring REST Docsは他にも、WebTestClient
やREST Assured
でも利用できます。
@SpringBootTest class HelloControllerTest { private MockMvc mockMvc; @BeforeEach public void setUp(WebApplicationContext context) { this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); } @Test void HelloControllerのテスト() throws Exception { mockMvc.perform(get("/")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.hello").value("world")); } }
ここまでは一般的なテストですね次に実装を書きます。
@RestController public class HelloController { @GetMapping("/") public Map<String, String> hello() { return Map.of("hello", "world"); } }
実装もごくごく一般的なコントローラーです。
テストを実行すると成功します。
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.282 s - in hirooka.dev.cdcanddocs.controller.HelloControllerTest
REST Docsを追加する
ベースとなる実装ができたところでテストにREST Docsの設定を入れていきます。
先程の@BeforeEach
で書いたMockMvc
の設定とテスト自体を少しいじります。
@SpringBootTest @ExtendWith(RestDocumentationExtension.class) class HelloControllerTest { private MockMvc mockMvc; @BeforeEach public void setUp( WebApplicationContext context, RestDocumentationContextProvider provider ) { this.mockMvc = MockMvcBuilders.webAppContextSetup(context) .apply(documentationConfiguration(provider)) .build(); } @Test void HelloControllerのテスト() throws Exception { mockMvc.perform(get("/")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.hello").value("world")) .andDo(document("index")); } }
僕自身がRestDocsを使うのははじめてなので少しだけ突っ込んでまとめようと思います。
まず、最初に@ExtendWith(RestDocumentationExtension.class)
を追加しています。このエクステンションを入れることで、@BforeEach
の引数でRestDocumentationContextProviderを受け取ることができます。このクラスはRestDocumentationContext
へのアクセスを提供します。
次に@BeforeEach
にでMockMvc
に設定を追加しています。 MockMvcRestDocumentationのdocumentationConfiguration()
スタティックメソッドを使ってMockMvc
にMockMvc用のRest Docs拡張である MockMvcRestDocumentationConfigurerを登録しています。ここまでで、事前準備は完了です。
次にTestクラスに着目すると、最後のandDo()
で MockMvcRestDocumentationのdocumennt()
スタティックメソッドを呼び出しています。このスタティックメソッドは第一引数にドキュメントの識別子と第二引数に可変引数としてSnippetインターフェースの実装を受け取ります。この実装は出力ドキュメントに対して付加情報を追加する際に利用します。
mvn test
を実行するとtarget/generated-snippets/index/*.adoc
にデフォルトでは以下の画像の6種類のasciidocが出力されます。
結果の出力はMarkDownなどでも行えるようですが、ここでは深く触れないよ言うにします。
Contractと連携する。
AsciiDocの出力まで終わったところで、Spring Cloud Contractとの連携を行ってみようと思います。
テストコードを以下のように修正します。
@Test void HelloControllerのテスト() throws Exception { mockMvc.perform(get("/")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.hello").value("world")) .andDo(document("index", SpringCloudContractRestDocs.dslContract())); }
document()
の引数にSpringCloudContractRestDocs.dslContract()
を渡すことで契約のDSLとスタブが自動生成されるようになります。
mvn clean install
を実行するとtarget配下が以下のように生成されることがわかります。
Consumer側からのStubの利用
いくつか方法がありますが@AutoConfigureStubRunner
を使った方法をこちらにまとめてますのでそちらを参照してください。
リクエストをバリデートするような実装に変えてみる
今までは、リクエストに対して固定値を返すだけのシンプルな実装でしたが、リクエストのパターンによって返すものを変えるような実装に変えてみます。 例えば以下のようなストーリーを想定します。
Rest DocsとCloud Contractの連携でリクエストのヴァリファイを行なうスタブを作成する場合、WireMockRestDocs
を利用します。
@Test void HelloControllerのOKテスト() throws Exception { mockMvc.perform(post("/") .contentType(MediaType.APPLICATION_JSON) .content("{\"value\": 10}")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.status").value("OK")) .andDo( WireMockRestDocs.verify() .contentType(MediaType.APPLICATION_JSON) .jsonPath("$[?(@.value >= 10)]")) .andDo(document("okPattern", SpringCloudContractRestDocs.dslContract())); } @Test void HelloControllerのNGテスト() throws Exception { mockMvc.perform(post("/") .contentType(MediaType.APPLICATION_JSON) .content("{\"value\": 9}")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.status").value("NG")) .andDo( WireMockRestDocs.verify() .contentType(MediaType.APPLICATION_JSON) .jsonPath("$[?(@.value < 10)]")) .andDo(document("ngPattern", SpringCloudContractRestDocs.dslContract())); }
同じくmvn clean install
をすればstubが生成されます。
参考資料
Docker Engine APIを使ってcURLでDokcer Engineを操作する
はじめに
Docker のクライアントは公式だとGoとかPythonのものはあるようですが、UNIXドメインソケットを使ったHTTPでの操作も可能なようです。このブログではcURLを用いてDocker Engineに対するコマンドを実行してみたいと思います。
Docker Engine API
Dokcer Engine APIはDocker Engineが提供するHTTP APIです。Docker ClinentなどはこのAPIを使ってEngineとコミュニケーションをとっています。大体のコマンドははこのAPIに対応付けられているようで、HTTPを使ってDokcer Engineの操作を行なうことができます。現在の最新のバージョンはv1.40みたいです。また、Docker Engine APIのOpenAPI Specificationはここから取得できます。
前述通り、実際にこのAPIへアクセスする場合はUNIXドメインソケットを使うようです例えばcURLで起動中のコンテナ一覧を取得場合は以下のようにします。
(公式のサンプルはここからみれます)
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES $ curl --unix-socket /var/run/docker.sock http:/v1.40/containers/json []
現状、動作中のコンテナは一つも無いで空のリストが返ってきています。
使ってみる
環境
$ uname -srvmpio Linux 5.4.0-52-generic #57-Ubuntu SMP Thu Oct 15 10:57:00 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.1 LTS Release: 20.04 Codename: focal $ docker version Client: Docker Engine - Community Version: 19.03.12 API version: 1.40 Go version: go1.13.10 Git commit: 48a66213fe Built: Mon Jun 22 15:45:36 2020 OS/Arch: linux/amd64 Experimental: false Server: Docker Engine - Community Engine: Version: 19.03.12 API version: 1.40 (minimum version 1.12) Go version: go1.13.10 Git commit: 48a66213fe Built: Mon Jun 22 15:44:07 2020 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.2.13 GitCommit: 7ad184331fa3e55e52b890ea95e65ba581ae3429 runc: Version: 1.0.0-rc10 GitCommit: dc9208a3303feef5b3839f4323d9beb36df0a9dd docker-init: Version: 0.18.0 $ curl --version curl 7.68.0 (x86_64-pc-linux-gnu) libcurl/7.68.0 OpenSSL/1.1.1f zlib/1.2.11 brotli/1.0.7 libidn2/2.2.0 libpsl/0.21.0 (+libidn2/2.2.0) libssh/0.9.3/openssl/zlib nghttp2/1.40.0 librtmp/2.3 Release-Date: 2020-01-08 Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp Features: AsynchDNS brotli GSS-API HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM NTLM_WB PSL SPNEGO SSL TLS-SRP UnixSockets
コンテナの作成
alpineのイメージつかってコンテナを作成します。
$ curl --unix-socket /var/run/docker.sock -H "Content-Type: application/json" \ > -d '{"Image": "alpine", "Cmd": ["echo", "hello world"]}' \ > -X POST http:/v1.40/containers/create {"Id":"339e9914b446b768e0e1da6b313165f9a8187793175f068884e6c1597d6a8ea3","Warnings":[]}
コンテナ作成は/containers/create
に対してPOSTで、必要な情報をJson形式で送ってやることで実行することができます。
コンテナの実行
次に先程作成したコンテナを実行します。
$ curl --unix-socket /var/run/docker.sock -X POST http:/v1.40//containers/339e9914b446/start
dokcer run
に相当するような操作です。 POSTのリクエストを/containers/${IMAGE_ID}/start
に送ってやることで実行可能です。
インスペクト
コンテナのインスペクトの実行をしてみます。
/containers/${IMAGE_ID}/json
にGETリクエストを送ってやることで実行可能です。
$ curl --unix-socket /var/run/docker.sock "http:/v1.40/containers/339e9914b446/json" | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 4369 0 4369 0 0 4266k 0 --:--:-- --:--:-- --:--:-- 4266k { "Id": "339e9914b446b768e0e1da6b313165f9a8187793175f068884e6c1597d6a8ea3", "Created": "2020-10-24T12:37:07.655320189Z", "Path": "echo", "Args": [ "hello world" ], "State": { "Status": "exited", "Running": false, "Paused": false, "Restarting": false, "OOMKilled": false, "Dead": false, "Pid": 0, "ExitCode": 0, "Error": "", "StartedAt": "2020-10-24T12:39:50.497526437Z", "FinishedAt": "2020-10-24T12:39:50.520324354Z" }, "Image": "sha256:f70734b6a266dcb5f44c383274821207885b549b75c8e119404917a61335981a", "ResolvConfPath": "/var/lib/docker/containers/339e9914b446b768e0e1da6b313165f9a8187793175f068884e6c1597d6a8ea3/resolv.conf", "HostnamePath": "/var/lib/docker/containers/339e9914b446b768e0e1da6b313165f9a8187793175f068884e6c1597d6a8ea3/hostname", "HostsPath": "/var/lib/docker/containers/339e9914b446b768e0e1da6b313165f9a8187793175f068884e6c1597d6a8ea3/hosts", "LogPath": "/var/lib/docker/containers/339e9914b446b768e0e1da6b313165f9a8187793175f068884e6c1597d6a8ea3/339e9914b446b768e0e1da6b313165f9a8187793175f068884e6c1597d6a8ea3-json.log", "Name": "/agitated_swirles", "RestartCount": 0, "Driver": "overlay2", "Platform": "linux", "MountLabel": "", "ProcessLabel": "", "AppArmorProfile": "docker-default", "ExecIDs": null, "HostConfig": { "Binds": null, "ContainerIDFile": "", "LogConfig": { "Type": "json-file", "Config": {} }, "NetworkMode": "default", "PortBindings": null, "RestartPolicy": { "Name": "", "MaximumRetryCount": 0 }, "AutoRemove": false, "VolumeDriver": "", "VolumesFrom": null, "CapAdd": null, "CapDrop": null, "Capabilities": null, "Dns": null, "DnsOptions": null, "DnsSearch": null, "ExtraHosts": null, "GroupAdd": null, "IpcMode": "private", "Cgroup": "", "Links": null, "OomScoreAdj": 0, "PidMode": "", "Privileged": false, "PublishAllPorts": false, "ReadonlyRootfs": false, "SecurityOpt": null, "UTSMode": "", "UsernsMode": "", "ShmSize": 67108864, "Runtime": "runc", "ConsoleSize": [ 0, 0 ], "Isolation": "", "CpuShares": 0, "Memory": 0, "NanoCpus": 0, "CgroupParent": "", "BlkioWeight": 0, "BlkioWeightDevice": null, "BlkioDeviceReadBps": null, "BlkioDeviceWriteBps": null, "BlkioDeviceReadIOps": null, "BlkioDeviceWriteIOps": null, "CpuPeriod": 0, "CpuQuota": 0, "CpuRealtimePeriod": 0, "CpuRealtimeRuntime": 0, "CpusetCpus": "", "CpusetMems": "", "Devices": null, "DeviceCgroupRules": null, "DeviceRequests": null, "KernelMemory": 0, "KernelMemoryTCP": 0, "MemoryReservation": 0, "MemorySwap": 0, "MemorySwappiness": null, "OomKillDisable": false, "PidsLimit": null, "Ulimits": null, "CpuCount": 0, "CpuPercent": 0, "IOMaximumIOps": 0, "IOMaximumBandwidth": 0, "MaskedPaths": [ "/proc/asound", "/proc/acpi", "/proc/kcore", "/proc/keys", "/proc/latency_stats", "/proc/timer_list", "/proc/timer_stats", "/proc/sched_debug", "/proc/scsi", "/sys/firmware" ], "ReadonlyPaths": [ "/proc/bus", "/proc/fs", "/proc/irq", "/proc/sys", "/proc/sysrq-trigger" ] }, "GraphDriver": { "Data": { "LowerDir": "/var/lib/docker/overlay2/136d71a0857b85896e2c74cd22c82efbccf2739862fe56c0e00427afaef50ad2-init/diff:/var/lib/docker/overlay2/98748a873a839203b278e9bf5bd0086685aaa2e9b0fae44d74393e7cdfcb1691/diff", "MergedDir": "/var/lib/docker/overlay2/136d71a0857b85896e2c74cd22c82efbccf2739862fe56c0e00427afaef50ad2/merged", "UpperDir": "/var/lib/docker/overlay2/136d71a0857b85896e2c74cd22c82efbccf2739862fe56c0e00427afaef50ad2/diff", "WorkDir": "/var/lib/docker/overlay2/136d71a0857b85896e2c74cd22c82efbccf2739862fe56c0e00427afaef50ad2/work" }, "Name": "overlay2" }, "Mounts": [], "Config": { "Hostname": "339e9914b446", "Domainname": "", "User": "", "AttachStdin": false, "AttachStdout": false, "AttachStderr": false, "Tty": false, "OpenStdin": false, "StdinOnce": false, "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], "Cmd": [ "echo", "hello world" ], "Image": "alpine", "Volumes": null, "WorkingDir": "", "Entrypoint": null, "OnBuild": null, "Labels": {} }, "NetworkSettings": { "Bridge": "", "SandboxID": "055a82d24ed80b14fb2ff1e2d588192464199d3f51dab021efa2fcf0dc6a5df9", "HairpinMode": false, "LinkLocalIPv6Address": "", "LinkLocalIPv6PrefixLen": 0, "Ports": {}, "SandboxKey": "/var/run/docker/netns/055a82d24ed8", "SecondaryIPAddresses": null, "SecondaryIPv6Addresses": null, "EndpointID": "", "Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "IPAddress": "", "IPPrefixLen": 0, "IPv6Gateway": "", "MacAddress": "", "Networks": { "bridge": { "IPAMConfig": null, "Links": null, "Aliases": null, "NetworkID": "7713a5ebd4018030b9f7c851707246986f794acfba01f7da0f3794769e712945", "EndpointID": "", "Gateway": "", "IPAddress": "", "IPPrefixLen": 0, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "MacAddress": "", "DriverOpts": null } } } }
ログの取得
curl --unix-socket /var/run/docker.sock "http:/v1.40/containers/339e9914b446/logs?stdout=1" --output - hello world
コンテナのログを取得する場合は、 POSTのリクエストを/containers/${IMAGE_ID}/logs
に送ってやることで実行可能です。
最後に
このブログでは基本的にコンテナの操作だけを行いましたが基本なんの操作でもできそうです。
RouterFunctionでリクエストをインターセプトする
はじめに
Router Functionを利用しているときのサーブレットフィルターとかインターセプターみたいなのってどうやってやるんだろって言うのが気になったのでちょっと調べてみます。
やってみる
環境
環境は以下の通り
$ java --version openjdk 15 2020-09-15 OpenJDK Runtime Environment (build 15+36-1562) OpenJDK 64-Bit Server VM (build 15+36-1562, mixed mode, sharing) $ mvn --version Apache Maven 3.6.3 Maven home: /usr/share/maven Java version: 15, vendor: Oracle Corporation, runtime: /home/someone/.sdkman/candidates/java/15-open Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-51-generic", arch: "amd64", family: "unix"
プロジェクトはSpring Initializrで作成して設定は以下のようにしました。
ざっくりやるとこ
大きくは以下の2つの手順を踏むっぽいです
- HandlerFilterFunctionを実装ししたFilter関数を作成する
- RouterFunctionにFilterとして登録する
下準備
まずはハンドラーを作成して、Router Functionクラスに登録します。
@Component public class HelloResource { public Mono<ServerResponse> greeting(ServerRequest request){ String name = request.queryParam("name").orElse("world"); return ServerResponse.ok().body(Mono.just(String.format("Hello, %s", name)), String.class); } }
@SpringBootApplication public class RouterFunctionFilterApplication { private HelloResource helloResource; public RouterFunctionFilterApplication(HelloResource helloResource) { this.helloResource = helloResource; } public static void main(String[] args) { SpringApplication.run(RouterFunctionFilterApplication.class, args); } @Bean public RouterFunction router() { return RouterFunctions.route(GET("/hello"), helloResource::greeting); } }
ハンドラーはクエリストリングで名前を受け取って挨拶を返すだけのものです。
cURLでリクエストを送ります。
$ curl localhost:8080/hello?name=henohenomoheji Hello, henohenomoheji
ここまでで下準備は完了です。
HandlerFilterFunctionを実装ししたFilter関数を作成する
Router Functionでリクエストをインターセプトする場合フィルターを行なうためのクラス(もしくは関数)を作成する必要があります。このFilterはHandlerFilterFunctionインターフェースを実装することでフィルター関数を作成します。
このブログではリクエストを受け取ってその情報をログに出力するだけの簡単なフィルターを作成します。
単にフィルター関数をラムダとして書いてやることもできますが、わかりやすさのために一旦フィルタークラスを作成しようと思います。
HandlerFilterFunctionのfilter(ServerRequest request, HandlerFunction<T> next)
のjavadocをみると以下のように書かれています。
/** * Apply this filter to the given handler function. The given * {@linkplain HandlerFunction handler function} represents the next entity in the chain, * and can be {@linkplain HandlerFunction#handle(ServerRequest) invoked} in order to * proceed to this entity, or not invoked to block the chain. * @param request the request * @param next the next handler or filter function in the chain * @return the filtered response * @see ServerRequestWrapper */ Mono<R> filter(ServerRequest request, HandlerFunction<T> next);
引数としてリクエストと次のフィルター(もしくはハンドラー)を表すHandlerFunction受け取るみたいですね。
HandlerFunctionインターフェースはhandle(ServerRequest request)
メソッドを持っていてこいつにリクエストを渡すことで次のフィルターをチェインできるっぽいです。また、javadocによるとその戻り値はresponseになるようなので、レスポンスになにか共通処理を入れたい場合はその戻り値に対してゴニョゴニョできるみたいです。
今回の場合は戻り値に特に処理はくわえないので、たんにhandle(ServerRequest request)
の実行をそのままレスポンスとして返してやれば良さそうです。
具体的には以下のようなクラスを作成します。
public class RequestLoggingFilter implements HandlerFilterFunction { private final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class); @Override public Mono filter(ServerRequest request, HandlerFunction next) { logger.info(String.format("%s, %s", request, request.queryParams())); return next.handle(request); } }
これでフィルターはできました。
RouterFunctionに作成したフィルターを登録する
作成したフィルターをRouterFunctionに登録します。
@SpringBootApplication public class RouterFunctionFilterApplication { private final HelloResource helloResource; public RouterFunctionFilterApplication(HelloResource helloResource) { this.helloResource = helloResource; } public static void main(String[] args) { SpringApplication.run(RouterFunctionFilterApplication.class, args); } @Bean public RouterFunction router() { return RouterFunctions.route(GET("/hello"), helloResource::greeting) .filter(new RequestLoggingFilter()); } }
RouterFunctions.BuilderのfilterメソッドにHndlerFunctionの実装を渡してやると内部でRouterFunctions.FilteredRouterFunctionと呼ばれるWrapされたRouterFunctionを返してくれます。
リクエストを送ると以下のログが出力されました。
2020-10-17 11:48:37.360 INFO 20922 --- [or-http-epoll-2] d.h.r.RequestLoggingFilter : HTTP GET /hello, {name=[henohenomoheji]}
複数フィルターを登録する
別のフィルターをラムダで作って登録します。単に、fitelr()
メソッドを再度呼び出して登録してやれば良さそうです。
@Bean public RouterFunction router() { return RouterFunctions.route(GET("/hello"), helloResource::greeting) .filter(new RequestLoggingFilter()) .filter((r, n)->{ logger.info("filtered 1"); return n.handle(r); }); }
リクエストを送ると、ログには以下のように出力されました。
2020-10-17 11:43:20.276 INFO 20430 --- [or-http-epoll-2] d.h.r.RouterFunctionFilterApplication : filtered 1 2020-10-17 11:43:20.277 INFO 20430 --- [or-http-epoll-2] d.h.r.RequestLoggingFilter : HTTP GET /hello, {name=[henohenomoheji]}
フィルターは後から登録されたものが先に実行されます。
まだ、ちゃんと調べてませんが、フィルターやハンドラーはRouterFunctionのコンポジットパターンとして扱われるようで、基本的には後から登録されたものが先に実行されるような構造になっているみたいです。
QuarkusでHealth Checkのエンドポイントを作成する
はじめに
KubernetesにはLiveness ProbeをReadiness Probeといった概念があり、アプリケーションにそれぞれを確認するためのエンドポイントを作成する場合があります。
Spring Boot(2.3以上)などではそのエンドポイントが用意されていますが、Quarkusにもそれぞれのエンドポイントを作るやりたか(正確にはMicroprofileの実装であるsmallryeのQuarkus拡張)があったので試してみようかと思います。
基本的に以下のドキュメントにしたがってやる形で試してみようと思います。
やってみる
環境
実行環境は以下の通り
$ java --version openjdk 11.0.8 2020-07-14 OpenJDK Runtime Environment 18.9 (build 11.0.8+10) OpenJDK 64-Bit Server VM 18.9 (build 11.0.8+10, mixed mode) $ mvn --version Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f) Maven home: /snap/intellij-idea-ultimate/253/plugins/maven/lib/maven3 Java version: 11.0.8, vendor: N/A, runtime: /home/yuya-hirooka/.sdkman/candidates/java/11.0.8-open Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-48-generic", arch: "amd64", family: "unix"
プロジェクトはQuarkus - Start coding with code.quarkus.ioで作成します。 設定は以下の通り
デフォルトのエンドポイントを確認する
実は特になにもせずともデフォルトでRedinessとLivenessのエンドポイントは用意されています。
/health/live
: アプリケーション本体が立ち上がっているかどうか/health/ready
: DBの接続チェックなどを含めたアプリケーションがアクセスを受け入れる準備できているかどうか/health
: RedinessとLiveness両方の情報を返す
アプリケーションを起動し以下のエンドポイントにアクセスしてみます。
$ curl -i http://localhost:8080/health HTTP/1.1 200 OK content-type: application/json; charset=UTF-8 content-length: 46 { "status": "UP", "checks": [ ] } $ curl -i http://localhost:8080/health/ready HTTP/1.1 200 OK content-type: application/json; charset=UTF-8 content-length: 46 { "status": "UP", "checks": [ ] } $ curl -i http://localhost:8080/health/live HTTP/1.1 200 OK content-type: application/json; charset=UTF-8 content-length: 46 { "status": "UP", "checks": [ ] }
ここで、UP
がアプリケーションがアクティブな状態を示し、DOWN
がアプリケーションがインアクティブであることを示します。
見ての通りこの状態だとアプリケーションはなにもチェックせず、自分自身が起動していることのみ確認することができます。
任意のチェックを行い結果を返す必要がある場合は、少し手をくわえてやる必要があります。
Health Cheackを作成する
例えば、Readinessのチェックではデータベースとの接続がうまく行っているか否かの確認を行いたい場合があります。そのような場合にはReadinessのHealth Checkを実装してやる必要があります。
まずは単にHealth Checkを実装します。
import org.eclipse.microprofile.health.HealthCheck; import org.eclipse.microprofile.health.HealthCheckResponse; import org.eclipse.microprofile.health.Readiness; import javax.enterprise.context.ApplicationScoped; @Readiness @ApplicationScoped public class ReadinessCheck implements HealthCheck { @Override public HealthCheckResponse call() { return HealthCheckResponse.up("test"); } }
Healthチェックを行なう際はorg.eclipse.microprofile.health.HealthCheck
を実装したCDIのBeanとして作成します。この際、クラスに@Rediness
をつけると/health/ready
に対するHealth Cheackを実装でき、@Liveness
をつけると/health/live
に対する実装を行えます。このブログでは@Rediness
での実装しか行いませんが、基本的には全く同じことが@Liveness
でも行えるはずです。
UPの状態の結果を返す場合はHealthCheckResponse.up("任意の文字列")
を利用します。
この状態で、cURLを叩くと以下のようなレスポンスを返します。
$ curl -i http://localhost:8080/health/ready HTTP/1.1 200 OK content-type: application/json; charset=UTF-8 content-length: 121 { "status": "UP", "checks": [ { "name": "test", "status": "UP" } ] }
実装をデータベースへの接続を確認するように修正します。
ここでは仮想的にDBの接続が失敗するような場合における実装をしてみます。
@Readiness @ApplicationScoped public class ReadinessCheck implements HealthCheck { @Override public HealthCheckResponse call() { try { checkDB(); return HealthCheckResponse.up("test"); } catch (Exception e) { e.printStackTrace(); return HealthCheckResponse.down("db connection"); } } private void checkDB(){ throw new RuntimeException("db connection falied"); } }
ここでは、checkDB()
というDBの接続を確認することを想定したメソッドを作成し、失敗を模擬的に表した、RuntimeException
を投げています。
もう一度、cURLを叩くと以下のようなレスポンスが返ってきます。
$ curl -i http://localhost:8080/health/ready HTTP/1.1 503 Service Unavailable content-type: application/json; charset=UTF-8 content-length: 134 { "status": "DOWN", "checks": [ { "name": "db connection", "status": "DOWN" } ] }
任意の情報を付加する
Health Checkに任意の情報を更かしたい場合はHealthCheckResponseBuilderを使います。
@Readiness @ApplicationScoped public class ReadinessCheck implements HealthCheck { @Override public HealthCheckResponse call() { try { checkDB(); return HealthCheckResponse.named("db connection") .up() .build(); } catch (Exception e) { e.printStackTrace(); return HealthCheckResponse .named("db connection") .withData("foo", "bar") .down().build(); } } private void checkDB(){ throw new RuntimeException("db connection falied"); } }
ここで、HealthCheckResponse.named("db connection")
はHealthCheckResponseBuilder
を返し、そのwithData("key", "value")
メソッドを使って任意のデータを返します。
cURLを叩くと以下のようなレスポンスが返ってきます。
$ curl -i http://localhost:8080/health/ready HTTP/1.1 503 Service Unavailable content-type: application/json; charset=UTF-8 content-length: 200 { "status": "DOWN", "checks": [ { "name": "db connection", "status": "DOWN", "data": { "foo": "bar" } } ] }
複数の条件を確認するようなチェックを行なう
例えば、DB接続の他に依存APIも立ち上がっていることが必要な場合、複数の状態を確認してHealth Checkの結果としてUP
を返す必要があります。
この場合、単にHealth Checkクラスを増やしてやるだけでOKです。
新たなHealth CheckクラスであるReadinessAPICheck.java
を作成します。
@Readiness @ApplicationScoped public class ReadinessAPICheck implements HealthCheck { @Override public HealthCheckResponse call() { try { chekAPI(); return HealthCheckResponse.up("dependent api"); } catch (Exception e) { e.printStackTrace(); return HealthCheckResponse.down("dependent api"); } } private void chekAPI(){ } }
cURLを叩くと以下のような結果が返ってきます。
$ curl -i http://localhost:8080/health/ready HTTP/1.1 503 Service Unavailable content-type: application/json; charset=UTF-8 content-length: 209 { "status": "DOWN", "checks": [ { "name": "api", "status": "UP" }, { "name": "db connection", "status": "DOWN" } ] }
最終的なチェック結果は各チェックのAND条件で決まります。
この場合、DB接続のほうがDOWN
となっているので最終的な結果もDOWN
となります。
Health UI
QuarkusのHealth Checkでは簡易的なUIもデフォルトで用意してくれています。http://${APP_URL}//health-ui/
にアクセスすることで、そのUIをみることができます。
ヘッダーの歯車ボタンを押すと設定を行なうことができます。
Pollの項目ではチェック結果更新のためのポーリングの時間を決められた間隔で設定することができます。
Configuration Property
application.propterties
には以下のような設定ポイントが用意されています。
プロパティ名 | 説明 | デフォルト値 |
---|---|---|
quarkus.health.extensions.enabled | Health Checkを有効にするか否かのフラグ | true (Boolean) |
quarkus.smallrye-health.root-path | Health Checkのルートパス | /health (String) |
quarkus.smallrye-health.liveness-path | Helth CheckのLivenessのパス | /live (String) |
quarkus.smallrye-health.readiness-path | Helth CheckのReadinessのパス | /ready (String) |
quarkus.smallrye-health.ui.root-path | Health UIへアクセするためのパス | /health-ui |
quarkus.smallrye-health.ui.always-include | Health UIをビルド常にビルドに含めるかどうかの設定。デフォルトではtestモードとdevモードのときだけHealth UIをビルドに含める | false (Boolean) |
quarkus.smallrye-health.ui.enable | Health UIを有効にするか否かのフラグ | ture (Boolean) |
参考
RouterFunctionでグローバルにエラーハンドリングして任意のレスポンスを返す
はじめに
@ControllerAdvance
や@ExceptionHandler,
を使ったグローバルなエラーハンドリングはやったことがあったのですが、そういえばRouterFanctionでやったことなかったなと思い試してみたいと思います。
やってみる
ざっくりやること
大きく2つのやることがあります。
- ハンドルされる ResponseStatusExceptionを継承したExceptionクラスを作成する
- AbstractErrorWebExceptionHandlerを継承したハンドラークラスを作成し、BeanとしてDIコンテナに登録する
環境
実行環境は以下の通り
$ java --version openjdk 15 2020-09-15 OpenJDK Runtime Environment (build 15+36-1562) OpenJDK 64-Bit Server VM (build 15+36-1562, mixed mode, shari $ mvn --version Apache Maven 3.6.3 Maven home: /usr/share/maven Java version: 15, vendor: Oracle Corporation, runtime: /home/somenone/.sdkman/candidates/java/15-open Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-48-generic", arch: "amd64", family: "unix"
プロジェクトはSpring Initializrで作成し、Bootのバージョンは2.3.4.RELEASE
です。
ハンドラーを作成してRouterに登録する
まずはExpectionを発生させるハンドラーや諸々を実装します。
@Component public class ExampleResource { public Mono<ServerResponse> throwUnexpectedException(ServerRequest serverRequest) { return throwRuntimeException() .flatMap(s -> ServerResponse.ok().contentType(MediaType.TEXT_PLAIN).body(s, String.class)); } public Mono<String> throwRuntimeException() { return Mono.error(new RuntimeException("something happened")); } }
Routerに登録します。
@SpringBootApplication public class ErrorHandlingWithRouterFunctionApplication { public static void main(String[] args) { SpringApplication.run(ErrorHandlingWithRouterFunctionApplication.class, args); } @Bean public RouterFunction<ServerResponse> route(ExampleResource exampleResource) { return RouterFunctions .route(GET("/unexpected"), exampleResource::throwUnexpectedException); } }
この状態で、/unexpected
に対してリクエストを送ると以下のようなレスポンスが来ます。
$ curl localhost:8080/unexpected {"timestamp":"2020-10-08T10:39:04.101+00:00","path":"/unexpected","status":500,"error":"Internal Server Error","message":"","requestId":"e06546f4-1"}
これはデフォルトではSpringのDefaultErrorWebExceptionHandlerがいい感じにハンドリングしてくれて値を返してくれているからです。
ちなみにこのハンドラーはAcceptリクエストヘッダーによってはHTMLのホワイトページを返してくれたりもします。
javadocによると以下のディレクトリにステータスコードの名前のHTMLを配置することで任意のエラーページを返すことも可能なようです。
例えば404のステータスコードでは以下の順序で探索が行われるようです。
'/<templates>/error/404.<ext>' '/<static>/error/404.html' '/<templates>/error/4xx.<ext>' '/<static>/error/4xx.html' '/<templates>/error/error' '/<static>/error/error.html'
ハンドルされる ResponseStatusExceptionを継承したExceptionクラスを作成する
ハンドラーは作成できたので次にスローする任意のExpeptionクラスを作成します。
このExpectionクラスはResponseStatusExceptionを継承します。これは、特定の HTTP レスポンスステータスコードに関連付けられた例外の基本クラスで、ステータスコード、理由、原因となったException等を持つことができます。
詳細は後述しますが、グローバルでハンドリングする際のエラーの情報を持つErrorAttributesはResponseStatusExceptionで保持する情報をもとに作成されます。
それではInternalServerErrorを表すInternalServerErrorException.java
を作成してみます。
InternalServerErrorExcepiton.java
public class InternalServerErrorException extends ResponseStatusException { public InternalServerErrorException(String message) { super(HttpStatus.INTERNAL_SERVER_ERROR, message); } }
ここでは、HttpStatus.INTERNAL_SERVER_ERROR
は固定値にしておき、messageはExceptionが発生する際に詰め込むようにします。
今回はこのメッセージをクライアント側に返すようにします。
作成したExceptionを投げるように先程のハンドラーの実装を書き換えます。
@Component public class ExampleResource { public Mono<ServerResponse> throwUnexpectedException(ServerRequest serverRequest) { return throwRuntimeException() .flatMap(s -> ServerResponse.ok().contentType(MediaType.TEXT_PLAIN).body(s, String.class)) .onErrorResume(RuntimeException.class, e -> Mono.error(new InternalServerErrorException("something happened"))); } public Mono<String> throwRuntimeException() { return Mono.error(new RuntimeException("something happened")); } }
throwRuntimeExpception()
で投げられる例外を.onErrorResume()
でキャッチして作成したInternalServerErrorException
に詰め替え再度スローしています。
ここまで書いておいてなんですが、こいつはわざわざ実装しなくてもResponseStatusException
を直接使うでも大丈夫だとは思います。
AbstractErrorWebExceptionHandlerを継承したハンドラークラスを作成し、BeanとしてDIコンテナに登録する
メインコンテンツのグローバルなエラーハンドラーを実装していきます。
ハンドラーを実装するためにはAbstractErrorWebExceptionHandler を継承したクラスを作成しgetRoutingFunction(ErrorAttributes errorAttributes)
をオーバライドします。
GlobalErrorWebExceptionHandler.java
@Component @Order(-2) public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler { protected final static Logger logger = LoggerFactory.getLogger(GlobalErrorWebExceptionHandler.class); public GlobalErrorWebExceptionHandler(DefaultErrorAttributes g, ApplicationContext applicationContext, ServerCodecConfigurer serverCodecConfigurer) { super(g, new ResourceProperties(), applicationContext); super.setMessageWriters(serverCodecConfigurer.getWriters()); super.setMessageReaders(serverCodecConfigurer.getReaders()); } @Override protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(RequestPredicates.all(), r -> { ErrorAttributeOptions eao = ErrorAttributeOptions.defaults(); Map<String, Object> ea = getErrorAttributes(r, eao.including(ErrorAttributeOptions.Include.EXCEPTION, ErrorAttributeOptions.Include.MESSAGE) ); logger.warn(ea); return renderJsonResponse(ea); }); } private int getStatusCode(Map<String, Object> ea) { return (int) ea.get("status"); } private Mono<ServerResponse> renderJsonResponse(Map<String, Object> ea) { ea.remove("exception"); return ServerResponse.status(getStatusCode(ea)) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(ea)); } }
GlobalErrorWebExceptionHandlerではDefualtErrorAttributesに格納されたExceptionの情報をもとにログを出力することと、Mono<ServerResponse>
を作成することを行っています。
DefualtErrorAttributesでは以下のような情報を持つことができます。
- timestamp - エラーが抽出された時間
- status - HTTPのステータスコード
- error - エラーの理由
- exception - ルート例外のクラス名 (設定されている場合)
- message - 例外メッセージ (設定されている場合)
- errors BindingResult 例外からの ObjectError (設定されている場合)
- trace - 例外スタックトレース (設定されている場合)
- path - 例外が発生したときの URL パス
- requestId - リクエストのID
このなかで、 (設定されている場合)と書かれる4つの項目に関しては、ErrorAttributeOptions.Include
をオプションとして設定してgetErrorAttributes
に渡してやることで取得することができます。
また、このDefualtErrorAttributesはErrorAttributesを実装したクラスをDIコンテナに登録し、エラーハンドラーでコンストラクターインジェクションを行ってセットすることでカスタマイズすることもできます。
getErrorAttributes
はMap<String, Object>の形で保持する情報を返します。
この際のキーは上記のリストの英語部分のとおりです。
クラスに付与している@Order(-2)
はautoconfigクラスであるErrorWebFluxAutoConfigurationで@Order(-1)
で設定されているためより優先度を高くする必要があるためです。
最後に、renderJsonResponseではDefualtErrorAttributeの情報をもとにMono<ServerResponse>
を作成しています。この際に内部の例外の情報を返すことは好ましくないためexception
はマップから削除しています。
アプリケーションを再起動してcurlでアクセスしてみます。
$ curl localhost:8080/unexpected {"timestamp":"2020-10-08T12:18:07.761+00:00","path":"/unexpected","status":500,"error":"Internal Server Error","message":"something happened","requestId":"fe9c31ea-2"}y