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

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

Ограничение у метрик одно и оно арифметическое: Prometheus заводит отдельный временной ряд на каждую уникальную комбинацию значений тегов, поэтому новый тег не прибавляет ряды, а умножает их.

Счётчик order_created_total · один квадрат = один временной ряд тег channel — 3 значенияweb · mobile · api3 ряда + тег payment_method — 3 значенияcard · sbp · crypto3 × 3 = 9 рядов + тег status — 5 значений200 · 400 · 404 · 409 · 5009 × 5 = 45 рядов 45 рядов на метрику — штатный режим: Prometheus рассчитан на тысячи рядов на сервис + тег user_id — миллион значений45 × 1 000 000 = 45 000 000 рядоводин квадрат превратился в миллион — такую сетку не нарисоватьPrometheus ест память, замедляет запросы и отбрасывает данные Тег — только конечное и небольшое множество значенийuser_id — в трассировку: атрибуты span не ограничены по мощности

Каждый новый тег не прибавляет ряды, а умножает их: channel × payment_method × status — это 45 рядов на одну метрику, и Prometheus такое держит спокойно. Тот же user_id превращает 45 в 45 миллионов — поэтому детализация по конкретному заказу или пользователю живёт в трассировке, а не в тегах.

Обязательно

Как устроена связка 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.

сервис Micrometer считает /actuator/prometheus отдаёт текстом Prometheus scrape и TSDB Grafana графики и тревоги

Путь одного числа от счётчика в коде до графика: смотрите, что Prometheus сам приходит за метриками, а не сервис их отправляет.

Подключение

Две зависимости в 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 — три вопроса о любом сервисе, который обрабатывает запросы:

  • Rate — сколько запросов в секунду?
  • Errors — какой процент с ошибками?
  • Duration — как долго обрабатываются запросы?

Spring Boot Actuator + Micrometer собирают это автоматически через метрику http_server_requests_seconds. PromQL-запросы для Grafana:

Прежде чем читать эти запросы, одна вещь, на которой спотыкаются все при первом дашборде: абсолютное значение счётчика бесполезно. http_server_requests_seconds_count — это «сколько запросов обработано с момента старта процесса», число, которое только растёт и обнуляется при каждом перезапуске. График такого числа — пила, по которой ничего не видно. Смысл появляется только от производной: rate(...[5m]) даёт «запросов в секунду, в среднем за пять минут», increase(...[1h]) — «сколько за час».

Правило простое: любой счётчик в запросе обёрнут в rate или increase, и если в запросе имя счётчика встречается голым, запрос почти наверняка неверный. Приятный побочный эффект: обе функции знают про перезапуски и не считают падение счётчика до нуля отрицательным всплеском.

# 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».

Откуда берётся тег uri и как он взрывается

В запросах выше группировка идёт по uri, и здесь стоит остановиться, потому что это тот самый тег, который чаще всего рушит мониторинг целого сервиса.

В uri попадает шаблон пути, а не фактический адрес: у запроса GET /orders/42 значение тега будет /orders/{id}. Берётся оно не из строки запроса, а из того, как был найден обработчик: Spring запоминает сопоставленный шаблон и отдаёт его метрикам. Поэтому миллион разных заказов даёт один ряд, а не миллион.

А теперь три способа это сломать, каждый из которых встречается в жизни:

  • Путь собран вручную. Обработчик объявлен как /orders/** или адрес разбирается из строки внутри метода, без переменной пути в объявлении. Шаблона нет — в тег уезжает фактический путь, и каждый заказ создаёт свой ряд.
  • Запросы в никуда. Обращение по адресу, которому не соответствует ни один обработчик, шаблона тоже не имеет. Сканер, который перебирает адреса, за час создаёт десятки тысяч рядов. Spring по умолчанию сводит такие ответы к uri="NOT_FOUND" и uri="REDIRECTION" — но только если приложение действительно отвечает 404 само; собственный обработчик ошибок, отвечающий на всё, эту защиту снимает.
  • Параметры в пути вместо строки запроса. Адрес вида /search/красные кроссовки — это бесконечное множество значений, а значит, бесконечное множество рядов.

Как проверить у себя за минуту: открыть /actuator/metrics/http.server.requests и посмотреть список значений тега uri. Если в нём видны цифры, даты, идентификаторы или незнакомые пути — у вас уже утечка кардинальности. Как лечить: объявлять пути шаблонами с переменными, отдавать 404 штатным механизмом, а то, что шаблоном не выражается, приводить к общему значению самостоятельно.

Сколько стоит гистограмма

«Включите гистограмму» — совет с ценой, и цену стоит назвать в тех же рядах, которыми пугает раздел про кардинальность.

Гистограмма — это не одно число, а набор счётчиков «сколько запросов уложилось в границу»: по одному ряду на границу плюс сумма и общее число. percentiles-histogram: true включает подробную сетку границ Micrometer — это порядка 60–70 рядов на каждую комбинацию остальных тегов. Считаем на обычном сервисе: 20 путей × 4 метода × 3 статуса = 240 комбинаций, умножаем на ~65 — около пятнадцати тысяч рядов с одной метрики. Это ещё не авария, но уже заметная часть бюджета, а с несколькими сервисами счёт идёт на сотни тысяч.

Что с этим делают:

  • Задают свои границы вместо подробной сетки. Настройка slo (в новых версиях — service-level-objective) вместе с выключенной подробной гистограммой оставляет ровно те границы, которые вам нужны для целей: пять границ вместо шестидесяти пяти. Точность перцентилей при этом ниже, а для проверки «быстрее 500 мс» этого достаточно, потому что вопрос ставится про границу, а не про точное значение p95.
  • Ограничивают диапазон. minimum-expected-value и maximum-expected-value отсекают границы, которых в вашем сервисе не бывает: без них сетка тянется от миллисекунд до минут.
  • Включают не всем метрикам. Подробная гистограмма нужна на входящих запросах и на вызовах соседей, а не на каждом своём таймере.

Признак, что цену не посчитали: в /actuator/prometheus тысячи строк, а на дашборде три графика.

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запросы, ждущие соединения
kafka_consumer_fetch_manager_records_lag_maxотставание консьюмера Kafka от конца топика (с spring-kafka)

Пример алертов:

- 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")
            .description("Total orders created")
            .tag("channel", "web")
            .register(registry);
        this.paymentProcessingTimer = Timer.builder("payment.processing")
            .publishPercentileHistogram()
            .register(registry);
        this.orderAmountSummary = DistributionSummary.builder("order.amount")
            .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());
    }
}

У таймера стоит publishPercentileHistogram(), а не publishPercentiles(0.5, 0.95, 0.99), и разница здесь принципиальная. Второй вариант считает перцентили внутри одного экземпляра сервиса и отдаёт готовые числа — а готовые перцентили нельзя складывать между репликами: среднее от трёх p95 никаким p95 не является. publishPercentileHistogram() отдаёт бакеты, и p95 по всему сервису считает уже Prometheus тем самым histogram_quantile, который показан выше.

Использование в сервисе:

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

Декларативно: @Timed, @Counted и Observation

Всё, что показано выше, — ручная работа с реестром, и она нужна, когда метрика непростая. В обычном коде чаще встречается другой способ: попросить фреймворк замерить метод.

@Timed(value = "payment.processing", extraTags = {"provider", "acme"})
public PaymentResult charge(ChargeRequest request) { ... }

@Counted(value = "order.cancelled", recordFailuresOnly = false)
public void cancel(OrderId id) { ... }

Чтобы это работало, нужен один бин в конфигурации — аспект, который перехватывает вызовы:

@Bean
TimedAspect timedAspect(MeterRegistry registry) { return new TimedAspect(registry); }

@Bean
CountedAspect countedAspect(MeterRegistry registry) { return new CountedAspect(registry); }

Ограничение у этого способа ровно одно, зато обидное: аннотация работает через прокси, то есть только на вызовах извне бина. Метод, вызванный внутри того же класса, не замерится — типичное «я поставил аннотацию, а метрики нет».

Observation — следующий шаг и, если проект уже на новых версиях, предпочтительный. Смысл в том, что одно и то же наблюдение даёт сразу и метрику, и спан трассировки с теми же тегами:

Observation.createNotStarted("payment.charge", observationRegistry)
        .lowCardinalityKeyValue("provider", "acme")
        .highCardinalityKeyValue("orderId", orderId.toString())
        .observe(() -> gateway.charge(request));

Разница между двумя видами тегов здесь — самое ценное в этом API, потому что она снимает главный конфликт темы. Низкая кардинальность уходит и в метрику, и в спан: по ней группируют. Высокая уходит только в спан: orderId виден в трассировке при разборе конкретного случая и не создаёт ряда в метриках. То есть больше не нужно выбирать между «детализация» и «не взорвать кардинальность» — вопрос решается одним вызовом. Есть и аннотация @Observed с тем же поведением и тем же ограничением прокси.

Что брать: аннотации — для быстрого замера метода; Observation — когда вокруг этого места и метрика, и трассировка (внешние вызовы, обработка сообщений); ручной реестр — для бизнес-счётчиков, которые не привязаны к одному методу.

Как называть метрики

Prometheus использует соглашение: snake_case, единица измерения в имени. А Micrometer просит писать имена через точку — и сам переводит их в то, что ждёт бэкенд. Так и работает фасад: в коде order.created, в /actuator/prometheus уже order_created_total, а в Datadog то же имя поедет с точками. Поэтому snake_case вы пишете не в коде, а в запросах Grafana.

Правильно (слева имя в коде, справа — что увидит Prometheus):

  • order.created → order_created_total, суффикс счётчика добавится сам
  • payment.processing → payment_processing_seconds_count, _sum, _bucket — единицу времени дописывает регистри
  • order.amount с baseUnit("rubles") → order_amount_rubles
  • queue.size → queue_size, gauge с числом элементов

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

  • orderCreatedCount — camelCase: ни точек, ни принятого суффикса, среди соседних метрик имя выглядит чужим
  • paymentTime — нет единицы измерения
  • order.processing.ms — миллисекунды; и Micrometer, и Prometheus считают время в секундах, а единицу задают через baseUnit, а не в имени

Кардинальность: чего не должно быть в тегах

Временной ряд — это имя метрики плюс уникальный набор значений тегов. Тег status с пятью значениями умножает число рядов на пять; тег userId с миллионом значений — на миллион. Это называется кардинальностью, и Prometheus рассчитан на тысячи рядов на сервис, а не на миллионы: при переполнении он ест память, замедляет запросы и отбрасывает данные.

Правило: тег — только для конечного и небольшого множества значений (метод, статус, шаблон пути /orders/{id}, имя внешней системы). Идентификаторы, e-mail, свободный текст, URL с параметрами — никогда; их место — в логах и атрибутах трассировки. Перед добавлением тега спросите, сколько уникальных значений он даст через год.

Низкая кардинальность тегов — важнейшее правило

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, а у самого сервиса: реестр метрик держит все ряды в куче приложения. Следом ляжет и сервер Prometheus, которому эти ряды придётся хранить.

Если нужна детализация по конкретному пользователю или запросу — это задача для трассировки (Tempo/Jaeger), а не метрик. Каждый span в трассировке несёт произвольные атрибуты без ограничений по кардинальности.

Когда метрика не нужна

Соблазн после этой статьи — обвесить метриками всё. Четыре случая, где это не работает и где нужен другой инструмент.

Событие, которое важно поштучно. Метрика отвечает на вопрос «сколько» и не отвечает «какой именно». Если про каждое срабатывание нужно знать подробности (кто, что, почему отказали) — это запись в журнал, а не счётчик. Метрика говорит «отказов стало втрое больше», журнал говорит, каких именно. Обвешивать метриками с подробными тегами то, что уже есть в журнале, — прямой путь к взрыву кардинальности.

Бизнес-сумма в датчике. Gauge с текущей суммой заказов или числом записей в таблице врёт при нескольких копиях сервиса: каждая отдаёт своё значение, и что с ними делать, непонятно — сложение даёт число в N раз больше, среднее не значит ничего, а Prometheus по умолчанию покажет ряд от каждой копии отдельно. Правило: датчик годится для того, что относится к самой копии (её очередь, её пул соединений, её потоки), и не годится для общего состояния системы. Общая сумма и общее число — это запрос к базе или аналитическому хранилищу, и место такого числа на витрине бизнес-показателей, а не в мониторинге.

Редкие события. Событие раз в час превращает rate(...[5m]) в постоянный ноль с одинокими всплесками, а алерт по такому ряду либо молчит, либо кричит. Для редкого нужны длинные окна (increase(...[24h])) и порог по абсолютному числу, а не по доле; иногда правильнее проверять не поток, а факт: «последний успешный запуск был больше суток назад» — это про метку времени в датчике, а не про счётчик.

Деньги. DistributionSummary на суммы чеков выглядит красиво и почти всегда не нужен: распределение выручки смотрят в аналитике по настоящим данным, где можно пересчитать, разрезать по срезам и объяснить каждую копейку, а метрики — приближённые, с потерями при агрегации и сроком хранения в недели. В мониторинге от денег нужно другое: «сумма отказов за пять минут выше порога» — это сигнал о поломке, а не отчёт. И техническая мелочь напоследок: baseUnit("rubles") попадает в имя ряда (order_amount_rubles), а не в отдельное поле, так что сменить единицу потом означает сменить имя метрики и переписать дашборды.

Частые ошибки

Метрики есть в /actuator/metrics, а Prometheus их не видит. Значит, подключён только 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) и придерживайтесь одного стандарта во всех сервисах.

Дополнительно: при первом чтении можно пропустить

Глубже: экран первой минуты: шесть панелейрасширенное

RED и USE выше говорят, что снимать. Дежурному в первую минуту нужен один экран, на котором видно, что происходит, без поиска по сотне графиков, и он собирается из шести панелей.

Верхний ряд, RED сервиса. Запросы в секунду с разбивкой по классу кода (2xx, 4xx, 5xx) одной панелью, доля ошибок в процентах отдельной, задержка тремя линиями p50, p95, p99. По этим трём панелям видно все три вида инцидента: упал трафик (проблема снаружи или на входе), выросли ошибки, выросла задержка.

Средний ряд, разбивки. Задержка и ошибки по эндпоинту, верхняя десятка: инцидент почти всегда живёт в одном-двух маршрутах, и общий график его прячет. Ошибки по коду и по зависимости: 503 от одного соседа и таймауты к базе это разные разборы.

Нижний ряд, USE. Процессор против лимита, память рабочего набора против лимита, паузы сборщика мусора, пул соединений к базе (активные против максимума и ожидающие), пул потоков веб-сервера, отставание потребителей очередей. Насыщение любого из них объясняет рост задержки сверху.

Что делает экран читаемым. Отметки выкатов вертикальными линиями на всех панелях: половина инцидентов начинается в минуту выката, и без отметки это приходится вспоминать. Наложение той же метрики за прошлую неделю тонкой линией, чтобы «много» и «мало» читались без памяти о норме. Переменные экрана service и env, чтобы один шаблон работал для всех сервисов, а не сто экранов, разъехавшихся за год. Не больше восьми панелей: экран, который нужно листать, не экран первой минуты.

Как читать всплеск. Сначала трафик: вырос он или упал в момент всплеска задержки, и это отличает нагрузку от поломки. Потом ошибки по зависимости: если растут вместе с задержкой к одному соседу, виноват сосед. Потом насыщение: пул соединений упёрся в потолок раньше, чем выросла задержка, значит потолок и есть причина. И только потом в трассы и логи одного запроса, о чём статья про путь от алерта к логу. Экран не заменяет алерт, он отвечает на вопрос «где», когда алерт уже сказал «что».

Глубже: метрики асинхронной части: отставание, глубина, возраст, повторырасширенное

RED считает запросы, а половина работы сервиса происходит без запроса: потребители очередей, планировщик, отправитель outbox. У них своя тройка метрик, и без неё сервис «работает» по всем графикам, пока письма не уходят третий час.

Отставание и глубина. У потребителя Kafka это lag, расстояние в записях между концом партиции и позицией группы; у очереди RabbitMQ и SQS это число сообщений, ожидающих обработки; у outbox это число строк в состоянии «не отправлено». Число само по себе значит мало: 40 тысяч записей это десять секунд при четырёх тысячах в секунду и два часа при пяти, поэтому рядом ставят вторую метрику.

Возраст самого старого. Время между сейчас и отметкой самого старого необработанного: now - record.timestamp() у потребителя Kafka, min(created_at) среди неотправленных строк outbox, head_message_timestamp у очереди. Это метрика в секундах, и сигнал тревоги ставят на неё: «старше пяти минут» понятно и бизнесу, и дежурному, в отличие от «lag больше десяти тысяч». Планировщик измеряют тем же: время с последнего успешного прогона против ожидаемого интервала.

Повторы и недоставленное. Счётчик повторных попыток по причине и размер очереди недоставленных: рост повторов показывает деградацию соседа раньше, чем она станет отказом, а любое сообщение в очереди недоставленных платёжного топика это инцидент, а не число.

Пропускная способность обработки. Обработано в секунду против пришло в секунду, и таймер обработки одного сообщения с процентилями (spring_kafka_listener_seconds у Spring Kafka, свой Timer вокруг обработчика в остальных случаях). Когда обработано меньше, чем пришло, отставание растёт, и это видно раньше самого отставания.

Снимают это с двух сторон: из приложения (Micrometer отдаёт lag потребителя как kafka_consumer_fetch_manager_records_lag, остальное через свои Gauge и Timer) и из брокера экспортёром, который видит отставание и тогда, когда приложение лежит. Панель асинхронной части висит рядом с экраном первой минуты, и у неё те же переменные.

Коротко

  • 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 для числовых распределений. Имена метрик в коде — через точку (order.created); в snake_case с суффиксами _total и _seconds их переводит сам Micrometer, единицу задают через baseUnit.
  • Перцентили таймера публикуют бакетами (publishPercentileHistogram()), а не готовыми числами: готовые перцентили не складываются между репликами. Теги с высокой кардинальностью (user_id, order_id) взрывают Prometheus — только категориальные значения с малым числом вариантов.
  • Endpoint /actuator/prometheus закрывают от публичного доступа: отдельный порт или сетевые политики.
  • Экран первой минуты: RED сервиса сверху, разбивки по эндпоинту и зависимости в середине, USE снизу, отметки выкатов и прошлая неделя тонкой линией, не больше восьми панелей; читают трафик, затем ошибки по зависимости, затем насыщение.
  • Асинхронную часть меряют отставанием или глубиной, возрастом самого старого необработанного в секундах (на него тревога), повторами и недоставленным, обработано против пришло; и из приложения, и из брокера.
  • Счётчик читают только через rate или increase: абсолютное значение растёт от старта процесса и сбрасывается при перезапуске.
  • В теге uri лежит шаблон пути, и ломают его ручная сборка адреса, запросы в никуда и параметры в пути; подробная гистограмма стоит около 65 рядов на комбинацию тегов, поэтому вместо неё задают свои границы.
  • Декларативно: @Timed и @Counted через аспект (не работают на внутренних вызовах), Observation даёт и метрику, и спан, разделяя теги низкой и высокой кардинальности.

Что почитать дальше