Spring Boot Micrometer i Prometheus: metryki Actuator na produkcji

Integracja Spring Boot Micrometer z Prometheus w Spring Boot 4: model dostępu Actuator, konfiguracja scrapowania, metryki HTTP, sondy zdrowia i reguły alertów.

Monitoring Spring Boot Actuator z Micrometer i Prometheus

Spring Boot Actuator udostępnia endpointy zdrowia, metryk i diagnostyki, a Micrometer jest fasadą metryk, na której się opierają. Dodanie zależności micrometer-registry-prometheus daje endpoint /actuator/prometheus, który Prometheus odpytuje w stałym interwale. Przykłady poniżej dotyczą Spring Boot 4.1 (aktualne wydanie GA, wersja 4.2 jest na etapie release candidate), Micrometer 1.16 i klienta Prometheus Java 1.x.

Krótka odpowiedź

Integracja Spring Boot Micrometer z Prometheus to trzy kroki: dodanie spring-boot-starter-actuator i micrometer-registry-prometheus, dopisanie prometheus do management.endpoints.web.exposure.include, a następnie skierowanie scrape_config Prometheus na /actuator/prometheus. Metryki JVM, HTTP, HikariCP i systemowe są publikowane bez dodatkowego kodu.

Konfiguracja Spring Boot Micrometer: Actuator i registry Prometheus

Wymagane Zależności Maven

Starter Actuator aktywuje endpointy i dociąga Micrometer. Registry Prometheus formatuje każdy miernik w formacie ekspozycji Prometheus. Starter AspectJ jest potrzebny wyłącznie dla adnotacji @Timed i @Counted.

xml
<!-- pom.xml -->
<!-- Actuator + Micrometer + Prometheus (Spring Boot 4.x) -->
<dependencies>
    <!-- Spring Boot Actuator: monitoring endpoints, includes Micrometer -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!-- Micrometer Registry Prometheus (Prometheus Java client 1.x) -->
    <!-- Exposes metrics in Prometheus format at /actuator/prometheus -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>

    <!-- AspectJ for @Timed and @Counted -->
    <!-- Spring Boot 4 renamed spring-boot-starter-aop to spring-boot-starter-aspectj -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aspectj</artifactId>
    </dependency>
</dependencies>

Starsze tutoriale zawierają dwie pułapki związane z wersjami. Od Micrometer 1.13 (Spring Boot 3.3) micrometer-registry-prometheus bazuje na kliencie Prometheus Java 1.x; stary kod 0.x trafił do przestarzałego modułu micrometer-registry-prometheus-simpleclient, który dokumentacja Micrometer dla Prometheus przedstawia wyłącznie jako rozwiązanie przejściowe. Z kolei w Spring Boot 4 plik pom.xml, który nadal deklaruje spring-boot-starter-aop, nie daje się już rozwiązać: przewodnik migracji do Spring Boot 4.0 zmienia jego nazwę na spring-boot-starter-aspectj.

Konfiguracja Endpointów Actuator

Domyślnie po HTTP wystawiony jest wyłącznie endpoint health. Endpoint /actuator/prometheus istnieje od chwili, gdy registry znajdzie się na classpath, ale zwraca 404, dopóki nie zostanie dodany do listy ekspozycji.

Spring Boot 3.4 zastąpił przełączniki włącz/wyłącz (management.endpoints.enabled-by-default, management.endpoint.<id>.enabled) modelem dostępu, a stare właściwości są oznaczone jako przestarzałe. Wzorzec opt-in ustawia domyślny dostęp na none i przyznaje read-only dla każdego endpointu osobno:

yaml
# application.yml
# Actuator configuration for production (Spring Boot 3.4+ access model)
management:
  endpoints:
    # Every endpoint is inaccessible unless granted below
    access:
      default: none
    web:
      exposure:
        # Endpoints exposed over HTTP
        # health, info, prometheus are the minimum for monitoring
        include: health,info,prometheus,metrics,env,loggers
      base-path: /actuator
  endpoint:
    health:
      access: read-only
      show-details: when-authorized
      show-components: when-authorized
    info:
      access: read-only
    prometheus:
      access: read-only
    metrics:
      access: read-only
    env:
      access: read-only
      # Mask sensitive values for anonymous callers
      show-values: when-authorized
    loggers:
      # unrestricted allows POST to change a log level at runtime
      access: unrestricted

Ekspozycja i dostęp to dwa odrębne mechanizmy. exposure.include decyduje, czy endpoint jest mapowany po HTTP; access decyduje, na co endpoint pozwala po zmapowaniu. Plik właściwości, który nadal używa enabled: true, działa dalej z ostrzeżeniem o przestarzałości i to jest właściwy moment na migrację.

Opcja show-details: when-authorized wyświetla szczegóły zdrowia tylko uwierzytelnionym użytkownikom o odpowiedniej roli. Same endpointy zabezpiecza dedykowany SecurityFilterChain:

ActuatorSecurityConfig.javajava
// Securing Actuator endpoints (Spring Boot 4, Spring Security 7)
package com.example.monitoring.config;

import static org.springframework.security.web.access.IpAddressAuthorizationManager.hasIpAddress;

import org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration(proxyBeanMethods = false)
public class ActuatorSecurityConfig {

    @Bean
    SecurityFilterChain actuatorSecurityFilterChain(HttpSecurity http) throws Exception {
        return http
            .securityMatcher(EndpointRequest.toAnyEndpoint())
            .authorizeHttpRequests(auth -> auth
                // Health and info public for load balancers
                .requestMatchers(EndpointRequest.to("health", "info")).permitAll()
                // Prometheus accessible from the internal network only
                .requestMatchers(EndpointRequest.to("prometheus")).access(hasIpAddress("10.0.0.0/8"))
                // Other endpoints restricted to admins
                .anyRequest().hasRole("ACTUATOR_ADMIN")
            )
            .httpBasic(basic -> {})
            .build();
    }
}

W Spring Boot 4 klasa EndpointRequest została przeniesiona do org.springframework.boot.security.autoconfigure.actuate.web.servlet, co pokazuje dokumentacja endpointów Actuator. Javowe DSL AuthorizedUrl w authorizeHttpRequests nie ma metody hasIpAddress: reguła IP przechodzi przez IpAddressAuthorizationManager.hasIpAddress, dostępne od Spring Security 6.3.

Własne Metryki z Micrometer

Counter, Gauge i Timer Aplikacyjne

Micrometer oferuje kilka typów metryk dopasowanych do różnych przypadków użycia. Counter mierzy zdarzenia kumulatywne, gauge wartości chwilowe, a timer czas trwania operacji.

OrderMetricsService.javajava
// Custom business metrics service
package com.example.monitoring.metrics;

import io.micrometer.core.instrument.Counter;
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.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

@Service
public class OrderMetricsService {

    // Counter for orders created
    private final Counter ordersCreatedCounter;
    // Timer to measure processing duration
    private final Timer orderProcessingTimer;
    // Atomic value for pending orders gauge
    private final AtomicInteger pendingOrdersCount = new AtomicInteger(0);

    public OrderMetricsService(MeterRegistry registry) {
        // Exported as orders_created_total: the _total suffix is added by the registry
        this.ordersCreatedCounter = Counter.builder("orders.created")
            .description("Total number of orders created")
            .tag("channel", "web")
            .register(registry);

        // Timer with histogram buckets: percentiles computed server-side in PromQL
        this.orderProcessingTimer = Timer.builder("orders.processing.duration")
            .description("Order processing duration")
            .publishPercentileHistogram()
            .register(registry);

        // Gauge linked to atomic value
        // Read on each scrape
        Gauge.builder("orders.pending.count", pendingOrdersCount, AtomicInteger::get)
            .description("Number of orders pending processing")
            .register(registry);
    }

    public void recordOrderCreated() {
        ordersCreatedCounter.increment();
        pendingOrdersCount.incrementAndGet();
    }

    public void recordOrderProcessed(Runnable processingLogic) {
        // Measures execution duration
        orderProcessingTimer.record(processingLogic);
        pendingOrdersCount.decrementAndGet();
    }

    public <T> T recordOrderProcessedWithResult(Supplier<T> processingLogic) {
        return orderProcessingTimer.record(processingLogic);
    }
}

Przy registry Prometheus liczą się dwa szczegóły nazewnictwa. Nazwy mierników Micrometer używają kropek i są konwertowane do snake case (orders.processing.duration staje się orders_processing_duration_seconds). Countery automatycznie otrzymują sufiks _total, więc nazwanie countera orders.created.total jedynie dodaje szum.

publishPercentiles(0.5, 0.95, 0.99) oblicza percentyle wewnątrz każdej JVM i tych wartości nie da się agregować między instancjami. publishPercentileHistogram() publikuje buckety, które histogram_quantile() agreguje dla całej floty, i to właśnie tę opcję należy wybrać przy Prometheus.

Adnotacje @Timed i @Counted

Aby uniknąć kodu boilerplate, Micrometer dostarcza adnotacje, które instrumentują metody przez AspectJ.

PaymentService.javajava
// Automatic instrumentation with annotations
package com.example.monitoring.service;

import io.micrometer.core.annotation.Counted;
import io.micrometer.core.annotation.Timed;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    private final PaymentValidator paymentValidator;

    public PaymentService(PaymentValidator paymentValidator) {
        this.paymentValidator = paymentValidator;
    }

    // @Timed creates a Timer
    // Measures each call and publishes count, sum, max and histogram buckets
    @Timed(
        value = "payment.process.duration",
        description = "Payment processing duration",
        histogram = true
    )
    public PaymentResult processPayment(PaymentRequest request) {
        // Call through another bean: the proxy intercepts it
        paymentValidator.validate(request);
        return executePayment(request);
    }

    // @Counted increments a counter on each call
    // Useful for discrete events
    @Counted(
        value = "payment.refunds",
        description = "Total number of refunds"
    )
    public void refundPayment(String transactionId) {
        // Refund logic
    }
}
yaml
# application.yml
# Enables TimedAspect, CountedAspect and the other Micrometer annotation aspects
management:
  observations:
    annotations:
      enabled: true

Od Spring Boot 3.2 ręczne deklarowanie beanów TimedAspect i CountedAspect nie jest już konieczne: ustawienie management.observations.annotations.enabled=true przy spring-boot-starter-aspectj na classpath konfiguruje je automatycznie. Właściwość ma domyślnie wartość false, co tłumaczy większość zgłoszeń typu „moje @Timed nic nie robi”.

Ograniczenie AOP

Adnotacje @Timed i @Counted działają wyłącznie na publicznych metodach beanów Springa wywoływanych przez proxy. Wywołanie jednej metody z innej w tej samej klasie omija proxy i nie jest instrumentowane, a adnotacja na metodzie prywatnej jest ignorowana. Adnotowanie kontrolera Spring MVC, który jest już instrumentowany, daje zduplikowane obserwacje.

Gotowy na rozmowy o Spring Boot?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Własne Endpointy Zdrowia

Health Indicators Biznesowe

Health Indicators sprawdzają stan zewnętrznych zależności i krytycznych komponentów biznesowych. Spring Boot udostępnia domyślne wskaźniki dla baz danych, Redisa i innych typowych usług. W Spring Boot 4 API zdrowia znajduje się we własnym module spring-boot-health, a Health i HealthIndicator zostały przeniesione z org.springframework.boot.actuate.health do org.springframework.boot.health.contributor.

PaymentGatewayHealthIndicator.javajava
// Health indicator for payment gateway (Spring Boot 4 packages)
package com.example.monitoring.health;

import org.springframework.boot.health.contributor.Health;
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

import java.time.Duration;
import java.time.Instant;

@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final RestClient restClient;
    private final String gatewayHealthUrl;

    public PaymentGatewayHealthIndicator(RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder.build();
        this.gatewayHealthUrl = "https://api.payment-gateway.com/health";
    }

    @Override
    public Health health() {
        Instant start = Instant.now();

        try {
            // Call gateway health endpoint
            var response = restClient.get()
                .uri(gatewayHealthUrl)
                .retrieve()
                .toBodilessEntity();

            Duration responseTime = Duration.between(start, Instant.now());

            if (response.getStatusCode().is2xxSuccessful()) {
                return Health.up()
                    .withDetail("responseTime", responseTime.toMillis() + "ms")
                    .withDetail("statusCode", response.getStatusCode().value())
                    .build();
            } else {
                return Health.down()
                    .withDetail("statusCode", response.getStatusCode().value())
                    .withDetail("reason", "Unexpected status code")
                    .build();
            }
        } catch (Exception e) {
            Duration responseTime = Duration.between(start, Instant.now());

            return Health.down()
                .withDetail("error", e.getClass().getSimpleName())
                .withDetail("message", e.getMessage())
                .withDetail("responseTime", responseTime.toMillis() + "ms")
                .build();
        }
    }
}

Wskaźnik ten pojawia się w /actuator/health pod nazwą paymentGateway: to nazwa beana bez sufiksu HealthIndicator.

Grupy Zdrowia dla Kubernetes

Spring Boot 4 domyślnie włącza sondy liveness i readiness na każdej platformie, a nie tylko po wykryciu Kubernetes. Grupy /actuator/health/liveness i /actuator/health/readiness istnieją bez żadnej konfiguracji, a ręczne deklarowanie beanów LivenessStateHealthIndicator czy ReadinessStateHealthIndicator jest zbędne. Konfiguracja jest potrzebna tylko do dodania komponentów do grupy lub zdefiniowania własnych grup:

yaml
# application.yml
# Health groups configuration for Kubernetes
management:
  endpoint:
    health:
      probes:
        # true by default in Spring Boot 4
        enabled: true
        # Also serve the groups at /livez and /readyz on the main server port
        add-additional-paths: true
      group:
        # Readiness: can the application receive traffic?
        readiness:
          include: readinessState,db
        # Custom group for critical dependencies
        critical:
          include: db,paymentGateway
          show-details: when-authorized

Grupa liveness powinna ograniczać się do livenessState. Dodanie bazy danych lub zewnętrznej bramki do liveness sprawia, że Kubernetes restartuje każdy pod, gdy ta zależność przestaje działać, co zamienia częściową awarię w całkowitą. Zależności zewnętrzne należą do readiness, i to tylko wtedy, gdy aplikacja bez nich nie obsłuży żadnego żądania.

Sondy Kubernetes wskazują następnie na dedykowane endpointy:

yaml
# kubernetes-deployment.yml
# Kubernetes probes configuration
spec:
  containers:
    - name: order-service
      livenessProbe:
        httpGet:
          path: /livez
          port: 8080
        initialDelaySeconds: 30
        periodSeconds: 10
        failureThreshold: 3
      readinessProbe:
        httpGet:
          path: /readyz
          port: 8080
        initialDelaySeconds: 10
        periodSeconds: 5
        failureThreshold: 3

Dzięki add-additional-paths: true sondy pozostają na porcie aplikacji nawet wtedy, gdy Actuator działa na osobnym porcie zarządzania, więc niedziałający serwer główny zostaje wykryty przez sondę.

Integracja Spring Boot z Prometheus i Grafana

Konfiguracja Scrapowania w Prometheus

Prometheus zbiera metryki, cyklicznie odpytując endpoint /actuator/prometheus. Konfiguracja scrapowania definiuje cele i ścieżkę metryk, ponieważ Prometheus domyślnie używa /metrics.

yaml
# prometheus.yml
# Prometheus configuration for Spring Boot
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'spring-boot-apps'
    metrics_path: '/actuator/prometheus'
    scrape_interval: 10s
    static_configs:
      - targets:
          - 'order-service:9090'
          - 'payment-service:9090'
          - 'inventory-service:9090'
    # Relabeling to strip the port from the instance label
    relabel_configs:
      - source_labels: [__address__]
        target_label: instance
        regex: '([^:]+):\d+'
        replacement: '${1}'

  # Kubernetes service discovery
  - job_name: 'kubernetes-pods'
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      # Only scrape pods with annotation
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action: keep
        regex: true
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
        action: replace
        target_label: __metrics_path__
        regex: (.+)

Cele używają portu 9090, ponieważ konfiguracja produkcyjna opisana dalej przenosi Actuator na osobny port zarządzania. Bez management.server.port celem scrapowania jest port aplikacji.

Metryki Spring Boot Actuator Publikowane Domyślnie

Actuator z Micrometer udostępnia metryki JVM, procesu, HikariCP i HTTP bez żadnego kodu. Poniższe zapytania PromQL obejmują te, które na produkcji obserwuje się w pierwszej kolejności:

promql
# PromQL queries for JVM monitoring

# Heap memory usage, summed across heap pools
sum by (instance) (jvm_memory_used_bytes{area="heap"})

# Heap usage percentage
sum by (instance) (jvm_memory_used_bytes{area="heap"})
  / sum by (instance) (jvm_memory_max_bytes{area="heap"}) * 100

# Live threads
jvm_threads_live_threads

# Garbage collection: seconds spent paused per second
rate(jvm_gc_pause_seconds_sum[5m])

# GC pauses per minute
rate(jvm_gc_pause_seconds_count[1m]) * 60

# CPU used by the JVM process
process_cpu_usage

# Active database connections
hikaricp_connections_active

# Connection pool utilization
hikaricp_connections_active / hikaricp_connections_max * 100

jvm_memory_used_bytes{area="heap"} zwraca jedną serię na pulę pamięci (Eden, Survivor, Old Gen przy G1). Dzielenie jej przez maksimum bez sum by (instance) daje proporcje per pula, które na dashboardzie wyglądają błędnie.

Bean MeterBinder dodaje gauge na poziomie aplikacji, których nie obejmują wartości domyślne:

CustomJvmMetrics.javajava
// Additional JVM metrics
package com.example.monitoring.metrics;

import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.springframework.stereotype.Component;

import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;

@Component
public class CustomJvmMetrics implements MeterBinder {

    @Override
    public void bindTo(MeterRegistry registry) {
        OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();

        // System load average
        Gauge.builder("system.load.average", osBean, OperatingSystemMXBean::getSystemLoadAverage)
            .description("System load average over 1 minute")
            .register(registry);

        // Available processors count
        Gauge.builder("system.cpu.count", osBean, OperatingSystemMXBean::getAvailableProcessors)
            .description("Number of available processors")
            .register(registry);

        // Application uptime
        Gauge.builder("application.uptime.seconds",
                ManagementFactory.getRuntimeMXBean(),
                bean -> bean.getUptime() / 1000.0)
            .description("Application uptime in seconds")
            .register(registry);
    }
}

system.cpu.count i process.uptime są już publikowane przez wbudowane bindery Micrometer. Przykład pokazuje wzorzec, a rzeczywisty binder powinien rejestrować tylko to, czego brakuje w wartościach domyślnych.

Gotowe Dashboardy Grafana

Grafana oferuje dashboardy społeczności dla Spring Boot, które działają na standardowych metrykach Actuator. Dashboard pisany ręcznie używa typów paneli timeseries i stat:

json
{
  "annotations": {
    "list": []
  },
  "panels": [
    {
      "title": "Request Rate",
      "type": "timeseries",
      "targets": [
        {
          "expr": "sum by (method, uri, status) (rate(http_server_requests_seconds_count{application=\"$application\"}[5m]))",
          "legendFormat": "{{method}} {{uri}} - {{status}}"
        }
      ]
    },
    {
      "title": "Response Time P99",
      "type": "timeseries",
      "targets": [
        {
          "expr": "histogram_quantile(0.99, sum by (le, method, uri) (rate(http_server_requests_seconds_bucket{application=\"$application\"}[5m])))",
          "legendFormat": "{{method}} {{uri}}"
        }
      ]
    },
    {
      "title": "Error Rate",
      "type": "stat",
      "targets": [
        {
          "expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\",status=~\"5..\"}[5m])) / sum(rate(http_server_requests_seconds_count{application=\"$application\"}[5m])) * 100"
        }
      ]
    }
  ]
}

Wiele dashboardów skopiowanych ze starszych tutoriali nadal używa typów paneli graph i singlestat. Singlestat usunięto w Grafana 8, a Grafana 12 całkowicie usunęła obsługę AngularJS, więc takie panele renderują się dopiero po migracji do timeseries i stat.

Import w Grafana

Aby zaimportować dashboard społeczności: Grafana > Dashboards > New > Import, a następnie ID 4701 (JVM Micrometer) lub 12900 (SpringBoot APM). Po imporcie warto sprawdzić typy paneli: starsze rewizje dashboardów społeczności mogą nadal zawierać panele Angular, które Grafana 12 musi zmigrować.

Alerty z Prometheus

Kluczowe Reguły Alertów

Reguły alertów Prometheus wyzwalają powiadomienia, gdy metryki przekraczają progi krytyczne.

yaml
# alerting-rules.yml
# Alert rules for Spring Boot applications
groups:
  - name: spring-boot-alerts
    rules:
      # Alert if application is down
      - alert: ApplicationDown
        expr: up{job="spring-boot-apps"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Application {{ $labels.instance }} is down"
          description: "{{ $labels.instance }} has been down for more than 1 minute"

      # Alert on HTTP error rate
      - alert: HighErrorRate
        expr: |
          sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m])) by (application)
          /
          sum(rate(http_server_requests_seconds_count[5m])) by (application)
          > 0.05
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High error rate on {{ $labels.application }}"
          description: "Error rate is {{ $value | humanizePercentage }}"

      # Alert on P99 latency, aggregated per application
      - alert: HighLatency
        expr: |
          histogram_quantile(0.99,
            sum by (le, application) (rate(http_server_requests_seconds_bucket[5m]))
          ) > 2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High latency on {{ $labels.application }}"
          description: "P99 latency is {{ $value | humanizeDuration }}"

      # Heap memory alert
      - alert: HighHeapUsage
        expr: |
          sum by (instance) (jvm_memory_used_bytes{area="heap"})
          / sum by (instance) (jvm_memory_max_bytes{area="heap"})
          > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High heap memory usage on {{ $labels.instance }}"
          description: "Heap usage is at {{ $value | humanizePercentage }}"

      # Database connection pool exhausted alert
      - alert: DatabaseConnectionPoolExhausted
        expr: |
          hikaricp_connections_active
          / hikaricp_connections_max
          > 0.9
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Database connection pool nearly exhausted"
          description: "{{ $value | humanizePercentage }} of connections in use"

      # Excessive GC alert
      - alert: HighGCPause
        expr: |
          rate(jvm_gc_pause_seconds_sum[5m])
          / rate(jvm_gc_pause_seconds_count[5m])
          > 0.5
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High GC pause time"
          description: "Average GC pause is {{ $value | humanizeDuration }}"

Alerty te pokrywają najczęstsze problemy produkcyjne: dostępność, wydajność i zasoby. Reguła HighLatency wymaga sum by (le, ...): bez tego histogram_quantile oblicza osobny P99 dla każdej serii (per URI, metoda i status), a jeden wolny endpoint z trzema wywołaniami wyzwala alert.

Metryki HTTP i Bazy Danych w Spring Boot

Automatyczna Instrumentacja Żądań HTTP

Spring Boot instrumentuje każde przychodzące żądanie Spring MVC i WebFlux przez Observation API i publikuje timer http.server.requests z tagami method, uri, status, outcome i exception.

yaml
# application.yml
# HTTP metrics configuration
management:
  metrics:
    distribution:
      # Histogram buckets for server-side percentiles
      percentiles-histogram:
        http.server.requests: true
      # SLO buckets: count requests faster than each boundary
      slo:
        http.server.requests: 100ms, 500ms, 1s, 2s
    tags:
      # Common tags added to every meter
      application: ${spring.application.name}
      environment: ${spring.profiles.active:default}

Interfejsy WebMvcTagsContributor i WebMvcTagsProvider, obecne w wielu przykładach, zostały usunięte w Spring Boot 3.0. Własne tagi dodaje się teraz przez rozszerzenie DefaultServerRequestObservationConvention, zgodnie z dokumentacją metryk Spring Boot:

TenantObservationConvention.javajava
// Adds custom tags to http.server.requests (Spring Boot 3.0+)
package com.example.monitoring.config;

import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.http.server.observation.DefaultServerRequestObservationConvention;
import org.springframework.http.server.observation.ServerRequestObservationContext;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;

@Component
public class TenantObservationConvention extends DefaultServerRequestObservationConvention {

    @Override
    public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
        HttpServletRequest request = context.getCarrier();
        String userId = request.getHeader("X-User-Id");
        String tenantId = request.getHeader("X-Tenant-Id");

        // Keep the default tags and add two bounded ones
        return super.getLowCardinalityKeyValues(context).and(
            KeyValue.of("user.type", userId != null ? "authenticated" : "anonymous"),
            KeyValue.of("tenant", tenantId != null ? tenantId : "default")
        );
    }
}

Etykietami Prometheus stają się wyłącznie pary klucz-wartość o niskiej kardynalności. Tag tenant jest akceptowalny przy kilkudziesięciu tenantach, a przy tysiącach staje się problemem kardynalności. Wartości o wysokiej kardynalności (ID użytkownika, ID zamówienia) należą do getHighCardinalityKeyValues, które zasila wyłącznie trace'y. Stronę tracingu Observation API opisuje artykuł observability Spring Boot z OpenTelemetry.

Metryki HikariCP i Zapytań SQL

Metryki puli HikariCP są udostępniane automatycznie z prefiksem hikaricp i tagiem z nazwą puli, obok ogólnych gauge jdbc.connections. Ustawienie nazwy puli sprawia, że tag jest czytelny:

yaml
# application.yml
# HikariCP pool configuration
spring:
  datasource:
    hikari:
      # Becomes the "pool" tag on hikaricp_* metrics
      pool-name: OrderServicePool
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

Spring Boot nie mierzy czasu na poziomie pojedynczych zapytań. Projekt datasource-micrometer opakowuje DataSource i tworzy obserwację dla każdego połączenia, zapytania i result setu, co daje jednocześnie metryki i spany trace'ów. Linia 2.x jest przeznaczona dla Spring Boot 4, linia 1.x dla Spring Boot 3:

xml
<!-- pom.xml -->
<!-- JDBC observations: 2.x for Spring Boot 4, 1.x for Spring Boot 3 -->
<dependency>
    <groupId>net.ttddyy.observation</groupId>
    <artifactId>datasource-micrometer-spring-boot</artifactId>
    <version>2.0.1</version>
</dependency>

Zastępuje to ręczne podejście, w którym DataSource opakowywano w ProxyDataSourceBuilder z własnym listenerem: omijało ono autokonfigurację DataSource w Spring Boot i wymagało synchronizacji przy każdej aktualizacji Boota. Wolne zapytania zwykle wskazują na wzorzec N+1, opisany w artykule rozwiązania problemu N+1 w Spring Data JPA.

Najlepsze Praktyki Produkcyjne

Kardynalność Metryk

Nadmierna kardynalność degraduje wydajność Prometheus. Każda unikalna kombinacja tagów tworzy odrębną serię czasową.

AntiPatternHighCardinality.javajava
// AVOID: unbounded cardinality
package com.example.monitoring.antipattern;

@Service
public class AntiPatternHighCardinality {

    private final MeterRegistry registry;

    // BAD: userId creates one series per user
    public void trackUserAction(String userId, String action) {
        Counter.builder("user.actions")
            .tag("userId", userId)  // Millions of possible values
            .tag("action", action)
            .register(registry)
            .increment();
    }
}
GoodPracticeCardinality.javajava
// Controlled cardinality
package com.example.monitoring.bestpractice;

@Service
public class GoodPracticeCardinality {

    private final MeterRegistry registry;

    // GOOD: user category instead of ID
    public void trackUserAction(User user, String action) {
        Counter.builder("user.actions")
            .tag("userType", user.getSubscriptionType())  // FREE, PREMIUM, ENTERPRISE
            .tag("action", action)
            .register(registry)
            .increment();
    }

    // GOOD: let a timer histogram bucket durations
    public void trackResponseTime(long responseTimeMs) {
        Timer.builder("response.time")
            .publishPercentileHistogram()
            .register(registry)
            .record(Duration.ofMillis(responseTimeMs));
    }
}

Timer z bucketami histogramu odpowiada na pytania o latencję lepiej niż counter z ręcznie budowanym tagiem bucket: PromQL oblicza z bucketów dowolny percentyl, a granice bucketów pozostają spójne między usługami.

Konfiguracja Gotowa na Produkcję

yaml
# application-production.yml
# Production configuration (Spring Boot 4)
management:
  server:
    # Separate port for management endpoints, not exposed publicly
    port: 9090
  endpoints:
    access:
      default: none
    web:
      exposure:
        include: health,info,prometheus
  endpoint:
    health:
      access: read-only
      show-details: when-authorized
      probes:
        add-additional-paths: true
    info:
      access: read-only
    prometheus:
      access: read-only
  observations:
    annotations:
      enabled: true
  metrics:
    distribution:
      percentiles-histogram:
        http.server.requests: true
      minimum-expected-value:
        http.server.requests: 1ms
      maximum-expected-value:
        http.server.requests: 30s
    tags:
      application: ${spring.application.name}
      environment: production
      version: ${app.version:unknown}

Przy access.default: none endpointy takie jak env, beans, configprops i mappings są niedostępne bez wymieniania ich po kolei. management.prometheus.metrics.export.enabled ma domyślnie wartość true, a ustawienie step nie ma wpływu na Prometheus: to registry typu pull, a interwał scrapowania ustawia się w prometheus.yml.

Częste Błędy z Actuator i Prometheus

ObjawPrzyczynaRozwiązanie
/actuator/prometheus zwraca 404Endpoint nie jest na liście ekspozycji lub brakuje registryDodać prometheus do management.endpoints.web.exposure.include i sprawdzić, czy micrometer-registry-prometheus jest na classpath
Cel w Prometheus pokazuje „server returned HTTP status 401”Spring Security chroni wszystkie endpointy poza healthOtworzyć prometheus dla sieci scrapera lub skonfigurować basic auth w konfiguracji scrapowania
@Timed nie generuje metrykimanagement.observations.annotations.enabled ma domyślnie wartość false lub brakuje AspectJUstawić właściwość na true i dodać spring-boot-starter-aspectj
spring-boot-starter-aop nie zostaje znaleziony po aktualizacjiZmiana nazwy w Spring Boot 4Użyć spring-boot-starter-aspectj
Import Health lub HealthIndicator nie działa w Boot 4Pakiet przeniesiony do org.springframework.boot.health.contributorZaktualizować importy
Ostrzeżenia o przestarzałości management.endpoint.*.enabledModel włącz/wyłącz zastąpiony w Spring Boot 3.4Użyć management.endpoints.access.default i management.endpoint.<id>.access
Percentyle na dashboardzie mocno odbiegają od rzeczywistościWartości publishPercentiles uśredniane między podamiUżyć publishPercentileHistogram() i histogram_quantile()

Pytania Rekrutacyjne o Spring Boot Micrometer

Actuator pojawia się w większości rozmów rekrutacyjnych ze Spring Boot na stanowiska mid i senior. Odpowiedzi, które odróżniają doświadczony profil od juniora:

  • Różnica między liveness a readiness: liveness mówi Kubernetes, że należy zrestartować pod, readiness, że należy przestać kierować do niego ruch. Kandydat, który umieszcza bazę danych w grupie liveness, nigdy nie przeszedł z tym przez awarię.
  • Dlaczego Micrometer, a nie bezpośrednio klient Prometheus: Micrometer jest fasadą. Ten sam kod Counter i Timer eksportuje do Prometheus, Datadog lub OTLP po zmianie jednej zależności, a instrumentacja samego Springa (HTTP, pula JDBC, cache) jest napisana właśnie pod nią.
  • Percentyle po stronie klienta a histogramy: percentyli liczonych po stronie klienta nie da się agregować między instancjami, histogramy tak. To najczęstsze podchwytliwe pytanie w tym temacie.
  • Czym jest kardynalność i jak psuje Prometheus: jedna seria czasowa na każdą unikalną kombinację etykiet; tag z ID użytkownika mnoży zużycie pamięci przez liczbę użytkowników.
  • Jak zabezpieczyć Actuator: ograniczyć ekspozycję, ustawić poziomy dostępu, uruchomić zarządzanie na osobnym porcie i chronić je dedykowanym SecurityFilterChain.

Więcej pytań tego typu zawiera moduł pytania rekrutacyjne o Spring Boot Actuator.

Źródła

Co Zapamiętać o Spring Boot Micrometer i Prometheus

  • Trzy kroki do działającego scrapowania: starter Actuator, micrometer-registry-prometheus, prometheus na liście ekspozycji
  • Model dostępu od wersji 3.4: management.endpoints.access.default: none plus read-only dla każdego endpointu zastępuje przestarzałe flagi enabled
  • Zmiany w Spring Boot 4: spring-boot-starter-aspectj, API zdrowia w org.springframework.boot.health.contributor, EndpointRequest w org.springframework.boot.security.autoconfigure.actuate.web.servlet, sondy włączone domyślnie
  • Adnotacje wymagają przełącznika: management.observations.annotations.enabled=true, bez ręcznego beana TimedAspect
  • Tagi HTTP przez Observation API: rozszerzenie DefaultServerRequestObservationConvention, etykiety o niskiej kardynalności
  • Histogramy zamiast percentyli po stronie klienta: publishPercentileHistogram() i sum by (le) w każdym histogram_quantile
  • Liveness pozostaje lokalne: zależności zewnętrzne trafiają do readiness lub własnej grupy, nigdy do liveness

Te ustawienia domyślne dają usłudze Spring Boot 4 metryki, które poprawnie agregują się między podami, endpointy, które nie ujawniają niczego w sieci publicznej, oraz alerty reagujące na rzeczywiste incydenty, a nie na jeden wolny URI.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Wyzwanie dnia

Znajdziesz błąd w Spring Boot?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 26 października 2026

Tagi

#spring boot actuator
#micrometer
#prometheus
#monitoring
#observability

Udostępnij

Powiązane artykuły