- Spring
Spring Bootメトリクス|Prometheus設定とPromQL例
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 でグラフにする
- Grafana の Connections → Data sources で Prometheus を追加し、URL に Prometheus のアドレス(
http://prometheus:9090など)を入れる - Dashboards → New → Import で、公開されているダッシュボードの ID を入れる
- 自分のダッシュボードを作るなら、パネルに上の 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期連続の黒字決算。