📊 Мониторинг (демон)
📌 Описание
Сбор метрик системы и приложений на ноде: CPU, RAM, диск, сеть, GPU, состояние Docker и systemd. Метрики доступны через CLI, API демона и стримятся на платформу для дашборда и алертов.
🎯 Сценарии использования
asc status— сводка по серверу и приложениям в терминале.asc stats— потребление CPU, памяти, дискового пространства и I/O, а также сетевого I/O по приложениям (аналогdocker stats --no-stream);asc stats --liveдержит таблицу живой, обновляя её на месте (аналог обычногоdocker stats); root видит приложения всех пользователей, сгруппированные по владельцам.- Дашборд платформы показывает графики нагрузки нод в реальном времени.
- Алерт «приложение упало» → уведомление в Telegram (через платформу).
- AI-ассистент отвечает на «что жрёт память?» данными мониторинга.
🏗️ Техническое решение
- Системные метрики: CPU, память, диск (usage + I/O), сеть (rx/tx, ошибки, дропы) — напрямую из procfs (
/proc/stat,/proc/meminfo,/proc/loadavg,/proc/net/dev,/proc/uptime) иstatvfs(3)для дисков; без внешних крейтов. Дистрибутив-специфичного здесь нет; под macOS позже — отдельный сборщик за той же абстракцией. - Метрики приложений: per-container stats (Docker API), per-unit (systemd cgroups), per-process.
- Сетевые интерфейсы: список интерфейсов (IP, MAC, статус, скорость, тип), статистика трафика; управление интерфейсами — пост-MVP.
- Хранение: кольцевой буфер в памяти + короткая история в SQLite; долгая история — на стороне платформы.
- Доставка: пуш-стрим в туннель nodeservice (интервал 5–15 с, адаптивный); healthcheck-события — мгновенно.
- Health-статусы приложений:
running / stopped / unhealthy / unknown— на основе healthcheck изasc.yaml.
🧩 Реализация (текущий инкремент)
- Модуль
src/daemon/monitor/:system.rs— парсеры procfs (чистые функции над&str, покрыты unit-тестами) и снятие снапшота;mod.rs—Monitor: фоновый сэмплер в демоне, интервал и глубина истории задаются вconfig.toml([monitor] interval_ms = 100,history_samples = 300— 30 с истории в памяти при 100 мс). Старый ключinterval_secsиз конфигов, написанных до DMN-072, по-прежнему разбирается (чтобы такие файлы загружались), но интервал больше не задаёт и обратно не пишется — при следующем сохранении он исчезает изconfig.toml(DMN-075). Именно из-за него обновлённый демон мог продолжать отдавать один сэмпл в 10 с: в каждой установке старше DMN-072 лежитinterval_secs = 10. - Живой стрим (DMN-072): у
Monitor—tokio::sync::broadcast-канал; каждыйpush()рассылает сэмпл подписчикам (ёмкость 16 — отставший получатель просто теряет старые кадры черезLagged, не блокируя сэмплер).MonitorService.StreamSystemMetricsподписывается на канал вместо периодического опросаGetSystemMetrics— панель платформы получает кадры настолько часто, насколько их снимает демон. Подписка берётся до чтения буфера, а уже снятый сэмпл уходит первым кадром (DMN-075): подключившаяся панель рисует цифры сразу, а не через целый интервал сэмплирования. - CPU usage считается по дельте двух чтений
/proc/stat; скорости сети (байт/с) — по дельте счётчиков между сэмплами. - GPU-метрики:
gpu.rs— по записи на карту (вендор, модель, загрузка, VRAM used/total, температура, потребление). NVIDIA берётся изnvidia-smi --query-gpu=... --format=csv,noheader,nounits: у проприетарного драйвера нет ядерного интерфейса с загрузкой, поэтому вендорская утилита (она ставится вместе с драйвером) — единственный переносимый источник; вызов ограничен таймаутом в 5 с, зависший процесс убивается. AMD читается из sysfs-интерфейсаamdgpu(gpu_busy_percent,mem_info_vram_total/_used,hwmon/*/temp1_input,power1_average) — так же, как и все остальные метрики здесь. Источники определяются один раз: машина без GPU больше никогда не порождает процесс, а её списокgpusпросто остаётся пустым. Поскольку сэмпл может запускать процесс, фоновый сэмплер демона снимает его целиком на блокирующем потоке. - API:
MonitorServiceв proto (GetSystemMetrics,GetMetricsHistory) + REST-маршрутыGET /v1/metricsиGET /v1/metrics/history?limit=N— оба транспорта поверх общего слоя, как и остальной API (DMN-005). - Реально занятые порты (DMN-103):
MonitorService.ListListeningPorts+ RESTGET /v1/ports/listening, CLIasc ports --listening. Это не то же самое, что объявленные порты приложений (asc portsбез флага,AppService.ListAppPorts) — здесь порты читаются напрямую из/proc/net/{tcp,tcp6,udp,udp6}: TCP-сокеты в состоянииLISTEN, UDP-сокеты с нулевым удалённым адресом. Атрибуция по процессу обходит/proc/<pid>/fd/*(обход ограничен, терпим к ошибкам доступа — не-root вызывающий просто увидит меньше pid). Атрибуция по Docker-контейнеру резолвится сверкой со списком контейнеров (docker-proxy/dockerdсами по себе не говорят, за какой контейнер проксируют), а порт установленного, но остановленного приложения показывается зарезервированным, а не выглядит свободным. - Процессы хоста (DMN-119):
ProcessService.ListProcesses/SignalProcess+ RESTGET /v1/processes[?kernelThreads=true]иPOST /v1/processes/{pid}/signal, capabilityprocesses.src/daemon/monitor/processes.rsразбирает/proc/<pid>/{stat,status,cmdline,cgroup}:comm— всё до последней)вstat(процесс может назватьсяa (b) c), эффективный uid — из строкиUid:, id контейнера Docker — из пути cgroup (и/docker/<id>, иdocker-<id>.scope), потоки ядра отличаются поPF_KTHREADи по умолчанию скрыты. CPU — в стилеtop(100 = одно ядро), дельтой к предыдущему чтению, которое хранится в процессе демона: UI, опрашивающий раз в несколько секунд, получает долю с прошлого опроса без сна демона, и только «холодный» вызов (чтения нет или оно старше 60 с) замеряет ~300 мс. id контейнера разрешается в имя и приложение-владельца (его контейнер или compose-проект) через Engine — нефатально, как атрибуция портов. Сигналы — закрытый набор (TERM, KILL, HUP, INT, STOP, CONT, USR1, USR2 — не сырой номер: нумерация зависит от архитектуры). Процесс идентифицируется парой(pid, start_ticks): вызывающий возвращает увиденное время старта, и переиспользованный pid отвергается (FAILED_PRECONDITION/ REST 409), а не получает чужой сигнал. pid 1, сам демон и потоки ядра отвергаются доkill(2)(PERMISSION_DENIED/ 403); сигнал требует root-контекста, список открыт всем, какps. - CLI:
asc statusпоказывает CPU (usage, load average), память, диски и — если они есть — каждый GPU (модель, загрузка, VRAM) — метрики снимаются самим CLI без обращения к демону (автономность). - Per-app метрики: метод
usage()в трейтеAppDriverвозвращает накопительные счётчики (CPU-время в микросекундах, resident-память в байтах, байты чтения/записи блочных устройств, байты rx/tx по сети — I/O-счётчики —Option,None, когда рантайм не может их отдать); источники по рантаймам — Docker Engine API (/containers/<id>/stats, one-shot:blkio_stats.io_service_bytes_recursiveдля дискового I/O,networksдля сетевого — оба реальны, потому что у контейнера своя cgroup и своё сетевое пространство имён), cgroup v2 systemd-юнита (cpu.stat+memory.current+io.statдля дискового I/O; сетевой I/O всегдаNone— юнит делит сетевое пространство имён с хостом, читать там нечего),/proc/<pid>/stat+statm+ioдля процессов (дисковый I/O изread_bytes/write_bytes; сетевой I/O всегдаNoneпо той же причине, что и у systemd). CPU% считается по дельте двух сэмплов (~500 мс), как вdocker stats, и может превышать 100% на многоядерных машинах. - Аптайм приложения (DMN-089): поле
uptime_secsв сэмплахAppService.GetAppStats/StreamAppStats— секунды с момента текущего запуска. Источник времени старта свой для каждого вида: docker берётState.StartedAtиз уже выполняемогоinspect_container, process — полеstarttimeиз/proc/<pid>/statв сочетании сbtimeиз/proc/stat, systemd —ActiveEnterTimestampMonotonic(в микросекундах с загрузки, без разбора локализованной даты) с той же привязкой кbtime. Поле не выставляется, пока приложение не запущено. - CLI
asc stats: таблица ID / KIND / CPU % / MEM / QUOTA / DISK / NET/s / DISK/s / NET I/O / DISK I/O по запущенным приложениям — QUOTA — компактный бар использования (как вasc app disk), если у приложения задана дисковая квота, иначе прочерк; DISK — полный размер каталога приложения в любом случае; NET/s и DISK/s — текущая скорость («rx/s / tx/s» и «read/s / write/s»), дельта тех же двух сэмплов, из которых считается CPU%; NET I/O и DISK I/O — «rx / tx» и «read / write», накопительные суммы байт с момента запуска приложения (как колонки NET I/O / BLOCK I/O в самомdocker stats— не скорость); все четыре — прочерк, когда рантайм не может отдать пару (сеть доступна только для Docker-приложений, см. выше); сортировка--sort cpu|mem(по умолчанию cpu); у root — группировка по владельцам, как вasc app list. Остановленные приложения показываются с прочерками.--liveперерисовывает ту же таблицу на месте (очистка экрана + перерисовка) каждые ~500 мс — ровно то время, которое и так уходит на один сэмпл из-за дельты CPU, лишний sleep не нужен — до Ctrl+C. - Следующие инкременты: per-app метрики в API (
MonitorService), история в SQLite, healthcheck-статусы, пуш в туннель.
🔗 Связанные задачи
DMN-006, DMN-103, DMN-119, NODE-003, NODE-038, NODE-051, FE-004, FE-109, FE-156 в ROADMAP.md.