Spring Boot Micrometer і Prometheus: метрики Actuator у продакшні

Інтеграція Spring Boot Micrometer із Prometheus у Spring Boot 4: модель доступу Actuator, конфігурація скрейпінгу, HTTP-метрики, health-проби та правила алертів.

Моніторинг Spring Boot Actuator з Micrometer і Prometheus

Spring Boot Actuator надає ендпоінти здоров'я, метрик і діагностики, а Micrometer є фасадом метрик, на якому вони побудовані. Додавання залежності micrometer-registry-prometheus дає ендпоінт /actuator/prometheus, який Prometheus опитує з фіксованим інтервалом. Наведені нижче приклади орієнтовані на Spring Boot 4.1 (поточний GA-реліз, версія 4.2 на етапі release candidate), Micrometer 1.16 і Prometheus Java-клієнт 1.x.

Коротка відповідь

Інтеграція Spring Boot Micrometer із Prometheus складається з трьох кроків: додати spring-boot-starter-actuator і micrometer-registry-prometheus, вписати prometheus у management.endpoints.web.exposure.include, а потім спрямувати scrape_config Prometheus на /actuator/prometheus. Метрики JVM, HTTP, HikariCP і системні метрики публікуються без додаткового коду.

Налаштування Spring Boot Micrometer: Actuator і Prometheus Registry

Необхідні Залежності Maven

Стартер Actuator активує ендпоінти й підтягує Micrometer. Prometheus registry форматує кожен meter у форматі експозиції Prometheus. Стартер AspectJ потрібен лише для анотацій @Timed і @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>

У старіших туторіалах трапляються дві пастки, пов'язані з версіями. Починаючи з Micrometer 1.13 (Spring Boot 3.3), micrometer-registry-prometheus побудований на Prometheus Java-клієнті 1.x; старий код 0.x перенесено до застарілого модуля micrometer-registry-prometheus-simpleclient, який документація Micrometer для Prometheus подає лише як тимчасове рішення. А в Spring Boot 4 pom.xml, що досі оголошує spring-boot-starter-aop, більше не резолвиться: посібник з міграції на Spring Boot 4.0 перейменовує його на spring-boot-starter-aspectj.

Налаштування Ендпоінтів Actuator

За замовчуванням через HTTP доступний лише ендпоінт health. Ендпоінт /actuator/prometheus з'являється, щойно registry потрапляє на classpath, але повертає 404, доки його не додано до списку експозиції.

Spring Boot 3.4 замінив перемикачі увімк./вимк. (management.endpoints.enabled-by-default, management.endpoint.<id>.enabled) моделлю доступу, а старі властивості позначено як застарілі. Патерн opt-in встановлює доступ за замовчуванням none і надає read-only кожному ендпоінту окремо:

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

Експозиція і доступ є двома окремими механізмами. exposure.include визначає, чи мапиться ендпоінт через HTTP; access визначає, що ендпоінт дозволяє після мапінгу. Файл властивостей, який досі використовує enabled: true, продовжує працювати з попередженням про застарілість, і саме тоді варто мігрувати.

Опція show-details: when-authorized показує деталі стану лише автентифікованим користувачам із відповідною роллю. Самі ендпоінти захищає окремий 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();
    }
}

У Spring Boot 4 клас EndpointRequest перенесено до org.springframework.boot.security.autoconfigure.actuate.web.servlet, як показано в довіднику ендпоінтів Actuator. Java DSL AuthorizedUrl в authorizeHttpRequests не має методу hasIpAddress: IP-правило задається через IpAddressAuthorizationManager.hasIpAddress, доступний з Spring Security 6.3.

Кастомні Метрики з Micrometer

Лічильники, Gauge'и та Таймери Застосунку

Micrometer пропонує кілька типів метрик під різні сценарії. Лічильники вимірюють кумулятивні події, gauge'и вимірюють миттєві значення, а таймери вимірюють тривалість операцій.

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);
    }
}

З Prometheus registry важливі дві деталі іменування. Імена meter'ів у Micrometer використовують крапки й конвертуються у snake case (orders.processing.duration стає orders_processing_duration_seconds). Лічильники автоматично отримують суфікс _total, тож назва лічильника orders.created.total лише додає шум.

publishPercentiles(0.5, 0.95, 0.99) обчислює перцентилі всередині кожної JVM, і ці значення неможливо агрегувати між інстансами. publishPercentileHistogram() публікує бакети, які histogram_quantile() агрегує для всього флоту, і саме цю опцію слід обирати з Prometheus.

Анотації @Timed та @Counted

Щоб уникнути шаблонного коду, Micrometer надає анотації, які інструментують методи через 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

Починаючи зі Spring Boot 3.2, оголошувати біни TimedAspect і CountedAspect вручну більше не потрібно: властивість management.observations.annotations.enabled=true за наявності spring-boot-starter-aspectj на classpath автоматично їх конфігурує. Властивість за замовчуванням має значення false, і саме це пояснює більшість скарг «мій @Timed нічого не робить».

Обмеження AOP

Анотації @Timed та @Counted працюють лише на публічних методах бінів Spring, викликаних через проксі. Виклик одного методу з іншого в тому самому класі обходить проксі й не інструментується, а анотація на приватному методі ігнорується. Анотування контролера Spring MVC, який уже інструментовано, дає дубльовані спостереження.

Готовий до співбесід з Spring Boot?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Кастомні Health-Ендпоінти

Бізнесові Health Indicators

Health Indicators перевіряють стан зовнішніх залежностей та критичних бізнес-компонентів. Spring Boot постачає типові індикатори для баз даних, Redis і інших поширених сервісів. У Spring Boot 4 API здоров'я винесено в окремий модуль spring-boot-health, а Health і HealthIndicator перенесено з org.springframework.boot.actuate.health до 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();
        }
    }
}

Цей індикатор з'являється у /actuator/health під назвою paymentGateway: це ім'я біна без суфікса HealthIndicator.

Групи Здоров'я для Kubernetes

Spring Boot 4 вмикає liveness- і readiness-проби за замовчуванням на будь-якій платформі, а не лише тоді, коли виявляє Kubernetes. Групи /actuator/health/liveness і /actuator/health/readiness існують без жодного налаштування, а оголошувати біни LivenessStateHealthIndicator чи ReadinessStateHealthIndicator вручну зайве. Конфігурація потрібна лише для того, щоб додати компоненти до групи або визначити власні групи:

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

Група liveness має обмежуватися livenessState. Якщо додати до liveness базу даних чи зовнішній шлюз, Kubernetes перезапускатиме кожен под, коли ця залежність падає, і часткова відмова перетворюється на повну. Зовнішні залежності належать до readiness, і лише тоді, коли застосунок без них не може обслужити жодного запиту.

Проби Kubernetes відтак указують на відповідні ендпоінти:

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

Завдяки add-additional-paths: true проби залишаються на порту застосунку навіть тоді, коли Actuator працює на окремому порту керування, тож проба виявляє збій основного сервера.

Інтеграція Spring Boot з Prometheus та Grafana

Конфігурація Скрейпінгу Prometheus

Prometheus збирає метрики, періодично опитуючи ендпоінт /actuator/prometheus. Конфігурація скрейпінгу задає цілі та шлях до метрик, оскільки Prometheus за замовчуванням використовує /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: (.+)

Цілі використовують порт 9090, бо продакшн-конфігурація нижче переносить Actuator на окремий порт керування. Без management.server.port ціллю скрейпінгу є порт застосунку.

Метрики Spring Boot Actuator, що Публікуються за Замовчуванням

Actuator з Micrometer віддає метрики JVM, процесу, HikariCP і HTTP без жодного коду. Наведені PromQL-запити охоплюють ті метрики, за якими в продакшні стежать насамперед:

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"} повертає одну серію на кожен пул пам'яті (Eden, Survivor, Old Gen для G1). Ділення на максимум без sum by (instance) дає співвідношення по окремих пулах, які на дашборді виглядають некоректно.

Бін MeterBinder додає gauge'и рівня застосунку, яких немає серед стандартних:

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 і process.uptime вже публікуються вбудованими binder'ами Micrometer. Приклад демонструє патерн, а реальний binder має реєструвати лише те, чого бракує серед стандартних метрик.

Готові Дашборди Grafana

Grafana пропонує дашборди спільноти для Spring Boot, які працюють на стандартних метриках Actuator. Написаний вручну дашборд використовує типи панелей timeseries і 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"
        }
      ]
    }
  ]
}

Багато дашбордів, скопійованих зі старіших туторіалів, досі використовують типи панелей graph і singlestat. Singlestat видалено в Grafana 8, а Grafana 12 повністю прибрала підтримку AngularJS, тож такі панелі відображаються лише після міграції на timeseries і stat.

Імпорт у Grafana

Щоб імпортувати дашборд спільноти: Grafana > Dashboards > New > Import, далі ID 4701 (JVM Micrometer) або 12900 (SpringBoot APM). Після імпорту варто перевірити типи панелей: старіші ревізії дашбордів спільноти можуть досі містити Angular-панелі, які Grafana 12 мусить мігрувати.

Алерти з Prometheus

Ключові Правила Алертів

Правила алертів Prometheus спрацьовують і надсилають сповіщення, щойно метрики перевищують критичні пороги.

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 }}"

Ці алерти охоплюють найпоширеніші продакшн-проблеми: доступність, продуктивність і ресурси. Правилу HighLatency потрібен sum by (le, ...): без нього histogram_quantile обчислює окремий P99 для кожної серії (за URI, методом і статусом), і один повільний ендпоінт із трьома викликами запускає алерт.

HTTP-Метрики та Метрики Бази Даних у Spring Boot

Автоматична Інструментація HTTP-Запитів

Spring Boot інструментує кожен вхідний запит Spring MVC і WebFlux через Observation API та публікує таймер http.server.requests з тегами method, uri, status, outcome і 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}

Інтерфейси WebMvcTagsContributor і WebMvcTagsProvider, які трапляються в багатьох прикладах, видалено у Spring Boot 3.0. Кастомні теги тепер додаються через розширення DefaultServerRequestObservationConvention, як описано в довіднику метрик 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")
        );
    }
}

Мітками Prometheus стають лише пари ключ-значення з низькою кардинальністю. Тег tenant прийнятний за кількох десятків тенантів і перетворюється на проблему кардинальності за тисяч. Значення з високою кардинальністю (ID користувача, ID замовлення) належать до getHighCardinalityKeyValues, які живлять лише трейси. Трейсинг через Observation API детально розглянуто в статті observability Spring Boot з OpenTelemetry.

Метрики HikariCP та SQL-Запитів

Метрики пулу HikariCP віддаються автоматично з префіксом hikaricp і тегом з назвою пулу, поряд із загальними gauge'ами jdbc.connections. Задана назва пулу робить тег читабельним:

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 не надає. Проєкт datasource-micrometer обгортає DataSource і створює спостереження для кожного з'єднання, запиту та result set, що дає одночасно метрики і спани трейсів. Лінійка 2.x призначена для Spring Boot 4, лінійка 1.x для 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>

Це замінює ручний підхід, у якому DataSource обгортали в ProxyDataSourceBuilder з написаним вручну listener'ом: такий підхід обходив автоконфігурацію DataSource у Spring Boot і потребував синхронізації з кожним оновленням Boot. Повільні запити зазвичай вказують на патерн N+1, розглянутий у статті розв'язання проблеми N+1 у Spring Data JPA.

Найкращі Практики для Продакшну

Кардинальність Метрик

Надмірна кардинальність погіршує продуктивність Prometheus. Кожна унікальна комбінація тегів створює окрему часову серію.

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 із бакетами гістограми відповідає на питання про затримки краще, ніж лічильник із вручну зібраним тегом bucket: PromQL обчислює з бакетів будь-який перцентиль, а межі бакетів залишаються узгодженими між сервісами.

Готова до Продакшну Конфігурація

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}

За access.default: none ендпоінти на кшталт env, beans, configprops і mappings недоступні без їх переліку поодинці. management.prometheus.metrics.export.enabled за замовчуванням має значення true, а налаштування step не впливає на Prometheus: це pull-registry, а інтервал скрейпінгу задається в prometheus.yml.

Типові Помилки з Actuator і Prometheus

СимптомПричинаВиправлення
/actuator/prometheus повертає 404Ендпоінта немає у списку експозиції або бракує registryДодати prometheus до management.endpoints.web.exposure.include і перевірити, що micrometer-registry-prometheus є на classpath
Ціль у Prometheus показує «server returned HTTP status 401»Spring Security захищає всі ендпоінти, крім healthВідкрити prometheus для мережі скрейпера або налаштувати basic auth у конфігурації скрейпінгу
@Timed не створює метрикиmanagement.observations.annotations.enabled за замовчуванням false або бракує AspectJВстановити властивість у true і додати spring-boot-starter-aspectj
spring-boot-starter-aop не знаходиться після оновленняПерейменовано у Spring Boot 4Використати spring-boot-starter-aspectj
Імпорт Health або HealthIndicator не працює в Boot 4Пакет перенесено до org.springframework.boot.health.contributorОновити імпорти
Попередження про застарілість management.endpoint.*.enabledМодель увімк./вимк. замінено у Spring Boot 3.4Використати management.endpoints.access.default і management.endpoint.<id>.access
Перцентилі на дашборді сильно відрізняються від реальнихЗначення publishPercentiles усереднюються між подамиВикористати publishPercentileHistogram() і histogram_quantile()

Питання на Співбесіді про Spring Boot Micrometer

Actuator з'являється на більшості співбесід зі Spring Boot на позиції middle і senior. Відповіді, які відрізняють досвідченого фахівця від junior:

  • Різниця між liveness і readiness: liveness каже Kubernetes перезапустити под, readiness каже припинити спрямовувати на нього трафік. Кандидат, який додає базу даних до групи liveness, ніколи не проходив із цим через інцидент.
  • Чому Micrometer, а не напряму клієнт Prometheus: Micrometer є фасадом. Той самий код Counter і Timer експортує дані в Prometheus, Datadog чи OTLP після заміни однієї залежності, а власна інструментація Spring (HTTP, пул JDBC, кеші) написана саме під нього.
  • Перцентилі на боці клієнта проти гістограм: перцентилі, обчислені на боці клієнта, неможливо агрегувати між інстансами, гістограми можна. Це найпоширеніше питання з підступом на цю тему.
  • Що таке кардинальність і як вона ламає Prometheus: одна часова серія на кожну унікальну комбінацію міток; тег з ID користувача множить споживання пам'яті на кількість користувачів.
  • Як захистити Actuator: обмежити експозицію, задати рівні доступу, винести керування на окремий порт і захистити його окремим SecurityFilterChain.

Більше подібних питань зібрано в модулі питання на співбесіді про Spring Boot Actuator.

Джерела

Що Запам'ятати про Spring Boot Micrometer і Prometheus

  • Три кроки до робочого скрейпінгу: стартер Actuator, micrometer-registry-prometheus, prometheus у списку експозиції
  • Модель доступу з версії 3.4: management.endpoints.access.default: none плюс read-only для кожного ендпоінта замінює застарілі прапорці enabled
  • Зміни у Spring Boot 4: spring-boot-starter-aspectj, API здоров'я в org.springframework.boot.health.contributor, EndpointRequest в org.springframework.boot.security.autoconfigure.actuate.web.servlet, проби увімкнені за замовчуванням
  • Анотаціям потрібен перемикач: management.observations.annotations.enabled=true, без ручного біна TimedAspect
  • HTTP-теги через Observation API: розширення DefaultServerRequestObservationConvention, мітки з низькою кардинальністю
  • Гістограми замість клієнтських перцентилів: publishPercentileHistogram() і sum by (le) у кожному histogram_quantile
  • Liveness залишається локальним: зовнішні залежності йдуть у readiness або власну групу, ніколи в liveness

Ці налаштування дають сервісу на Spring Boot 4 метрики, що коректно агрегуються між подами, ендпоінти, які нічого не розкривають у публічній мережі, та алерти, що спрацьовують на реальні інциденти, а не на один повільний URI.

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Щоденний виклик

Чи знайдеш ти помилку в Spring Boot?

Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Anthony Fillion-Maillet

Автор:

Anthony Fillion-Maillet

Засновник SharpSkill

Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.

Оновлено 26 жовтня 2026 р.

Теги

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

Поділитися

Пов'язані статті