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

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

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

Сначала вопрос, потом имя

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

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

В этой статье разбирается только серверная HTTP-граница. Доступность браузера, DNS, сеть, база, очередь и ручное действие оператора остаются отдельными сигналами. Один counter не доказывает, что пользователь увидел корректный экран. Он показывает, что выбранная операция завершилась с указанным исходом.

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

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

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

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

Для длительности храните секунды и собирайте распределение наблюдений. Histogram даёт buckets, сумму и количество наблюдений. Summary также считает сумму и количество, но его квантили имеют другую семантику и требуют отдельного выбора. В первом сигнале не нужно обещать p95 или SLO. Сначала договоритесь, что операция имеет стабильное имя и что единица времени одинакова везде.

Суффикс _total показывает накопительную природу counter. В имени длительности есть _seconds. Не смешивайте миллисекунды и секунды под одним именем. Иначе запросы будут синтаксически корректными, но сравнение значений станет ложным.

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

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

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

Если значение label нельзя перечислить заранее, остановитесь и проверьте его смысл. Сколько рядов оно добавит? Какой запрос использует его? Можно ли нормализовать маршрут до шаблона, например /orders/:id, вместо полного URL с идентификатором заказа? Ответы должны быть в контракте до публикации метрики.

Учебный пример: четыре события и два snapshots

Ниже не клиент Prometheus и не результат нагрузки. Это учебный набор с выдуманными значениями. Он проверяет разрешённые labels, суммирование ошибок и поведение при неизвестной операции. В реальном сервисе инкремент выполняет выбранная библиотека, а результат нужно проверить на нужной версии сервера и с реальным интервалом scrape.

const allowedOperations = new Set(['catalog', 'checkout', 'profile']);<br>const allowedOutcomes = new Set(['success', 'error']);<br><br>const events = [{ operation: 'checkout', outcome: 'success', durationSeconds: 0.42 }, { operation: 'checkout', outcome: 'error', durationSeconds: 0.90 }, { operation: 'catalog', outcome: 'success', durationSeconds: 0.18 }, { operation: 'checkout', outcome: 'error', durationSeconds: 1.10 }];<br><br>for (const event of events) {<br>  if (!allowedOperations.has(event.operation)) throw new Error('unknown operation');<br>  if (!allowedOutcomes.has(event.outcome)) throw new Error('unknown outcome');<br>  if (event.durationSeconds &lt; 0) throw new Error('invalid duration');<br>}<br><br>// Учебный результат: checkout/error = 2.<br>// Это не production-данные и не готовый alert rule.

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

\"Схема
Сигнал отделяет факт завершения операции от следующего шага расследования.

Запрос и порог

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

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

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

Запрос зависит от точек scrape и от обработки reset counter. Локальная разность двух чисел в памяти не доказывает, что production PromQL вернёт такое же значение. При перезапуске процесса, задержке scrape или отсутствии ряда результат требует отдельной проверки. Нулевое значение также не всегда означает «ошибок не было»: ряд мог ещё не появиться.

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

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

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

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

Ограничения

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

Порог нельзя назначать по удобному числу. Нужны период наблюдения, стоимость ошибки, ожидаемый трафик и владелец реакции. Если трафик почти нулевой, две ошибки и две тысячи ошибок имеют одинаковое значение в абсолютном counter, но разный смысл для продукта. Для сравнения сервисов нужна доля ошибок или другой согласованный показатель, а не копирование окна.

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

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

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

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

"}