Самый частый вопрос на первом контакте звучит одинаково у всех: «А можно просто посмотреть, как это выглядит?» Ответов на него обычно два, и оба плохие. Первый — созвон с демонстрацией экрана: полчаса моего времени и полчаса чужого ради того, чтобы человек понял, что ему это не нужно. Второй — форма «оставьте телефон, и мы покажем»: она отсеивает ровно тех, кто пришёл посмотреть без обязательств, то есть почти всех.
Третий вариант — выложить работающий стенд наружу и дать ссылку без регистрации. Звучит просто ровно до момента, когда начинаешь считать, что именно ты выкладываешь: чужие данные, структуру своих дашбордов, доступ к хранилищу метрик и счёт за трафик. Ниже — что из этого оказалось настоящей проблемой, что мифом, какие ограничения у публичных дашбордов Grafana и почему собственная защита от абуза первым делом ударила по моим же посетителям.
Стенд, о котором речь: девять дашбордов реальной платформы, синтетическая «розничная сеть», обновление раз в 30 секунд, вход без учётной записи.
Масштаб: 217 панелей, и это измеримо снаружи
Девять дашбордов — это не девять экранов с тремя графиками. Состав можно снять не веря мне на слово: публичный дашборд Grafana отдаёт своё определение анонимно, обычным GET.
|
Дашборд |
Панелей всего |
Из них с запросами |
|---|---|---|
|
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-* — те самые, что платформа раскатывает каждому клиенту, — программно снимает с них всё несовместимое с публичным режимом и публикует результат. Разъехаться витрина с продуктом не может: она из него собирается. Побочный эффект приятный — обновление дашбордов идемпотентно, ссылки при пересборке сохраняются.
Вся хитрость в слове «снимает». Вот что именно приходится снимать и почему.
Что ломается в публичных дашбордах Grafana
Документация честно перечисляет ограничения, но список стоит прочитать до того, как вы соберёте дашборды, а не после.
Переменные не работают. Дословно: «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 панель отвечает «Invalid panel id». Публичный режим запрашивает данные по адресу /panels/<id>/query, а в шаблонах дашбордов id обычно не проставлены — они назначаются при импорте. Если вы, как я, публикуете шаблон программно, id придётся расставить самому.
Аннотации — главная мина. Поддерживается только источник -- Grafana -- с типом Annotations & Alerts, организационные не поддерживаются вовсе. Но интереснее другое: запрос аннотаций по тегам вернёт совпадения со всех дашбордов организации, а не только расшаренного. Если демо живёт в одной организации с боевыми дашбордами и там есть аннотации с тегами — вы отдаёте наружу подписи к чужим инцидентам. Это ровно тот класс утечки, который потом объясняешь словами «я не знал». Отдельная организация закрывает и это.
Ещё из списка неподдерживаемого: exemplars, library panels, Grafana Live и потоковые обновления, фронтенд-датасорсы и датасорсы через Reverse Proxy. Панель на фронтенд-датасорсе не покажет ошибку красиво — она просто останется без данных.
Кэш запросов и rate limiting для публичных дашбордов есть только в Enterprise. В OSS вы остаётесь с двумя сотнями панелей и любым числом зрителей один на один.
Синтетика, которая не выглядит синтетикой
Данные на стенде вымышленные, и это написано прямо на странице. Но вымышленные данные должны выглядеть как настоящие, иначе демо работает против вас: инженер, увидев ровный белый шум на графике CPU, закрывает вкладку за десять секунд.
Генератор — один контейнер на 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 запросов в секунду с адреса и burst=20. Настройка прожила месяцы без единой жалобы.
Открываю свежеопубликованный дашборд Linux — половина графиков пустая. Обновляю — пустые уже другие. Панели «плавают» от прогона к прогону, в логах ничего похожего на ошибку.
Причина в первой же строчке этой статьи, про 217 панелей. Открытие одного дашборда — это не один запрос, а запрос данных для каждой панели отдельно: у «Node Exporter Full» их сто семнадцать, и они уходят пачкой за секунду. Ограничитель делал ровно то, для чего поставлен: пропускал двадцать, остальные отбрасывал. С точки зрения nginx мой единственный посетитель был неотличим от скрейпера.
Починка простая, но сама постановка задачи стоила часа: burst для /grafana/api/public/ поднят до 200 — столько же, сколько у закрытого /grafana/, где та же арифметика работала всегда, — и до 50 для самой страницы. Зона осталась прежней, 20 запросов в секунду на адрес: защита на месте, всплеск на открытии разрешён.
Общее правило, которое я из этого унёс: лимиты надо считать не на пользователя, а на действие пользователя. Одно человеческое «открыть дашборд» — это сотня машинных запросов, и любой rate limit, поставленный по интуиции, окажется на порядок меньше нужного.
Отсюда же верхняя оценка нагрузки: 217 панелей с автообновлением раз в 30 секунд — 434 запроса в минуту на одного зрителя, открывшего все девять дашбордов. На практике меньше: панели, до которых не долистали, Grafana не запрашивает, и большинство открывает один дашборд, а не девять. Но проектировать надо от верхней оценки, потому что она наступит в день публикации ссылки.
Взгляд снаружи: что видит чужой
Полезное упражнение — открыть свой стенд без cookies и посмотреть, что отдаёт API.
GET /grafana/api/public/dashboards/<accessToken>
-
Определения панелей отдаются целиком — заголовки, описания, типы, раскладка. Структура вашего дашборда публична. Если в заголовке панели написано «Нагрузка на кластер БАНК-ПРОД-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; в демо-аккаунте нет ни одного реального имени или адреса.
Модель угроз здесь не «утекут метрики», а «утечёт структура» и «кто-нибудь подставит чужой идентификатор». Проверяются обе за пятнадцать минут, и делать это надо до того, как ссылка уйдёт в мир.
Чек-лист перед публикацией демо
-
Отдельный тенант и отдельная организация Grafana — не «почищенная копия» боевого. Проверьте, что ваш тенант с именем
demoне набит реальными хостами. -
Витрину собирайте из боевых шаблонов скриптом, а не рисуйте руками: иначе через месяц покажете то, чего у вас уже нет.
-
Уберите переменные, снимите
hide: true, проставьтеpanel id, проверьте запросы на каждом языке отдельно — LogsQL ломается не так, как PromQL. -
Убедитесь, что в организации демо нет аннотаций с тегами: запрос по тегам тянет их со всех дашбордов организации.
-
Синтетика — это инвентарь, суточный профиль, регулярные события, инцидент и корреляции. И лейблы: панель фильтрует по метке, которой у вас может не быть.
-
Заведите проверку наполненности панелей. Глазами вы её не сделаете, а пустая панель на витрине хуже отсутствующей.
-
Посчитайте панели с запросами и умножьте на частоту обновления — и приведите к этому числу лимиты на прокси. Одно открытие дашборда = сотня запросов.
-
Откройте
/api/public/dashboards/<token>без cookies и прочитайте заголовки панелей глазами постороннего. -
Напишите на самой странице, что данные синтетические. Не в подвале мелким шрифтом, а рядом с графиками.
Если у вас был опыт публичного демо с живыми данными — интересно, во что упирались вы. У меня больнее всего оказались две вещи, которых я не ждал вовсе: собственный rate limit и метки, которых ждал дашборд.
ссылка на оригинал статьи https://habr.com/ru/articles/1078300/