Spring Boot Actuator:ヘルスチェックとメトリクス

1. 概要

本番環境で動作するサービスは、次の2つの問いに答える必要があります:「それは生きているか?」と「今どのような状態で動作しているか?」。Spring Boot Actuatorはこの両方に答えます。主に /actuator/health と /actuator/metrics という組み込みエンドポイントを通じてです。

このガイドでは、Actuatorを有効にして、カスタム HealthIndicator を書き、Kubernetes向けのliveness/readiness probeを設定します。その後、Micrometerでビジネスメトリクスを記録し、Prometheusへエクスポートし、両方に対するテストを書きます。

2. インストール

Actuatorのstarterを追加する必要があります。このガイドではHTTPを通じてエンドポイントを提供するために spring-boot-starter-web も追加します:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

アプリケーションを起動して試してみます:

curl http://localhost:8080/actuator
{"_links":{"self":{...},"health":{...},"health-path":{...}}}

デフォルトではSpring Boot 3.xは health エンドポイントのみHTTP経由で公開します。その他のエンドポイントは存在しますが、外部からアクセスできません。これは意図的なセキュリティ上の選択です。

2.1. エンドポイントをさらに公開する

application.properties で公開したいエンドポイントを明確にリストします:

management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always

最初の行は3つのエンドポイント health、info、metrics を開きます。2行目は /actuator/health が全体的なステータスだけではなく、各コンポーネントの詳細を返すことを許可します。

注意: 本番環境では management.endpoints.web.exposure.include=* を使用することは避けてください。env、heapdump、configprops などのエンドポイントはパスワード、内部設定、メモリ内のデータを公開する可能性があります。

3. ヘルスチェック

3.1. デフォルトエンドポイント

上記の設定で /actuator/health を呼び出すと、次のような結果が得られます:

{
  "status": "UP",
  "components": {
    "diskSpace": {
      "status": "UP",
      "details": { "total": 499963174912, "free": 213456789504, "threshold": 10485760 }
    },
    "ping": { "status": "UP" }
  }
}

Spring Bootはclasspathに基づいて HealthIndicator を自動的に登録します。DataSourceがある場合、db も表示されます。Redisがある場合は redis が追加され、他のコンポーネントについても同様です。全体的なステータスは集計結果です:1つのコンポーネントがDOWNなら全体がDOWNになり、返されるHTTPステータスは503になります。

3.2. カスタムHealthIndicatorを書く

あなたの注文サービスが外部の決済ゲートウェイに依存していると仮定します。まず、シンプルなクライアントがあります:

public interface PaymentGatewayClient {
    boolean ping();
}

次に、このクライアントをチェックするindicatorを書きます:

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final PaymentGatewayClient client;

    public PaymentGatewayHealthIndicator(PaymentGatewayClient client) {
        this.client = client;
    }

    @Override
    public Health health() {
        try {
            if (client.ping()) {
                return Health.up().withDetail("provider", "VNPay").build();
            }
            return Health.down().withDetail("reason", "ping returned false").build();
        } catch (Exception e) {
            return Health.down(e).build();
        }
    }
}

注意する点:

  • beanの名前は paymentGatewayHealthIndicator です。Spring BootはHealthIndicatorのサフィックスを削除するため、JSONのキーは paymentGateway になります。
  • withDetail(...) は補足情報を追加します。show-detailsが許可する場合のみ表示されます。
  • Health.down(e) は自動的にエラーメッセージを details.error セクションに入れます。

決済ゲートウェイがエラーの場合の結果:

{
  "status": "DOWN",
  "components": {
    "diskSpace": { "status": "UP", ... },
    "paymentGateway": {
      "status": "DOWN",
      "details": { "error": "java.net.ConnectException: Connection refused" }
    },
    "ping": { "status": "UP" }
  }
}

注意: health() メソッドは毎回エンドポイントが呼び出されるたびに実行されます。ネットワーク呼び出しがタイムアウトなしで実行される場合、遅いdependencyは /actuator/health をハング状態にします。ヘルスチェックで使用されるクライアントに対して常に短いタイムアウトを設定すべきです(例えば1〜2秒)。

3.3. LivenessとReadiness Probe

Kubernetesは2種類のprobeを区別します:

  • Liveness:アプリケーションはハングしていないか?失敗した場合、podが再起動されます。
  • Readiness:アプリケーションはリクエストを受け付ける準備ができているか?失敗した場合、podはロードバランサーから削除されますが、再起動はされません。

Spring BootはKubernetes上で実行されていることを検出したとき、これら2つのグループを自動的に有効にします。ローカルで実行する場合、手動で有効にします:

management.endpoint.health.probes.enabled=true
management.endpoint.health.group.readiness.include=readinessState,paymentGateway

最初の行は /actuator/health/liveness と /actuator/health/readiness の2つのエンドポイントを作成します。2行目は paymentGateway をreadinessグループに含めます。決済ゲートウェイが停止した場合、podは無駄な再起動をせずにトラフィックの受け入れを停止するだけです。

curl http://localhost:8080/actuator/health/liveness
{"status":"UP"}

Kubernetesマニフェストの対応する設定:

livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080

注意: 外部dependency(データベース、決済API)をlivenessグループに含めないでください。データベースが停止した場合、すべてのpodはlivenessに失敗し、一斉に再起動されます。再起動はデータベースを修正できず、起動時にさらに負荷が増加します。

4. Micrometerのメトリクス

ActuatorはMicrometerを抽象化レイヤーとして使用します。追加の設定は不要です。既にJVM、CPU、HTTPリクエスト、コネクションプール、および他の多くのメトリクスがあります。

curl http://localhost:8080/actuator/metrics
{"names":["application.ready.time","http.server.requests","jvm.gc.pause","jvm.memory.used","process.cpu.usage", ...]}

メトリクスの詳細を表示するには、URLに名前を追加します。tag パラメータを使用してタグでフィルタリングできます:

curl "http://localhost:8080/actuator/metrics/http.server.requests?tag=uri:/orders"

4.1. 注文数のCounter

システムメトリクスだけでは不十分です。ビジネスメトリクスも必要です。以下の例は、販売チャネルごとに分類された、作成された注文の数を数えます:

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

@Service
public class OrderService {

    private final MeterRegistry registry;

    public OrderService(MeterRegistry registry) {
        this.registry = registry;
    }

    public Order createOrder(Customer customer, String channel) {
        Order order = new Order(customer.id(), channel);
        // ... 注文を保存
        Counter.builder("orders.created")
                .description("作成された注文の数")
                .tag("channel", channel)
                .register(registry)
                .increment();
        return order;
    }
}
public record Customer(long id, String name) {}
public record Order(long customerId, String channel) {}

MeterRegistry はSpring Bootによって自動設定され、インジェクションされます。同じ名前とタグで register(registry) を複数回呼び出すことは安全です。Micrometerが既存のカウンターを返すため、webチャネルを通じて3つの注文を作成した後:

curl http://localhost:8080/actuator/metrics/orders.created
{
  "name": "orders.created",
  "description": "作成された注文の数",
  "measurements": [ { "statistic": "COUNT", "value": 3.0 } ],
  "availableTags": [ { "tag": "channel", "values": ["web"] } ]
}

4.2. 決済処理時間のTimer

Timer は呼び出し回数をカウントし、合計時間と最大時間を測定します:

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

@Service
public class PaymentService {

    private final Timer paymentTimer;

    public PaymentService(MeterRegistry registry) {
        this.paymentTimer = Timer.builder("payment.process")
                .description("決済処理の時間")
                .register(registry);
    }

    public boolean pay(Order order) {
        return paymentTimer.record(() -> {
            // ... 決済ゲートウェイを呼び出す
            return true;
        });
    }
}

Timerはコンストラクタで一度作成され、その後再利用されます。呼び出しごとに変わるタグがないため。record(Supplier) はlambdaの実行時間を測定し、その結果を返します。エンドポイント /actuator/metrics/payment.process は3つの統計を表示します:COUNT、TOTAL_TIME、MAX。

4.3. すべてのメトリクスの共通タグ

複数のサービスがメトリクスを1つの場所にプッシュする場合、アプリケーション名をすべてのメトリクスに添付すべきです:

management.metrics.tags.application=order-service

注意: 大量または無制限の値を持つ値をタグとして使用しないでください。例えば、orderId、customerId、またはメールアドレス。各タグの組み合わせは独立したタイムシリーズを作成します。何百万もの注文は何百万ものシリーズを作成し、アプリケーションのメモリと監視システムを膨らませます。タグにはchannel、status、regionなどの小さく固定された値のセットのみを含めるべきです。

5. Prometheusへのエクスポート

/actuator/metrics エンドポイントは素早く確認するために適しています。長期保存とグラフを描画するために、Prometheusのようなシステムが必要です。対応するregistryを追加します:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

その後、prometheusエンドポイントを公開します:

management.endpoints.web.exposure.include=health,info,metrics,prometheus
curl http://localhost:8080/actuator/prometheus | grep -E "orders|payment"
# HELP orders_created_total 作成された注文の数
# TYPE orders_created_total counter
orders_created_total{application="order-service",channel="web"} 3.0
payment_process_seconds_count{application="order-service"} 5.0
payment_process_seconds_sum{application="order-service"} 0.412
payment_process_seconds_max{application="order-service"} 0.153

Micrometerはメトリクス名を自動的に変更してPrometheusの規約に従います:ドットをアンダースコアに変更し、counterには_totalサフィックスが付き、timerは秒で計算されます。残りはPrometheusの設定です。パスを /actuator/prometheus をスクレイプします。

6. テスト

6.1. HealthIndicatorをテストする

HealthIndicator は通常のクラスなので、Spring contextを起動せずにテストできます:

import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;

import static org.assertj.core.api.Assertions.assertThat;

class PaymentGatewayHealthIndicatorTest {

    @Test
    void whenPingSucceeds_thenStatusIsUp() {
        var indicator = new PaymentGatewayHealthIndicator(() -> true);

        Health health = indicator.health();

        assertThat(health.getStatus()).isEqualTo(Status.UP);
        assertThat(health.getDetails()).containsEntry("provider", "VNPay");
    }

    @Test
    void whenPingThrows_thenStatusIsDownWithError() {
        var indicator = new PaymentGatewayHealthIndicator(() -> {
            throw new IllegalStateException("timeout");
        });

        Health health = indicator.health();

        assertThat(health.getStatus()).isEqualTo(Status.DOWN);
        assertThat(health.getDetails().get("error").toString()).contains("timeout");
    }
}

PaymentGatewayClient は1つのメソッドのみを持っているため、mockの代わりにlambdaを渡します。

6.2. メトリクスをテストする

Micrometerは SimpleMeterRegistry を提供します。これはメトリクスをメモリに保存し、ユニットテストに非常に便利です:

import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class OrderServiceTest {

    @Test
    void whenOrdersCreated_thenCounterIncrementsPerChannel() {
        var registry = new SimpleMeterRegistry();
        var service = new OrderService(registry);
        var customer = new Customer(1L, "An");

        service.createOrder(customer, "web");
        service.createOrder(customer, "web");
        service.createOrder(customer, "mobile");

        assertThat(registry.get("orders.created").tag("channel", "web").counter().count())
                .isEqualTo(2.0);
        assertThat(registry.get("orders.created").tag("channel", "mobile").counter().count())
                .isEqualTo(1.0);
    }
}

メトリクスが存在しない場合、registry.get(...) は MeterNotFoundException をスロー します。これにより、テストはメトリクス名の入力ミスもキャッチできます。

7. セキュリティと一般的なエラー

管理ポートを分離します。 Actuatorを別のポートで実行できます。内部ネットワークのみで開きます:

management.server.port=8081

この場合、アプリケーションは8080で提供され、/actuator/** は8081のみにあります。firewallまたはingressでこのポートをブロックで十分です。Kubernetesでは、プローブの設定でポートを8081に変更することを忘れないでください。

ヘルス詳細を制限します。 本番環境では show-details=when-authorized をSpring Securityと組み合わせて使用します。always の代わりに。この場合、認証されていないユーザーは {"status":"UP"} のみを見ます。

一般的なエラー:

  • /actuator/metrics を呼び出して404を受け取った:metrics を exposure.include に追加していません。
  • ヘルスチェックがランダムに失敗します:indicatorがタイムアウトなしでネットワークを呼び出すか、probeのタイムアウトよりも応答時間が長い。
  • データベースが不安定な場合、podが継続的に再起動されます:データベースがlivenessグループにあります。
  • メモリが時間とともに段階的に増加します:メトリクスが無制限の数の値を持つタグを使用しています。
  • /actuator/prometheus エンドポイントが見つかりません:micrometer-registry-prometheus がない、またはエンドポイントがまだ公開されていません。

8. 結論

Spring Boot Actuatorはヘルスチェックとメトリクスをほぼ無料で提供します。あなたの仕事は正しく設定することです:必要なエンドポイントのみを公開し、重要なdependencyのためにタイムアウトを持つ HealthIndicator を書き、Kubernetesが適切に対応するためにlivenessをreadinessから分離します。

メトリクスについては、Micrometerがあなたを助けます。わずか数行でビジネス用の Counter と Timer を書き、その後Prometheusへ1つのdependencyでエクスポートします。タグの値の数を少なくし、SimpleMeterRegistryを使用してテストを書き、リファクタリングのたびにメトリクスが静かに壊れないようにしましょう。

spring-bootactuatormicrometer監視prometheus