В 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 и метки, которых ждал дашборд.
Комментарии (6)

ShIV03
03.09.2026 18:32-
одна фраза >"Отсеивает ровно тех, кто пришёл посмотреть без обязательств, то есть почти всех" - дает понять "(это) Мышление втюхивателя".
- риторически: "о каких обязательствах?" при «А можно просто посмотреть, как это выглядит?» (фраза говорит "я - не понял (что вы предлагаете)");
- попадал в такую (неприятную) ситуацию, со стороны спрашивающего:
2. Почти 20 лет назад, "на курсах повышения квалификации менеджеров", лектор - директор фирмы-разработчика, представил свою концепцию "Серебрянной пули" (сейчас, называется "No-Code") и упомянул "у нас уже есть реализация".
Через несколько лет (15 лет назад), я попросил показать начальнику одного отдела (в котором сильно сказывалась "нехватка рук").
Прислали автора реализации, с мышлением программиста. Я выступал "переводчиком".
Эффект: Начальник - так и не смог понять "Зачем (мне это)?".
После, он мне сказал "Слишком 'широкий шаг', дай нам CMS, остальное - станет понятно только после наполнения". Что мы и сделали, и "отстали" (он сказал "мы позовем, как созреем").
Через какое-то время, сменился начальник. Мы снова провели презентацию (тем же составом). Эффект - тот же.
Но я уже "начал понимать" - ("реализован аля VSCode" - сейчас, это называется "Low-Code", и) "(чтобы донести суть до спеца) Нужны не программисты!" (документация ПО написана была тем же автором, и предполагала наполнение программированием (читай "Программистами"): свой язык (логики - "программирования"), классы (аттрибуты), ограниченный набор готовых/докупаемых инструментов-обработчиков: почты, ...).
Заставлять "программировать" работников не технического отдела - "перебор".
Директор фирмы-разработчика - остался недоволен мною.
И на мое недоумение "Вы - нам несколько лет назад расказывали: Запоминает действия работника, и потом повторяет, т.е. без программирования", отвечал "Это - про Концепцию. А реализация - пока так не умеет".
"Ну, значит, подождем, пока не научится. А садить своих программистов на 'обучение' - не реально" (суть ПО - "Ассистент" текущей работы, т.е. программировать придется непрерывно).
3. К чему эта история:
- несовпадение "Красивой заманухи" с "Реализацией (в примере - куцей) ", прояснить которое и цель "смотрящего";
- "почти все" - не все, а часть, "клюёт" на "новинку" (да, хочет только посмотреть "А что это, вообще" - т.е. честно. Чтобы потом подумать "А как это приспособить, когда встречу ещё раз").
Но часть вины, и на втюхивателях - слишком много, или расплывчато "наобещали".
4. Не по теме: Современные AI-ассистенты - мне напоминают историю своей "Замануха-LowCode": как бы не "пыжились" простые пользователи одними запросами добиться результата, понадобятся (и Скиллы программиста, и) сами програмисты - для обеспечения Качества.
Но не все задачи требуют качества (например, для себя и разово).
Т.ч. какая-то часть - получится только запросами (новое понимание "No-Code").

I_vod Автор
03.09.2026 18:32@ShIV03 Про формулировку — согласен, плохая. «Без обязательств» это слово из моей головы, а не из головы того, кто спрашивает. Человек ничего не отказывается на себя брать, он просто ещё не понял, что ему предлагают. Фраза выдаёт взгляд «посетитель = недозаявка».
Переписал не фразу, а всё вступление: там была продажная рамка про созвоны и формы, которой в технической статье вообще нечего делать. Спасибо, что ткнули.
При этом сделал я, кажется, ровно то, к чему ваша история и ведёт. Не понял — значит надо показать, а не рассказывать.
А разрыв между концепцией и реализацией — самая близкая мне часть. Презентацией он не закрывается, просто всплывает позже и болезненнее. Поэтому на стенде висят девять пустых панелей, которые я не стал заполнять синтетикой: у клиента на виртуалке они будут пустыми точно так же, и пусть это будет видно сразу, а не через две недели. И поэтому «данные синтетические» написано рядом с графиками, а не мелким шрифтом внизу.
Ваш «переводчик» — хороший тест, кстати. Если демо нужен переводчик, это ещё не демо.
-
dinar_007
Раз уж витрина у вас всё равно собирается скриптом, я бы ещё проверку на утечки туда же прикрутил. Например, после сборки сходить в публичный API вообще без cookies и прогнать заголовки/описания по набору запрещённых шаблонов: реальные домены, подсети, имена хостов и т.д. А то сейчас получается, что пустые панели проверяются автоматически, а случайно засветившийся "BANK-PROD-1" все еще нужно самому замечать.
I_vod Автор
Справедливо, но с двумя поправками к схеме. Первая: чёрный список заменить белым. Инвентарь стенда закрытый и известный —
srv-*, 10.20.30.0/24, четыре вымышленных домена. Значит правило не «не должно встречаться запрещённое», а «всё, что похоже на имя хоста, адрес или домен, обязано быть из списка, иначе падаем». ВашBANK-PROD-1чёрный список поймает только если я его заранее угадал; белый поймает и тот, который я не предвидел.Вторая: заголовков и описаний мало. Реальное имя с большей вероятностью приедет не из JSON дашборда, а из самих данных — значением метки в ответе
/panels/<id>/query. Прогонять надо и ответы тоже.И запускать не только при сборке. Тенант может испортиться потом — достаточно, чтобы кто-то направил агент в демо-аккаунт. Поэтому отдельной периодической проверкой, и при срабатывании снимать публикацию, а не писать warning в лог.
I_vod Автор
добавил разделом в статью, белый список вместо чёрного, проверяю и ответы
/panels/<id>/query