springbootのアプリケーションのメトリクス監視のために

:::note warn
最新のSpringBoot3系については[こちら](https://hirabay.net/?p=271)の記事でまとめました！
:::

https://hirabay.net/?p=271

# 概要
- springbootのアプリケーションのメトリクス監視をしたい
    - http.client.requests
        - APIのレスポンスタイムはどれくらいなのか？
        - APIのリクエスト数はどれくらいなのか？
    - http.server.requests
        - 内部で使用しているAPIのレスポンスはどれくらいなのか
- 取得したメトリクスはprometheusやgrafanaで可視化！
    - ※本記事では触れません

# 使うライブラリ
※抜粋

```groovy:build.gradle
buildscript {
    ext {
        springBootVersion = '2.1.1.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-actuator')
    compile('org.springframework.boot:spring-boot-actuator-autoconfigure')
    compile('io.micrometer:micrometer-registry-prometheus')
}
```

- `spring-boot-starter-web`
    - メトリクスの取得には関係なし、メトリクス取得対象のAPI作成のために使用
- `spring-boot-actuator`
    - メトリクスを取得するための機能はこの中に入っている
- `spring-boot-actuator-autoconfigure`
    - `spring-boot-actuator`が提供している機能をDIコンテナに登録する役割
- `micrometer-registry-prometheus`
    - 取得したメトリクスをprometheusから読み取れる形式に変換してくれる

# 監視対象のアプリケーション
「Hello World!」と返すだけのAPI


```java
@Controller
@SpringBootApplication
@Slf4j
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @GetMapping
    @ResponseBody
    public String hello() {
        return "Hello World!";
    }
}
```

# http.server.requests
まずはアプリケーション自身のメトリクス取得！

## 実装
実装上必要なのは
- application.ymlに設定を記載（もちろんapplication.propertiesでも可）
だけ！！！

```yaml:application.yml
management:
  endpoints:
    web:
      exposure:
        include: "prometheus"
      base-path: "/"
  server:
    port: 9990
  metrics:
    web:
      server:
        requests-metric-name: "http.server.requests"
    distribution:
      percentiles:
        "http.server.requests": 0.5,0.99
```
- この設定をしておくとlocalhost:9990/prometheusにアクセスするとメトリクスが取得できる！
- `management.metrics`配下の設定値はなくても動きます
    - `requests-metric-name`はメトリクスの名前を任意のものに変更するための設定値です
    - `percentiles`を設定しておくとパーセンタイルが取得できます
        - この設定だと５０％ileと99%ile

いざ、bootRun！
何回かアクセス（`curl localhost:8080`）
メトリクス取得！

```bash:※抜粋
$ curl localhost:9990/prometheus
# TYPE http_server_requests_seconds summary
http_server_requests_seconds{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/",quantile="0.5",} 0.0
http_server_requests_seconds{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/",quantile="0.99",} 0.0
http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/",} 7.0
http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/",} 0.086270892
```
めでたい！

## 誰が何をしているの？
メトリクスを取ってくれているのは`org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter`。
主な処理があつまっているがこのメソッドかな

```java:WebMvcMetricsFilter#filterAndRecordMetrics
	private void filterAndRecordMetrics(HttpServletRequest request,
			HttpServletResponse response, FilterChain filterChain)
			throws IOException, ServletException {
		TimingContext timingContext = TimingContext.get(request);
		if (timingContext == null) {
			timingContext = startAndAttachTimingContext(request);
		}
		try {
			filterChain.doFilter(request, response);
			if (!request.isAsyncStarted()) {
				// Only record when async processing has finished or never been started.
				// If async was started by something further down the chain we wait
				// until the second filter invocation (but we'll be using the
				// TimingContext that was attached to the first)
				Throwable exception = (Throwable) request
						.getAttribute(DispatcherServlet.EXCEPTION_ATTRIBUTE);
				record(timingContext, response, request, exception);
			}
		}
		catch (NestedServletException ex) {
			response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
			record(timingContext, response, request, ex.getCause());
			throw ex;
		}
	}
```

このクラスは`OncePerRequestFilter`を継承しているから
Bean登録さえしてあればメトリクスを取ってくれるのね！

Bean登録は？
僕はよく`beans`エンドポイントを有効にして調べちゃいます

```yaml:application.yml
management:
  endpoints:
    web:
      exposure:
        include: "prometheus,beans"
      base-path: "/"
  server:
    port: 9990
```
prometheusに追加してbeansを追加すると
localhost:9990/beansでbean登録されている一覧がとれる

```json
webMvcMetricsFilter: {
aliases: [ ],
  scope: "singleton",
  type: "org.springframework.boot.web.servlet.FilterRegistrationBean",
  resource: "class path resource 
 [org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfiguration.class]",
  dependencies: [
    "prometheusMeterRegistry",
    "webMvcTagsProvider"
  ]
},
```
ふむふむ、`WebMvcMetricsAutoConfiguration`か

```java
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@ConditionalOnBean(MeterRegistry.class)
```
使用している`2.1.1`だとこんなconditionalだから自動でメトリクス集計してくれているみたい
（逆にライブラリを入れるとメトリクス集計しちゃうから不要だったらライブラリいれちゃダメかも、当たり前か。。）

# http.client.requests

## 実装
- 意識しなきゃいけないのは`RestTemplate`
- RestTemplateBuilderから直接buildするときは特に意識する必要なし
- 独自のbuilderを使いたいときを想定して実装
    - customizersメソッドにBean登録されているMetricsRestTemplateCustomizerを渡すだけ
- 何も記載しなくてもBean登録はされてるけど、一応99%ile取得を想定してapplication.ymlも修正

```yaml:application.yml
management:
  endpoints:
    web:
      exposure:
        include: "prometheus,beans"
      base-path: "/"
  server:
    port: 9990
  metrics:
    web:
      server:
        requests-metric-name: "http.server.requests"
      client:
        requests-metric-name: "http.client.requests"
    distribution:
      percentiles:
        "http.server.requests": 0.5,0.99
        "http.client.requests": 0.5,0.99
```


```java
@Controller
@SpringBootApplication
@Slf4j
public class Application {
    private RestTemplate restTemplate;

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

    @GetMapping
    @ResponseBody
    public String hello() {
        return restTemplate.getForObject(URI.create("http://localhost:8080/hello"), String.class);
    }

    public Application(
            RestTemplateBuilder restTemplateBuilder,
            MetricsRestTemplateCustomizer metricsRestTemplateCustomizer
    ) {
        // デフォルトのRestTemplateBuilderを使う場合は意識する必要ない
        // this.restTemplate = restTemplateBuilder
        //         .build();

        this.restTemplate = restTemplateBuilder
                .customizers(metricsRestTemplateCustomizer)
                .build();
    }

    @GetMapping("/hello")
    @ResponseBody
    public String helloApi() {
        return "Hello World!";
    }
}
```

こんな感じでメトリクスが取れる

```bash
$ curl http://localhost:9990/prometheus
# TYPE http_client_requests_seconds summary
http_client_requests_seconds{clientName="localhost",method="GET",status="200",uri="/hello",quantile="0.5",} 0.06291456
http_client_requests_seconds{clientName="localhost",method="GET",status="200",uri="/hello",quantile="0.99",} 0.06291456
http_client_requests_seconds_count{clientName="localhost",method="GET",status="200",uri="/hello",} 1.0
http_client_requests_seconds_sum{clientName="localhost",method="GET",status="200",uri="/hello",} 0.064232646
```

## 誰が何をしているの？
- メトリクスを取ってくれているのは`org.springframework.boot.actuate.metrics.web.client.MetricsClientHttpRequestInterceptor`。
- このクラスは先ほどの実装で登録したcustomizer（`MetricsRestTemplateCustomizer`）が使用している
- `MetricsRestTemplateCustomizer`は`RestTemplateMetricsConfiguration`によってBean登録されている
- デフォルトの（Bean登録されている）RestTemplateBuilderを使う場合は、`RestTemplateBuilder`のコンストラクタの引数がRestTemplateCustomizerの配列になっているから、登録されるCustomizerを全部拾ってくれるのね！
- conditionalはRestTemplateを見ているからRestTemplateを使っていたらcustomizerはBean登録されているのね！

```java
@ConditionalOnClass(RestTemplate.class)
```


# さいごに
99%ileが簡単に取れてビックリ
作った人すごい。。。