{"index":267,"slug":"editorial-2020-08-practice-metrics-basics","title":"Метрики приложения: как превратить сбой в проверяемый сигнал","excerpt":"Общий график запросов не отвечает, где возникла ошибка и что проверять дальше. Разбираем небольшой контракт для HTTP-операции: границу события, тип метрики, ограниченные labels, учебный запрос и действие после порога.","contentHtml":"

Представим рабочую проверку в августе 2020 года. Дежурный инженер видит, что график HTTP-запросов растёт, но не может определить, в какой операции появились ошибки. Он открывает несколько дашбордов, сверяет несвязанные пики и вручную ищет строки в журнале. За это время задерживается разбор инцидента, а первое исправление строится на догадке.

Быстрый способ ухудшить сигнал — добавить в label полный URL, идентификатор пользователя или текст исключения. Prometheus получит множество почти уникальных временных рядов, но следующий шаг расследования не станет яснее. В этой заметке соберём небольшой учебный контракт: одна серверная HTTP-граница, стабильное имя операции, два исхода и отдельный способ искать контекст в логах или трассировке.

Сценарий и вопрос

Начните не с названия метрики, а с вопроса, на который должен ответить запрос. Например: «Были ли ошибки завершения checkout за последние десять минут и какую проверку выполнить, если их не меньше двух?» Здесь явно заданы операция, исход, окно и действие. Такой вопрос ограничивает модель: мы измеряем завершённую серверную операцию, а не весь пользовательский путь.

Граница события проходит там, где обработчик уже знает результат. Инкремент в начале обработчика считает попытки, но не доказывает успех. Для ошибок нужен тот же момент завершения и тот же словарь операций. Если внешний платёжный провайдер отвечает позже, это уже другая граница, которую следует измерять отдельным сигналом.

Контракт сигнала для учебного сценария
ВеличинаТип и единицаLabelsВопросНе доказывает
store_http_requests_totalcounter, завершённые запросыoperation, outcomeКакая операция и с каким исходом завершилась?Причину ошибки и путь до пользователя
store_http_request_duration_secondshistogram, секундыoperationКак распределяется длительность?Причину медленного ответа
increase(...) за 10 минутзапрос к counteroperation=checkout, outcome=errorПересекла ли фикстура учебный порог?Production-порог и SLO

Тип метрики следует из состояния

store_http_requests_total — накопительный counter. Он увеличивается при событии и может сброситься к нулю после перезапуска процесса. Сырое значение редко отвечает на вопрос о недавнем окне; для количества событий за интервал применяют функцию над изменением counter. В учебном запросе ниже используется increase(), а для скорости событий обычно применяют rate().

Gauge описывает состояние, которое может расти и уменьшаться: число запросов в работе, свободную память или температуру. Нельзя использовать gauge для числа ошибок за всё время работы процесса: новое значение затрёт предыдущий снимок. Тип выбирают по форме величины, а не по удобству вызова библиотеки.

Длительность лучше хранить в секундах и собирать как распределение. Классическая histogram представляет наблюдения bucket-счётчиками, а также суммой и количеством; по bucket-данным можно вычислять квантили и агрегировать экземпляры при согласованных границах. Summary тоже публикует сумму и количество, но его заранее рассчитанные квантили нельзя пересчитать для другого окна и нельзя корректно агрегировать между репликами. Поддержку native histograms и точный API конкретной библиотеки нужно проверять отдельно: учебный контракт ниже её не предполагает.

Имя метрики должно показывать величину и базовую единицу: суффикс _total у накопительного счётчика и _seconds у длительности. Не смешивайте миллисекунды и секунды под одним именем. Иначе запросы останутся синтаксически правильными, но сравнение будет ложным.

Выбор типа по форме данных
ТипЧто измеряетПримерПроверка ошибки выбора
CounterНакопленные события, с возможным resetЗавершённые HTTP-запросыЗначение не должно уменьшаться в обычной работе
GaugeТекущий снимок, который меняется в обе стороныЗапросы в работеНе применять rate() к gauge
Classic histogramРаспределение в заданных bucket, сумма и количествоДлительность запросаПроверить границы bucket и возможность агрегации
SummaryСумму, количество и заданные квантили окнаЛокальная оценка latencyНе складывать квантили разных экземпляров

Labels должны отвечать на вопрос

Каждая уникальная комбинация имени метрики и значений labels образует отдельный временной ряд. Поэтому operation должен брать значения из короткого словаря: например, catalog, checkout, profile. Для outcome достаточно success и error. Такой набор можно перечислить заранее, проверить тестом и использовать в запросах.

Не добавляйте в этот counter user_id, email, полный URL, request ID, текст исключения или произвольный статус из запроса. Это контекст отдельного события, а не агрегированное измерение. Поместите его в журнал или трассу и свяжите с метрикой через correlation ID. Label должен разделять небольшое число веток, а не хранить историю каждого пользователя.

Сначала зафиксируйте словарь значений label и вопрос, который он должен отвечать. Затем проверьте, сколько рядов создаст каждая комбинация и какой запрос использует это измерение. После этого можно публиковать сигнал. Если маршрут содержит ID заказа, нормализуйте его до шаблона вроде /orders/:id. Если и шаблонов слишком много, оставьте в метрике только операцию, а конкретный адрес ищите по логу.

Воспроизводимый учебный пример

Следующий фрагмент не подключает клиент Prometheus и не выдаёт результат production-нагрузки. Это маленькая проверка контракта, которую можно запустить в Node.js: она принимает четыре события, отбрасывает неизвестные значения и считает ошибки только после проверки их границы.

const allowedOperations = new Set(['catalog', 'checkout', 'profile']);\nconst allowedOutcomes = new Set(['success', 'error']);\nconst events = [\n  { operation: 'checkout', outcome: 'success', durationSeconds: 0.42 },\n  { operation: 'checkout', outcome: 'error', durationSeconds: 0.90 },\n  { operation: 'catalog', outcome: 'success', durationSeconds: 0.18 },\n  { operation: 'checkout', outcome: 'error', durationSeconds: 1.10 },\n];\n\nfor (const event of events) {\n  if (!allowedOperations.has(event.operation)) throw new Error('unknown operation');\n  if (!allowedOutcomes.has(event.outcome)) throw new Error('unknown outcome');\n  if (event.durationSeconds < 0) throw new Error('invalid duration');\n}\n\nconst checkoutErrors = events.filter((event) =>\n  event.operation === 'checkout' && event.outcome === 'error',\n).length;\nconsole.log(checkoutErrors); // 2

Ожидаемый вывод — 2. Это не значение Prometheus и не готовое правило оповещения. В настоящем обработчике после такой проверки библиотека увеличит counter с теми же labels и передаст длительность в histogram. Точный вызов зависит от языка и клиента, поэтому его нельзя выдавать за универсальный API.

Отрицательный путь здесь обязателен. Если новый endpoint молча создаёт неизвестное значение operation, график может продолжить работать, а стоимость хранения и смысл агрегации изменятся. В рабочем коде ошибку нужно вернуть вызывающему слою или записать в отдельный технический сигнал, а не продолжать публикацию с неподтверждённым label.

Схема выбора метрики: вопрос об операции, counter с operation и outcome, длительность в секундах, учебный запрос и действие после порога
Сигнал отделяет факт завершения операции от следующего шага расследования.

Запрос и границы порога

Для учебной серии запрос может выглядеть так:

sum(increase(store_http_requests_total{operation=\"checkout\",outcome=\"error\"}[10m])) >= 2

Выражение суммирует изменение counter за десять минут и проверяет условие. Число 2 выбрано для упражнения: одна ошибка показывает, что label работает, две переводят пример в следующую ветку. Оно не получено из трафика, не является допустимой долей ошибок и не должно копироваться в production alert.

increase() работает по диапазону наблюдений и учитывает reset counter, но результат зависит от точек scrape и наличия ряда. Локальная разность двух чисел в памяти не доказывает, что PromQL вернёт такое же значение. После перезапуска процесса, задержки scrape или пропуска ряда нужна отдельная проверка. Отсутствие ряда также не следует автоматически читать как ноль ошибок.

Если сервис запущен в нескольких экземплярах, sum() объединяет их только при совместимом наборе labels. До запроса проверьте, не добавляет ли инфраструктура собственные измерения, и решите, нужно ли сохранить разрез по экземпляру для диагностики. Для пользовательского опыта метрику надо связать с логом, трассировкой и клиентской проверкой: серверный counter не доказывает, что браузер отобразил правильный экран.

Симптом → причина → проверка → действие

Диагностика первого сигнала
СимптомПричинаПроверкаДействие
Есть общий рост, но не видна операцияНет ограниченного label operationПосмотреть series и словарь операцийДобавить короткий словарь и тест неизвестного значения
График быстро растёт числом рядовВ label попал ID, URL или другой unbounded valueНайти значения с высокой уникальностьюПеренести контекст в лог или trace
Ошибки считают попыткиCounter увеличивается до определения исходаСопоставить точку инкремента с завершениемРазделить attempts и outcomes или перенести инкремент
Порог ведёт себя неверно после перезапускаСырые значения counter сравнивают как gaugeПроверить reset и range vectorИспользовать rate() или increase()
Нет записи до первой ошибкиРяд появляется только после событияЗапросить известный labelset заранееИнициализировать нулевую серию, если это оправдано

Порядок внедрения

  1. Запишите операционный вопрос: операция, исход, окно и действие после порога.
  2. Выберите границу завершения и определите, что считается успехом и ошибкой.
  3. Составьте allowlist для labels и оставьте только измерения, которые можно перечислить и агрегировать.
  4. Назовите метрики по одной величине и базовой единице; проверьте _total и _seconds.
  5. Добавьте проверки неизвестной операции, исхода и отрицательной длительности.
  6. Прогоните контролируемую серию и сравните ожидаемое число ошибок с exposition или API выбранной библиотеки.
  7. Проверьте PromQL на тестовом Prometheus; смоделируйте reset, отсутствие ряда и задержку scrape.
  8. Свяжите каждую ветку порога с действием: открыть лог, повторить сценарий, проверить релиз или ничего не делать при учебном нуле.

Ограничения

Эта схема показывает контракт сигнала, но не причину ошибки. Для причины понадобятся логи, трассы, код ответа, версия релиза и контекст внешних зависимостей. Она не измеряет путь браузера, DNS, сеть и базу, если запрос не дошёл до сервера. Она также не выбирает за команду SLO и не говорит, сколько ложных срабатываний допустимо.

Точный синтаксис клиента, поддержка native histograms и формат exposition зависят от версии и языка. Для старой установки сначала сверяйте версию Prometheus и документацию используемой библиотеки. Нельзя переносить учебный порог 2, словарь операций или bucket-границы в production без наблюдения и отдельного решения владельца сервиса.

Учебные операции, события, длительности и порог выдуманы. Здесь нет production-нагрузки, измеренного уменьшения инцидентов или готового alert. Проверяемый результат скромнее: один вопрос превращён в контракт, который можно прогнать, запросить и связать с конкретным следующим действием.

Критерий готовности

Сигнал готов к первой проверке, если другой инженер без устного объяснения может назвать границу события, перечислить допустимые labels, воспроизвести учебный вывод 2, выполнить запрос и пройти отрицательный путь с неизвестным label. После reset и пропущенного ряда команда понимает, что означает «нет данных», а что — «ошибок не было». Если пункт не выполняется, уточняйте контракт и проверку, а не добавляйте новые labels.

Проверяемые источники

"}