В 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 дашборда, но и ответы на запросы панелей: реальное имя с большей вероятностью приедет значением метки, а не заголовком. Запускать периодически, а не только при сборке — тенант может испортиться потом, если кто-нибудь направит агент в демо-аккаунт.

Чек-лист

  1. Отдельный тенант и отдельная организация Grafana. Проверьте, что ваш тенант с именем demo не набит реальными хостами.

  2. Витрину собирать из боевых шаблонов скриптом, а не рисовать руками.

  3. Снять переменные, снять hide: true, проставить panel id. Запросы проверять на каждом языке отдельно: LogsQL ломается не так, как PromQL.

  4. Убедиться, что в организации демо нет теговых аннотаций.

  5. Синтетика — это инвентарь, суточный профиль, регулярные события, инцидент и корреляции. Плюс метки, по которым фильтрует дашборд.

  6. Автоматическая проверка наполненности панелей.

  7. Автоматическая проверка на утечки по белому списку, включая ответы /panels/<id>/query.

  8. Посчитать панели с запросами и привести к этому числу burst на прокси. Одно открытие дашборда — сотня запросов.

  9. Написать на странице, что данные синтетические, рядом с графиками.

Если делали публичное демо на живых данных — расскажите, во что упирались. У меня хуже всего оказались две вещи, которых я не ждал: собственный rate limit и метки, которых ждал дашборд.