Как я перевёл свой self-hosted мониторинг с JSONL на SQLite и заставил модули просить разрешения. DevOps.. DevOps. docker.. DevOps. docker. Go.. DevOps. docker. Go. golang.. DevOps. docker. Go. golang. nextjs.. DevOps. docker. Go. golang. nextjs. Open source.. DevOps. docker. Go. golang. nextjs. Open source. self-hosted.. DevOps. docker. Go. golang. nextjs. Open source. self-hosted. SQLite.. DevOps. docker. Go. golang. nextjs. Open source. self-hosted. SQLite. мониторинг серверов.. DevOps. docker. Go. golang. nextjs. Open source. self-hosted. SQLite. мониторинг серверов. Системное администрирование.

Мне нужно было открыть страницу и увидеть, что происходит на удалённых серверах: нагрузка, процессы, диски, логи. Всё, что я находил, делилось на два лагеря. В первом система, под которую поднимаешь несколько сервисов и потом живёшь с их конфигами. Во втором простая штука, которая выглядит как из прошлого десятилетия и под себя почти не настраивается. Красивого, простого и гибкого одновременно я не нашёл. Мне это надоело, и я написал своё.

Получился Pulse: мониторинг Linux- и Windows-серверов, который ставится на свою машину. Бэкенд на Go, интерфейс на Next.js, лицензия MIT. Состоит из трёх репозиториев: панель (Monitor), агент (Agent) и отдельный модуль для Docker.

Как я перевёл свой self-hosted мониторинг с JSONL на SQLite и заставил модули просить разрешения - 1

Про сам проект в README написано достаточно. Здесь я хочу рассказать о другом: после первого публичного релиза (v0.4.0) я пересмотрел три решения, которые мне самому перестали нравиться. Хранилище, модель модулей и то, как панель обращается к каталогу модулей. На момент написания актуальны Monitor v0.6.3 и Agent 0.5.0.

Как всё устроено в двух абзацах

Агент работает на каждом наблюдаемом сервере и сам отправляет отчёты в панель по HTTPS. Входящий порт на сервере открывать не нужно, сертификат панели проверяется всегда, режима «не проверять» нет. По умолчанию агент собирает данные раз в 500 мс, интервал можно поднять до 5 секунд. Он собирает CPU и память, процессы, диски, соединения, системные логи и GPU, если есть NVIDIA и nvidia-smi. Если показатель недоступен, панель рисует прочерк, а не ноль.

Панель принимает отчёты, хранит историю и показывает всё это в браузере. Модуль расширяет агента: это отдельная программа, которая собирает что-то своё, и описание того, как это показать.

Как я перевёл свой self-hosted мониторинг с JSONL на SQLite и заставил модули просить разрешения - 2

Хранилище: почему я ушёл от JSONL

В v0.4.0 всё лежало в файлах. История метрик каждого агента шла в свой .jsonl, логи в общий logs.jsonl, события и реестр серверов в отдельных JSON. Метрики записывались раз в 10 секунд, графики читали последние 24 часа. Для старта это было удобно: открыл файл глазами, увидел, что происходит. Бэкап это просто копия каталога.

Цена тоже быстро проявилась. Архив JSONL не чистился автоматически, и в README мне пришлось написать «следите за свободным местом». Это звучит как оправдание, и по сути им и было. Бэкап по инструкции делался так: остановить панель, скопировать весь каталог данных, запустить обратно. Хранение логов по серверам настроить было нельзя.

В версии 0.5.0 телеметрия, логи, история метрик и события переехали в SQLite в режиме WAL. Почему именно он: агенты по умолчанию шлют отчёты каждые полсекунды, а браузер в это же время читает графики. В WAL читатели не блокируют писателя, для такой нагрузки это подходящий режим. Что я получил:

Миграция возобновляемая. Первый запуск после обновления импортирует старую историю до того, как панель начнёт отвечать по HTTP. Для больших архивов это занимает время, и я так и пишу в заметках к релизу: сделайте бэкап и не паникуйте, если панель пару минут молчит.

Резервные копии стали согласованными, то есть остановка панели для копии больше не нужна.

Появилась настройка хранения логов по серверам. Здесь я намеренно выбрал осторожный вариант: у уже существующих узлов автоматическая очистка выключена. Обновление не должно удалять чужие логи только потому, что у меня появилась новая настройка.

Файлы владельца и модулей остались в прежних хранилищах, их я пока не трогал.

Как я перевёл свой self-hosted мониторинг с JSONL на SQLite и заставил модули просить разрешения - 3

Модули: от «запусти и прочитай stdout» к вызовам с разрешениями

В первой версии протокола модуль был устроен предельно просто. Модуль это один JSON-файл с полями настроек, описанием представлений (table, cards, chart, logs) и бинарниками сборщика под linux-amd64, linux-arm64 и windows-amd64. Агент скачивает нужный бинарник, проверяет SHA-256 и запускает как отдельный процесс. Процесс получает JSON на stdin и печатает один JSON в stdout:

stdin:  { "protocol": 1, "config": { "endpoint": "local-service" } }
stdout: { "service": { "status": "running" } }

Интерфейс модуль не пишет, его строит панель по описанию представлений. Для чтения данных этого хватает. На практике я упёрся в три вещи. Поле permissions в пакете было просто текстом для человека и ничего не ограничивало. Модуль мог только отдавать данные, но не выполнять действия. И каждый цикл запускал процесс заново, без собственного состояния.

В 0.5.0 появились нативные компоненты v2. Их возможности по документации такие: периодические и ручные вызовы, явные разрешения, проверка хешей и прав перед запуском, отмена вызова и постоянные квитанции. Последнее мне казалось самым важным. Если вызов вроде «перезапустить контейнер» ушёл, а ответ потерялся, повторять его вслепую нельзя: можно перезапустить дважды. Поэтому для ручных операций агент хранит квитанцию и не выполняет операцию повторно, если исход неизвестен. Результаты, которые уходят в панель, ограничены по размеру и не содержат секретов.

Первым на v2 перешёл Docker-модуль (2.0.0-alpha1). Теперь он показывает ID контейнеров, логи выбранного контейнера и стандартные графики, а ещё умеет start, restart и stop. Эти действия по умолчанию недоступны: владелец должен отдельно выдать право actions.execute, а компонент включить на каждом нужном узле. Если вы меняете контракт выполнения у компонента v2, права нужно подтвердить заново. При переходе с v1 на v2 старый модуль отключается, но его бинарники и данные сохраняются. Пакет v1 лежит рядом как docker-module-v1.json.

Как я перевёл свой self-hosted мониторинг с JSONL на SQLite и заставил модули просить разрешения - 4
Как я перевёл свой self-hosted мониторинг с JSONL на SQLite и заставил модули просить разрешения - 5

Теперь честно о границах. Разрешения это граница в протоколе панели и агента, а не изоляция на уровне ОС. Процесс сборщика по-прежнему живёт с правами агента, и если агент работает от root, то и модуль тоже. Доступ к Docker socket на хосте и так почти равен root, поэтому модуль Docker с правом управления нужно ставить осознанно. Кроме того, триггеры по событиям и долгоживущие задачи в v2 пока не реализованы.

Маркетплейс, которому не нужно доверять сразу

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

Несколько решений, которые я принимал с оглядкой на злоумышленника:

Адрес каталога принимается только как HTTPS без пути, query и учётных данных. HTTP разрешён только для явно указанного loopback IP. Имена, которые резолвятся в частные адреса, и редиректы блокируются. Это защита от SSRF: панель делает запрос по адресу, который вводит владелец, и не должна превращаться в прокси к внутренней сети.

Панель передаёт каталогу только фильтры поиска и запросы выбранного модуля. Телеметрия, логи, конфигурация узлов и данные для входа не передаются. Агенты к каталогу вообще не обращаются.

При установке панель заново запрашивает опубликованную версию, проверяет размер и дайджест полного JSON, затем проверяет заголовки ELF/PE и хеши бинарников.

Идентификаторы модулей глобальны. Установка из каталога не заменяет уже существующий модуль с тем же ID, даже если издатель другой. Иначе подмена установленного модуля сводилась бы к регистрации такого же ID.

В 0.6.0 я добавил обновление модулей через каталог и через ручной импорт JSON. Источник каталога привязан к издателю. Если контракт v2 поменялся, нужно новое согласие на права, а совместимые настройки сохраняются. Предварительные версии проверяются отдельным переключателем.

Как я перевёл свой self-hosted мониторинг с JSONL на SQLite и заставил модули просить разрешения - 6

Одобрение в каталоге я прямо не считаю гарантией безопасности нативного кода, так написано и в документации. Публичный каталог пока не открыт: на сайте сейчас один модуль, Docker. Клиент в панели готов.

Агент на одном сервере с панелью

Ещё одна мелочь, на которую ушло больше времени, чем я ожидал. Агент ставят и на тот же сервер, где работает сам Monitor за публичным прокси. Агенту нужно прийти на конкретный IP и порт, но TLS при этом должен по-прежнему проверяться по домену. В 0.5.0 для этого появился флаг -connect-address: агент подключается напрямую по IP:порт, но сохраняет Host, SNI и проверку сертификата.

./pulse-agent -url https://monitor.example.com 
  -connect-address 127.0.0.1:8443 
  -server SERVER_ID -token-file agent.token -state agent-state

Пример условный, адреса подставьте свои. Отдельно в 0.6.0 адрес узла в панели стал необязательным (он только справочный), а его смена больше не отзывает токен. Раньше это ломало подключение у тех, кто просто поправил подпись.

Что осталось сырым

Версии 0.x, и до v1.0 совместимость не гарантируется. Об изменениях я пишу в changelog обоих проектов. Docker-модуль v2 это альфа. Песочницы для модулей нет. Intel и AMD GPU без гарантированной телеметрии. После обновления на 0.5.0 откатываться на старые бинарники без бэкапа не стоит: новые файлы старыми версиями не читаются, и я не обещаю обратной совместимости. Три последних релиза (0.6.1, 0.6.2, 0.6.3) это только вёрстка карточек обзора на узких экранах.

Где посмотреть

Сайт и руководства: https://pulse.hubba-cloud.ru/
Monitor: https://github.com/HubbaBubbaPrepod/pulse-monitor
Agent: https://github.com/HubbaBubbaPrepod/pulse-agent
Docker-модуль: https://github.com/HubbaBubbaPrepod/pulse-module-docker

Если у вас есть опыт с моделью прав для нативных плагинов без песочницы, напишите в комментариях, что я мог упустить. Особенно интересно про повторное выполнение операций с неизвестным исходом: я выбрал «не повторять, хранить квитанцию», и мне любопытно, где это ломается.

Автор: zxchubbabubba

Источник