Публичное демо без регистрации: как показать живой мониторинг и не подставиться

от автора

Самый частый вопрос на первом контакте звучит одинаково у всех: «А можно просто посмотреть, как это выглядит?» Ответов на него обычно два, и оба плохие. Первый — созвон с демонстрацией экрана: полчаса моего времени и полчаса чужого ради того, чтобы человек понял, что ему это не нужно. Второй — форма «оставьте телефон, и мы покажем»: она отсеивает ровно тех, кто пришёл посмотреть без обязательств, то есть почти всех.

Третий вариант — выложить работающий стенд наружу и дать ссылку без регистрации. Звучит просто ровно до момента, когда начинаешь считать, что именно ты выкладываешь: чужие данные, структуру своих дашбордов, доступ к хранилищу метрик и счёт за трафик. Ниже — что из этого оказалось настоящей проблемой, что мифом, какие ограничения у публичных дашбордов 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; в демо-аккаунте нет ни одного реального имени или адреса.

Модель угроз здесь не «утекут метрики», а «утечёт структура» и «кто-нибудь подставит чужой идентификатор». Проверяются обе за пятнадцать минут, и делать это надо до того, как ссылка уйдёт в мир.

Чек-лист перед публикацией демо

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

  2. Витрину собирайте из боевых шаблонов скриптом, а не рисуйте руками: иначе через месяц покажете то, чего у вас уже нет.

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

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

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

  6. Заведите проверку наполненности панелей. Глазами вы её не сделаете, а пустая панель на витрине хуже отсутствующей.

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

  8. Откройте /api/public/dashboards/<token> без cookies и прочитайте заголовки панелей глазами постороннего.

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

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

ссылка на оригинал статьи https://habr.com/ru/articles/1078300/