{ "index": 265, "slug": "editorial-2020-08-field-metrics-basics", "title": "Метрики приложения: как превратить график в проверяемое действие", "excerpt": "Пользователь видит ошибку, а общий график запросов не объясняет причину. Разбираем counter, окно, labels и проверку учебного порога без выдуманных production-выводов.", "contentHtml": "
Рассмотрим дежурный сценарий: пользователь сообщает, что checkout иногда завершается ошибкой. На dashboard линия requests_total растёт ровно, аварий в логах нет. Команда смотрит на общий график и не понимает, что проверять дальше. Цена ошибки — не только пропущенный сбой. Можно поднять шумный alert, увеличить timeout без причины или потратить час на чтение логов, которые не связаны с нужной операцией.
Вопрос здесь не в количестве линий на графике. Метрика помогает принять решение, когда её границы совпадают с вопросом: названы операция, событие, окно и следующий шаг. Один общий счётчик показывает, что система что-то делала. Он не показывает, сколько ошибок произошло в checkout и можно ли связать их с конкретным запросом.
\nДо выбора типа метрики запишите наблюдаемый симптом без диагноза: какой пользовательский путь нарушен, в какое время это происходит и какое действие нельзя делать вслепую. В нашем примере нужно ответить на узкий вопрос: сколько завершённых запросов checkout закончилось ошибкой за последние десять минут?
\nТакой вопрос сразу задаёт границы. Событие — завершённый запрос с известным исходом. Операция — checkout. Окно — десять минут. Следующий шаг после сигнала — открыть журнал или trace, а не объявить причину найденной. Если поменять любую из этих границ, число перестанет отвечать на исходный вопрос.
\nДля завершённых запросов подходит counter — накопительная метрика событий. Она растёт и может сброситься при перезапуске процесса. Для вопроса «сколько ошибок checkout появилось за десять минут» нужен прирост counter, а не его последняя сырая величина.
\nДля вопроса «сколько запросов выполняется сейчас» нужен gauge: он описывает текущее состояние и может расти или уменьшаться. Для вопроса «как распределилась длительность» нужна отдельная метрика наблюдений, например histogram. Не называйте один тип другим только потому, что все они отображаются линиями.
\nПоследнее значение store_http_requests_total зависит от времени жизни процесса. Оно не равно числу ошибок за выбранное окно. В PromQL такой вопрос выражает increase():
# Учебный запрос. Не production-alert без проверки окружения.\nsum(increase(store_http_requests_total{operation="checkout", outcome="error"}[10m])) >= 2\nВыражение выбирает series с двумя ограниченными labels, считает прирост за десять минут и сравнивает его с учебной границей. Оно не сообщает причину ошибки. После срабатывания нужно открыть связанный журнал, trace или изолированный сценарий.
\nincrease() учитывает reset counter после перезапуска target и рассчитывает прирост на указанном диапазоне. Поэтому ручная разность двух крайних snapshots полезна для объяснения идеи, но не должна подменять выполнение PromQL. На результат влияют время samples, scrape interval, пропуски scrape и границы range vector.
operation=checkout отделяет логическую операцию. outcome=error отделяет ошибку от успешного завершения. Набор значений ограничен заранее: операции и исходы берутся из небольшого списка. Такой label позволяет агрегировать данные и сравнивать ветки, не создавая отдельное имя метрики для каждой ветки.
Не добавляйте в labels request_id, email, полный URL или другой идентификатор, который почти уникален для каждого события. Каждая комбинация labels создаёт отдельную time series. Уникальный идентификатор превратит counter в поток почти одноразовых рядов. График потеряет агрегирование, а Prometheus получит дополнительные затраты памяти, CPU, диска и сети. Конкретный запрос ищут в журнале по корреляционному идентификатору.
# Хорошая граница метрики\nstore_http_requests_total{operation="checkout", outcome="error"}\n\n# Плохая граница: уникальный label разрушает агрегацию\nstore_http_requests_total{operation="checkout", request_id="8f2d..."}\nЕсли нужна доля ошибок, знаменатель должен описывать ту же границу. Ошибки, посчитанные внутри приложения, нельзя без проверки делить на все попытки, посчитанные на proxy. Сначала убедитесь, что обе величины относятся к одной операции, одному времени и одной точке завершения.
\nНиже приведены выдуманные snapshots. Они нужны только для проверки арифметики. В этом примере counter ошибки checkout равен нулю в 10:00, единице в 10:05 и двум в 10:10:
\n# Не production telemetry. Значения заданы для учебного примера.\nstore_http_requests_total{operation="checkout",outcome="error"}\n2020-08-01T10:00:00Z 0\n2020-08-01T10:05:00Z 1\n2020-08-01T10:10:00Z 2\n\n# В упрощённой модели: 2 - 0 = 2\n# Учебная граница >= 2 пересечена.\nПовторить разбор можно в таком порядке: зафиксировать ту же series, подставить три значения в выбранное окно, проверить арифметику 2 - 0 = 2, затем выполнить PromQL на доступном сервере метрик. Отдельно повторите пример с последним значением 1: он не должен пересекать границу >= 2. Это проверяет учебную логику, но не проверяет exporter, scrape или alert delivery.
В настоящем Prometheus increase() работает по samples range vector, а не по обещанию ровно трёх точек. Она корректирует reset counter и экстраполирует расчёт на границы окна. Поэтому простая разность полезна как контрольный пример, но не как формула для ручного production-расчёта.
| Симптом | Возможная причина | Проверка | Действие |
|---|---|---|---|
Пользователь сообщает об ошибке, а общий requests_total растёт | Счётчик смешивает операции и исходы | Проверить metric name и доступные labels | Выбрать bounded operation и outcome; не менять timeout |
increase(...[10m]) = 0 | В series не было прироста или путь не инструментирован | Сверить operation с жалобой, время scrape и target | Не объявлять систему здоровой; проверить журнал и покрытие |
| Последняя величина counter большая | Процесс давно работает | Сравнить прирост за окно, а не абсолютное значение | Использовать increase() |
| У каждой ошибки отдельная series | В label попал request ID или полный URL | Найти динамические значения и посчитать series | Убрать уникальное поле из metric contract; оставить его в логе |
| Порог срабатывает после редкого scrape | Окно и частота сбора не согласованы | Сопоставить range vector, scrape interval и пропуски | Изменить окно или сбор после проверки владельца alert |
| Две ошибки есть, но причина неизвестна | Метрика агрегирует событие без контекста | Найти trace или журнал по времени и correlation ID | Открыть ограниченную ветку расследования |
Таблица разделяет наблюдение и действие. Значение два в учебном примере — не универсальный порог. В production порог зависит от объёма трафика, допустимой доли ошибок, стоимости ложного сигнала, времени реакции и владельца. Если эти условия не названы, число выглядит точным, но не является рабочим правилом.
\nМетрика не видит событие, которое не дошло до точки instrumentation. Ошибка в браузере может произойти до приложения. Пропущенный scrape оставит окно неполным. Перезапуск процесса изменит сырое значение counter, хотя функция запроса умеет учитывать reset. Неправильный label может спрятать нужную операцию в общей series.
\nПоэтому ноль ошибок не равен нулю пользовательских проблем. Проверьте, что target жив, series существует, timestamp попадает в окно, а журнал содержит тот же путь. Если условия не выполняются, честный результат — «сигнал недостаточен», а не «система исправна». Это отрицательное решение экономит время и не создаёт ложной уверенности.
\nУчебные snapshots и код выше не измеряют память Prometheus, latency, нагрузку, доставку alert или поведение exporter. Они не подтверждают production-результаты. Их задача — показать связь между операционным вопросом, типом метрики, labels, окном и проверкой. Для реального внедрения нужен отдельный стенд с известным scrape, контролируемым запросом и сохранённым ответом PromQL.
\nРазбор готов, если другой инженер может повторить его без устной подсказки: назвать series и её labels, объяснить момент увеличения counter, выполнить запрос с указанным окном, увидеть результат на доступном наборе данных и перейти от срабатывания к одному связанному журналу или trace. Дополнительно он должен показать отрицательный путь: почему ноль не закрывает жалобу, если metric не покрывает endpoint или scrape пропущен.
\nincrease(), counter reset и расчёта прироста в range vector.