Spring Boot ✕ Prometheus ✕ Grafana でアプリケーションのモニタリング

spring-bootのアプリケーションのモニタリングを検討しはじめた。
モニタリングについてはほぼ初心者状態…

今回モニタリングしたい項目はメモリ、スレッドなどシステム的な項目ではなく、
アプリケーションに紐づく値（例えばAPIごとのスループットや呼び出し回数）であったので、
クライアントライブラリが充実しており、アプリケーションに紐づくモニタリング項目が出力できそうな[Prometheus](https://prometheus.io/)とSpring Bootの組み合わせを調べてみた。
（＋可視化のためのツールとして[Grafana](https://grafana.com/)も組み合わせた。）

# 環境

自身の環境は以下。

* OS: macOS Sierra 10.12.6
* Java: 1.8.0_102
* Spring Boot: 1.5.10.RELEASE
* Docker For Mac: 17.12.0-ce-mac49
* Prometheus: v2.1.0
* Grafana: 5.0.0-beta4

# Prometheusについて

[Prometheus](https://prometheus.io/)は公式ページのトップに

> From metrics to insight.
Power your metrics and alerting with a leading
open-source monitoring solution.

と書いてあるように、オープンソースのモニタリングツールだ。

Prometheusの一番の特徴はメトリクス収集方式としてPull型を採用していることだ（と思う）。
あとはKubernetesと相性がよいことだと思う。
他の特徴や詳しい説明については、この記事が参考になったので確認してほしい。
https://developers.cyberagent.co.jp/blog/archives/3814/

Prometheusには多くのクライアントライブラリがあり、主な言語はサポートされている。（正式かどうかはともかくとしてだが、、、）
https://prometheus.io/docs/instrumenting/clientlibs/
またJavaクライアントライブラリには、Spring、Spring Bootもサポートされている。

クライアントライブラリを使用することで、WEBアプリケーションのAPIごとのスループットなどが可視化できるので、今回はJavaクライアントライブラリ、Spring Bootを使用して、モニタリングの設定やその設定で何がモニタリングできるかを確認してみた。

今回、対象とするのはWEBアプリケーションの場合であり、バッチなどの短期間で終わる処理は対象外とする。
バッチなどはPrometheusの[pushgateway](https://github.com/prometheus/pushgateway)という機構を使用する必要があるが、今回は対象外。

# Spring BootアプリケーションへのPrometheusクライアントの設定方法

PrometheusはPull側のメトリクス収集方式を採用しているため、
Spring BootアプリケーションでのPrometheusクライアントの役割は、アプリケーションのメトリックスを取得してエンドポイントとして公開すること。

それを実現するのは[simpleclient_spring_boot](https://github.com/prometheus/client_java/tree/master/simpleclient_spring_boot)ライブラリとなる。

ここでsimpleclient_spring_bootライブラリの内部と使い方について説明しておく。

## simpleclient_spring_boot

### 設定方法

事前にSpring Webアプリケーションを作っておく必要がある。ここでは、Mavenの依存関係に`spring-boot-starter-web`を追加しておく。
またSpring Boot用Prometheusクライアントライブラリを依存関係に追加する。

```xml:pom.xml
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<!-- Prometheus library for spring boot-->
	<dependency>
		<groupId>io.prometheus</groupId>
		<artifactId>simpleclient_spring_boot</artifactId>
		<version>0.2.0</version>
	</dependency>
```

ライブラリが提供しているアノテーションが２つある。

* `@EnablePrometheusEndpoint`
* `@EnableSpringBootMetricsCollector`

これらのアノテーションをアプリケーション側に設定する。

```java:SpringBootPrometheusGrafanaSampleApplication.java
@SpringBootApplication
@EnablePrometheusEndpoint
@EnableSpringBootMetricsCollector
public class SpringBootPrometheusGrafanaSampleApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootPrometheusGrafanaSampleApplication.class, args);
	}
}
```

これだけで、Springが提供しているメトリクスが`/prometheus`のエンドポイントに表示されるようになる。
詳しいライブラリの説明は以下。

### `@EnablePrometheusEndpoint`

SpringのWEBアプリケーション上で、`/prometheus`のエンドポイントを登録するためのアノテーション。
単体では`/prometheus`のエンドポイントに空のメトリクス（空レスポンス）しか表示されない。

メトリックスは別途、追加する必要があり、後述するLogやGarbage Collectionなどの設定もここのエンドポイントに追加される。
（したがって、`@EnablePrometheusEndpoint`は必須だが、後述する`@EnableSpringBootMetricsCollector`はそのメトリックスが必要でなければ、必ずしも付与する必要はない。）

クラスのシーケンスは以下のようにシンプルである。

| EnablePrometheusEndpoint | → | PrometheusEndpointConfiguration | → | PrometheusEndpoint,PrometheusMvcEndpoint |
|---|---|---|---|---|


処理概要もまとめておいた。

| クラス | 処理概要 |
| ------------------------------- | ------------------------------- |
| PrometheusEndpointConfiguration | Bean登録を行うConfigurationクラス。 |
| PrometheusEndpoint | `/prometheus`エンドポイントの追加。 |
| PrometheusMvcEndpoint | `/prometheus`エンドポイントの呼び出し処理。レスポンスのフィルターを行う。 |

レスポンスのフィルタは以下のような形で行えることを確認している。
フィルタをしないと、全項目が出力される。
`http://localhost:8080/prometheus?name[]=jvm_threads_daemon&name[]=jvm_memory_bytes_used`


### EnableSpringBootMetricsCollector

Spring Actuatorで持っているメトリックスの追加。
（正確にはBean登録済みの）PublicMetricsインターフェースを実装しているものが追加される。
ex. TomcatPublicMetrics, SystemPublicMetrics

クラスのシーケンス、処理概要は以下の通り。

| EnableSpringBootMetricsCollector | → | PrometheusMetricsConfiguration | → | SpringBootMetricsCollector |
|---|---|---|---|---|

| クラス | 処理概要 |
| ------------------------------- | ------------------------------- |
| PrometheusMetricsConfiguration | Bean登録を行うConfigurationクラス。 |
| SpringBootMetricsCollector | (Bean登録済みの）PublicMetricsインターフェースの実装クラスに対して、メトリックスの収集を行う。 |

## Logメトリックスの追加

メトリックスにLogレベルごとの集計を追加することができる。


```:出力例
# HELP logback_appender_total Logback log statements at various log levels
# TYPE logback_appender_total counter
logback_appender_total{level="debug",} 0.0
logback_appender_total{level="warn",} 0.0
logback_appender_total{level="trace",} 0.0
logback_appender_total{level="error",} 0.0
logback_appender_total{level="info",} 49.0
```


設定は以下。

```xml:pom.xml
	<dependency>
		<groupId>io.prometheus</groupId>
		<artifactId>simpleclient_logback</artifactId>
		<version>0.2.0</version>
	</dependency>
```

`resources/logback-spring.xml`を`InstrumentedAppender`とrootレベルで`METRICS`を追加する。

```xml:logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/base.xml"/>

    <appender name="METRICS" class="io.prometheus.client.logback.InstrumentedAppender" />

    <root level="INFO">
        <appender-ref ref="METRICS" />
    </root>

</configuration>
```

## Garbage collection, Memory pools, JMX, Classloading, Thread counts の追加。

若干、Spring-Actuatorで持っているメトリックス値とかぶるが、設定は以下。

```xml:pom.xml
	<dependency>
		<groupId>io.prometheus</groupId>
		<artifactId>simpleclient_hotspot</artifactId>
		<version>0.2.0</version>
	</dependency>
```

`DefaultExports.initialize()`を初期処理で呼び出す。

```java:SpringBootPrometheusGrafanaSampleApplication.java
@SpringBootApplication
@EnablePrometheusEndpoint
@EnableSpringBootMetricsCollector
public class SpringBootPrometheusGrafanaSampleApplication {

	public static void main(String[] args) {
		DefaultExports.initialize();
		SpringApplication.run(SpringBootPrometheusGrafanaSampleApplication.class, args);
	}
}
```

## メソッド単位の所要時間メトリックスの追加

Spring AOPを使って、メソッド単位でログを取得できる。
`@EnablePrometheusTiming`を追加する。

```
@SpringBootApplication
@EnablePrometheusEndpoint
@EnableSpringBootMetricsCollector
@EnablePrometheusTiming
public class SpringBootPrometheusGrafanaSampleApplication {

}
```

メソッドに`@PrometheusTimeMethod`を付与する。

```java:HelloController.java
@RestController
public class HelloController {

    @GetMapping("/hello")
    @PrometheusTimeMethod(name = "hello_controller_say_hello_duration_seconds", help = "Some helpful info here")
    public String sayHello() {
        return "hello";
    }
}
```

AOPを使っているので、同じクラス内でのメソッド呼び出しやstaticメソッドなどでは`@PrometheusTimeMethod`が効かないことに注意が必要。

## 個別実装

上記まではほぼ設定だけでメトリックスが取得できたが、もしそれでは取得することができないアプリケーションの値があればここで紹介する個別実装を行う必要がある。

Prometheusが提供しているメトリックスタイプはCounter,Gauge,Summary,Histogramの４種類。
それぞれのタイプの詳細は[公式ページ](https://prometheus.io/docs/concepts/metric_types/)を参考にしてほしい。

今回はCounterの実装例を作った。

```java:CounterController.java
@RestController
public class CounterController {
    private static int num = 0;
    private static final Counter requests = Counter.build().name("count_requests_total").help("Total count requests.").register();

    @GetMapping("count")
    public int count() {
        requests.inc();
        return ++num;
    }
}
```

リクエストがあれば、回数を増やして返すようなコントローラであり、その回数をメトリックスとして保存している。

# Prometheus,Grafanaの設定

[Grafana](https://grafana.com/)はオープンソースの可視化ツールで、
Prometheusの公式サイトでもGrafanaを使用することがおすすめされている。
https://prometheus.io/docs/visualization/grafana/

ここではSpring Bootアプリ、Prometheus、GrafanaをDockerで構築する。
詳しい説明はしないが、`Dockerfile`,`docker-compose.yml`は以下のようにした。

```:Dockerfile
FROM maven:3.5.2-jdk-8-alpine
ADD . /code
WORKDIR /code
RUN mvn clean package -DskipTests=true
CMD mvn spring-boot:run
```

```docker-compose.yml
version: '3'
services:
  web:
    build: .
    ports:
      - 8080:8080
    volumes:
      - .:/code
  prometheus:
    image: prom/prometheus
    container_name: prometheus
    volumes:
      - ./env/prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - 9090:9090
  grafana:
    image: grafana/grafana
    container_name: grafana
    ports:
      - 3000:3000
    env_file:
      - ./env/grafana.env
```

`grafana.env`はGrafanaの設定をいじるためのファイルとして用意したが、とくに設定が必要なかったので、空ファイル。

## prometheus.yml

ほぼ[Getting started](https://prometheus.io/docs/prometheus/latest/getting_started/)に載っている内容。。。
すいません、Prometheusの設定に詳しくなくて、、、

```
global:
  scrape_interval:     15s # By default, scrape targets every 15 seconds.

  # Attach these labels to any time series or alerts when communicating with
  # external systems (federation, remote storage, Alertmanager).
  external_labels:
    monitor: 'codelab-monitor'

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: 'prometheus'

    # Override the global default and scrape targets from this job every 5 seconds.
    scrape_interval: 5s

    static_configs:
      - targets: ['localhost:9090']

# sample
scrape_configs:
  - job_name:       'sample-random'

    # Override the global default and scrape targets from this job every 5 seconds.
    scrape_interval: 5s
    
    metrics_path: /prometheus

    static_configs:
      - targets: ['web:8080']
```

簡単に補足しておくと、`  - job_name: 'prometheus'`の部分がPrometheus自身のモニタリングで、`  - job_name:       'sample'`の部分が今回のWEBアプリケーションへの設定部分になっている。

注意点は２つで、

* Prometheusがモニタリングするデフォルトのエンドポイントは`/metrics`のため、変更のため`metrics_path: /prometheus`を追加
* WEBアプリケーションがDocker内のため、targetsのURLにDockerのnameを使っている

## PrometheusのGUI確認

Prometheusのデフォルトのポートは9090なので、`http://localhost:9090/`にアクセスすると、Prometheusが表示される。

`http://localhost:9090/graph`で基本的な表示ができる。
例えば、ログレベル別にログ数を表示してみる。

ドロップダウンリストから[logback_appender_total]を選択し、[Execute]を押下すればよい。

![スクリーンショット 2018-02-26 12.12.43.png](https://qiita-image-store.s3.amazonaws.com/0/110216/644224f1-a484-a374-89b0-3f60c82934c2.png)

グラフ表示にする場合は、[Graph]タブにする。
![スクリーンショット 2018-02-26 12.13.12.png](https://qiita-image-store.s3.amazonaws.com/0/110216/5f6be16f-b84a-6b07-6e65-cb0731917b5f.png)

## GrafanaのGUI確認

Grafanaのデフォルトポートは3000なので、`http://localhost:3000/`にアクセスする。
とくに設定していなければ、`admin/admin`でログインできる。

まずはデータソースにPrometheusを設定する必要があるので、[Configuration] -> [Data Sources]を選択し、データソースの追加画面へいく。
![スクリーンショット 2018-02-26 12.17.15.png](https://qiita-image-store.s3.amazonaws.com/0/110216/2b535dc7-57b2-4a16-e804-7b150dd73926.png)

データソースの設定は以下の通り。

|項目| 値 |
|---|-----|
|Type|Prometheus|
|URL|http://localhost:9090|
|Access|direct|
|Scrap interval|5s|

![スクリーンショット 2018-02-26 12.21.27.png](https://qiita-image-store.s3.amazonaws.com/0/110216/3e577ff2-3d72-77e2-8667-ac25810794b9.png)


次にダッシュボードを作成する。
[Create] -> [Dashboard] -> [Graph]（ここは表示したいものにあわせて変更する）と選ぶ。

![スクリーンショット 2018-02-26 12.30.11.png](https://qiita-image-store.s3.amazonaws.com/0/110216/09c38bf5-cac8-c95b-7806-7b8996c2b7a0.png)


表示されたグラフを選択し、[Edit]を選択する。

![スクリーンショット 2018-02-26 12.24.37.png](https://qiita-image-store.s3.amazonaws.com/0/110216/4a052617-83f8-61a9-bb8e-d153ed4cfb72.png)

|項目| 値 |
|---|-----|
|Data Source|prometheus|
|A|logback_appender_total|

を追加すればグラフが表示される。

![スクリーンショット 2018-02-26 12.26.02.png](https://qiita-image-store.s3.amazonaws.com/0/110216/2af1685a-3706-954f-72b7-a45088c663fb.png)

# 所感

Prometheusを使い、クライアントソースに軽く手をいれることで、アプリケーションのモニタリングにおいてほしい情報は取得できるようになった。
修正量も大したことはないので、使い勝手は良さそう。
とくにSpring Bootアプリであれば、すぐに使えるようになるのは魅力的。

ただ、他のモニタリングツールでもアプリケーションの情報が多く取れるものがある。
たとえば、New Relic APMであれば、トランザクションごとのスループットは表示することができる。
そのように他のモニタリングツールとの比較は今後、必要になってくる。

# GitHub
今回のサンプルソースはいつものようにGitHubに公開している。
https://github.com/aha-oretama/spring-boot-prometheus-grafana-sample

# 参考

* https://prometheus.io/
* https://github.com/prometheus/client_java
* http://www.baeldung.com/spring-boot-actuators