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

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.
<!-- 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 кожному ендпоінту окремо:
# 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:
// 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'и вимірюють миттєві значення, а таймери вимірюють тривалість операцій.
// 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.
// 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
}
}# 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 нічого не робить».
Анотації @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.
// 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 вручну зайве. Конфігурація потрібна лише для того, щоб додати компоненти до групи або визначити власні групи:
# 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 відтак указують на відповідні ендпоінти:
# 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.
# 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 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 * 100jvm_memory_used_bytes{area="heap"} повертає одну серію на кожен пул пам'яті (Eden, Survivor, Old Gen для G1). Ділення на максимум без sum by (instance) дає співвідношення по окремих пулах, які на дашборді виглядають некоректно.
Бін MeterBinder додає gauge'и рівня застосунку, яких немає серед стандартних:
// 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:
{
"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 > Dashboards > New > Import, далі ID 4701 (JVM Micrometer) або 12900 (SpringBoot APM). Після імпорту варто перевірити типи панелей: старіші ревізії дашбордів спільноти можуть досі містити Angular-панелі, які Grafana 12 мусить мігрувати.
Алерти з Prometheus
Ключові Правила Алертів
Правила алертів Prometheus спрацьовують і надсилають сповіщення, щойно метрики перевищують критичні пороги.
# 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.
# 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:
// 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. Задана назва пулу робить тег читабельним:
# 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:
<!-- 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. Кожна унікальна комбінація тегів створює окрему часову серію.
// 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();
}
}// 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 обчислює з бакетів будь-який перцентиль, а межі бакетів залишаються узгодженими між сервісами.
Готова до Продакшну Конфігурація
# 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 4.0 Migration Guide: перейменування
spring-boot-starter-aopнаspring-boot-starter-aspectj, проби увімкнені за замовчуванням, новий модульspring-boot-health - Spring Boot 3.4 Release Notes:
enabled-by-defaultіendpoint.<id>.enabledзастарілі на користь моделі доступу - Довідник ендпоінтів Spring Boot Actuator: властивості доступу, пакет
EndpointRequest, health indicators вorg.springframework.boot.health.contributor, групи проб і додаткові шляхи - Довідник метрик Spring Boot:
DefaultServerRequestObservationConvention, метрики HikariCP іjdbc.connections, застарілість simpleclient - Довідник observability Spring Boot:
management.observations.annotations.enabledі вимога AspectJ - Документація Micrometer для Prometheus: registry на Prometheus Java-клієнті 1.x і застарілий модуль simpleclient
- datasource-micrometer на GitHub: 2.x для Spring Boot 4, 1.x для Spring Boot 3
- Grafana: видалення AngularJS у Grafana 12: панелі Graph і Singlestat замінено на Time series і Stat
- Релізи Spring Boot: 4.1.2 як поточний GA-реліз, 4.2.0-RC1 як pre-release
Що Запам'ятати про 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Засновник SharpSkill
Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.
Оновлено 26 жовтня 2026 р.
Теги
Поділитися
Пов'язані статті

Спостережуваність Spring Boot у 2026: OpenTelemetry, розподілене трасування та питання на співбесідах
Повний посібник зі спостережуваності Spring Boot з OpenTelemetry та Micrometer Tracing. Налаштування розподіленого трасування, Observation API, експорт OTLP та підготовка до технічних співбесід.

Структуроване логування Spring Boot 2026: JSON-логи для production з Logback та OpenTelemetry
Повний посібник зі структурованого логування Spring Boot 4.x. Нативна підтримка JSON, starter OpenTelemetry, MDC tracing та інтеграція з ELK Stack для спостережуваності у production.

Фріланс робота Java Spring Boot розробника у 2026: де шукати контракти, денні ставки, як підготуватися
Де знайти фріланс контракти Spring Boot у 2026 році, актуальні денні ставки в Європі та як підготуватися до технічних співбесід.