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-эндпоинтов, каждый из которых делегирует сбор данных соответствующему сервису:

@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<SystemMetricsResponse> getMetrics() {
        return Mono.fromCallable(_systemMetricsService::getMetrics)
                .subscribeOn(Schedulers.boundedElastic());
    }

    @GetMapping("/linux")
    public Mono<LinuxDetailsResponse> getLinuxDetails() {
        return Mono.fromCallable(_linuxDetailsService::getDetails)
                .subscribeOn(Schedulers.boundedElastic());
    }
    // ... остальные эндпоинты
}

Каждый метод оборачивает синхронный вызов сервиса в Mono и выполняет его на Schedulers.boundedElastic(), что предотвращает блокировку реактивного пула потоков при чтении файловых систем.

4.3 Сбор метрик CPU и памяти

Сервис SystemMetricsService демонстрирует ключевой паттерн проекта — чтение метрик хоста из смонтированной файловой системы с fallback на OSHI:

@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 на основе разницы тиков между двумя снимками:

@Service
public class SystemProcessesService {

    public synchronized List<ProcessInfoResponse> 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:

@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 секуннда:

@Injectable({ providedIn: 'root' })
export class SystemMetricsService {
  private readonly http = inject(HttpClient);
  private readonly refreshInterval = 1000;

  readonly metrics$: Observable<SystemMetrics> = timer(0, this.refreshInterval).pipe(
    switchMap(() =>
      this.http.get<SystemMetrics>('/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<SystemProcess[]> = timer(0, this.refreshInterval).pipe(
    switchMap(() =>
      this.http.get<SystemProcess[]>('/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 инъецирует сервис метрик и предоставляет данные для шаблона:

@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 — выход).
<nav class="view-switcher">
  <button (click)="setViewMode('monitor')">Monitor</button>
  <button (click)="setViewMode('htop')">htop</button>
  <button (click)="setViewMode('htop-original')">htop-original</button>
</nav>

6. Сборка и упаковка

6.1 Gradle-сборка

Процесс сборки автоматически строит фронтенд и упаковывает его в бэкенд:

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:

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 определяет сервис с доступом к файловой системе хоста:

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: Клонирование репозитория

git clone https://gitea.jambotron.com/liosha84/htop-web.git
cd htop-web

Шаг 2: Сборка приложения

cd backend
./gradlew clean build
cd ..

Gradle автоматически соберёт Angular-фронтенд и упакует его в JAR-файл бэкенда.

Шаг 3: Запуск контейнера

docker compose up -d --build

Приложение доступно по адресу http://localhost:8088.

Шаг 4: Проверка статуса

# Статус контейнера
docker compose ps

# Логи в реальном времени
docker compose logs -f htop-web

Шаг 5: Остановка

docker compose down

7.3 Продвинутое развёртывание с Caddy

Для продакшн-развёртывания рекомендуется использовать обратный прокси с автоматическим HTTPS:

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 хоста, что обеспечивает точные метрики:

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. Сборка выполняется аналогично:

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

Для клонирования:

git clone https://gitea.jambotron.com/liosha84/htop-web.git
cd htop-web

Docker-образ

Готовый Docker-образ опубликован в контейнерном реестре Gitea:

gitea.jambotron.com/liosha84/htop-web:latest

Быстрый запуск одним командой:

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.