Когда сервис уходит в прод, возникает вопрос: «всё ли работает нормально прямо сейчас?». Логи показывают события постфактум, а трассировки — отдельные запросы. Метрики — это непрерывная картина: сколько запросов в секунду, какой процент с ошибками, сколько памяти занято, не переполнен ли пул соединений. Именно метрики первыми сигнализируют об аномалии, ещё до жалоб пользователей.
Как устроена связка Micrometer + Prometheus
Раньше каждый инструмент мониторинга требовал своей библиотеки. Prometheus — одна зависимость, Datadog — другая. Пришлось бы переписывать код при смене бэкенда.
Micrometer решает это как SLF4J для логов: унифицированный API для записи метрик, а конкретный бэкенд подключается отдельно. Пишешь counter.increment() — Micrometer отправит значение в Prometheus, Datadog или любой другой настроенный registry.
Prometheus — система хранения временных рядов. Она сама приходит к сервису по расписанию (обычно раз в 15 секунд) и забирает метрики с endpoint /actuator/prometheus в текстовом формате. Потом в Grafana строишь дашборды, настраиваешь алерты.
Схема: сервис → /actuator/prometheus → Prometheus scraper → Prometheus TSDB → Grafana.
Подключение
Две зависимости в build.gradle.kts:
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-registry-prometheus")
Открыть endpoint в application.yml:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
После старта GET /actuator/prometheus отдаёт сотни строк с метриками JVM, HTTP, пулов соединений — всё автоматически, без единой строки кода.
Стандартные теги для всех метрик
Частая проблема: на дашборде видно метрики, но непонятно — это прод или стейджинг? Версия 1.2.3 или 1.3.0?
Решение — добавить теги service, env, version один раз в конфигурации, и они автоматически появятся на каждой метрике:
spring:
application:
name: order-service
management:
metrics:
tags:
service: ${spring.application.name}
env: ${ENV:dev}
version: ${BUILD_VERSION:unknown}
Теперь в Grafana можно фильтровать: service="order-service", env="prod" — и видеть только нужное. И сравнивать деплои: version="1.2.3" против version="1.2.4".
Важно: не нужно добавлять .tag("service", "order-service") вручную в каждой метрике — глобальные теги применяются автоматически. Дублирование вызовет ошибку IllegalArgumentException: duplicate tag.
RED-метод для HTTP: что смотреть в первую очередь
RED — три вопроса о любом request-driven сервисе:
- Rate — сколько запросов в секунду?
- Errors — какой процент с ошибками?
- Duration — как долго обрабатываются запросы?
Spring Boot Actuator + Micrometer собирают это автоматически через метрику http_server_requests_seconds. PromQL-запросы для Grafana:
# Rate — запросов в секунду по endpoint
sum(rate(http_server_requests_seconds_count[5m])) by (uri, method)
# Errors — доля 5xx ошибок
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m])) by (uri)
# Duration — p95 латентность
histogram_quantile(0.95, sum by (le, uri) (rate(http_server_requests_seconds_bucket[5m])))
Чтобы видеть p95/p99 латентность, нужно включить гистограмму:
management:
metrics:
distribution:
percentiles-histogram:
http.server.requests: true
slo:
http.server.requests: 100ms,500ms,1s,5s
slo задаёт bucket-границы: Prometheus будет считать, сколько запросов уложились в 100ms, 500ms и так далее. Это позволяет формулировать SLO: «95% запросов быстрее 500ms».
USE-метод для ресурсов: память, потоки, соединения
USE — три вопроса о любом ресурсе:
- Utilization — насколько занят ресурс?
- Saturation — есть ли очередь ожидающих?
- Errors — есть ли ошибки на уровне ресурса?
Spring Boot автоматически экспортирует метрики JVM и инфраструктуры:
| Метрика | Что показывает |
|---|---|
jvm_memory_used_bytes{area="heap"} | занятая heap-память |
jvm_memory_max_bytes{area="heap"} | максимум heap |
jvm_gc_pause_seconds_sum | суммарное время GC-пауз |
executor_active_threads{name="taskExecutor"} | активные потоки в пуле |
hikaricp_connections_active | активные соединения с БД |
hikaricp_connections_pending | запросы, ждущие соединения |
Пример алертов:
- alert: HeapMemoryHigh
expr: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} > 0.85
for: 10m
- alert: HikariPoolSaturated
expr: hikaricp_connections_pending > 0
for: 5m
hikaricp_connections_pending > 0 означает, что запросы стоят в очереди за соединением с базой — верный признак насыщения пула.
Свои бизнес-метрики через MeterRegistry
Встроенных метрик хватает для инфраструктуры. Но бизнес-события — «заказ создан», «платёж обработан», «сумма чека» — нужно добавлять самостоятельно.
Micrometer предлагает четыре инструмента:
- Counter — монотонно растущий счётчик. Подходит для событий: создан заказ, отклонён платёж.
- Gauge — текущее значение, может и расти и падать. Подходит для состояний: размер очереди, число активных пользователей.
- Timer — длительность операции с гистограммой. Подходит для измерения времени обработки.
- DistributionSummary — произвольные числа с гистограммой. Подходит для сумм чеков, размеров файлов.
@Component
public class OrderMetrics {
private final Counter orderCreatedCounter;
private final Timer paymentProcessingTimer;
private final DistributionSummary orderAmountSummary;
public OrderMetrics(MeterRegistry registry) {
this.orderCreatedCounter = Counter.builder("order_created_total")
.description("Total orders created")
.tag("channel", "web")
.register(registry);
this.paymentProcessingTimer = Timer.builder("payment_processing_seconds")
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry);
this.orderAmountSummary = DistributionSummary.builder("order_amount_rubles")
.baseUnit("rubles")
.register(registry);
}
public void orderCreated() { orderCreatedCounter.increment(); }
public void recordPaymentDuration(Duration duration) {
paymentProcessingTimer.record(duration);
}
public void recordOrderAmount(BigDecimal amount) {
orderAmountSummary.record(amount.doubleValue());
}
}
Использование в сервисе:
@Service
@RequiredArgsConstructor
public class CreateOrderService {
private final OrderRepository orderRepository;
private final OrderMetrics metrics;
@Transactional
public Order create(CreateOrderCommand command) {
var order = orderRepository.save(Order.create(command));
metrics.orderCreated();
metrics.recordOrderAmount(order.amount());
return order;
}
}
Как называть метрики
Prometheus использует соглашение: snake_case, единица измерения в имени.
Правильно:
order_created_total— счётчик с суффиксом_totalpayment_processing_seconds— таймер с_secondsorder_amount_rubles— сводка с единицей валютыqueue_size— gauge, число элементов
Неправильно:
orderCreatedCount— camelCase, нет_totalpaymentTime— нет единицы измеренияorder_processing_ms— Prometheus предпочитает секунды, не миллисекунды
Низкая мощность тегов — важнейшее правило
Prometheus хранит отдельный временной ряд для каждой уникальной комбинации значений тегов. Значит, если тег user_id принимает миллион значений, метрика порождает миллион временных рядов — и это только для одной метрики.
Хорошие теги — это категории с небольшим числом значений:
// Правильно — 3-10 значений на тег
counter.tag("channel", "web") // web, mobile, api
.tag("payment_method", "card") // card, sbp, crypto
.increment();
Плохие теги — уникальные идентификаторы:
// Неправильно — миллион значений = миллион временных рядов
counter.tag("user_id", String.valueOf(userId)) // уникален для каждого пользователя
.tag("order_id", String.valueOf(orderId)) // уникален для каждого заказа
.increment();
Миллион user_id × миллион order_id × несколько тегов окружения = счёт на миллиарды временных рядов. Prometheus не справится, scraper упадёт с OOM.
Если нужна детализация по конкретному пользователю или запросу — это задача для трассировки (Tempo/Jaeger), а не метрик. Каждый span в трассировке несёт произвольные атрибуты без ограничений по мощности.
Частые ошибки
Micrometer без Prometheus registry. Если подключить только micrometer-core, метрики будут собираться во встроенный SimpleMeterRegistry и храниться только в памяти. При рестарте всё потеряется, Prometheus ничего не увидит. Нужен micrometer-registry-prometheus.
Открытый endpoint /actuator/prometheus. Этот endpoint может раскрыть чувствительные данные: суммы выручки, счётчики платежей, бизнес-KPI. В продакшене его доступ ограничивают сетевыми политиками — только скрейпер Prometheus из namespace мониторинга. Удобный приём — вынести actuator на отдельный порт:
management:
server:
port: 8081
Тогда бизнес-трафик идёт на 8080, actuator-трафик — на 8081, который не выставляется наружу через Ingress.
Нестандартные имена тегов окружения. Если один сервис пишет app=foo, другой — service_name=foo, кросс-сервисные дашборды в Grafana не работают. Договоритесь на уровне конфигурации (management.metrics.tags) и придерживайтесь одного стандарта во всех сервисах.
Коротко
- Micrometer — фасад для метрик (как SLF4J для логов), Prometheus — бэкенд хранения,
/actuator/prometheus— точка сбора. - Теги
service,env,versionнастраиваются один раз черезmanagement.metrics.tagsи применяются глобально. - RED (Rate, Errors, Duration) — три вопроса для HTTP;
http_server_requests_secondsсобирается автоматически. - USE (Utilization, Saturation, Errors) — три вопроса для ресурсов; JVM, Hikari, пулы потоков собираются автоматически.
- Свои метрики:
Counterдля событий,Gaugeдля состояний,Timerдля длительности,DistributionSummaryдля числовых распределений. - Имена метрик:
snake_case, единица в имени (_seconds,_total,_bytes). - Теги с высокой мощностью (
user_id,order_id) взрывают Prometheus — только категориальные значения с малым числом вариантов. - Endpoint
/actuator/prometheusзакрывают от публичного доступа: отдельный порт или сетевые политики.
Что почитать дальше
- Трассировка запросов — когда нужна детализация по конкретному запросу, а не агрегат.
- Логирование — структурированные JSON-логи и контекст через MDC.
- Конфигурация observability — management port, профили Logback.
- Health checks — liveness vs readiness и кастомные индикаторы.