---
module: agent.srv-monitor
title: Server Monitor — CPU/memory/disk/service health watcher for SRV devices
tab: Architect
order: 30
audience: dev
---

> **Internal · how it's built.** Confirmed against `hiveops-module-srv-monitor` source (`com.hiveops.srvmonitor`) 2026-07-01. See [technical.agent] for agent-wide module-system and event-pipeline context.

## Maven module

`hiveops-module-srv-monitor` (artifact `hiveops-module-srv-monitor`, own version `1.0.0`, parented by `hiveops-agent-parent` — currently `4.4.3`). Packaged as a standalone drop-in jar (`<finalName>${project.artifactId}</finalName>` → `hiveops-module-srv-monitor.jar`).

Dependencies (all **provided** scope — supplied by the running agent, not fat-packed):
- `hiveops-core` — the `AgentModule` SPI, `ModuleContext`, `PlatformService`, HTTP helpers.
- `hiveops-agent-journal` — the `IncidentEventClient` and `CreateJournalEventRequest` DTO (the shared journal-events pipeline).
- `log4j-api`, `commons-lang3` — logging + `StringUtils`/`NumberUtils` parsing.

Registered via ServiceLoader SPI: `src/main/resources/META-INF/services/com.hiveops.core.module.AgentModule` → `com.hiveops.srvmonitor.ServerMonitorModule`.

## Classes and flow

Five source classes, no sub-packages:

| Class | Role |
|---|---|
| `ServerMonitorModule` | `AgentModule` implementation — lifecycle, config, scheduler, transition/threshold logic, event build+send |
| `ServerMonitorCollector` | Interface: `ServerMetrics collect(List<String> monitoredServices)` |
| `LinuxServerMonitorCollector` | Linux implementation — `/proc/stat`, `/proc/meminfo`, `File.listRoots()`, `systemctl is-active` |
| `WindowsServerMonitorCollector` | Windows implementation — PowerShell (`Get-Counter`, CIM `Win32_*`, `Get-Service`) |
| `ServerMetrics` | Immutable value object: `cpuPercent`, `memoryPercent`, `List<DiskUsage>`, `Map<String,Boolean> serviceStatus` |

### Lifecycle (`AgentModule`)

`initialize()` → `isEnabled()` → `start()` → `stop()`, the standard module lifecycle from [technical.agent].

- **`initialize(ModuleContext)`** — reads `srv.monitor.enabled`; if false, logs and returns with `ready=false`. Resolves the endpoint (`agent.endpoint` preferred, else `incident.endpoint`); if neither, logs "no endpoint configured" and stays inactive. Builds `HttpClientSettings` via `HttpClientHelper.loadSettingsFromProperties(props, prefix)` where `prefix` is `"agent"` or `"incident"` to match the chosen endpoint. Parses thresholds/interval/samples, splits `srv.monitor.services` on commas. Constructs the `IncidentEventClient` (with `atmName` + `country` from the context) and selects the collector via **`context.getPlatformService().isWindows()`**. Sets `ready=true`.
- **`isEnabled()`** returns `ready` — so a module that failed the enable gate or endpoint check reports disabled and is never started by the registry.
- **`start()`** creates a single daemon-thread `ScheduledExecutorService` (thread name `srv-monitor`) and `scheduleAtFixedRate(this::poll, 0, intervalSeconds, SECONDS)` — first poll immediately, then every interval.
- **`stop()`** `shutdownNow()` on the scheduler. **`isHealthy()`** = scheduler exists and isn't terminated.
- **`getDependencies()`** is empty — no ordering constraints against other modules. **`getLogPackage()`** = `com.hiveops.srvmonitor` (routes to `logs/modules/srv-monitor.log`).

### Poll → detect → emit

`poll()` (the fixed-rate task) is a pure poll loop — no log tailing, no hooks:

1. `collector.collect(monitoredServices)` gathers one `ServerMetrics` snapshot.
2. Four checkers each append zero or one `CreateJournalEventRequest` to a batch list, based on **state transition** against the checker's stored last-state:
   - `checkCpu` — increments `cpuHighCount` while at/above threshold; emits `SERVER_CPU_HIGH` only when `!cpuHighState && cpuHighCount >= sustainedSamples`. Dropping below threshold resets the counter and, if it was high, emits `SERVER_CPU_NORMAL`.
   - `checkMemory` — emits `SERVER_MEMORY_HIGH`/`SERVER_MEMORY_NORMAL` on the `memHighState` boolean flip (no debounce).
   - `checkDisks` — per-mount `Map<String,Boolean> diskHighState`; emits `SERVER_DISK_LOW`/`SERVER_DISK_NORMAL` per filesystem on flip.
   - `checkServices` — per-service `Map<String,Boolean> serviceDownState`; emits `SERVICE_DOWN`/`SERVICE_UP` on flip.
3. If the batch is non-empty, `client.sendEvents(events)`.

All transition state (`cpuHighState`, `memHighState`, the two maps) is **in-memory only** — an agent restart clears it, so the module re-emits a HIGH on the next breaching poll after restart if the condition persists.

### Collectors (platform abstraction)

The module picks a collector at init via the agent's shared `PlatformService.isWindows()` — the same platform-detection facade the rest of the agent uses (see [technical.agent] "platform abstraction"). Beyond that choice each collector does its own low-level I/O; it does **not** route metric collection through `SystemCommands`/`PathResolver`.

- **Linux** — CPU: reads the `cpu ` line of `/proc/stat` twice, 1 s apart (`Thread.sleep(1000)` inside the poll thread), and diffs busy/total jiffies. Memory: `MemTotal`/`MemAvailable` from `/proc/meminfo` → `(total-available)/total`. Disk: `File.listRoots()` → `getTotalSpace()`/`getFreeSpace()` (root filesystems only). Services: `systemctl is-active <name>`, up iff first output line trims to `active`.
- **Windows** — all via `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command`. CPU: `Get-Counter '\Processor(_Total)\% Processor Time'`. Memory: `Win32_OperatingSystem` `TotalVisibleMemorySize`/`FreePhysicalMemory`. Disk: `Win32_LogicalDisk` (per drive letter). Services: `Get-Service -Name @(...)`, up iff `Status = Running`; any queried service not returned is marked down. A daemon stderr-drainer thread reads PowerShell stderr and a non-zero exit is logged.

## How events reach the platform

`ServerMonitorModule` builds each event with `CreateJournalEventRequest.builder()`:

```
agentAtmId   = atmName            (from ModuleContext)
deviceType   = "SRV"              (constant DEVICE_TYPE)
eventType    = <one of the 8 strings>
eventDetails = human string       (e.g. "CPU at 92.3% (threshold 85.0%, sustained 3 samples)")
eventSource  = "HIVEOPS_AGENT"    (constant EVENT_SOURCE)
```

and sends via the shared journal-events client:

```
ServerMonitorModule
   │  IncidentEventClient.sendEvents(List<CreateJournalEventRequest>)
   │  (HttpClientSettings from agent.endpoint / incident.endpoint)
   ▼
POST {endpoint}/api/journal-events          ← one HTTP POST per event
   │  (via agent-proxy, X-Institution-Key/auth on the shared client)
   ▼
hiveops-incident journal-events ingest  →  SRV device auto-registers, events stored
```

Note: `sendEvents` is **not** a batch call — it loops and issues one `POST /api/journal-events` per event (the DTO comment states the backend "may not support batch operations"). This is the same endpoint and `IncidentEventClient` the `journal-events` module uses for ATM events; `srv-monitor` reuses it wholesale, only changing `deviceType` to `SRV`. In the current topology `agent.endpoint` points at **agent-proxy**, which forwards to incident's ingest — see [technical.agent] for the agent → agent-proxy → incident path.
