Spring BootのメトリクスをPrometheus+Grafanaで可視化する


Spring Boot Actuator入門 でヘルスチェックやエンドポイント公開はできた。次は「本番でちゃんとメトリクスを監視したい」というフェーズですよね。

この記事では Micrometer → Prometheus → Grafana という監視パイプラインをローカルで組み立て、最終的にカスタムメトリクスをダッシュボードで確認するところまでやります。

全体の流れ

3コンポーネントの構成です。

  • Spring Boot + Micrometer — JVMやHTTPのメトリクスを自動計装して /actuator/prometheus で公開する
  • Prometheus — エンドポイントを定期スクレイプして時系列データとして保存する
  • Grafana — Prometheusをデータソースにしてダッシュボードを描画する

依存関係を追加する

spring-boot-starter-actuatormicrometer-registry-prometheus の2つが必須です。後述の @Timed アノテーションを使う場合は spring-boot-starter-aop も必要になります。

// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'io.micrometer:micrometer-registry-prometheus'
    implementation 'org.springframework.boot:spring-boot-starter-aop' // @Timedを使う場合に必要
}

Mavenの場合はこちらです。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- @Timedを使う場合に必要 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Spring Boot 3.x では micrometer-registry-prometheus のグループIDが io.micrometer になっています。バージョン管理はSpring Boot BOMに任せておけば大丈夫です。

application.ymlでエンドポイントを公開する

デフォルトではWebに公開されていないので、application.yml で明示的に指定します。

management:
  endpoints:
    web:
      exposure:
        include: health, info, prometheus
        # YAML配列形式でも同じ動作: include: [health, info, prometheus]

起動後に curl http://localhost:8080/actuator/prometheus を叩くと、こんなOpenMetrics形式のレスポンスが返ってきます。

# HELP jvm_memory_used_bytes The amount of used memory
# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{area="heap",id="G1 Eden Space"} 2.3068672E7

Docker ComposeでPrometheusとGrafanaを起動する

docker-compose.ymlprometheus.yml の2ファイルを用意します。

# docker-compose.yml
services:
  prometheus:
    image: prom/prometheus:v2.51.0
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus  # データ永続化(再起動後もメトリクス履歴が消えない)

  grafana:
    image: grafana/grafana:10.4.0
    ports:
      - "3000:3000"
    environment:
      # ※ローカル開発専用。本番環境では必ず強いパスワードに変更してください
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana_data:/var/lib/grafana

volumes:
  prometheus_data:
  grafana_data:

prometheus_datagrafana_data の両方を定義しておくと、コンテナを再起動してもデータが消えません。イメージのバージョンは記事執筆時点のものを固定しています。

# prometheus.yml
global:
  scrape_interval: 30s  # ローカル開発は30s、本番は15sが一般的

scrape_configs:
  - job_name: 'spring-boot'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['host.docker.internal:8080']
        # Linuxの場合:
        # - Docker Desktop for Linux: host.docker.internalが使用可能
        # - Docker Engine直接インストール: 172.17.0.1:8080(docker network inspect bridge でGateway IP確認)
        #   または network_mode: host 利用時は localhost:8080

docker compose up -d で起動したら http://localhost:9090/targets を開き、spring-bootのStateが UP になっていることを確認してください。

GrafanaにPrometheusを登録する

http://localhost:3000(admin/admin)でGrafanaにログインします。起動直後は502が表示されることがありますが、数秒待ってリロードすれば解消されます。

  1. Connections > Data sources > Add new data source でPrometheusを選択
  2. URLに http://prometheus:9090 を入力(docker network内の名前解決)
  3. Save & Test で「Data source is working」を確認

公式ダッシュボードをインポートする

Dashboards > Import からIDを入力するだけで充実したダッシュボードが手に入ります。

  • 4701 — JVM Micrometer(ヒープメモリ・GC・スレッド数など)。Prometheus単体で完全動作します。
  • 17175 — Spring Boot Observability(Spring Boot 3.x対応の統合ダッシュボード)。ログ関連パネルはLoki未設定のためNo dataになりますが、JVMメトリクス・HTTPリクエスト系パネルは正常に表示されます。

注意: よく紹介される 10280(Spring Boot 2.1 Statistics)は Spring Boot 2.x 向けです。Spring Boot 3.x ではHTTPメトリクスの名称・タグ構造が変更されているため、インポートしてもHTTPリクエスト関連パネルが「No data」になります。3.x を使っている場合は 4701 または 17175 を使いましょう。

Import後にデータソースとして先ほど登録したPrometheusを選ぶと、すぐにメトリクスが流れ込んできます。

カスタムメトリクスを追加する

MeterRegistry をDIするだけで、ビジネスロジックのメトリクスも簡単に追加できます。

@Service
public class OrderService {

    private final Counter orderCounter;
    private final AtomicInteger pendingOrders;

    public OrderService(MeterRegistry registry) {
        this.orderCounter = Counter.builder("order.created.total")
                .description("Total number of orders created")
                .register(registry);

        this.pendingOrders = new AtomicInteger(0);
        Gauge.builder("order.pending", pendingOrders, AtomicInteger::get)
                .description("Number of pending orders")
                .register(registry);
    }

    public void createOrder(Order order) {
        orderCounter.increment();
        pendingOrders.incrementAndGet();
    }
}

メソッド実行時間を計測したい場合は @Timed アノテーションが手軽です。

import io.micrometer.core.annotation.Timed;

@Timed(value = "order.process.time", description = "Time taken to process order")
public void processOrder(Long orderId) {
    // 注文処理
}

@Timed を使うには TimedAspect のBean登録が必須です。これがないと @Timed は完全に無視されます。

@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
    return new TimedAspect(registry);
}

なお、同一クラス内からのself-invocation(AOPプロキシを経由しない呼び出し)では計測されない点にも注意してください。

カスタムメトリクスをGrafanaで確認する

追加したメトリクスはGrafanaの Explore 画面からすぐに確認できます。

  1. Explore を開き、Metrics Browserで order_created_total を検索
  2. Counterはそのままではなく rate() 関数で増加率を見るのが定番です
rate(order_created_total[5m])

[5m] は「5分間の平均増加率」を意味します。時間窓は scrape_interval の4倍以上が目安で、[1m] のように狭くするとサンプル数が少なくグラフが不安定になります。

  1. グラフが描けたら右上の Add to dashboard でパネルをダッシュボードに追加
  2. Gaugeの現在値は Stat または Gauge ビジュアライゼーションを選ぶとわかりやすい

Save dashboard でチームと共有できる状態になります。

本番環境でのセキュリティ設定

Prometheusエンドポイントにはメモリ使用率やスレッド数などの情報が含まれるため、外部に公開しないようにしましょう。

最もシンプルな方法は 管理ポートの分離 です。

management:
  server:
    port: 8081
  endpoints:
    web:
      exposure:
        include: health, prometheus

8081 ポートをPrometheusが動くクラスター内ネットワーク専用にして、ファイアウォールで 8080(アプリポート)だけを外部公開する構成が確実です。

Spring Securityを既に導入している場合は SecurityFilterChain でActuatorエンドポイントへのアクセスを認証済みユーザーに限定することもできます。

@Bean
public SecurityFilterChain actuatorSecurityFilterChain(HttpSecurity http) throws Exception {
    http.securityMatcher("/actuator/**")
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/actuator/health").permitAll()
            .anyRequest().authenticated()
        )
        .httpBasic(Customizer.withDefaults())
        .csrf(csrf -> csrf.disable());
    return http.build();
}

ポート分離と組み合わせることで多層防御になります。exposure.include には必要なエンドポイントだけを列挙するのが大原則です。* で全公開するのは開発環境だけにしておきましょう。

まとめ

Micrometer → Prometheus → Grafanaのパイプラインは、一度組んでしまえば後はダッシュボードを育てていくだけです。公式ダッシュボードのインポートでJVMメトリクスはすぐ見えるようになりますし、カスタムメトリクスを追加するのも MeterRegistry のDI一つで済みます。

Dockerコンテナ化の記事 と組み合わせれば、本番相当の監視付きコンテナ環境をローカルで再現できます。ログの可視化については Logback・SLF4Jの記事 も参考にしてみてください。