← назад к разделу

Когда сервис уходит в прод, возникает вопрос: «всё ли работает нормально прямо сейчас?». Логи показывают события постфактум, а трассировки — отдельные запросы. Метрики — это непрерывная картина: сколько запросов в секунду, какой процент с ошибками, сколько памяти занято, не переполнен ли пул соединений. Именно метрики первыми сигнализируют об аномалии, ещё до жалоб пользователей.

Как устроена связка 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 — счётчик с суффиксом _total
  • payment_processing_seconds — таймер с _seconds
  • order_amount_rubles — сводка с единицей валюты
  • queue_size — gauge, число элементов

Неправильно:

  • orderCreatedCount — camelCase, нет _total
  • paymentTime — нет единицы измерения
  • 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 и кастомные индикаторы.