Skip to content

📊 Мониторинг (демон) ​

📌 Описание ​

Сбор метрик системы и приложений на ноде: 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 + REST GET /v1/ports/listening, CLI asc 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 + REST GET /v1/processes[?kernelThreads=true] и POST /v1/processes/{pid}/signal, capability processes. 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.

Распространяется по лицензии MIT.