В Grafana есть режим публикации дашборда наружу: генерируется токен, по нему дашборд открывается без учётной записи, только на чтение. В интерфейсе это две галочки.
Я потратил на эти две галочки несколько дней. Не потому, что там сложно нажать, а потому, что публикация мониторинга наружу ломается в местах, о которых не думаешь заранее: переменные, лимиты запросов, метки, которых ждёт дашборд. Ниже разбор всего, что я собрал по дороге — девять дашбордов, синтетический контур и один эпизод, где мою же витрину положил мой же nginx.
Сколько запросов на самом деле стоит один дашборд
Начну с числа, потому что дальше всё упирается в него.
Публичный дашборд отдаёт своё определение анонимно, обычным GET. Значит, состав можно посчитать снаружи, не заходя в Grafana:
bash
curl -s https://<host>/grafana/api/public/dashboards/<accessToken> \ | jq '[.dashboard.panels[] | select(.type != "row")] | length'
У меня по девяти дашбордам получилось так:
Дашборд | Панелей всего | Из них с запросами |
|---|---|---|
Linux-серверы | 133 | 117 |
Kubernetes | 36 | 33 |
Windows-серверы | 21 | 21 |
1С (кластер) | 19 | 16 |
Журналы | 11 | 5 |
Сеть (SNMP) | 7 | 7 |
Docker | 6 | 6 |
Сайты и порталы | 6 | 6 |
Периферия | 6 | 6 |
Итого | 245 | 217 |
Разница между колонками — ряды и текстовые панели, запросов они не делают.
Двести семнадцать панелей, у каждой свой запрос, окно шесть часов, автообновление раз в 30 секунд. Дальше это число всплывёт дважды.
Отдельный тенант
Сначала я хотел показать боевой дашборд с обезличенными именами. Отказался после того, как прикинул объём работы: обезличить надо не только имена хостов, но и подсети на сетевых панелях, имена баз в подписях к сеансам 1С, топ процессов по памяти, где рядом с rphost стоит какая-нибудь внутренняя служба заказчика. Это ревизия двух сотен панелей, и достаточно пропустить одну.
Отдельно смешно, что у меня уже был тенант с именем demo — тот, на котором я гоняю тесты. Публиковать его было нельзя: внутри реальные имена хостов и адрес прод-сервера. «Демо» по названию, прод по содержимому.
Поэтому витрина живёт в отдельном тенанте demo-retail: свой accountID, своя организация в Grafana. Боевые ряды туда физически не попадают, запрос уходит в другой тенант.
Дашборды не рисуются руками
Отдельные «красивые дашборды для демо» умирают за месяц: продукт уезжает, витрина остаётся.
Поэтому демо-дашбордов как объектов у меня нет. Есть скрипт mkdemo.py, который берёт боевые шаблоны tpl-* (те же, что платформа раскатывает клиентам), приводит их к публичному режиму и публикует. Идемпотентно: при пересборке ссылки сохраняются.
Приведение к публичному режиму — это конкретный список операций, и вот из чего он состоит.
Что приходится снимать с дашборда
Переменные. Дословно из документации: «Variables and queries including variables are not supported». Боевой дашборд построен вокруг $host в выпадающем списке — один набор панелей на любое число серверов. В публичном режиме списка нет. Либо агрегат по всем, либо фиксированные значения в запросах. Проверить, что переменные действительно вырезаны, можно снаружи: в JSON публичного дашборда templating.list пуст.
Запросы после снятия переменных. С PromQL относительно мирно. LogsQL сломался: фильтр
host:in($host) level:in($level) ${filter:raw}
после наивной подстановки превращается в host:in(.*) level:in(*), а VictoriaLogs такое отвергает, панели журналов остаются пустыми. Правильно вырезать фильтры-переменные целиком, оставив *, а не подставлять внутрь них «звёздочку».
hide: true. Скрытый таргет в боевом дашборде — обычное дело, вспомогательный ряд для трансформации. В публичном режиме он не скрывается, а исчезает вместе с данными. Флаг надо снимать.
panel id. Публичный режим ходит за данными по адресу /panels/<id>/query. В шаблонах дашбордов id обычно не проставлены, они назначаются при импорте. Если публикуете шаблон программно, id придётся расставить самому, иначе панель отвечает «Invalid panel id».
Аннотации. Поддерживается только источник -- Grafana -- с типом Annotations & Alerts, организационные не поддерживаются. И отдельно: запрос аннотаций по тегам вернёт совпадения со всех дашбордов организации, а не только расшаренного. То есть если демо стоит в одной организации с боевыми дашбордами и там есть теговые аннотации, наружу поедут подписи к чужим инцидентам. Отдельная организация закрывает это заодно с остальным.
Из остального неподдерживаемого: exemplars, library panels, Grafana Live и потоковые обновления, фронтенд-датасорсы и датасорсы через Reverse Proxy. Панель на фронтенд-датасорсе не ругается, она просто остаётся без данных.
Кэш запросов и rate limiting для публичных дашбордов — Enterprise. В OSS вы один на один с двумя сотнями панелей и произвольным числом зрителей.
Генератор
Один контейнер на python:3.12-alpine, лимит памяти 64 МБ. За тик в 30 секунд отдаёт 1276 серий и около шестидесяти строк журнала. Это вся стоимость «живости».
Первое, что понадобилось, — не формулы, а инвентарь. Вымышленная розничная сеть: два Linux-сервера (srv-app-01, srv-db-01), Windows-сервер приложений 1С, сервер кластера 1С, шесть контейнеров Docker, три узла и восемь подов Kubernetes, коммутатор Cisco и маршрутизатор MikroTik, четыре сайта, одиннадцать единиц периферии — принтеры, IP-камеры, датчики температуры и протечки, ИБП, кассы, ТСД. Адресация 10.20.30.0/24.
Второе — время. Случайное блуждание не годится, у настоящих метрик есть структура:
Суточный профиль. Рабочий день 9–19 МСК, два пика, около одиннадцати и около четырёх.
Регулярные события. Ночной перезапуск
rphost: кластер 1С действительно перезапускает рабочие процессы по лимиту памяти, и ступенька на графике должна быть.Инцидент. В 14:30 на десять минут растут CPU и время ответа, «API партнёров» отдаёт 502, доля ошибок в журналах ползёт вверх.
Корреляции. В инциденте едут вместе CPU, latency, коды ответа и логи. Метрики, живущие независимо друг от друга, выдают генератор быстрее всего: смотрящий листает два графика рядом.
Третье съело больше всего времени. Синтетика должна быть достоверной по меткам, а не только по значениям. Дашборд Kubernetes фильтрует kube_node_status_allocatable по unit="core" и unit="byte". Генератор отдавал метрику без метки unit: значения есть, панели «Node CPU Number of cores» и «Node Memory Ratio» пустые. Та же история с узловыми сериями cAdvisor, у которых дашборд ждёт id="/".
Диагностируется это отвратительно. Метрика в хранилище лежит, запрос синтаксически верен, панель пуста, ошибок нигде нет.
Проверка наполненности
Отсюда взялся checkdash.py: обходит все панели всех дашбордов и считает, сколько из них вернули хоть что-нибудь. Иначе «наполнить девять дашбордов» превращается в листание глазами.
Последний прогон: 208 панелей из 217 с данными.
Девять пустых оставил сознательно. Восемь на дашборде Linux — часы PPS, температуры hwmon, вентиляторы, блоки питания, частотное масштабирование процессора. На виртуальной машине этого не бывает, у клиента на VM они будут пустыми ровно так же. Нарисовать туда синтетику означало бы показать больше, чем даёт продукт. Девятая — «Flow Per Hour» на Windows, ей нужен час истории.
Пустая панель на витрине не всегда баг, и хорошо бы уметь отличать одно от другого не вручную.
Как витрину положил мой же nginx
У меня давно стоит защита от скрейперов: зона на 20 запросов в секунду с адреса. Настройка прожила месяцы без жалоб.
Открываю свежеопубликованный дашборд Linux — половина графиков пустая. Обновляю — пустые уже другие. Панели плавают от прогона к прогону, в логах приложения ничего.
Причина в числе из начала статьи. Открытие дашборда — это не один запрос, а запрос на каждую панель отдельно. У «Node Exporter Full» их сто семнадцать, уходят пачкой за секунду. Ограничитель делал то, для чего поставлен: пропускал двадцать, остальное отбрасывал. Для nginx мой единственный посетитель выглядел как скрейпер.
Лечится увеличением burst на публичном пути:
nginx
limit_req_zone $binary_remote_addr zone=grafana:10m rate=20r/s; location /grafana/api/public/ { limit_req zone=grafana burst=200 nodelay; proxy_pass http://grafana; }
Про nodelay стоит сказать отдельно, я на этом спотыкался. Без него burst не отбрасывает лишние запросы, а ставит их в очередь и растягивает по времени согласно rate. Формально ошибок нет, но открытие дашборда превращается в медленную построчную прорисовку на несколько секунд. С nodelay всплеск уходит сразу, а средняя скорость по-прежнему ограничена зоной.
У меня в итоге burst=200 на /grafana/api/public/ (столько же, сколько на закрытом /grafana/, где та же арифметика работала всегда) и 50 на саму страницу. Зона осталась прежней.
Правило, которое я отсюда унёс: считать лимиты не на пользователя, а на действие пользователя. Одно человеческое «открыть дашборд» — это сотня машинных запросов, и лимит, поставленный по интуиции, окажется меньше нужного на порядок.
Отсюда же верхняя оценка нагрузки: 217 панелей с обновлением раз в 30 секунд дают 434 запроса в минуту на одного зрителя, открывшего все девять дашбордов. На практике меньше — панели, до которых не долистали, Grafana не запрашивает, и обычно открывают один дашборд. Но проектировать надо от верхней оценки: она наступает в день публикации ссылки.
Что видно снаружи
Полезное упражнение — открыть свой стенд без cookies и посмотреть на ответ API.
bash
curl -s https://<host>/grafana/api/public/dashboards/<accessToken> | jq '.dashboard.panels[0]'
Что там оказалось:
Определения панелей отдаются целиком. Заголовки, описания, типы, раскладка. Структура дашборда публична. Если в заголовке панели написано «Нагрузка на кластер БАНК-ПРОД-1», это теперь публичный факт.
Запросы вырезаны. В каждой панели
targetsсхлопнут до[{"refId":"A"}]. PromQL наружу не уходит, его выполняет сервер по номеру панели. Документация подтверждает: «Arbitrary queries cannot be run against your data sources through externally shared dashboards».UID датасорса виден. Без доступа бесполезен, но напоминает, что токен не непрозрачная коробка.
Что проверял руками перед публикацией: /grafana/ анониму отдаёт 302 на логин; заголовок X-WEBAUTH-USER: admin, присланный клиентом, прав не даёт, он затирается на nginx; публичный API с чужим uid отвечает 404; в демо-аккаунте нет реальных имён и адресов.
Опасность здесь не в том, что утекут метрики, а в том, что утечёт структура и что кто-нибудь подставит чужой идентификатор. Обе проверки занимают минут пятнадцать.
Чего пока нет
Проверка на утечки у меня до сих пор ручная, и это дыра. Наполненность панелей считает скрипт, а случайно засветившийся в заголовке БАНК-ПРОД-1 я должен заметить глазами.
Правильно делать так: после сборки сходить в публичный API без cookies и прогнать всё, что вернулось, через белый список. Именно белый, а не чёрный: инвентарь стенда закрытый и известный (srv-*, 10.20.30.0/24, четыре вымышленных домена), поэтому правило звучит как «всё, что похоже на имя хоста, адрес или домен, обязано быть из списка». Чёрный список ловит только то, что вы заранее угадали.
И проверять надо не только JSON дашборда, но и ответы на запросы панелей: реальное имя с большей вероятностью приедет значением метки, а не заголовком. Запускать периодически, а не только при сборке — тенант может испортиться потом, если кто-нибудь направит агент в демо-аккаунт.
Чек-лист
Отдельный тенант и отдельная организация Grafana. Проверьте, что ваш тенант с именем
demoне набит реальными хостами.Витрину собирать из боевых шаблонов скриптом, а не рисовать руками.
Снять переменные, снять
hide: true, проставитьpanel id. Запросы проверять на каждом языке отдельно: LogsQL ломается не так, как PromQL.Убедиться, что в организации демо нет теговых аннотаций.
Синтетика — это инвентарь, суточный профиль, регулярные события, инцидент и корреляции. Плюс метки, по которым фильтрует дашборд.
Автоматическая проверка наполненности панелей.
Автоматическая проверка на утечки по белому списку, включая ответы
/panels/<id>/query.Посчитать панели с запросами и привести к этому числу
burstна прокси. Одно открытие дашборда — сотня запросов.Написать на странице, что данные синтетические, рядом с графиками.
Если делали публичное демо на живых данных — расскажите, во что упирались. У меня хуже всего оказались две вещи, которых я не ждал: собственный rate limit и метки, которых ждал дашборд.