# htop-web: Веб-интерфейс мониторинга системных ресурсов на базе Spring Boot WebFlux и Angular --- ## Аннотация В статье рассматривается архитектура и реализация **htop-web** — самохостимого веб-приложения для мониторинга системных ресурсов Linux-серверов в реальном времени. Приложение объединяет реактивный бэкенд на Spring Boot WebFlux с фронтендом на Angular 22, упаковывая оба компонента в единый Docker-контейнер. Ключевая особенность решения — сбор метрик хост-системы из контейнеризованной среды через монтирование файловых систем `/proc` и `/sys` хоста, что позволяет получить точные данные о CPU, памяти, дисках и сетевых интерфейсах реального сервера, а не виртуальной среды контейнера. **Ключевые слова:** мониторинг, Spring Boot, WebFlux, Angular, Docker, контейнеризация, OSHI, системные метрики. --- ## 1. Введение Мониторинг системных ресурсов является критически важной задачей при администрировании серверной инфраструктуры. Классические инструменты, такие как `htop`, `top`, `vmstat` и `iostat`, предоставляют детальную информацию, однако требуют доступа к терминалу и не подходят для удалённого мониторинга через веб-браузер. Существующие веб-решения мониторинга (Grafana + Prometheus, Netdata, Zabbix) предлагают мощные возможности визуализации и агрегации, но часто требуют значительных ресурсов и сложной настройки. Для небольших инфраструктур и индивидуальных серверов возникает потребность в лёгком, самодостаточном решении, которое: - Разворачивается за несколько команд; - Не требует внешних баз данных или брокеров сообщений; - Предоставляет информацию в реальном времени; - Занимает минимальные ресурсы хоста. **htop-web** отвечает этим требованиям, предоставляя компактный веб-дашборд, который собирает метрики напрямую из файловых систем Linux и отображает их через REST API и реактивный фронтенд. --- ## 2. Обзор существующих решений | Решение | Тип | Ресурсы | Настройка | Особенности | |---|---|---|---|---| | **htop / top** | CLI | Минимальные | Нет | Требует терминала, нет веб-интерфейса | | **Grafana + Prometheus** | Веб | Высокие | Сложная | Агрегация, алерты, требует Node Exporter | | **Netdata** | Веб | Средние | Средняя | Real-time, но тяжёлый агент | | **Zabbix** | Веб | Высокие | Сложная | Корпоративный мониторинг, алертинг | | **Glances** | Веб/CLI | Низкие | Простая | Python-зависимости, ограниченный UI | | **htop-web** | Веб | Низкие | Простая | Один контейнер, нет внешних зависимостей | htop-web занимает нишу между CLI-инструментами и тяжёлыми системами мониторинга, предлагая минималистичный подход с фокусом на простоту развёртывания. --- ## 3. Архитектура системы ### 3.1 Общая схема ``` ┌─────────────────────────────────────────────────────┐ │ Веб-браузер │ │ (Angular 22, TypeScript) │ └──────────────────────┬──────────────────────────────┘ │ HTTP (polling, 1 сек) ▼ ┌─────────────────────────────────────────────────────┐ │ Docker-контейнер │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ Spring Boot WebFlux (Java 21) │ │ │ │ │ │ │ │ REST API: /api/system/* │ │ │ │ ├─ /metrics — CPU, RAM, ОС │ │ │ │ ├─ /processes — список процессов │ │ │ │ ├─ /linux — uptime, load average │ │ │ │ ├─ /cpu — детали по ядрам │ │ │ │ ├─ /linux/memory — детальная память │ │ │ │ ├─ /linux/disks — диски и файловые сист. │ │ │ │ └─ /linux/network — сетевые интерфейсы │ │ │ │ │ │ │ │ Сервисы сбора метрик: │ │ │ │ ├─ SystemMetricsService │ │ │ │ ├─ SystemProcessesService │ │ │ │ ├─ LinuxDetailsService │ │ │ │ ├─ CpuDetailsService │ │ │ │ ├─ LinuxMemoryService │ │ │ │ ├─ LinuxDiskService │ │ │ │ └─ LinuxNetworkService │ │ │ │ │ │ │ │ Статические ресурсы Angular │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ Монтированные тома хоста: │ │ /host/root ← / (корень хоста, read-only) │ │ /host/sys ← /sys (системные данные, read-only) │ └─────────────────────────────────────────────────────┘ ``` ### 3.2 Ключевые принципы архитектуры 1. **Single-container deployment** — фронтенд и бэкенд упакованы в один JAR-файл и один Docker-контейнер. 2. **Host filesystem mounting** — метрики хоста получаются через монтирование `/proc` и `/sys` хоста в контейнер. 3. **Reactive backend** — Spring WebFlux обеспечивает неблокирующую обработку запросов. 4. **RxJS polling** — фронтенд использует реактивные потоки для периодического опроса API. 5. **Fallback mechanism** — при недоступности файлов хоста используется библиотека OSHI как запасной вариант. --- ## 4. Реализация бэкенда ### 4.1 Технологический стек | Компонент | Технология | Версия | |---|---|---| | Язык | Java | 21 | | Фреймворк | Spring Boot | 4.1.2 | | Реактивность | Spring WebFlux | 4.1.2 | | Системные метрики | OSHI | 7.7.2 | | Сборка | Gradle | 8.x | ### 4.2 REST API контроллер Контроллер `SystemMetricsController` экспонирует шесть REST-эндпоинтов, каждый из которых делегирует сбор данных соответствующему сервису: ```java @RestController @RequestMapping("/api/system") public class SystemMetricsController { private final SystemMetricsService _systemMetricsService; private final LinuxDetailsService _linuxDetailsService; private final CpuDetailsService _cpuDetailsService; private final LinuxMemoryService _linuxMemoryService; private final LinuxDiskService _linuxDiskService; private final LinuxNetworkService _linuxNetworkService; @GetMapping("/metrics") public Mono getMetrics() { return Mono.fromCallable(_systemMetricsService::getMetrics) .subscribeOn(Schedulers.boundedElastic()); } @GetMapping("/linux") public Mono getLinuxDetails() { return Mono.fromCallable(_linuxDetailsService::getDetails) .subscribeOn(Schedulers.boundedElastic()); } // ... остальные эндпоинты } ``` Каждый метод оборачивает синхронный вызов сервиса в `Mono` и выполняет его на `Schedulers.boundedElastic()`, что предотвращает блокировку реактивного пула потоков при чтении файловых систем. ### 4.3 Сбор метрик CPU и памяти Сервис `SystemMetricsService` демонстрирует ключевой паттерн проекта — чтение метрик хоста из смонтированной файловой системы с fallback на OSHI: ```java @Service public class SystemMetricsService { private static final Path HOST_ROOT = Path.of("/host/root"); private static final Path HOST_PROC = HOST_ROOT.resolve("proc"); public synchronized SystemMetricsResponse getMetrics() { CpuTicks currentTicks = readHostCpuTicks(); double cpuUsage = 0.0; if (currentTicks != null && _previousHostCpuTicks != null) { long totalDelta = currentTicks.total() - _previousHostCpuTicks.total(); long idleDelta = currentTicks.idle() - _previousHostCpuTicks.idle(); if (totalDelta > 0 && idleDelta >= 0 && idleDelta <= totalDelta) { cpuUsage = (double) (totalDelta - idleDelta) / totalDelta; } } // ... расчёт памяти и формирование ответа } private CpuTicks readHostCpuTicks() { Path statFile = HOST_PROC.resolve("stat"); // Чтение /host/root/proc/stat и парсинг строки "cpu ..." // Возвращает null при ошибке — триггер для fallback на OSHI } } ``` Алгоритм расчёта загрузки CPU основан на сравнении тиков процессора между двумя последовательными чтениями файла `/proc/stat`. Формула: $$CPU_{usage} = \frac{\Delta_{total} - \Delta_{idle}}{\Delta_{total}}$$ где $\Delta_{total}$ — разница суммарных тиков всех состояний процессора, а $\Delta_{idle}$ — разница тиков в состоянии idle. ### 4.4 Сбор информации о процессах Сервис `SystemProcessesService` парсит `/proc/[pid]/stat` для каждого процесса, вычисляя загрузку CPU на основе разницы тиков между двумя снимками: ```java @Service public class SystemProcessesService { public synchronized List getProcesses() { Path procRoot = Files.isDirectory(HOST_PROC) ? HOST_PROC : PROC; long nowNanos = System.nanoTime(); double elapsedSeconds = (nowNanos - _previousSampleNanos) / 1_000_000_000.0; try (var entries = Files.list(procRoot)) { entries.filter(path -> path.getFileName().toString().matches("\\d+")) .forEach(path -> { int pid = Integer.parseInt(path.getFileName().toString()); ProcessSnapshot current = readProcess(path, pid, bootTimeSeconds); // Расчёт CPU usage на основе delta тиков // ... }); } // Fallback: getProcessesFromOshi() при недоступности /proc } } ``` Расчёт CPU usage процесса: $$CPU_{process} = \frac{\Delta_{ticks}}{CLK\_TCK \times \Delta_{seconds}} \times \frac{100}{N_{cores}}$$ где $CLK\_TCK$ — количество тиков системы в секунду (обычно 100), а $N_{cores}$ — количество логических ядер. ### 4.5 Детали Linux-системы Сервис `LinuxDetailsService` предоставляет информацию о системе через OSHI: ```java @Service public class LinuxDetailsService { public LinuxDetailsResponse getDetails() { String hostname = _operatingSystem.getNetworkParams().getHostName(); String distribution = _operatingSystem.getFamily(); String kernelVersion = _operatingSystem.getVersionInfo().getVersion(); long uptimeSeconds = _operatingSystem.getSystemUptime(); double[] loadAverages = _processor.getSystemLoadAverage(3); return new LinuxDetailsResponse( linux, hostname, distribution, kernelVersion, uptimeSeconds, safeLoad(loadAverages, 0), // 1 мин safeLoad(loadAverages, 1), // 5 мин safeLoad(loadAverages, 2), // 15 мин _logicalProcessors ); } } ``` --- ## 5. Реализация фронтенда ### 5.1 Технологический стек | Компонент | Технология | Версия | |---|---|---| | Фреймворк | Angular | 22 | | Язык | TypeScript | 5.x | | Реактивность | RxJS | 7.x | | Стили | SCSS | — | ### 5.2 Сервис опроса метрик Фронтенд использует RxJS `timer` для периодического опроса API с интервалом 1 секуннда: ```typescript @Injectable({ providedIn: 'root' }) export class SystemMetricsService { private readonly http = inject(HttpClient); private readonly refreshInterval = 1000; readonly metrics$: Observable = timer(0, this.refreshInterval).pipe( switchMap(() => this.http.get('/api/system/metrics').pipe( catchError((error: unknown) => { console.error('Failed to load system metrics', error); return of(null); }), ), ), shareReplay({ bufferSize: 1, refCount: true }), ); readonly processes$: Observable = timer(0, this.refreshInterval).pipe( switchMap(() => this.http.get('/api/system/processes').pipe( catchError((error: unknown) => of([] as SystemProcess[])), ), ), shareReplay({ bufferSize: 1, refCount: true }), ); // ... остальные потоки: linux$, cpu$, memory$, disks$, network$ } ``` Оператор `shareReplay` обеспечивает многоподписку: все компоненты, подписанные на один поток, получают одни и те же данные без дублирования HTTP-запросов. ### 5.3 Компонент дашборда Компонент `Dashboard` инъецирует сервис метрик и предоставляет данные для шаблона: ```typescript @Component({ selector: 'app-dashboard', standalone: true, imports: [AsyncPipe, DecimalPipe, JsonPipe], templateUrl: './dashboard.html', styleUrl: './dashboard.scss', }) export class Dashboard { private readonly _systemMetricsService = inject(SystemMetricsService); readonly metrics$ = this._systemMetricsService.metrics$; readonly processes$ = this._systemMetricsService.processes$; readonly linux$ = this._systemMetricsService.linux$; readonly cpu$ = this._systemMetricsService.cpu$; readonly memory$ = this._systemMetricsService.memory$; readonly disks$ = this._systemMetricsService.disks$; readonly network$ = this._systemMetricsService.network$; getSortedProcesses(processes: SystemProcess[]): SystemProcess[] { return processes .filter(p => p.name.toLowerCase().includes(this.processFilter)) .sort((a, b) => { switch (this.processSort) { case 'cpu': return a.cpuUsagePercent - b.cpuUsagePercent; case 'memory': return a.residentMemoryBytes - b.residentMemoryBytes; case 'name': return a.name.localeCompare(b.name); case 'pid': return a.pid - b.pid; } }); } } ``` ### 5.4 Три режима отображения Дашборд поддерживает три режима визуализации: 1. **Monitor** — карточный интерфейс с метриками CPU, памяти, дисков и сети. 2. **htop** — табличное представление процессов, стилизованное под классический htop. 3. **htop-original** — имитация терминального интерфейса htop с функциональными клавишами (F3 — поиск, F4 — фильтр, F6 — сортировка, F10 — выход). ```html ``` --- ## 6. Сборка и упаковка ### 6.1 Gradle-сборка Процесс сборки автоматически строит фронтенд и упаковывает его в бэкенд: ```groovy tasks.register('buildFrontend', Exec) { dependsOn 'installFrontendDependencies' workingDir file('../frontend') commandLine 'npm', 'run', 'build', '--', '--configuration', 'production' } tasks.register('copyFrontend', Copy) { dependsOn 'buildFrontend' from(file('../frontend/dist/frontend/browser')) into(file('src/main/resources/static')) } tasks.named('processResources') { dependsOn 'copyFrontend' } ``` Порядок выполнения: 1. `npm ci` — установка зависимостей фронтенда; 2. `npm run build` — production-сборка Angular; 3. `copyFrontend` — копирование статических файлов в `src/main/resources/static`; 4. `bootJar` — упаковка JAR с фронтендом внутри. ### 6.2 Docker-образ Dockerfile минимален и использует официальный образ Eclipse Temurin: ```dockerfile FROM eclipse-temurin:21-jre WORKDIR /app COPY backend/build/libs/backend-0.0.1-SNAPSHOT.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"] ``` Образ весит около 300 МБ и не содержит инструментов разработки. --- ## 7. Развёртывание через Docker Compose ### 7.1 Базовая конфигурация Файл `docker-compose.yml` определяет сервис с доступом к файловой системе хоста: ```yaml services: htop-web: image: htop-web:latest build: context: . dockerfile: Dockerfile container_name: htop-web restart: unless-stopped pid: host volumes: - /sys:/host/sys:ro - /:/host/root:ro ports: - "8088:8080" ``` **Ключевые параметры:** | Параметр | Назначение | |---|---| | `pid: host` | Разделение PID-пространства хоста для видимости процессов | | `/:/host/root:ro` | Чтение `/proc` хоста для метрик CPU, памяти и процессов | | `/sys:/host/sys:ro` | Доступ к системным данным ядра | | `restart: unless-stopped` | Автоматический перезапуск при сбое | ### 7.2 Пошаговая инструкция #### Шаг 1: Клонирование репозитория ```bash git clone https://gitea.jambotron.com/liosha84/htop-web.git cd htop-web ``` #### Шаг 2: Сборка приложения ```bash cd backend ./gradlew clean build cd .. ``` Gradle автоматически соберёт Angular-фронтенд и упакует его в JAR-файл бэкенда. #### Шаг 3: Запуск контейнера ```bash docker compose up -d --build ``` Приложение доступно по адресу `http://localhost:8088`. #### Шаг 4: Проверка статуса ```bash # Статус контейнера docker compose ps # Логи в реальном времени docker compose logs -f htop-web ``` #### Шаг 5: Остановка ```bash docker compose down ``` ### 7.3 Продвинутое развёртывание с Caddy Для продакшн-развёртывания рекомендуется использовать обратный прокси с автоматическим HTTPS: ```yaml services: htop-web: image: gitea.jambotron.com/liosha84/htop-web:latest container_name: htop-web restart: unless-stopped pid: host volumes: - /sys:/host/sys:ro - /:/host/root:ro labels: caddy: htop-web.example.com caddy.reverse_proxy: "{{upstreams 8080}}" networks: - jambotron-net networks: jambotron-net: external: true ``` Caddy автоматически выдаёт TLS-сертификат через Let's Encrypt и маршрутизирует запросы на контейнер. ### 7.4 Развёртывание на различных платформах #### Linux-сервер На Linux-сервере контейнер получает прямой доступ к `/proc` и `/sys` хоста, что обеспечивает точные метрики: ```bash docker compose up -d --build ``` #### Docker Desktop на Windows Docker Desktop на Windows запускает Linux-контейнеры в виртуальной машине (WSL2 или Hyper-V). В этом случае метрики отражают состояние виртуальной машины, а не хостовой Windows-системы: > **Важно:** Метрики из контейнера не представляют полную картину Windows-хоста. Для мониторинга Windows рекомендуется использовать нативные инструменты или развёртывание на Linux. #### Raspberry Pi и ARM-устройства htop-web поддерживает ARM-архитектуру благодаря кроссплатформенности Java и OSHI. Сборка выполняется аналогично: ```bash docker compose up -d --build ``` Образ `eclipse-temurin:21-jre` доступен для `arm64` и `arm/v7`. --- ## 8. Безопасность Мониторинг системы раскрывает конфиденциальную информацию. При развёртывании необходимо учитывать: 1. **Аутентификация** — не размещайте htop-web в открытом доступе без защиты. Используйте обратный прокси с базовой аутентификацией или OAuth. 2. **HTTPS** — всегда используйте TLS при доступе из внешних сетей. 3. **Минимальные привилегии** — контейнер запускается с read-only доступом к файловой системе хоста, что снижает риски. 4. **Сетевая изоляция** — ограничьте доступ к порту 8088 через firewall или Docker-сети. --- ## 9. Сравнение с аналогами ### 9.1 Функциональное сравнение | Функция | htop-web | Netdata | Glances | Grafana+Prometheus | |---|---|---|---|---| | Веб-интерфейс | ✓ | ✓ | ✓ | ✓ | | CPU-метрики | ✓ | ✓ | ✓ | ✓ | | Память | ✓ | ✓ | ✓ | ✓ | | Процессы | ✓ | ✓ | ✓ | ✗ | | Диски | ✓ | ✓ | ✓ | ✓ | | Сеть | ✓ | ✓ | ✓ | ✓ | | Развёртывание | 1 контейнер | 1 контейнер | pip install | 3+ компонента | | Ресурсы | ~100 МБ RAM | ~200 МБ RAM | ~50 МБ RAM | ~500+ МБ RAM | | История данных | ✗ | ✓ | ✗ | ✓ | | Алертинг | ✗ | ✓ | ✓ | ✓ | | Графики | Прогресс-бары | Графики | Прогресс-бары | Графики | ### 9.2 Позиционирование htop-web ориентирован на сценарии, где важны: - **Простота** — одна команда для запуска; - **Минимальные ресурсы** — контейнер потребляет ~100 МБ RAM; - **Отсутствие внешних зависимостей** — нет баз данных, брокеров, агентов; - **Визуальное сходство с htop** — привычный интерфейс для системных администраторов. Для задач, требующих исторических данных, алертинга и мультихостного мониторинга, рекомендуется использовать Grafana + Prometheus или Netdata. --- ## 10. Заключение htop-web демонстрирует, что эффективный инструмент мониторинга может быть реализован как компактное самодостаточное приложение. Ключевые достижения проекта: 1. **Единый контейнер** — Angular-фронтенд и Spring Boot-бэкенд упакованы в один JAR, что упрощает развёртывание и обновление. 2. **Точные метрики хоста** — монтирование `/proc` и `/sys` хоста позволяет контейнеризованному приложению получать реальные данные сервера, а не метрики виртуальной среды контейнера. 3. **Реактивная архитектура** — Spring WebFlux на бэкенде и RxJS на фронтенде обеспечивают эффективную обработку данных в реальном времени. 4. **Fallback-механизм** — библиотека OSHI служит запасным источником метрик при недоступности файлов хоста. 5. **Минимальные требования** — приложение работает на устройствах с 512 МБ RAM, включая Raspberry Pi. Перспективы развития включают добавление исторического хранения метрик, алертинга по пороговым значениям и поддержки WebSocket для push-обновлений вместо polling. --- ## Список литературы 1. Spring Boot Documentation. — https://docs.spring.io/spring-boot/docs/current/reference/html/ 2. Spring WebFlux Reactive Web. — https://docs.spring.io/spring-framework/reference/web/reactive.html 3. Angular Documentation. — https://angular.dev/ 4. RxJS Documentation. — https://rxjs.dev/ 5. OSHI — Operating System and Hardware Information. — https://github.com/oshi/oshi 6. Docker Documentation. — https://docs.docker.com/ 7. Linux `/proc` filesystem. — man 5 proc 8. The htop source code. — https://github.com/htop-dev/htop 9. Netdata — Distributed real-time monitoring. — https://github.com/netdata/netdata 10. Grafana — The open observability platform. — https://github.com/grafana/grafana --- ## 11. Скачивание и демо ### Живое демо Попробовать htop-web в действии можно по адресу: **https://htop-web.jambotron.com/** Демо-инстанс развёрнут на Linux-сервере с доступом к реальным системным метрикам. ### Репозиторий Исходный код проекта доступен на Gitea: **https://gitea.jambotron.com/liosha84/htop-web** Для клонирования: ```bash git clone https://gitea.jambotron.com/liosha84/htop-web.git cd htop-web ``` ### Docker-образ Готовый Docker-образ опубликован в контейнерном реестре Gitea: **gitea.jambotron.com/liosha84/htop-web:latest** Быстрый запуск одним командой: ```bash docker run -d --name htop-web --pid host \ -v /:/host/root:ro -v /sys:/host/sys:ro \ -p 8088:8080 \ gitea.jambotron.com/liosha84/htop-web:latest ``` --- *Статья подготовлена на основе исходного кода проекта htop-web, версия 0.0.1-SNAPSHOT.*