Всем привет, на связи Тимур, senior Android-разработчик с более чем 5 годами опыта. Сейчас работаю в платформенной команде продуктовой компании — мы отвечаем за стабильность, производительность и всё то, что “никто не замечает, пока всё работает, и все замечают, когда перестаёт”.
В какой-то момент простого дашборда уже не хватает. Хочется, чтобы метрики не просто “где-то красиво лежали”, а сами приходили и били в лицо, когда что-то пошло не так.
В моей истории это был дашборд по перформансу (о нем думаю расскажу в следующих статьях), а пока давайте рассмотрим случай с Hitch Rate (лагучесть UI) в Android-приложении.
Данные летят в ClickHouse, поверх них — Grafana, и нужно:
посчитать 90 перцентиль "лагучести" по каждому экрану;
если метрика выше порога — слать алерт в Slack;
в сообщении сразу видеть: какие именно экраны плохие и с какими значениями.
На бумаге задача звучит как “ну сейчас за 15 минут сделаю”. В реальности я:
ломал голову над Unified Alerts;
ловил ошибки вроде only data source queries may be inputs to a classic condition;
получал пустые сообщения в Slack;
упирался в Invalid evaluation interval и странное поведение For / evaluation interval в rule groups.
В этой статье пройду по тому же пути, только аккуратно и без боли:
расскажу, как устроен Grafana Unified Alerting, чем отличаются queries и expressions, как собрать нормальное Slack-уведомление и где вас ждут грабли.
Что такое Hitch Rate и зачем он бизнесу
Если совсем по-простому:
приложение рисует кадры с частотой 60 FPS (или 90/120 на некоторых устройствах),
каждый кадр должен успеть отрендериться до следующего vsync,
если рендер занял слишком много времени — кадр «дёрнулся» или «подвис».
Hitch Rate — это доля таких проблемных кадров.
Например, если из 1000 кадров:
920 — ок,
80 — лаги,
то Hitch Rate = 80 / 1000 = 8%.
Можно думать так:
FPS отвечает за “сколько кадров в секунду”, а Hitch Rate — за “сколько из них были неприятными”.
Для бизнеса это тоже не абстракция:
Это прямая метрика UX-плавности: у экрана с Hitch Rate 3% ощущение «шёлковое», с 15% — “ну что-то подлагивает”.
-
Можно поставить простой SLA:
«p90 Hitch Rate на ключевых экранах < 10%».
Дальше уже можно строить нормальную историю: «после оптимизации карточки товара Hitch Rate с 18% упал до 7%, конверсия выросла на X%».
Короче, это цифра, за которую не стыдно спорить с бизнесом.
Осталось только научиться по ней алертиться.
1. Как вообще устроен Grafana Unified Alerting
Начиная с версии 8+ у Grafana единая модель алертов. Если раньше можно было жить в режиме “алерт у панели”, то сейчас всё крутится вокруг нескольких сущностей:
-
Alert rule — правило, которое:
выполняет один или несколько queries (запросов в БД / Prometheus / и т.д.),
опционально прогоняет результаты через expressions (Reduce, Math, Threshold),
выбирает один query или expression как condition (если он “true” → алерт Firing).
-
Rule group — набор правил, у которых:
общий evaluation interval (как часто считаться),
общая синхронизация (чтобы всё считалось в один тик).
-
Это как cron-job на уровне группы:
“раз в минуту посчитать все performance-алерты”.
Contact point — куда слать уведомление (Slack, email, Telegram и т.п.).
-
Notification policy — кого и при каких лейблах уведомлять.
алерты с team = mobile → в Slack-канал #mobile-alerts;
алерты с env = prod → ещё и в #oncall.
И один важный момент, который ломает голову всем, кто привык к “один алерт = одно число”:
Один alert rule может создавать много alert instances
— по одному на каждую комбинацию label’ов (например, по одному на каждый экран).
Это не баг, а фича. Но к ней надо привыкнуть.
2. Query: считаем p90 Hitch Rate по экранам
Возьмём обобщённый ClickHouse-запрос.
Пусть у нас есть таблица metrics.mobile_events с JSON-полем event_json, где лежит Hitch Rate и название экрана.
В Grafana пишем Query A (упрощённо):
/* Hitch Rate p90 per screen */
SELECT
screen,
round(
quantile(0.90)(
toFloat64OrNull(JSON_VALUE(event_json, '$.hitch_rate'))
),
2
) AS hitch_p90
FROM metrics.mobile_events
WHERE
app = 'my.app.id'
AND $__timeFilter(timestamp)
AND event_type = 'PERFORMANCE'
AND JSON_VALUE(event_json, '$.hitch_rate') IS NOT NULL
GROUP BY
screen
HAVING
hitch_p90 > ${hitch_threshold} -- например, 5
ORDER BY
hitch_p90 DESC;
В Grafana этот запрос (Query A) вернёт таблицу примерно такого вида:
screen |
hitch_p90 |
MAIN |
7 |
SPLASH |
4 |
PROFILE |
3 |
FAVORITES |
1 |
Grafana превратит каждую строку в отдельную серию с label’ом:
screen="MAIN"
screen="SPLASH"
…
Это фундаментально важно:
Вы не получаете “одну метрику Hitch Rate”, вы получаете целый набор метрик — по одной на каждую комбинацию лейблов.
3. Почему алерт может «раздвоиться»: один rule — много инстансов
Теперь представим, что мы выбрали Query A как alert condition (без Expressions) и сказали:
“Если hitch_p90 > 10 — стреляй”.
Grafana делает следующее:
для каждой серии (screen="MAIN", screen="SPLASH"…) создаёт отдельный alert instance;
-
получается пачка алертов:
alertname = HighHitchRate, screen = MAIN
alertname = HighHitchRate, screen = SPLASH
alertname = HighHitchRate, screen = CART
…
В UI это видно в View rule → State: там список инстансов с разными screen и значением Value.
На этом шаге многие думают “что за дичь, я же хотел один алерт”. Но правда в том, что это очень удобно:
вы не получаете абстрактное “что-то плохо с Hitch Rate”;
вы видите конкретно: “плохо с MAIN, SPLASH и CART”.
4. Queries vs Expressions: что за B, C и откуда ошибки
В Unified Alerting есть два основных сценария:
-
Простой:
Только Query A, без Expressions.
Alert condition = A.
Классика: “если A > threshold — алерт”.
Это похоже на “старую” Grafana: одно значение → одно условие.
-
С выражениями (Expressions):
Query A — данные.
Expression B (Reduce) — считаем что-то поверх A (count, max, avg и т.д.).
Expression C (Threshold) — проверяем условие (например, B > 0).
Alert condition = C.
-
Пример сценария:
A возвращает hitch_p90 по всем экранам;
B считает Series count — сколько экранов превысили порог;
C проверяет: “если count > 0 → алерт”.
Типичные грабли:
-
only data source queries may be inputs to a classic condition, B is a Expression
→ вы пытаетесь использовать Classic condition на Expression, а он понимает только raw queries;
-
failed to parse expression 'B': invalid math command type: expr: non existent function A
→ в выражении обращаетесь к A как к функции, а не как к входу.
Мораль простая:
либо живём только с Query A и делаем его condition;
либо строим чёткую цепочку A → B → C и помечаем C “Set as alert condition”;
classic condition при этом обычно вообще не нужен.
5. Slack: почему шаблон даёт пустоту или мусор
Второй набор граблей — шаблоны сообщений.
5.1. Контекст шаблона в Grafana-managed alerts
Для Grafana-управляемых алертов (Unified Alerting) в Slack-шаблоне доступны, например:
{{ range .Alerts }} {{ .Status }} // firing, resolved {{ .Labels }} // карта лейблов {{ .Annotations }} // description, summary и т.д. {{ .ValueString }} // уже красиво отформатированное значение {{ end }}Типичный рабочий шаблон:Типичный рабочий шаблон:
High Hitch Rate Alert Проблемные экраны (p90 > {{ .Annotations.threshold }}%): {{ range .Alerts }} - {{ index .Labels "screen" }} — p90 hitch = {{ .ValueString }} {{ end }}Распространённые ошибки:
Использовать {{ .Alerts.Firing }} или .Matches — это синтаксис Alertmanager, а не Grafana-managed alerts → в ответ приходит пустота.
Использовать {{ .Values }} — и получать страшный вывод вида:
[ var='A' labels={screen=MAIN} value=7 ], [ var='B' labels={screen=MAIN} value=1 ]потому что .Values — это “сырые значения всех series”, а не итог алерта.
5.2. Как красиво вывести именно экраны
Тот же запрос A с screen в лейбле даёт нам удобный шаблон:
:rotating_light: High Hitch Rate Alert Проблемные экраны (p90 > {{ .Annotations.threshold }}%): {{ range .Alerts }} - {{ index .Labels "screen" }} — p90 hitch = {{ .ValueString }} {{ end }}Если у screen другое имя (например, metric), просто подставляем его.
6. Почему значение выглядит как 1e+00, и что с этим делать
В UI и в уведомлениях иногда можно увидеть что-то вроде:
Value = 1e+00
Value = 2.2e+00
Это просто формат float в научной нотации.
Чтобы не форматировать руками, лучше использовать:
{{ .ValueString }}а не .Value.
7. Rule groups, evaluation interval и ошибка “Invalid evaluation interval”
Ещё один “приятный сюрприз”:
Invalid evaluation interval. Evaluation interval should be smaller or equal to 'For' values for existing rules in this group.
Что происходит:
правила объединены в rule groups;
у группы есть evaluation interval — например, 1 минута;
у каждого правила есть For — сколько времени условие должно быть true, прежде чем алерт станет Firing.
Ограничение:
evaluation interval ≤ For для всех правил в группе.
Пример:
у группы mobile-performance стоит evaluation interval = 1m;
вы создаёте новое правило с For = 30s;
получаете ошибку.
И наоборот:
если хотите правило, которое считается раз в неделю (evaluation interval = 7d),
то либо оно живёт в отдельной группе,
либо For у него не может быть меньше 7 дней.
Метафора:
Rule group — это общий “метроном”.
Все правила в группе обязаны шагать в такт с ним.
8. Как протестировать алерт и Slack, не дожидаясь “раз в неделю”
Пара практических лайфхаков:-
Временно уменьшить evaluation interval и For
Поставить eval interval, например, 1m.
Поставить For = 0m (или маленькое значение).
Немного подождать, посмотреть, что rule переходит в Alerting, и прилетает нотификация.
-
Использовать Preview в редакторе правила
В разделе alert rule есть вкладка Preview.
Она показывает, вернёт ли ваш query/condition сейчас firing/ok и какие значения.
-
Сделать временный тестовый rule
С тем же запросом,
но более жёстким/мягким порогом, чтобы гарантированно получить Firing,
и отдельным Slack contact point.
Так можно спокойно отладить шаблон и формат сообщения, не трогая боевой алерт.
9.Итог: с какой мыслью хочется уйти
Grafana Unified Alerting на старте ощущается как:
“Я просто хотел, чтобы мне в Slack приходила цифра,
а в итоге изучаю маленький оркестр с дирижёром, группами и собственной DSL”.
Но если принять три опорные идеи:
Query может вернуть много серий → это нормальный способ сказать “у тебя много объектов (экранов/подов/серверов)”.
Expressions — это просто ещё один слой вычислений поверх query, а не какая-то магия.
В Slack-шаблоне нужно итерироваться по .Alerts и брать .Labels + .ValueString, а не пытаться тащить всё через .Values.
— то Grafana Alerting перестаёт быть страшной, и превращается в очень удобный инструмент:
метрики продолжают “красиво лежать” на дашбордах;
а когда что-то пошло не так, они сами приходят и говорят “эй, посмотри сюда, тут Main-экран с p90 Hitch Rate 18%”.
Если вы только начинаете разбираться с Unified Alerting — надеюсь, эта статья сэкономит вам несколько часов жизни, пару бессмысленных “почему Slack пустой?” и одну-две нервные клетки ?