Spring Boot のメトリクスを Prometheus で集めるには、spring-boot-starter-actuator と micrometer-registry-prometheus を入れ、management.endpoints.web.exposure.include に prometheus を足します。 これで /actuator/prometheus が Prometheus の形式(テキスト)でメトリクスを返し、Prometheus がそれを定期的に取りに来ます。既定で公開されるエンドポイントは health だけなので、依存を入れただけでは 404 になります。

// build.gradle.kts(Spring Boot 4.1.1。版は Boot が決める)
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-webmvc")   // Boot 4 の名前(spring-boot-starter-web は非推奨)
    implementation("org.springframework.boot:spring-boot-starter-actuator")
    runtimeOnly("io.micrometer:micrometer-registry-prometheus")
}
# application.yml
spring:
  application:
    name: order-service
management:
  endpoints:
    web:
      exposure:
        include: health,prometheus        # 既定は health だけ
  metrics:
    tags:
      application: ${spring.application.name}   # すべてのメトリクスに付ける共通タグ
    distribution:
      percentiles-histogram:
        http.server.requests: true        # p95 を PromQL で計算するために必要
やりたいこと 書くもの
p95(95%のリクエストが収まる時間) histogram_quantile(0.95, sum by (le, uri) (rate(http_server_requests_seconds_bucket[5m])))
1秒あたりのリクエスト数 sum by (uri) (rate(http_server_requests_seconds_count[5m]))
エラー率(5xx の割合) sum(rate(http_server_requests_seconds_count{outcome="SERVER_ERROR"}[5m])) / sum(rate(http_server_requests_seconds_count[5m]))
ヒープの使用量 sum by (application) (jvm_memory_used_bytes{area="heap"})
DB の接続待ち hikaricp_connections_pending

確認環境:Spring Boot 4.1.1(Spring Framework 7.0.9、Micrometer 1.17.1、Prometheus Java クライアント 1.7.0、HikariCP 7.0.2 は Boot の依存管理の版)。Boot の自動設定・プロパティの既定値は Boot v4.1.1 のソースとリファレンスで、http.server.requests のタグは Spring Framework v7.0.9 のソースで確かめました。Micrometer 1.17.1(micrometer-core・micrometer-registry-prometheus・micrometer-java21)、Prometheus Java クライアント 1.7.0、HikariCP 7.0.2 のメトリクス部分は GitHub のタグのソースからビルドし、OpenJDK 21.0.10(Linux)で Boot と同じ部品(JVM・システム・HikariCP のメトリクス)を登録して、実際の出力を取りました。その出力を Prometheus 2.45.3 に5秒おきに集めさせ、この記事の PromQL を実行して値を確かめています(確認日 2026年10月13日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring Bootメトリクス|Prometheus設定とPromQL例」

しくみ:Micrometer・Actuator・Prometheus の役割

部品 役割
Micrometer アプリの中で数を数え、時間を測る部品(メーター)。Boot が自動で作る
micrometer-registry-prometheus Micrometer の値を Prometheus の形式に変える
Actuator の /actuator/prometheus その形式のテキストを HTTP で返す
Prometheus 決まった間隔で /actuator/prometheus を取りに行き(スクレイプ)、時系列として保存する
PromQL Prometheus に保存した値を集計する問い合わせ言語
Grafana PromQL の結果をグラフにする

アプリからは送らず、Prometheus が取りに来ます(プル型)。アプリ側で用意するのは、取りに来られる URL だけです。

依存(Boot 4)

Boot 4 ではスターターが細かく分かれました。v4.1.1 のソースでは、spring-boot-starter-actuator が spring-boot-starter-micrometer-metrics・micrometer-observation・micrometer-jakarta9 を含んでいます。Prometheus に出すための部品は別で、micrometer-registry-prometheus を足します。

<!-- pom.xml(Maven) -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-registry-prometheus</artifactId>
  <scope>runtime</scope>
</dependency>
依存 役割 版(Boot 4.1.1 が決める)
spring-boot-starter-actuator /actuator/* とメトリクスの自動設定 4.1.1
io.micrometer:micrometer-registry-prometheus Prometheus 形式で出す(新しいクライアント prometheus-metrics-* を使う) 1.17.1
io.micrometer:micrometer-registry-prometheus-simpleclient 以前のクライアント(simpleclient)を使う版 1.17.1
io.micrometer:micrometer-java21 仮想スレッドのメトリクス(jvm.threads.virtual.*) 1.17.1

micrometer-registry-prometheus の説明文には、互換性で困ったときは micrometer-registry-prometheus-simpleclient に戻せると書かれています。新しく作るなら micrometer-registry-prometheus を使います。Boot 4 のスターターの一覧は、関連記事「Spring Boot starter一覧|用途別の選び方とBoot 4の新旧対応表」で扱っています。

/actuator/prometheus を公開する

公開の設定

設定 既定値(v4.1.1 のソース) 意味
management.endpoints.web.exposure.include health HTTP で公開するエンドポイント
management.endpoints.web.exposure.exclude なし 公開しないエンドポイント
management.endpoints.web.base-path /actuator エンドポイントの前に付くパス
management.server.port アプリと同じ 管理用のポートを分ける
management.prometheus.metrics.export.enabled true Prometheus への出力を止めるときに false
management.prometheus.metrics.export.descriptions true # HELP 行に説明を出す

include: "*" はすべてのエンドポイント(env・heapdump など)を出すので、本番では必要なものだけを書きます。管理用のポートを分けると、外向けのロードバランサーには 8080 だけを通し、Prometheus には 8081 だけを見せる、という分け方ができます。

management:
  server:
    port: 8081                 # /actuator/* はこちらのポートだけで受ける
  endpoints:
    web:
      exposure:
        include: health,prometheus

エンドポイントの一覧と公開の雛形は、関連記事「Spring Boot Actuator設定|エンドポイント一覧と公開設定の雛形」で扱っています。

Spring Security を使っているとき

Spring Security を入れると /actuator/prometheus にも認証がかかり、Prometheus からは 401 になります。Prometheus からの取得を通すには、次のように許可します。

@Bean
SecurityFilterChain actuatorChain(HttpSecurity http) throws Exception {
    http.securityMatcher(EndpointRequest.toAnyEndpoint())
        .authorizeHttpRequests(auth -> auth
            .requestMatchers(EndpointRequest.to("health", "prometheus")).permitAll()
            .anyRequest().hasRole("ADMIN"));
    return http.build();
}

管理用のポートを分けて、そのポートにはネットワークの外から届かないようにする方法もあります。Security の書き方は、関連記事「Spring Security入門|SecurityFilterChainとDB認証」で扱っています。

返す形式と includedNames

v4.1.1 の PrometheusScrapeEndpoint は、リクエストの Accept ヘッダーで3つの形式を返し分けます。

形式 Content-Type 使われる場面
Prometheus テキスト 0.0.4 text/plain; version=0.0.4; charset=utf-8 既定。curl で見るときもこれ
OpenMetrics 1.0 application/openmetrics-text; version=1.0.0; charset=utf-8 Accept でこの形式を求められたとき。最後に # EOF が付く
Protobuf application/vnd.google.protobuf; … Accept で Protobuf を求められたとき

?includedNames= で、返すメトリクスをしぼれます。ここに書く名前は _total・_count を付けない元の名前です。ソースからビルドしたレジストリで試した結果です。

includedNames に書いた名前 返ったもの
http_server_requests_seconds _count と _sum
http_server_requests_seconds_count 何も返らない
orders_placed orders_placed_total
orders_placed_total 何も返らない
http_server_requests_seconds_max _max

出力の読み方

ソースからビルドした Micrometer 1.17.1 に、Boot と同じ JVM・システム・HikariCP のメトリクスを登録して scrape() した出力の一部です(全体は 225 行、16KB でした)。

# HELP http_server_requests_seconds  
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{application="demo",error="none",exception="none",method="GET",outcome="SUCCESS",status="200",uri="/api/orders/{id}"} 2
http_server_requests_seconds_sum{application="demo",error="none",exception="none",method="GET",outcome="SUCCESS",status="200",uri="/api/orders/{id}"} 0.043416125
http_server_requests_seconds_count{application="demo",error="IllegalStateException",exception="IllegalStateException",method="POST",outcome="SERVER_ERROR",status="500",uri="/api/orders"} 1
# HELP http_server_requests_seconds_max  
# TYPE http_server_requests_seconds_max gauge
http_server_requests_seconds_max{application="demo",error="none",exception="none",method="GET",outcome="SUCCESS",status="200",uri="/api/orders/{id}"} 0.030561354
# HELP jvm_memory_used_bytes The amount of used memory
# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{application="demo",area="heap",id="G1 Old Gen"} 3613504.0
jvm_memory_used_bytes{application="demo",area="nonheap",id="Metaspace"} 7589192.0
# HELP hikaricp_connections_active Active connections
# TYPE hikaricp_connections_active gauge
hikaricp_connections_active{application="demo",pool="HikariPool-1"} 3.0

Micrometer の名前は、Prometheus に出るときに次のように変わります。

Micrometer の書き方 Prometheus での名前
. 区切りの名前 http.server.requests _ 区切り http_server_requests
Timer(時間を測る) …_seconds_count・…_seconds_sum・…_seconds_max(単位は秒)
Counter(増えるだけの数) …_total
Gauge(今の値) 名前のまま。baseUnit があれば _bytes などが付く
DistributionSummary(大きさの分布) …_count・…_sum・…_max。baseUnit("yen") なら order_amount_yen_count
ヒストグラムを有効にした Timer 上に加えて …_seconds_bucket{le="…"}

Timer の単位は、Micrometer で Duration.ofMillis(20) と記録しても、Prometheus では 0.02(秒)です。 PromQL やグラフで「ms」と思って読むと1000倍ずれます。

名前の末尾が .created などだと消える

orders.created という名前の Counter は、orders_total として出力されました。 Prometheus Java クライアント 1.7.0 の PrometheusNaming は、_total・_created・_bucket・_info(と . 区切りの同じ語)を「出力のときに自動で付く語」として予約していて、名前の末尾から取り除きます。

登録した名前(Counter) 出力された名前
orders.created orders_total
orders.placed orders_placed_total

「作成数」を数えるなら orders.placed・orders.registered のように、予約語で終わらない名前にします。

promtool で名前を点検する

Prometheus に付いている promtool check metrics に出力を渡すと、名前の付け方の点検ができます。上の出力では次の指摘が出ました。

http_server_requests_seconds no help text
orders_placed_total no help text
jvm_compilation_time_ms_total metric names should not contain abbreviated units
process_cpu_time_ns_total metric names should not contain abbreviated units
system_cpu_count non-histogram and non-summary metrics should not have "_count" suffix

no help text は説明文が無いという指摘で、自作のメトリクスは .description("…") を書くと消えます。jvm_…・process_…・system_… の指摘は Micrometer 側の名前なので、そのまま使って差し支えありません。

標準で取れるメトリクスの一覧

Boot 4.1.1 の自動設定(JvmMetricsAutoConfiguration・SystemMetricsAutoConfiguration など)が登録するものです。名前・型・単位は、Micrometer 1.17.1 と HikariCP 7.0.2 をソースからビルドして実際に出力したものです(… は同じ名前の続き)。

HTTP(Spring MVC・WebFlux)

Prometheus での名前 型 意味
http_server_requests_seconds_count / _sum summary(ヒストグラム有効時は histogram) 終わったリクエストの数と合計時間
http_server_requests_seconds_max gauge 直近の最大の時間
http_server_requests_seconds_bucket histogram 時間の分布。percentiles-histogram を有効にしたときだけ
http_server_requests_active_seconds_* summary 処理中のリクエスト(LongTaskTimer)
http_client_requests_seconds_* summary RestClient・WebClient などで外へ送ったリクエスト

http.server.requests のタグは、Spring Framework 7.0.9 の DefaultServerRequestObservationConvention が決める5つと、Micrometer の DefaultMeterObservationHandler が足す error です。

タグ 値の例 決まり方
uri /api/orders/{id} パスのパターン(/api/orders/123 ではない)。無いときは root・NOT_FOUND・REDIRECTION・UNKNOWN
method GET HTTP メソッド。知らないものは UNKNOWN
status 200 応答のステータス
outcome SUCCESS・CLIENT_ERROR・SERVER_ERROR ステータスの分類(REDIRECTION・INFORMATIONAL もある)
exception none・IllegalStateException 例外のクラス名(短い名前)
error none・IllegalStateException 同上(Micrometer が付ける)

uri がパターンになるので、ID ごとにメトリクスが増えることはありません。 ただし、パターンを持たない 404 や、@RequestMapping を通らない処理は NOT_FOUND・UNKNOWN にまとめられます。名前は management.observations.http.server.requests.name で変えられます。

JVM

Prometheus での名前 型 意味
jvm_memory_used_bytes{area,id} gauge メモリの使用量(area は heap・nonheap、id は領域名)
jvm_memory_committed_bytes / jvm_memory_max_bytes gauge 確保済みの量・上限
jvm_memory_usage_after_gc gauge GC 後に長く残る領域の使用率(0〜1)
jvm_gc_pause_seconds_* summary GC で止まった時間(GC が起きると出る)
jvm_gc_overhead gauge GC に使った CPU の割合(0〜1)
jvm_gc_memory_allocated_bytes_total / jvm_gc_memory_promoted_bytes_total counter 確保した量・Old 領域(長く生きるオブジェクトの領域)へ移った量
jvm_gc_live_data_size_bytes / jvm_gc_max_data_size_bytes gauge GC 後の Old 領域の大きさ・上限
jvm_threads_live_threads / …_daemon_threads / …_peak_threads gauge スレッドの数
jvm_threads_states_threads{state} gauge 状態ごとのスレッド数(runnable・waiting など)
jvm_threads_started_threads_total counter 作られたスレッドの数
jvm_threads_virtual_pinned_seconds_* summary 仮想スレッドが固定(pin)された時間(micrometer-java21 が必要)
jvm_threads_virtual_submit_failed_total counter 仮想スレッドの開始・再開の失敗
jvm_classes_loaded_classes / jvm_classes_unloaded_classes_total gauge / counter 読み込んだクラスの数
jvm_buffer_memory_used_bytes{id} gauge ダイレクトバッファなどの使用量
jvm_compilation_time_ms_total counter JIT コンパイルに使った時間(ミリ秒)
jvm_info{version,vendor,runtime} gauge JVM の版(値は常に 1)

仮想スレッドのメトリクスの読み方は、関連記事「Java 仮想スレッド入門|10万タスク実測と使いどころ判断表」で扱っています。

プロセス・システム

Prometheus での名前 型 意味
process_cpu_usage / system_cpu_usage gauge このプロセス・マシン全体の CPU 使用率(0〜1)
process_cpu_time_ns_total counter 使った CPU 時間(ナノ秒)
system_cpu_count gauge 使える CPU の数
system_load_average_1m gauge 1分間のロードアベレージ
process_uptime_seconds / process_start_time_seconds gauge 起動してからの時間・起動した時刻
process_files_open_files / process_files_max_files gauge 開いているファイルの数・上限
disk_free_bytes{path} / disk_total_bytes{path} gauge ディスクの空き・全体
application_started_time_seconds / application_ready_time_seconds gauge 起動にかかった時間(Boot の StartupTimeMetricsListener)

HikariCP(DB の接続プール)

DataSource が HikariCP のとき、Boot の DataSourcePoolMetricsAutoConfiguration が HikariCP 自身のメトリクスをつなぎます。タグ pool はプール名です。

Prometheus での名前 型 意味
hikaricp_connections gauge 接続の総数
hikaricp_connections_active / _idle gauge 使用中・空いている接続
hikaricp_connections_pending gauge 接続を待っているスレッドの数
hikaricp_connections_max / _min gauge プールの上限・下限
hikaricp_connections_timeout_total counter 接続の取得が時間切れになった回数
hikaricp_connections_acquire_seconds_* summary 接続を借りるまでの時間
hikaricp_connections_usage_seconds_* summary 接続を使っていた時間
hikaricp_connections_creation_seconds_* summary 接続を新しく作る時間

hikaricp_connections_pending が 0 より大きい状態が続くなら、プールが足りないか、接続を長く持つ処理があります。 接続数の考え方は、関連記事「PostgreSQL too many clientsの直し方|接続数と接続プール」で扱っています。

そのほか(条件つき)

名前の前半 出る条件
logback_events_total{level} Logback を使っているとき(ログのレベルごとの件数)
executor_* ThreadPoolTaskExecutor・ThreadPoolTaskScheduler の Bean があるとき
tomcat_* server.tomcat.mbeanregistry.enabled=true のとき(既定は出ない)
http_client_requests_* Boot が作った RestClient.Builder・WebClient.Builder・RestTemplateBuilder から作ったクライアント
spring_data_repository_invocations_* Spring Data のリポジトリを使っているとき
cache_* キャッシュに名前を付けて登録したとき

ヒストグラムと p95

percentiles-histogram を有効にする

PromQL で p95 を計算するには、_bucket(ヒストグラム)が必要です。既定では出ません。 management.metrics.distribution.percentiles-histogram.http.server.requests=true で有効にします(all で全メーター)。

有効にして出力した結果、1つのタグの組み合わせごとに _bucket が 69 行(1ms〜30s の 68 区切り++Inf)増えました。区切りの値は、次のように切りのよい数ではありません。

http_server_requests_seconds_bucket{…,le="0.001"} 0
http_server_requests_seconds_bucket{…,le="0.001048576"} 0
…
http_server_requests_seconds_bucket{…,le="0.447392426"} 2
http_server_requests_seconds_bucket{…,le="0.536870911"} 2
…
http_server_requests_seconds_bucket{…,le="30.0"} 2
http_server_requests_seconds_bucket{…,le="+Inf"} 2

le="0.5" の行はありません。 「500ms 以内に返した割合」を正確に出したいときは、次の slo で区切りを足します。

slo:決まった区切りを足す

management:
  metrics:
    distribution:
      slo:
        http.server.requests: 100ms,500ms

同じ指定(serviceLevelObjectives(100ms, 500ms))で 80・120・300・450・700ms を記録した出力です。

http_server_requests_seconds_bucket{uri="/api/search",le="0.1"} 1
http_server_requests_seconds_bucket{uri="/api/search",le="0.5"} 4
http_server_requests_seconds_bucket{uri="/api/search",le="+Inf"} 5

distribution の設定一覧(management.metrics.distribution.)

設定(distribution. の後ろ) 出るもの 何台分もまとめて計算できるか
percentiles-histogram.<名前> _bucket(68区切り) できる(histogram_quantile)
slo.<名前> 指定した区切りの _bucket できる
percentiles.<名前> {quantile="0.95"} の行(アプリ内で計算) できない(台ごとの値の平均は p95 にならない)
minimum-expected-value.<名前> / maximum-expected-value.<名前> _bucket の範囲を狭める ―
expiry.<名前> / buffer-length.<名前> percentiles の計算に使う期間 ―

アプリを2台以上で動かすなら、percentiles ではなく percentiles-histogram か slo を使います。

カスタムメトリクス(Counter・Timer・Gauge)

完成コード

package com.example.order;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

@Service
public class OrderMetricsService {

    private final MeterRegistry registry;
    private final Timer paymentTimer;
    private final DistributionSummary amountSummary;
    private final Queue<String> mailQueue = new ConcurrentLinkedQueue<>();

    public OrderMetricsService(MeterRegistry registry) {
        this.registry = registry;

        // Timer:時間を測る。Prometheus では payment_call_seconds_*
        this.paymentTimer = Timer.builder("payment.call")
                .description("決済APIの呼び出し時間")
                .publishPercentileHistogram()          // p95 を PromQL で出すため
                .register(registry);

        // DistributionSummary:時間以外の大きさの分布。order_amount_yen_*
        this.amountSummary = DistributionSummary.builder("order.amount")
                .description("注文の金額")
                .baseUnit("yen")
                .register(registry);

        // Gauge:今の値。オブジェクトと値の取り出し方を渡す。job_queue_size
        Gauge.builder("job.queue.size", mailQueue, Queue::size)
                .description("送信待ちのメール")
                .tag("queue", "mail")
                .register(registry);
    }

    public void placeOrder(String channel, long amountYen) {
        // Counter:増えるだけの数。タグ付きは使うたびに builder で取る(同じ名前・タグなら同じものが返る)
        Counter.builder("orders.placed")
                .description("注文の件数")
                .tag("channel", channel)               // web / app など、種類が限られる値だけ
                .register(registry)
                .increment();
        amountSummary.record(amountYen);
    }

    public PaymentResult pay(PaymentRequest req) {
        return paymentTimer.record(() -> callPaymentApi(req));
    }

    public void enqueueMail(String to) {
        mailQueue.add(to);
    }

    private PaymentResult callPaymentApi(PaymentRequest req) {
        // 外部 API の呼び出し
        return new PaymentResult("ok");
    }

    public record PaymentRequest(String orderId, long amount) {}
    public record PaymentResult(String status) {}
}

このコードの各メーターを、ソースからビルドしたレジストリで出力した形です。

# HELP orders_placed_total 注文の件数
# TYPE orders_placed_total counter
orders_placed_total{application="demo",channel="web"} 3.0
# HELP payment_call_seconds 決済APIの呼び出し時間
# TYPE payment_call_seconds summary
payment_call_seconds_count{application="demo"} 20
payment_call_seconds_sum{application="demo"} 2.1
# HELP job_queue_size 送信待ちのメール
# TYPE job_queue_size gauge
job_queue_size{application="demo",queue="mail"} 42.0
# HELP order_amount_yen 注文の金額
# TYPE order_amount_yen summary
order_amount_yen_count{application="demo"} 1
order_amount_yen_sum{application="demo"} 3980.0

どれを使うか

測りたいもの 使うメーター 例
起きた回数(増えるだけ) Counter 注文数、送信失敗の数
かかった時間 Timer 外部 API の呼び出し、バッチ1件の処理
今の値(上がり下がりする) Gauge キューの長さ、キャッシュの件数
時間以外の大きさの分布 DistributionSummary 金額、ファイルサイズ
長く続く処理の経過時間 LongTaskTimer 数分かかるバッチ

Gauge は弱い参照で持たれる

registry.gauge("名前", obj) の obj を、自分のフィールドなどで持ち続けないと、GC(ガベージコレクション)のあとで値が NaN になります。Micrometer の Gauge は、渡したオブジェクトを弱い参照(GC で消えてよい参照)で持つためです。試した結果です。

registry.gauge("queue.weak", new AtomicInteger(5));       // 戻り値を捨てた
Gauge.builder("queue.strong", new AtomicInteger(5), AtomicInteger::get)
     .strongReference(true).register(registry);
System.gc();
queue_strong 5.0
queue_weak NaN

Gauge に渡すオブジェクトは、Bean のフィールドなどアプリが持ち続けるものにします。

同じ名前で型を変えると例外

同じ名前で Timer と Counter を登録すると、次の例外になりました。

java.lang.IllegalArgumentException: There is already a registered meter of a different type (PrometheusTimer vs. Counter) with the same name: api.calls2

@Timed・@Counted で付ける

メソッドに @Timed・@Counted を付けて測ることもできます。v4.1.1 のソースでは、management.observations.annotations.enabled=true と AOP(spring-boot-starter-aspectj)の両方があるときに有効になります。

management:
  observations:
    annotations:
      enabled: true
@Timed(value = "report.build", description = "レポート作成の時間")
public Report build(Long id) { … }

AOP を使うので、同じクラスの中から呼んだメソッドは測られません。AOP の落とし穴は、関連記事「Spring AOPの使い方|@Aspectのコード全文と指示子一覧」で扱っています。

共通タグとメーターの無効化

@Bean
MeterRegistryCustomizer<MeterRegistry> commonTags() {
    // management.metrics.tags.region=tokyo と同じ
    return registry -> registry.config().commonTags("region", "tokyo");
}
management:
  metrics:
    enable:
      jvm.buffer: false          # jvm.buffer で始まるメーターを出さない
      tomcat: false

タグの付けすぎ(カーディナリティ)

カーディナリティは、1つのメトリクス名にできる時系列(タグの値の組み合わせ)の数です。Prometheus は時系列ごとにメモリと保存領域を使うので、ユーザー ID や注文 ID のように値が増え続けるものをタグにすると、アプリと Prometheus の両方が重くなります。

実測:userId をタグにしたとき

api.calls に userId のタグを付けて、ユーザーの数だけ登録しました(ソースからビルドした Micrometer 1.17.1、OpenJDK 21.0.10)。

登録のしかた ユーザー数 時系列(出力の行) 出力のサイズ ヒープの増加
Counter 10 10 386 バイト 0.1MB
Counter 10,000 10,000 359KB 5.9MB
Timer+ヒストグラム 10 720 40KB 0.2MB
Timer+ヒストグラム 10,000 720,000 42MB 62.4MB
Counter+maximumAllowableTags(上限100) 10,000 100 3KB 0.1MB

ヒストグラムを有効にした Timer は、タグの組み合わせ1つで 72 行(_bucket 69 行+_count・_sum・_max)になります。1万ユーザーでは1回の取得が 42MB になり、Prometheus が5秒おきに取りに来ると、それだけで重い負荷です。

タグにしてよい値・よくない値

タグの値 判断 理由
HTTP メソッド、ステータス、outcome ○ 種類が数個〜数十個
パスのパターン(/api/orders/{id}) ○ エンドポイントの数だけ
例外のクラス名 ○ 種類が限られる
チャネル(web・app)、プラン(free・pro) ○ 種類が決まっている
実際のパス(/api/orders/123) × ID の数だけ増える
ユーザー ID、注文 ID、セッション ID × 増え続ける
例外のメッセージ × 文に値が入ると毎回違う
メールアドレス、IP アドレス × 増え続ける。個人情報でもある
時刻・日付 × 増え続ける

ID ごとの調査が必要なときは、メトリクスではなくログやトレース(分散トレーシング)に残します。

Boot の安全装置:max-uri-tags

Boot 4.1.1 は http.server.requests の uri タグの値を 100 種類までに制限します(management.metrics.web.server.max-uri-tags、クライアント側は management.metrics.web.client.max-uri-tags、どちらも既定 100)。101種類目からはメーターを作らず、ログに次の警告を1回だけ出します(v4.1.1 の MaximumAllowableTagsMeterFilter の文)。

Reached the maximum number of 'uri' tags for 'http.server.requests'.

この警告が出たら、RestClient の呼び出しで URL を文字列連結している(uri("/users/" + id))など、パターンになっていない箇所を探します。uri("/users/{id}", id) のように変数で渡すと、パターンが uri タグになります。自作のメーターには、Micrometer の MeterFilter.maximumAllowableTags で同じ制限をかけられます。

@Bean
MeterFilter limitUserTags() {
    return MeterFilter.maximumAllowableTags("api.calls", "userId", 100, MeterFilter.deny());
}

Prometheus の設定

prometheus.yml

global:
  scrape_interval: 15s          # 取りに行く間隔(既定 1m)
  evaluation_interval: 15s      # ルールを評価する間隔

rule_files:
  - rules.yml

scrape_configs:
  - job_name: order-service
    metrics_path: /actuator/prometheus     # 既定は /metrics なので必ず書く
    static_configs:
      - targets: ["app1:8081", "app2:8081"]
        labels:
          env: prod
promtool check config prometheus.yml
prometheus --config.file=prometheus.yml

ブラウザで Prometheus の画面(既定は :9090)を開き、Status → Targets で UP になっていれば取れています。metrics_path を書き忘れると /metrics を取りに行き、404 で DOWN になります。Prometheus の構築全体は、関連記事「Prometheus Grafana入門|監視構築とPromQL一覧」で扱っています。

取得にかかる時間と件数

Prometheus は取得ごとに scrape_duration_seconds・scrape_samples_scraped を記録します。この記事の検証用のアプリ(ヒストグラムを有効にした http.server.requests 5種類+JVM など)では、1回の取得で 411 件、8ms でした。件数が急に増えたら、タグの付けすぎを疑います。

PromQL 例(p95・エラー率)

Boot と同じ名前・タグで http.server.requests を記録し続けるアプリ(約50件/秒、/api/orders/{id} 5〜50ms・/api/orders 50〜300ms のうち5%が 500・/api/search 100〜800ms)を Prometheus 2.45.3 で5秒おきに集め、この節の PromQL を実行した値です。

リクエスト数

# 1秒あたりのリクエスト数(uri・メソッドごと)
sum by (uri, method) (rate(http_server_requests_seconds_count[1m]))
{method="GET", uri="/api/orders/{id}"}  34.85
{method="POST", uri="/api/orders"}      10.44
{method="GET", uri="/api/search"}        4.53

rate は「範囲の中で1秒あたりどれだけ増えたか」を返します。範囲([1m])は、取得間隔の4倍以上にするのが目安です。

p95(histogram_quantile)

histogram_quantile(0.95, sum by (le, uri) (rate(http_server_requests_seconds_bucket[1m])))
uri 実際の時間の範囲 p95 の結果
/api/orders/{id} 3〜50ms 0.0478(47.8ms)
/api/orders 50〜300ms 0.3207(320.7ms)
/api/search 100〜800ms 0.7655(765.5ms)

/api/orders は最大 300ms なのに、p95 が 320ms になりました。 histogram_quantile は _bucket の区切りの中を直線で補って計算するので、区切り(ここでは 268ms〜358ms)の幅だけずれます。正確な値ではなく目安として読み、決まった値(500ms など)で判断したいときは slo で区切りを足します。

sum by (le, …) の le を落とすと、結果は空になります。 histogram_quantile は le ごとの値から計算するためです。

エラー率

# 全体の 5xx の割合
sum(rate(http_server_requests_seconds_count{outcome="SERVER_ERROR"}[1m]))
  / sum(rate(http_server_requests_seconds_count[1m]))

結果は 0.01496(約1.5%)でした。/api/orders(全体の20%)の5% なので、計算どおりです。

uri ごとに出すときは注意があります。

# uri ごと(5xx が1件も無い uri は結果から消える)
sum by (uri) (rate(http_server_requests_seconds_count{status=~"5.."}[1m]))
  / sum by (uri) (rate(http_server_requests_seconds_count[1m]))
{uri="/api/orders"}  0.0714

5xx の時系列が無い uri は、割り算の左側に無いので結果から消えます。 0 として並べたいときは、or で 0 の値を補います。

(
  sum by (uri) (rate(http_server_requests_seconds_count{status=~"5.."}[1m]))
  or sum by (uri) (rate(http_server_requests_seconds_count[1m])) * 0
)
/ sum by (uri) (rate(http_server_requests_seconds_count[1m]))
{uri="/api/orders"}        0.0650
{uri="/api/orders/{id}"}   0
{uri="/api/search"}        0

よく使う PromQL の一覧

見たいもの PromQL 検証用アプリでの値
平均の応答時間 sum by (uri) (rate(http_server_requests_seconds_sum[1m])) / sum by (uri) (rate(http_server_requests_seconds_count[1m])) /api/search 0.436秒
最大の応答時間 max by (uri) (http_server_requests_seconds_max) /api/search 0.8秒
500ms 以内の割合(slo で区切りを足したとき) sum(rate(http_server_requests_seconds_bucket{le="0.5"}[5m])) / sum(rate(http_server_requests_seconds_count[5m])) ―
ヒープの使用量(MB) sum(jvm_memory_used_bytes{area="heap"}) / 1024 / 1024 9.97
5分間の注文数 increase(orders_placed_total[5m]) 836
GC で止まった時間の割合 sum(rate(jvm_gc_pause_seconds_sum[5m])) ―
CPU 使用率(%) process_cpu_usage * 100 ―
接続待ちのスレッド max by (pool) (hikaricp_connections_pending) ―
取れていないアプリ up == 0 ―

increase も rate と同じく、アプリの再起動で Counter が 0 に戻っても、その分を補って計算します。Counter の値(orders_placed_total)をそのまま見ると、再起動のたびに 0 に戻ります。

アラートのルール

# rules.yml
groups:
  - name: order-service
    rules:
      - record: job_uri:http_server_requests:rate1m
        expr: sum by (job, uri) (rate(http_server_requests_seconds_count[1m]))
      - record: job_uri:http_server_requests_seconds:p95_5m
        expr: histogram_quantile(0.95, sum by (job, uri, le) (rate(http_server_requests_seconds_bucket[5m])))
      - alert: HighErrorRate
        expr: |
          sum by (job) (rate(http_server_requests_seconds_count{outcome="SERVER_ERROR"}[5m]))
            / sum by (job) (rate(http_server_requests_seconds_count[5m])) > 0.05
        for: 5m
        labels:
          severity: page
        annotations:
          summary: "{{ $labels.job }} の 5xx が 5% を超えています"
      - alert: SlowP95
        expr: job_uri:http_server_requests_seconds:p95_5m > 1
        for: 10m
      - alert: InstanceDown
        expr: up == 0
        for: 1m

このファイルは promtool check rules rules.yml で SUCCESS: 5 rules found になりました。record は重い計算を前もってしておき、グラフとアラートで使い回すためのルールです。

Grafana でグラフにする

  1. Grafana の Connections → Data sources で Prometheus を追加し、URL に Prometheus のアドレス(http://prometheus:9090 など)を入れる
  2. Dashboards → New → Import で、公開されているダッシュボードの ID を入れる
  3. 自分のダッシュボードを作るなら、パネルに上の PromQL を入れる
ダッシュボード(grafana.com) ID 前提
JVM (Micrometer) 4701 すべてのメトリクスに application タグ(management.metrics.tags.application)
Spring Boot 3.x Statistics 19004 Micrometer の Prometheus 形式のメトリクス

ダッシュボード 4701 の説明には、job ではなく application という共通タグで絞り込むと書かれています。management.metrics.tags.application を設定していないと、変数の選択肢が空になり、グラフが出ません。 自分で作るときは、ダッシュボードの変数に label_values(jvm_info, application) を使うと、アプリ名で切り替えられます。

パネル PromQL 単位(Grafana の Unit)
リクエスト数 sum by (uri) (rate(http_server_requests_seconds_count{application="$application"}[$__rate_interval])) requests/sec
p95 histogram_quantile(0.95, sum by (le, uri) (rate(http_server_requests_seconds_bucket{application="$application"}[$__rate_interval]))) seconds
エラー率 上のエラー率の式に application="$application" を足す percent (0.0-1.0)
ヒープ sum by (id) (jvm_memory_used_bytes{application="$application", area="heap"}) bytes
DB 接続 hikaricp_connections_active{application="$application"} none

$__rate_interval は、Grafana が取得間隔に合わせて rate の範囲を決める変数です。

表示されない・値がおかしいとき

症状 原因 直し方
/actuator/prometheus が 404 公開していない(既定は health だけ) exposure.include に prometheus
公開したのに 404 micrometer-registry-prometheus が無い(エンドポイント自体が作られない) 依存を足す
Prometheus から 401・403 Spring Security の認証がかかっている EndpointRequest.to("prometheus") を許可
Targets が DOWN(404) metrics_path を書いていない(既定は /metrics) metrics_path: /actuator/prometheus
histogram_quantile が空 _bucket が出ていない、または sum by に le が無い percentiles-histogram を有効にする、le を残す
le="0.5" の割合が出ない 既定の区切りに 0.5 が無い slo で 500ms を足す
値が1000倍ずれている Timer の単位は秒 Grafana の Unit を seconds にする
Counter の名前が想定と違う .created・.total・.info で終わる名前は末尾が消える 予約語で終わらない名前にする
Gauge が NaN 渡したオブジェクトが GC された アプリが持ち続けるオブジェクトを渡す
uri が UNKNOWN・NOT_FOUND ばかり パターンを持たないリクエスト(404、フィルターで返した応答など) ハンドラーを通る形にする
Reached the maximum number of 'uri' tags uri タグの値が 100 種類を超えた URL を変数で渡す。必要なら max-uri-tags を上げる
hikaricp_* が無い DataSource が HikariCP でない、または DB を使っていない ―
tomcat_* が無い Tomcat の MBean レジストリが無効(既定) server.tomcat.mbeanregistry.enabled=true
http_client_requests_* が無い RestClient.create() などで自分で作った Boot が用意する RestClient.Builder から作る

よくある質問

Q. Spring Boot 3 から 4 に上げると、メトリクスの名前は変わりますか。 A. この記事の名前は、Boot 4.1.1 が使う Micrometer 1.17.1 で出力したものです。Boot 4 で大きく変わったのはスターターとパッケージの分かれ方で、たとえば MeterRegistryCustomizer のパッケージは org.springframework.boot.micrometer.metrics.autoconfigure になりました。上げる前後で /actuator/prometheus の出力を保存し、名前の一覧(# TYPE の行)を比べると、ダッシュボードやアラートへの影響を確かめられます。移行全体は関連記事「Spring Boot 4移行の手順|3から4の変更点一覧とチェックリスト」で扱っています。

Q. /actuator/metrics と /actuator/prometheus は何が違いますか。 A. /actuator/metrics は人が見るための JSON で、名前を指定して1つずつ見ます。/actuator/prometheus は Prometheus が取りに来るためのテキストで、すべてを一度に返します。

Q. Kubernetes ではどう取りに行かせますか。 A. Prometheus Operator を使っているなら ServiceMonitor に path: /actuator/prometheus を書きます。使っていない場合は、Prometheus の kubernetes_sd_configs で Pod を見つけさせます。どちらも metrics_path の指定が要る点は同じです。

Q. OpenTelemetry(OTLP)で送る方法もありますか。 A. あります。Boot 4.1.1 には spring-boot-starter-opentelemetry があり、Micrometer の値を OTLP で送れます。Prometheus に取りに来させる形か、送る形かで選びます。

まとめ

  • 依存は spring-boot-starter-actuator と micrometer-registry-prometheus。版は Boot 4.1.1 が決める(Micrometer 1.17.1)
  • 既定で公開されるのは health だけ。exposure.include に prometheus を足す
  • Timer の単位は秒。Counter は _total、名前が .created で終わると末尾が消える
  • p95 には percentiles-histogram が必要。1つのタグの組み合わせで _bucket が 69 行増える
  • histogram_quantile は区切りの中を補った目安。決まった値で判断するなら slo
  • ユーザー ID などをタグにしない。ヒストグラム付きで1万種類にすると、1回の取得が 42MB になった
  • uri ごとのエラー率は、5xx が無い uri が消えるので or … * 0 で補う
  • Grafana の公開ダッシュボードには application タグが要るものがある

参考・出典

確認日はいずれも 2026年10月13日です。

  • Spring Boot Reference「Metrics」:https://docs.spring.io/spring-boot/reference/actuator/metrics.html
  • Spring Boot Reference「Endpoints」:https://docs.spring.io/spring-boot/reference/actuator/endpoints.html
  • Spring Boot v4.1.1 ソース(PrometheusMetricsExportAutoConfiguration・PrometheusScrapeEndpoint・MetricsProperties・MaximumAllowableTagsMeterFilter・JvmMetricsAutoConfiguration):https://github.com/spring-projects/spring-boot/tree/v4.1.1
  • Spring Framework v7.0.9 ソース(DefaultServerRequestObservationConvention):https://github.com/spring-projects/spring-framework/tree/v7.0.9
  • Micrometer v1.17.1 ソース:https://github.com/micrometer-metrics/micrometer/tree/v1.17.1
  • Micrometer Documentation「Prometheus」:https://docs.micrometer.io/micrometer/reference/implementations/prometheus.html
  • Prometheus Java クライアント v1.7.0 ソース(PrometheusNaming):https://github.com/prometheus/client_java/tree/v1.7.0
  • HikariCP 7.0.2 ソース(MicrometerMetricsTracker):https://github.com/brettwooldridge/HikariCP/tree/HikariCP-7.0.2
  • Prometheus Documentation「Querying basics」「Histograms and summaries」:https://prometheus.io/docs/prometheus/latest/querying/basics/
  • Grafana Labs「JVM (Micrometer)」:https://grafana.com/grafana/dashboards/4701-jvm-micrometer/
  • Grafana Labs「Spring Boot 3.x Statistics」:https://grafana.com/grafana/dashboards/19004-spring-boot-statistics/

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring Bootメトリクス|Prometheus設定とPromQL例」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?