CLOVER🍀

That was when it all began.

PrometheusのJVM Clientを試してみる

これは、なにをしたくて書いたもの?

  • PrometheusでHello World的なことはやったはいいものの、もうちょっと掘り下げようと
  • 自分で、アプリケーションからメトリクスをエクスポートするものを書いてみたら理解が進むのではないか

ということで、PrometheusのJVM Clientを使って、JavaアプリケーションからMetricをエクスポートすることを
試してみようと思います。
※ふつうに使う場合はMicrometer経由になると思いますが、今回は勉強のために

今回は、PrometheusのJVM Client 0.6.0を対象にします。

Prometheus Client Libraries/JVM Client

Prometheusには、クライアントライブラリがあり、自分でMetricを定義したり、定義済みのものを利用したりすることが
できます。

Client libraries | Prometheus

Java…JVM向けのものは、こちらです。

GitHub - prometheus/client_java: Prometheus instrumentation library for JVM applications

メトリクスのタイプごとの使い方。

Instrumenting

また、いくつかのライブラリなどに対するCollectorなども含まれています。

Included Collectors

Data Model/Metric Type

とまあ、こう書いても用語がわからないとなんとも?な感じがするので、ひとつひとつ見ていきましょう。

Data model | Prometheus

Prometheusでは、時系列でデータを持ちます。メトリクスは、ラベル(名前と値)によるディメンションとともに、
タイムスタンプ付きのストリームとして保存されます。

メトリクスやラベルの名前に使うことができる文字は、正規表現で定められているので、ドキュメントを確認しましょう。

メトリクスとラベルの構文はこんな感じで

<metric name>{<label name>=<label value>, ...}

例はこちらです。

api_http_requests_total{method="POST", handler="/messages"}

続いて、Metrics Type。

Metric types | Prometheus

Prometheusでは、4つの種類のメトリクス(Counter、Gause、Histgram、Summary)を提供しています。特に型に関する情報が
あるわけではありませんが、将来変わるかもしれません。

Counter

値が単純増加する、累積メトリクスです。再起動時に0にリセットすることが可能です。

counter.inc();

counter.inc(value);

リクエスト数や、タスクの完了数、エラー数などに使われるでしょう。

値が減少する可能性のあるものには、使うことができません。そのような場合はGaugeを利用します。

Gauge

任意に値を増減できる、単一の値を表すメトリクスです。

gause.inc();
gause.dec();

gause.inc(value);
gause.dec(value);

gause.set(value);

メモリの使用量や起動しているプロセス数など、上下に変動する可能性があるものに使われます。
また、同時リクエスト数など上下に変動する可能性があるカウンタとしても使われれます。

Histgram

Histgramは、観測値(通常はリクエスト時間やレスポンスサイズなど)をサンプリングし、バケットとして構成します。
また、観測値の合計も取得することができます。

timer.startTimer();

...

timer.observeDuration();

// or
timer.time(() -> ...);


responseSize.observe(value);

Histgramは、複数の値を扱います。

例えば、メトリクスの基本名が"basename"の場合、それらにサフィックスが付けられた形で累積カウンタ(bucket)、
全体の合計値(sum)、イベントのカウント数(count)が公開されます。

Histgramは常に累積的な値です。HistgramとSummaryの違いは以下のドキュメントを参照してね、とのことです。

Histograms and summaries | Prometheus

Summary

SummaryはHistgramに似ていて、観測値(通常はリクエスト時間やレスポンスサイズなど)をサンプリングします。

timer.startTimer();

...

timer.observeDuration();

// or
timer.time(() -> ...);


responseSize.observe(value);

観測した総数と値の合計値も提供されますが、スライディングウィンドウで構成されたquantiles(分位数)を提供します。

SummaryもHistgramと同様に、複数の値を扱います。

例えば、メトリクスの基本名が"basename"の場合、分位数(quantile)、 そしてサフィックスが付けられた形で
全体の合計値(sum)、イベントのカウント数(count)が公開されます。

で、Histgramとの違いはこちらを参照、と。

Histograms and summaries | Prometheus

環境

今回の環境は、こちら。

$ java -version
openjdk version "1.8.0_191"
OpenJDK Runtime Environment (build 1.8.0_191-8u191-b12-0ubuntu0.18.04.1-b12)
OpenJDK 64-Bit Server VM (build 25.191-b12, mixed mode)


$ mvn -version
Apache Maven 3.6.0 (97c98ec64a1fdfee7767ce5ffb20918da4f719f3; 2018-10-25T03:41:47+09:00)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 1.8.0_191, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-8-openjdk-amd64/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-44-generic", arch: "amd64", family: "unix"

Prometheus上での表示確認は、このエントリ上からは省略しますが、Prometheus 2.7.1で確認しました。

準備

まずは、Maven依存関係。

こちらを参考に、以下を追加しましょう。

Using / Assets

        <!-- The client -->
        <dependency>
          <groupId>io.prometheus</groupId>
          <artifactId>simpleclient</artifactId>
          <version>0.6.0</version>
        </dependency>

        <!-- Hotspot JVM metrics-->
        <dependency>
          <groupId>io.prometheus</groupId>
          <artifactId>simpleclient_hotspot</artifactId>
          <version>0.6.0</version>
        </dependency>

これが、基本セットみたいです。

Prometheus向けにメトリクスをエクスポートするには追加のライブラリが必要なようですが、それは後ほど書くことに
しましょう。

サンプルプログラムは、JAX-RS(RESTEasy+Undertow)で書くことにします。

        <!-- Servlet Container & JAX-RS Implementation -->
        <dependency>
          <groupId>io.undertow</groupId>
          <artifactId>undertow-servlet</artifactId>
          <version>2.0.17.Final</version>
        </dependency>
        <dependency>
          <groupId>org.jboss.resteasy</groupId>
          <artifactId>resteasy-undertow</artifactId>
          <version>3.6.2.Final</version>
        </dependency>
        <dependency>
          <groupId>org.jboss.resteasy</groupId>
          <artifactId>resteasy-servlet-initializer</artifactId>
          <version>3.6.2.Final</version>
        </dependency>

サンプルプログラムを書く

では、サンプルプログラムを書いていきましょう。まずは、エントリポイントなどから。

Undertowを使って、アプリケーションを起動するクラス。
src/main/java/org/littlewings/prometheus/client/Server.java

package org.littlewings.prometheus.client;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.ServletException;

import io.prometheus.client.exporter.HTTPServer;
import io.prometheus.client.exporter.MetricsServlet;
import io.prometheus.client.hotspot.DefaultExports;
import io.undertow.Undertow;
import io.undertow.server.HttpHandler;
import io.undertow.servlet.Servlets;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.DeploymentManager;
import io.undertow.servlet.api.ServletContainerInitializerInfo;
import io.undertow.servlet.api.ServletInfo;
import io.undertow.servlet.util.DefaultClassIntrospector;
import org.jboss.resteasy.plugins.servlet.ResteasyServletInitializer;

public class Server {
    public static void main(String... args) throws NoSuchMethodException, ServletException, IOException {
        String host = "0.0.0.0";
        int port = 8080;
        String contextPath = "";

        Set<Class<?>> jaxrsClasses = new HashSet<>(Arrays.asList(JaxrsActivator.class));

        DeploymentInfo deployment =
                Servlets
                        .deployment()
                        .setClassLoader(Server.class.getClassLoader())
                        .setDeploymentName("prometheus-client")
                        .setContextPath(contextPath)
                        /* 後で */
                        .addServletContainerInitializer(
                                new ServletContainerInitializerInfo(ResteasyServletInitializer.class,
                                        DefaultClassIntrospector.INSTANCE.createInstanceFactory(ResteasyServletInitializer.class),
                                        jaxrsClasses)
                        );

        DeploymentManager manager = Servlets.defaultContainer().addDeployment(deployment);
        manager.deploy();
        HttpHandler serverHandler = manager.start();

        Undertow server =
                Undertow
                        .builder()
                        .addHttpListener(port, host)
                        .setHandler(serverHandler)
                        .build();

        server.start();
    }

    /* 後で */
}

内容は一部、後で書きます。

JAX-RSの有効化。
src/main/java/org/littlewings/prometheus/client/JaxrsActivator.java

package org.littlewings.prometheus.client;

import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("")
public class JaxrsActivator extends Application {
    Set<Object> singletons = new HashSet<>();

    public JaxrsActivator() {
        singletons.add(new CounterResource());
        singletons.add(new MetricsResource());
    }

    @Override
    public Set<Object> getSingletons() {
        return singletons;
    }
}

登場するリソースクラスは、この後に書きます。

メトリクスを定義する

それでは、以下を参考にメトリクスに情報を追加していく、JAX-RSリソースクラスを書いてみましょう。

Instrumenting

今回は、4つのメトリクスの種類のうち、CounterとSummaryを使うことにします。

名前が紛らわしくてなんですが、呼び出し回数に応じてカウントアップ(inc)および、現在のカウンタの値を返す(current)
JAX-RSリソースクラスを定義してみます。 src/main/java/org/littlewings/prometheus/client/CounterResource.java

package org.littlewings.prometheus.client;

import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.prometheus.client.Counter;
import io.prometheus.client.Summary;

@Path("counter")
public class CounterResource {
    AtomicInteger counter = new AtomicInteger(0);

    private static final Counter incCallCounter =
            Counter.build().name("inc_call_count").help("counter increment count").register();
    private static final Counter methodCallCounter =
            Counter.build().name("method_call_count").labelNames("method", "url").help("counter method call count").register();
    private static final Summary incRequestsSummary =
            Summary.build().name("inc_requests_summary").help("increment request summary").register();
    private static final Summary currentRequestsSummary =
            Summary.build().name("current_val_requests_summary").labelNames("url").help("current val request summary").register();

    @GET
    @Path("inc")
    @Produces(MediaType.TEXT_PLAIN)
    public int inc() {
        return incRequestsSummary.time(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextLong(1000L));

                return counter.incrementAndGet();
            } finally {
                incCallCounter.inc();
                methodCallCounter.labels("inc", "/counter/inc").inc();
            }
        });
    }

    @GET
    @Path("current")
    @Produces(MediaType.TEXT_PLAIN)
    public int current() {
        return currentRequestsSummary.labels("/counter/current").time(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextLong(1000L));

                return counter.get();
            } finally {
                methodCallCounter.labels("current", "/counter/current").inc();
            }
        });
    }
}

どちらのメソッドも、ランダムにスリープするようにしています。

各クラスの使い方は、ドキュメントに記載のまま…。

Instrumenting

とはいえ、若干説明はしつつ。

今回、4つのメトリクスを定義しました。

    private static final Counter incCallCounter =
            Counter.build().name("inc_call_count").help("counter increment count").register();
    private static final Counter methodCallCounter =
            Counter.build().name("method_call_count").labelNames("method", "url").help("counter method call count").register();
    private static final Summary incRequestsSummary =
            Summary.build().name("inc_requests_summary").help("increment request summary").register();
    private static final Summary currentRequestsSummary =
            Summary.build().name("current_val_requests_summary").labelNames("url").help("current val request summary").register();

CounterとSummary、それぞれ2つずつ。

Counterは、「inc」というメソッドに対するCounterと、メソッド呼び出し自体をカウントするCounterの2つを用意しています。

    private static final Counter incCallCounter =
            Counter.build().name("inc_call_count").help("counter increment count").register();
    private static final Counter methodCallCounter =
            Counter.build().name("method_call_count").labelNames("method", "url").help("counter method call count").register();

この2つの違いは、ラベルですね。ラベルを使う方は、2つ値を取るようにしました。
※今回のラベルは、サンプルの意味合いがとても強いです…

メトリクスを定義する時、必須なのはnameとhelpです。定義したら、registerメソッドで登録します。

引数なしのregisterメソッドを呼び出すとデフォルトのCollectorRegistryに登録しますが、別途指定することもできるようです。

https://github.com/prometheus/client_java/blob/parent-0.6.0/simpleclient/src/main/java/io/prometheus/client/Collector.java#L131-L141

あまり使いそうにないですが…。

ちなみに、メトリクスはstatic finalで定義するのがオススメらしいです。

Registering Metrics

続いて、Summary。こちらは、リソースメソッド単位でメトリクスを定義してみることにしました。ラベルは、片方にだけ
付けています。

    private static final Summary incRequestsSummary =
            Summary.build().name("inc_requests_summary").help("increment request summary").register();
    private static final Summary currentRequestsSummary =
            Summary.build().name("current_val_requests_summary").labelNames("url").help("current val request summary").register();

Summaryにはquantile(分位点:パーセンタイル、誤差)、maxAgeSeconds(ウィンドウサイズ:デフォルト10分)、
ageBuckets(スライディングウィンドウの実装で保持するバケット数:デフォルト5)が指定できますが、今回は省略…。

まあ、このあたりで使われるパラメーターですね。

https://github.com/prometheus/client_java/blob/parent-0.6.0/simpleclient/src/main/java/io/prometheus/client/TimeWindowQuantiles.java

で、実際に使っているところがこちら。

    @GET
    @Path("inc")
    @Produces(MediaType.TEXT_PLAIN)
    public int inc() {
        return incRequestsSummary.time(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextLong(1000L));

                return counter.incrementAndGet();
            } finally {
                incCallCounter.inc();
                methodCallCounter.labels("inc", "/counter/inc").inc();
            }
        });
    }

Summary#timeで実行時間を記録しています。

        return incRequestsSummary.time(() -> {

また、Counter#incでカウンタをアップしています。

                incCallCounter.inc();
                methodCallCounter.labels("inc", "/counter/inc").inc();

この時、2つ目のCounterはlabelsメソッドで引数を2つ取っていますが、これはCounterの定義時にラベル名を2つ定義したので、
そちらと対になります。

    private static final Counter methodCallCounter =
            Counter.build().name("method_call_count").labelNames("method", "url").help("counter method call count").register();

続いて、もうひとつのサンプル側。

    @GET
    @Path("current")
    @Produces(MediaType.TEXT_PLAIN)
    public int current() {
        return currentRequestsSummary.labels("/counter/current").time(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextLong(1000L));

                return counter.get();
            } finally {
                methodCallCounter.labels("current", "/counter/current").inc();
            }
        });
    }

こちらは、Summaryの方がlabelsを設定する必要があります。定義の時点で、labelNamesに指定している引数はひとつ
ですからね。

    private static final Summary currentRequestsSummary =
            Summary.build().name("current_val_requests_summary").labelNames("url").help("current val request summary").register();

ここまでで、メトリクスを記録する定義ができあがりました。

メトリクスをエクスポートする

定義したメトリクスを、Prometheusが収集できるようにするには、エクスポートする必要があります。

方法はいくつかあります。

Exporting / HTTP

MetricsServletを使う

サーブレットコンテナ環境であれば、MetricsServletを使うのが手っ取り早いかもしれません。

Maven依存関係に、「simpleclient_servlet」を加えます。

        <!-- Exposition Servlet-->
        <dependency>
          <groupId>io.prometheus</groupId>
          <artifactId>simpleclient_servlet</artifactId>
          <version>0.6.0</version>
        </dependency>

あとは、サーブレットコンテナにデプロイするだけです。

    〜省略〜

        DeploymentInfo deployment =
                Servlets
                        .deployment()
                        .setClassLoader(Server.class.getClassLoader())
                        .setDeploymentName("prometheus-client")
                        .setContextPath(contextPath)
                        .addServlet(createPrometheusServlet())  // サーブレットを追加
                        .addServletContainerInitializer(
                                new ServletContainerInitializerInfo(ResteasyServletInitializer.class,
                                        DefaultClassIntrospector.INSTANCE.createInstanceFactory(ResteasyServletInitializer.class),
                                        jaxrsClasses)
                        );

    〜省略〜

    // MetricsServletを定義
    static ServletInfo createPrometheusServlet() {
        return new ServletInfo("prometheus-metrics", MetricsServlet.class).addMappings("/prometheus/metrics");
    }

なお、「simpleclient_servlet」にはMetricsFilterというServletについてのメトリクス(Histgram)を記録するクラスも
含まれています。

https://github.com/prometheus/client_java/blob/parent-0.6.0/simpleclient_servlet/src/main/java/io/prometheus/client/filter/MetricsFilter.java

HTTPServerを使う

他の環境であれば、HTTPServerを使うのも良いです。

Maven依存関係に、「simpleclient_httpserver」を加えます。

        <!-- Exposition HTTPServer-->
        <dependency>
          <groupId>io.prometheus</groupId>
          <artifactId>simpleclient_httpserver</artifactId>
          <version>0.6.0</version>
        </dependency>

使い方は、バインドするポートや、InetSocketAddressなどを指定してインスタンスを作成すれば起動します。

        HTTPServer metricsHttpServer = new HTTPServer(9000);

止める時は、stop。

metricsHttpServer.stop();

今回は、このあたりに書いておきました。

    public static void main(String... args) throws NoSuchMethodException, ServletException, IOException {
        HTTPServer metricsHttpServer = new HTTPServer(9000);
        // metricsHttpServer.stop();

        String host = "0.0.0.0";
        int port = 8080;
        String contextPath = "";

        Set<Class<?>> jaxrsClasses = new HashSet<>(Arrays.asList(JaxrsActivator.class));
自分で書く

MetricsServletやHTTPServerを見てみるとわかるのですが、やっていることはTextFormat#write004メソッドを使って
CollectorRegistryに登録されたメトリクスの内容を書き出しているだけです。
※「004」というのは、現在のPrometheusで利用するフォーマットのバージョンが0.0.4だからかと…(Content-Typeにも利用)

https://github.com/prometheus/client_java/blob/parent-0.6.0/simpleclient_servlet/src/main/java/io/prometheus/client/exporter/MetricsServlet.java#L44-L48 https://github.com/prometheus/client_java/blob/parent-0.6.0/simpleclient_httpserver/src/main/java/io/prometheus/client/exporter/HTTPServer.java#L53-L67

というわけで、自分で書いてみるとこんな感じになります。
src/main/java/org/littlewings/prometheus/client/MetricsResource.java

package org.littlewings.prometheus.client;

import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;

import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.exporter.common.TextFormat;

@Path("metrics")
public class MetricsResource {
    @GET
    @Produces(TextFormat.CONTENT_TYPE_004)
    public Response metrics() {
        StreamingOutput streamingOutput = os -> {
            Writer writer = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8));
            TextFormat.write004(writer, CollectorRegistry.defaultRegistry.metricFamilySamples());
            writer.flush();
        };

        return Response.ok(streamingOutput).build();
    }
}

3種類紹介しましたが、今回は全部設定しました。

確認してみる

まずは、作成したカウンタに対するエンドポイント(inc、current)を呼び出してみます。

incを3回呼び出し。

$ time curl localhost:8080/counter/inc
1
real    0m0.577s
user    0m0.010s
sys 0m0.008s


$ time curl localhost:8080/counter/inc
2
real    0m0.921s
user    0m0.006s
sys 0m0.007s


$ time curl localhost:8080/counter/inc
3
real    0m0.683s
user    0m0.002s
sys 0m0.005s

currentを2回呼び出し。

$ time curl localhost:8080/counter/current
3
real    0m0.955s
user    0m0.000s
sys 0m0.015s


$ time curl localhost:8080/counter/current
3
real    0m0.584s
user    0m0.005s
sys 0m0.008s

メトリクスを見てみましょう。

MetricsServlet。

$ curl -i localhost:8080/prometheus/metrics
HTTP/1.1 200 OK
Connection: keep-alive
Transfer-Encoding: chunked
Content-Type: text/plain; version=0.0.4;charset=utf-8
Date: Sat, 02 Feb 2019 16:06:40 GMT

# HELP inc_requests_summary increment request summary
# TYPE inc_requests_summary summary
inc_requests_summary_count 3.0
inc_requests_summary_sum 2.01000899
# HELP inc_call_count counter increment count
# TYPE inc_call_count counter
inc_call_count 3.0
# HELP current_val_requests_summary current val request summary
# TYPE current_val_requests_summary summary
current_val_requests_summary_count{url="/counter/current",} 2.0
current_val_requests_summary_sum{url="/counter/current",} 1.496479562
# HELP method_call_count counter method call count
# TYPE method_call_count counter
method_call_count{method="inc",url="/counter/inc",} 3.0
method_call_count{method="current",url="/counter/current",} 2.0

incに対するメトリクス(Counter、Summary)。

# HELP inc_requests_summary increment request summary
# TYPE inc_requests_summary summary
inc_requests_summary_count 3.0
inc_requests_summary_sum 2.01000899
# HELP inc_call_count counter increment count
# TYPE inc_call_count counter
inc_call_count 3.0

currentに対するメトリクス(Summary)。

# HELP current_val_requests_summary current val request summary
# TYPE current_val_requests_summary summary
current_val_requests_summary_count{url="/counter/current",} 2.0
current_val_requests_summary_sum{url="/counter/current",} 1.496479562

メソッドを跨いでのCounter。

# TYPE method_call_count counter
method_call_count{method="inc",url="/counter/inc",} 3.0
method_call_count{method="current",url="/counter/current",} 2.0

こう見ると、メトリクスとしての値、ラベルの名前と値の関係がわかりそうですね。Summaryの「sum」の部分は、
スリープを入れた分が反映されてそうな感じですね。

なお、Content-Typeはこちら。

Content-Type: text/plain; version=0.0.4;charset=utf-8

続いて、HTTPServerで取得。結果はMetricsServletと同じなので、省略。

$ curl -i localhost:9000
HTTP/1.1 200 OK
Date: Sat, 02 Feb 2019 16:06:53 GMT
Content-type: text/plain; version=0.0.4; charset=utf-8
Content-length: 698

〜省略〜

自前で定義した、メトリクス取得用のJAX-RSリソースクラスで取得。こちらも結果は同じなので、省略。

$ curl -i localhost:8080/metrics
HTTP/1.1 200 OK
Connection: keep-alive
Transfer-Encoding: chunked
Content-Type: text/plain;version=0.0.4;charset=utf-8
Date: Sat, 02 Feb 2019 16:10:22 GMT

〜省略〜

というわけで、動かすことができましたね。

HotSpot JVM Metrics

ところで、最初に「simpleclient_hotspot」というものを追加した割に、その結果が現れていません。

        <!-- Hotspot JVM metrics-->
        <dependency>
          <groupId>io.prometheus</groupId>
          <artifactId>simpleclient_hotspot</artifactId>
          <version>0.6.0</version>
        </dependency>

こちらを使うには、以下の1行を加えます。

DefaultExports.initialize();

今回は、mainメソッドの最初に入れてみました。

    public static void main(String... args) throws NoSuchMethodException, ServletException, IOException {
        DefaultExports.initialize();

        HTTPServer metricsHttpServer = new HTTPServer(9000);
        // metricsHttpServer.stop();

        String host = "0.0.0.0";
        int port = 8080;
        String contextPath = "";

この状態でメトリクスを取得すると、JavaVMから取得したメトリクスが大量に表示されるようになります。

$ curl -i localhost:8080/prometheus/metrics
HTTP/1.1 200 OK
Connection: keep-alive
Transfer-Encoding: chunked
Content-Type: text/plain; version=0.0.4;charset=utf-8
Date: Sat, 02 Feb 2019 16:14:17 GMT

# HELP jvm_gc_collection_seconds Time spent in a given JVM garbage collector in seconds.
# TYPE jvm_gc_collection_seconds summary
jvm_gc_collection_seconds_count{gc="PS Scavenge",} 1.0
jvm_gc_collection_seconds_sum{gc="PS Scavenge",} 0.005
jvm_gc_collection_seconds_count{gc="PS MarkSweep",} 0.0
jvm_gc_collection_seconds_sum{gc="PS MarkSweep",} 0.0
# HELP inc_call_count counter increment count
# TYPE inc_call_count counter
inc_call_count 0.0
# HELP jvm_buffer_pool_used_bytes Used bytes of a given JVM buffer pool.
# TYPE jvm_buffer_pool_used_bytes gauge
jvm_buffer_pool_used_bytes{pool="direct",} 32728.0
jvm_buffer_pool_used_bytes{pool="mapped",} 0.0
# HELP jvm_buffer_pool_capacity_bytes Bytes capacity of a given JVM buffer pool.
# TYPE jvm_buffer_pool_capacity_bytes gauge
jvm_buffer_pool_capacity_bytes{pool="direct",} 32728.0
jvm_buffer_pool_capacity_bytes{pool="mapped",} 0.0
# HELP jvm_buffer_pool_used_buffers Used buffers of a given JVM buffer pool.
# TYPE jvm_buffer_pool_used_buffers gauge
jvm_buffer_pool_used_buffers{pool="direct",} 2.0
jvm_buffer_pool_used_buffers{pool="mapped",} 0.0

〜省略〜

このDefaultExports#initializeがなにをしているのかなのですが、GCやスレッドの情報など、MXBeanから取得できる情報を
CollectorRegistryに登録します。

https://github.com/prometheus/client_java/blob/parent-0.6.0/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/DefaultExports.java#L21-L33

で、メトリクスの収集時にMXBeanから取得して表示する、と。

この場合、自分でメトリクスを記録するコードをMXBeanの方に書くわけにはいかないので、Custom Collectors(〜MetricFamily)
というものを使って、メトリクスを記録しているかのように見せています。

Custom Collectors

自分でCounterやGauge、Summary、Histramといったものを直接使ってメトリクスを記録できない場合に使うので、
ダイレクトに値を記録する方法になるようです。

ところで、今回はHotSpot JVMからメトリクスを取得できるようなライブラリを使用しましたが、他にもいろいろあるようです。

Included Collectors

Log4j、Log4j2、Logback、Guava Cache、Caffeine、Hibernate、Jetty、Springなどなど。

利用できそうなものは、見てみるとよいでしょう。

まとめ

PrometheusのJavaクライアントを使って、自分でメトリクスを定義したり、記録したメトリクスをエクスポートしてみたり
ということを試してみました。

やっぱり、自分でひとつひとつ書いた方が理解が進みますね。Prometheusのメトリクスの種類とかとも、ちゃんと
向き合ったりしましたし。時間はかかりましたが…。

勉強になりました。