Когда сервис уходит в прод, возникает вопрос: «всё ли работает нормально прямо сейчас?». Логи показывают события постфактум, а трассировки — отдельные запросы. Метрики — это непрерывная картина: сколько запросов в секунду, какой процент с ошибками, сколько памяти занято, не переполнен ли пул соединений. Именно метрики первыми сигнализируют об аномалии, ещё до жалоб пользователей.
Ограничение у метрик одно и оно арифметическое: Prometheus заводит отдельный временной ряд на каждую уникальную комбинацию значений тегов, поэтому новый тег не прибавляет ряды, а умножает их.
Каждый новый тег не прибавляет ряды, а умножает их: 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.
Путь одного числа от счётчика в коде до графика: смотрите, что 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_rublesqueue.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даёт и метрику, и спан, разделяя теги низкой и высокой кардинальности.
Что почитать дальше
- Трассировка запросов — когда нужна детализация по конкретному запросу, а не агрегат.
- Логирование — структурированные JSON-логи и контекст через MDC.
- Конфигурация observability — management port, профили Logback.
- Health checks — liveness vs readiness и кастомные индикаторы.