LoginSignup
8
9

More than 1 year has passed since last update.

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

Last updated at Posted at 2019-04-20

最新のSpringBoot3系についてはこちらの記事でまとめました!

概要

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

使うライブラリ

※抜粋

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

@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でも可)
    だけ!!!
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を設定しておくとパーセンタイルが取得できます
      • この設定だと50%ileと99%ile

いざ、bootRun!
何回かアクセス(curl localhost:8080
メトリクス取得!

※抜粋
$ 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
主な処理があつまっているがこのメソッドかな

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エンドポイントを有効にして調べちゃいます

application.yml
management:
  endpoints:
    web:
      exposure:
        include: "prometheus,beans"
      base-path: "/"
  server:
    port: 9990

prometheusに追加してbeansを追加すると
localhost:9990/beansでbean登録されている一覧がとれる

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

@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も修正
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
@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!";
    }
}

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

$ 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)が使用している
  • MetricsRestTemplateCustomizerRestTemplateMetricsConfigurationによってBean登録されている
  • デフォルトの(Bean登録されている)RestTemplateBuilderを使う場合は、RestTemplateBuilderのコンストラクタの引数がRestTemplateCustomizerの配列になっているから、登録されるCustomizerを全部拾ってくれるのね!
  • conditionalはRestTemplateを見ているからRestTemplateを使っていたらcustomizerはBean登録されているのね!
@ConditionalOnClass(RestTemplate.class)

さいごに

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

8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9