Navigation
ELOWEN · Documentation

Last updated: October 20, 2026

Browse documentation · Host resource metrics
Developer reference

Host resource metrics

What it is

Settings → System shows five rings for the whole host — CPU, memory, disk, IO and network — plus uptime as a plain number. Clicking a ring opens the stored history for that resource, in the same modal the Sandbox Project history draws: one range, the metric toggles, the shared time-series chart. Both metrics routes are administrator-only (notAdmin → 403); the /system panel they sit beside is readable by any signed-in account.

Owning code and consumers

  • Live readings: src/hostMetrics/resources.ts. One fresh read per request — CPU from os.cpus(), memory from os.totalmem()/os.freemem(), disk from hostDiskUsage in src/api/systemDiagnostics.ts, IO from /proc/diskstats counting whole physical block devices only. A device is skipped when it has no directory of its own under /sys/block (a partition), when it lists slaves (a composite such as dm-* or md-*), or when it is a loop device, because each of these repeats I/O another line already reports. Network comes from /proc/net/dev with loopback excluded.
  • Sampling: src/hostMetrics/sampler.ts, scheduled once a minute from src/daemon/maintenance.ts. The daemon calls repositories.maintenance.maintain('hostMetrics', undefined); the operation takes no now from the loop and reads the host itself, so the sample's own sampledAt is the clock it appends and prunes with. Core assembly supplies the process-owned sampler and its independent reader baseline; maintenance passes the bound events.hostResources repository to append, roll up and prune through PostgreSQL. Sampling happens outside retried SQL transactions. The first sample is requested one minute after boot. No sample is taken at boot. A failed sample reaches the daemon and is logged with daemon.host_metrics_failed; shutdown drains admitted samples before closing the pool.
  • Storage and reads: packages/db/src/events/hostResources.ts over the resource_samples (minute) and resource_rollups (hourly) tables, renamed by packages/db/migrations/0005_resource_history_subject.sql.
  • Routes: src/api/routes/system.ts — GET /system/metrics (live) and GET /system/metrics/history (stored), both notAdmin → 403.
  • Shared vocabulary: packages/plugin-shared/resourceHistory.mjs, the same module the Sandbox Project history uses. Metric ids, units, windows, retention and the bucket shaper live there once, so a host and a Project curve cannot round or label the same measurement differently.
  • Browser: web/modules/settings/HostMetrics.tsx (rings) and web/components/ui/ResourceHistoryModal.tsx (history), composed from the shared ResourceMeters, Modal, DateRangeFilter and TimeSeriesChart. Labels, figures and the chart come from web/lib/resourceHistoryChart.js, the generated byte-identical copy of elowen-plugin-ui-kit/resourceHistory that inlines the shared vocabulary (scripts/generate-resource-history.mjs).

Subject-aware history

events.hostResources.append(subject, samples) and read(subject, selection, now) require an explicit 'host' or 'computer:<projectId>' subject. The primary keys are (subject, metric, bucket_at); migration 0005 preserves every existing host value under host. prepareSample still projects the seven canonical series. The host sampler and system route pass host; CLI data messages append the computer's first reading of each minute. Duplicate arrivals, including reconnects, cannot increment rollups twice. prune(now) shares one 1000-row budget across all subjects and both retention tiers.

Computer appends take a key-share lock on the live local Project before writing. Project removal holds the deletion lock and deletes both subjects' tables for that computer in its own transaction, so a late frame cannot recreate orphaned history. Host rows remain independent. The computer read route requires canExecute membership, including for administrators; host history stays administrator-only.

Cadence, retention and resolution

The sampler writes one sample a minute (all seven wire series) and the corresponding hourly rollup. Detail rows live 48 hours, hourly rollups 30 days; one response carries at most 720 buckets, and an automatic resolution moves from 1 m to 5 m to 1 h as the range grows. Retention, windows, resolution and the cap all come from RESOURCE_HISTORY in the shared module, so the numbers the daemon stores and the numbers the browser asks for cannot drift apart. The live route reports the sampling cadence from there, and the maintenance loop schedules the sampler with it.

Deliberate differences from the Sandbox Project history

  • The host is one scope: no project id, and no machine generation to fence, so a point's generation is always null. A bucket nothing was measured in stays an explicit missing point so the curve leaves a gap.
  • The host has no quotas, so IO and network are absolute rates with no percentage, while CPU, memory and disk carry a share of the host's own ceiling — its core count, its total memory and the size of the filesystem that holds the database. Disk is sampled every 60 s rather than the Project tier's 300 s because statfs is free; there is no filesystem walk behind it.
  • A source the host cannot read is reported as unavailable, never as a zero and never as an exception into the maintenance loop. The host has no stopped or sampling state.
  • The completed sampler reply also carries the numeric problem-report projection described in Problem reporting boundary. Boot caches the reply for the loop watchdog; warning emission reads only that cache. HostResourceRead narrows ready readings to required figures, while the public shared resource wire shape remains unchanged.

Delta baselines

The host and CLI share the physical-disk classifier, Linux parsers and delta-per-second helper in packages/plugin-shared/computerMetrics.mjs. CPU, IO and network are rates, so every reader keeps its own previous counters. The sampler holds its own baselines and must never share readSystemDiagnostics()'s single previous value: two readers of one baseline would each consume the other's delta and both would report nonsense. The live card's reader is a third, independent baseline, which is why the route serves a freshly measured snapshot instead of reading the stored samples back.

Wire contract

The live snapshot is declared in src/shared/wireContract.ts (HostResourceSnapshot). The history response is the shared ResourceHistoryResponse (its series of ResourceHistoryPoint), declared once in packages/plugin-shared/resourceHistory.d.mts; the wire contract re-exports the response and the point for the web; the window values the route accepts are the shared RESOURCE_WINDOW_IDS. A malformed history query is rejected at the boundary (zod) and by the shared cross-field rules; the reply is a 400 with a stable error code (invalid_history_query for a malformed query or an invalid metric selection, invalid_history_range, unsupported_history_resolution, range_outside_retention). An absent history store answers 503 history_unavailable.