Merge pull request #552 from Hornwitser/additional-system-info

Additional system info
This commit is contained in:
Hornwitser authored and GitHub committed 2024-01-17 20:28:06 +01:00
commit 80f0c63113
18 files changed
+206 -134

No files matched your search

+26 -26
View File
@@ -53,8 +53,8 @@ export default class Controller {
config: lib.ControllerConfig;
app: Application;
/** True if (@link Controller.systemMetrics} has changed since last time it was saved */
systemMetricsDirty = false;
/** True if (@link Controller.systems} has changed since last time it was saved */
systemsDirty = false;
/** True if {@link Controller.hosts} has changed since last time it was saved */
hostsDirty = false;
/** True if {@link Controller.instances} has changed since last time it was saved */
@@ -102,7 +102,7 @@ export default class Controller {
let databaseDirectory = config.get("controller.database_directory");
await fs.ensureDir(databaseDirectory);
const systemMetrics = await Controller.loadSystemMetrics(path.join(databaseDirectory, "metrics.json"));
const systems = await Controller.loadSystems(path.join(databaseDirectory, "systems.json"));
const hosts = await Controller.loadHosts(path.join(databaseDirectory, "hosts.json"));
const instances = await Controller.loadInstances(path.join(databaseDirectory, "instances.json"));
const saves = await Controller.loadSaves(path.join(databaseDirectory, "saves.json"));
@@ -115,7 +115,7 @@ export default class Controller {
const modStore = await lib.ModStore.fromDirectory(modsDirectory);
return [
systemMetrics,
systems,
hosts,
instances,
saves,
@@ -131,7 +131,7 @@ export default class Controller {
configPath: string,
config: lib.ControllerConfig,
public systemMetrics = new Map<lib.SystemMetrics["id"], lib.SystemMetrics>(),
public systems = new Map<lib.SystemInfo["id"], lib.SystemInfo>(),
/** Mapping of host id to host info */
public hosts = new Map<number, HostInfo>(),
/** Mapping of instance id to instance info */
@@ -162,7 +162,7 @@ export default class Controller {
});
// Handle subscriptions for all internal properties
this.subscriptions.handle(lib.SystemMetricsUpdateEvent, this.handleSystemMetricsSubscription.bind(this));
this.subscriptions.handle(lib.SystemInfoUpdateEvent, this.handleSystemInfoSubscription.bind(this));
this.subscriptions.handle(lib.HostUpdatesEvent, this.handleHostSubscription.bind(this));
this.subscriptions.handle(lib.InstanceDetailsUpdatesEvent, this.handleInstanceDetailsSubscription.bind(this));
this.subscriptions.handle(
@@ -444,30 +444,30 @@ export default class Controller {
const systemMetricsIntervalSeconds = this.config.get("controller.system_metrics_interval");
if (systemMetricsIntervalSeconds > 0) {
this.systemMetricsInterval = setInterval(
this.updateSystemMetrics.bind(this),
this.updateSystems.bind(this),
systemMetricsIntervalSeconds * 1000
);
}
}
async updateSystemMetrics() {
async updateSystems() {
try {
const requests: Promise<lib.SystemMetrics>[] = [];
const requests: Promise<lib.SystemInfo>[] = [];
for (let hostConnection of this.wsServer.hostConnections.values()) {
if (!hostConnection.connected) {
continue;
}
requests.push(hostConnection.send(new lib.SystemMetricsRequest()));
requests.push(hostConnection.send(new lib.SystemInfoRequest()));
}
requests.push(lib.gatherSystemMetrics("controller"));
requests.push(lib.gatherSystemInfo("controller"));
const newMetrics = await Promise.all(requests);
for (const metric of newMetrics) {
this.systemMetrics.set(metric.id, metric);
this.systems.set(metric.id, metric);
}
this.systemMetricsDirty = true;
this.subscriptions.broadcast(new lib.SystemMetricsUpdateEvent(newMetrics));
this.systemsDirty = true;
this.subscriptions.broadcast(new lib.SystemInfoUpdateEvent(newMetrics));
} catch (err: any) {
logger.error(`Unexpected error updating system metrics:\n${err.stack ?? err.message}`);
logger.error(`Unexpected error updating system infos:\n${err.stack ?? err.message}`);
}
}
@@ -507,9 +507,9 @@ export default class Controller {
}
let databaseDirectory = this.config.get("controller.database_directory");
if (this.systemMetricsDirty) {
this.systemMetricsDirty = false;
await Controller.saveSystemMetrics(path.join(databaseDirectory, "metrics.json"), this.systemMetrics);
if (this.systemsDirty) {
this.systemsDirty = false;
await Controller.saveSystems(path.join(databaseDirectory, "systems.json"), this.systems);
}
if (this.hostsDirty) {
@@ -539,8 +539,8 @@ export default class Controller {
await lib.invokeHook(this.plugins, "onSaveData");
}
static async loadSystemMetrics(filePath: string): Promise<Map<lib.SystemMetrics["id"], lib.SystemMetrics>> {
let json: Static<typeof lib.SystemMetrics.jsonSchema>[];
static async loadSystems(filePath: string): Promise<Map<lib.SystemInfo["id"], lib.SystemInfo>> {
let json: Static<typeof lib.SystemInfo.jsonSchema>[];
try {
json = JSON.parse(await fs.readFile(filePath, { encoding: "utf8" }));
@@ -551,11 +551,11 @@ export default class Controller {
return new Map();
}
return new Map(json.map(m => lib.SystemMetrics.fromJSON(m)).map(m => [m.id, m]));
return new Map(json.map(m => lib.SystemInfo.fromJSON(m)).map(m => [m.id, m]));
}
static async saveSystemMetrics(filePath: string, systemMetrics: Map<lib.SystemMetrics["id"], lib.SystemMetrics>) {
await lib.safeOutputFile(filePath, JSON.stringify([...systemMetrics.values()], null, "\t"));
static async saveSystems(filePath: string, systems: Map<lib.SystemInfo["id"], lib.SystemInfo>) {
await lib.safeOutputFile(filePath, JSON.stringify([...systems.values()], null, "\t"));
}
static async loadHosts(filePath: string): Promise<Map<number, HostInfo>> {
@@ -741,11 +741,11 @@ export default class Controller {
}
}
async handleSystemMetricsSubscription(request: lib.SubscriptionRequest) {
const systemMetrics = [...this.systemMetrics.values()].filter(
async handleSystemInfoSubscription(request: lib.SubscriptionRequest) {
const systems = [...this.systems.values()].filter(
metric => metric.updatedAt > request.lastRequestTime,
);
return systemMetrics.length ? new lib.SystemMetricsUpdateEvent(systemMetrics) : null;
return systems.length ? new lib.SystemInfoUpdateEvent(systems) : null;
}
/**
@@ -23,6 +23,7 @@ export default class HostConnection extends BaseConnection {
registerData: lib.RegisterHostData,
connector: WsServerConnector,
controller: Controller,
remoteAddress: string,
) {
super(connector, controller);
@@ -35,6 +36,7 @@ export default class HostConnection extends BaseConnection {
registerData.version,
this.plugins,
true,
remoteAddress,
previousHostInfo?.publicAddress,
previousHostInfo?.tokenValidAfter,
0,
+7 -1
View File
@@ -16,9 +16,11 @@ export default class HostInfo {
public plugins: Map<string, string>,
/** True if this host is currently connected to controller */
public connected: boolean = false,
/** IP this host last connected from */
public remoteAddress: string = "",
/** Value of host.public_address configured for this host */
public publicAddress: string = "",
/** Unix timestamp in seconds host token must be issued afterto be valid */
/** Unix timestamp in seconds host token must be issued after to be valid */
public tokenValidAfter: number = 0,
/** Millisecond Unix timestamp this entry was last updated at */
public updatedAt: number = 0,
@@ -32,6 +34,7 @@ export default class HostInfo {
"version": Type.String(),
"plugins": Type.Record(Type.String(), Type.String()),
"connected": Type.Boolean(),
"remote_address": Type.String(),
"public_address": Type.String(),
"token_valid_after": Type.Number(),
"updated_at": Type.Number(),
@@ -45,6 +48,7 @@ export default class HostInfo {
json.version,
new Map(Object.entries(json.plugins)),
json.connected,
json.remote_address,
json.public_address,
json.token_valid_after,
json.updated_at,
@@ -59,6 +63,7 @@ export default class HostInfo {
version: this.version,
plugins: Object.fromEntries(this.plugins),
connected: this.connected,
remote_address: this.remoteAddress,
public_address: this.publicAddress,
token_valid_after: this.tokenValidAfter,
updated_at: this.updatedAt,
@@ -72,6 +77,7 @@ export default class HostInfo {
this.name,
this.id,
this.connected,
this.remoteAddress,
this.publicAddress,
this.tokenValidAfter,
this.updatedAt,
+1 -1
View File
@@ -284,7 +284,7 @@ ${err.stack}`
);
}
connection = new HostConnection(data, connector, this.controller);
connection = new HostConnection(data, connector, this.controller, this.remoteAddr(req));
connector.on("close", () => {
if (this.hostConnections.get(data.id) === connection) {
this.hostConnections.delete(data.id);
+3 -3
View File
@@ -354,7 +354,7 @@ export default class Host extends lib.Link {
this.snoopEvent(lib.InstanceWhitelistUpdateEvent, this.handleWhitelistUpdateEvent.bind(this));
this.handle(lib.InstanceAssignInternalRequest, this.handleInstanceAssignInternalRequest.bind(this));
this.handle(lib.InstanceUnassignInternalRequest, this.handleInstanceUnassignInternalRequest.bind(this));
this.handle(lib.SystemMetricsRequest, this.handleSystemMetricsRequest.bind(this));
this.handle(lib.SystemInfoRequest, this.handleSystemInfoRequest.bind(this));
this.handle(lib.HostMetricsRequest, this.handleHostMetricsRequest.bind(this));
this.fallbackRequest(
lib.InstanceSaveDetailsListRequest, this.fallbackInstanceSaveDetailsListRequest.bind(this),
@@ -750,8 +750,8 @@ export default class Host extends lib.Link {
return instanceConnection;
}
async handleSystemMetricsRequest() {
return lib.gatherSystemMetrics(this.config.get("host.id"));
async handleSystemInfoRequest() {
return lib.gatherSystemInfo(this.config.get("host.id"));
}
async handleHostMetricsRequest() {
+25 -10
View File
@@ -204,13 +204,18 @@ export class LogMessageEvent {
}
}
export class SystemMetrics {
export class SystemInfo {
constructor(
/**
* Id of the host these metrics originate from, or the string
* "controller" if these metrics are for the controller.
*/
public id: number | "controller",
public hostname: string,
public node: string,
public kernel: string,
public machine: string,
public cpuModel: string,
public coreRatios: number[],
public memoryCapacity: number,
public memoryAvailable: number,
@@ -223,6 +228,11 @@ export class SystemMetrics {
static jsonSchema = Type.Object({
"id": Type.Union([Type.Number(), Type.Literal("controller")]),
"hostname": Type.String(),
"node": Type.String(),
"kernel": Type.String(),
"machine": Type.String(),
"cpuModel": Type.String(),
"coreRatios": Type.Array(Type.Number()),
"memoryCapacity": Type.Number(),
"memoryAvailable": Type.Number(),
@@ -235,6 +245,11 @@ export class SystemMetrics {
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.id,
json.hostname,
json.node,
json.kernel,
json.machine,
json.cpuModel,
json.coreRatios,
json.memoryCapacity,
json.memoryAvailable,
@@ -278,31 +293,31 @@ export class SystemMetrics {
}
}
export class SystemMetricsRequest {
declare ["constructor"]: typeof SystemMetricsRequest;
export class SystemInfoRequest {
declare ["constructor"]: typeof SystemInfoRequest;
static type = "request" as const;
static src = "controller" as const;
static dst = "host" as const;
static Response = SystemMetrics;
static Response = SystemInfo;
}
export class SystemMetricsUpdateEvent {
declare ["constructor"]: typeof SystemMetricsUpdateEvent;
export class SystemInfoUpdateEvent {
declare ["constructor"]: typeof SystemInfoUpdateEvent;
static type = "event" as const;
static src = "controller" as const;
static dst = "control" as const;
static permission = "core.system_metrics.subscribe" as const;
static permission = "core.system.subscribe" as const;
constructor(
public updates: SystemMetrics[],
public updates: SystemInfo[],
) { }
static jsonSchema = Type.Object({
"updates": Type.Array(SystemMetrics.jsonSchema),
"updates": Type.Array(SystemInfo.jsonSchema),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.updates.map(update => SystemMetrics.fromJSON(update)));
return new this(json.updates.map(update => SystemInfo.fromJSON(update)));
}
}
+5 -2
View File
@@ -64,8 +64,9 @@ export class HostDetails {
public name: string,
public id: number,
public connected: boolean,
public publicAddress?: string,
public tokenValidAfter?: number,
public remoteAddress = "",
public publicAddress: string = "",
public tokenValidAfter: number = 0,
/** Millisecond Unix timestamp this entry was last updated at */
public updatedAt = 0,
public isDeleted = false,
@@ -76,6 +77,7 @@ export class HostDetails {
"name": Type.String(),
"id": Type.Integer(),
"connected": Type.Boolean(),
"remoteAddress": Type.Optional(Type.String()),
"publicAddress": Type.Optional(Type.String()),
"tokenValidAfter": Type.Optional(Type.Number()),
"updatedAt": Type.Optional(Type.Number()),
@@ -88,6 +90,7 @@ export class HostDetails {
json.name,
json.id,
json.connected,
json.remoteAddress,
json.publicAddress,
json.tokenValidAfter,
json.updatedAt,
+2 -2
View File
@@ -25,8 +25,8 @@ export const dataClasses: (RequestClass<unknown, unknown> | EventClass<unknown>)
controller.LogSetSubscriptionsRequest,
controller.LogQueryRequest,
controller.LogMessageEvent,
controller.SystemMetricsRequest,
controller.SystemMetricsUpdateEvent,
controller.SystemInfoRequest,
controller.SystemInfoUpdateEvent,
controller.DebugDumpWsRequest,
controller.DebugWsMessageEvent,
+4 -4
View File
@@ -130,11 +130,11 @@ definePermission({
description: "Modify the controller config or entries of the controller config.",
});
definePermission({
name: "core.system_metrics.subscribe",
title: "Subscribe to system metrics updates",
name: "core.system.subscribe",
title: "Subscribe to system updates",
description:
"Subscribe to be notified when the system metrics detailing cpu, memory and disk usage for the " +
"controller and hosts are updated.",
"Subscribe to be notified when the system info detailing system specific information along with metrics " +
"such as cpu, memory and disk usage for the controller and hosts are updated.",
});
definePermission({
name: "core.host.get_config",
+11 -5
View File
@@ -2,9 +2,9 @@ import fs from "fs-extra";
import os from "os";
import util from "util";
import { Gauge } from "./prometheus";
import { SystemMetrics } from "./data";
import { SystemInfo } from "./data";
function cpuModel() {
function filterCpuModel() {
const model = os.cpus()[0].model;
// Remove irelevant clutter
return model
@@ -16,6 +16,7 @@ function cpuModel() {
.trim() // AMD CPUs may have trailing spaces in the model.
;
}
const cpuModel = filterCpuModel();
export const systemInfo = new Gauge(
"system_info",
@@ -27,7 +28,7 @@ export const systemInfo = new Gauge(
systemInfo.labels({
"kernel": os.type(),
"machine": os.machine(),
"cpu_model": cpuModel(),
"cpu_model": cpuModel,
"hostname": os.hostname(),
"node": process.version,
}).set(1);
@@ -133,7 +134,7 @@ function minZip<T>(a: T[], b: T[]): [T, T][] {
let previousTotalCpuMs: number[] = [];
let previousIdleCpuMs: number[] = [];
export async function gatherSystemMetrics(id: SystemMetrics["id"]) {
export async function gatherSystemInfo(id: SystemInfo["id"]) {
const cpus = os.cpus();
const currentTotalCpuMs = cpus.map(({ times }) => times.user + times.idle + times.irq + times.nice + times.sys);
const currentIdleCpuMs = cpus.map(({ times }) => times.idle);
@@ -151,8 +152,13 @@ export async function gatherSystemMetrics(id: SystemMetrics["id"]) {
diskAvailable = stats.bavail * stats.bsize;
}
return new SystemMetrics(
return new SystemInfo(
id,
os.hostname(),
process.version,
os.type(),
os.machine(),
cpuModel,
cpuUsage,
os.totalmem(),
os.freemem(),
@@ -8,27 +8,34 @@ import {
MetricDiskUsed, MetricDiskRatio,
} from "./system_metrics";
import { useAccount } from "../model/account";
import { useSystemMetrics } from "../model/system_metrics";
import { useSystems } from "../model/system";
import ControllerConfigTree from "./ControllerConfigTree";
import PageLayout from "./PageLayout";
import webUiPackage from "../../package.json";
const { Title } = Typography;
export default function ControllerPage() {
let account = useAccount();
const [systemMetrics] = useSystemMetrics();
const metrics = systemMetrics.get("controller");
const [systems] = useSystems();
const system = systems.get("controller");
return <PageLayout nav={[{ name: "Controller" }]}>
<h2>Controller</h2>
<Descriptions bordered size="small" column={{ xs: 1, md: 2, lg: 2, xl: 2, xxl: 2 }}>
<Descriptions.Item label="CPU Usage"><MetricCpuRatio metrics={metrics} /></Descriptions.Item>
<Descriptions.Item label="Cores"><MetricCpuUsed metrics={metrics} /></Descriptions.Item>
<Descriptions.Item label="Memory Usage"><MetricMemoryRatio metrics={metrics} /></Descriptions.Item>
<Descriptions.Item label="Memory"><MetricMemoryUsed metrics={metrics} /></Descriptions.Item>
<Descriptions.Item label="Disk Usage"><MetricDiskRatio metrics={metrics} /></Descriptions.Item>
<Descriptions.Item label="Disk"><MetricDiskUsed metrics={metrics} /></Descriptions.Item>
<Descriptions.Item label="Version">{webUiPackage.version}</Descriptions.Item>
<Descriptions.Item label="Node.js">{system?.node}</Descriptions.Item>
<Descriptions.Item label="OS Kernel">{system?.kernel}</Descriptions.Item>
<Descriptions.Item label="Machine">{system?.machine}</Descriptions.Item>
<Descriptions.Item label="Hostname" span={2}>{system?.hostname}</Descriptions.Item>
<Descriptions.Item label="CPU Model" span={2}>{system?.cpuModel}</Descriptions.Item>
<Descriptions.Item label="CPU Usage"><MetricCpuRatio system={system} /></Descriptions.Item>
<Descriptions.Item label="Cores"><MetricCpuUsed system={system} /></Descriptions.Item>
<Descriptions.Item label="Memory Usage"><MetricMemoryRatio system={system} /></Descriptions.Item>
<Descriptions.Item label="Memory"><MetricMemoryUsed system={system} /></Descriptions.Item>
<Descriptions.Item label="Disk Usage"><MetricDiskRatio system={system} /></Descriptions.Item>
<Descriptions.Item label="Disk"><MetricDiskUsed system={system} /></Descriptions.Item>
</Descriptions>
{account.hasPermission("core.log.follow") && <>
<Title level={5} style={{ marginTop: 16 }}>Console</Title>
+17 -10
View File
@@ -19,7 +19,7 @@ import {
MetricDiskUsed, MetricDiskRatio,
} from "./system_metrics";
import { formatTimestamp } from "../util/time_format";
import { useSystemMetrics } from "../model/system_metrics";
import { useSystems } from "../model/system";
const { Title } = Typography;
@@ -29,8 +29,8 @@ export default function HostViewPage() {
let control = useContext(ControlContext);
let account = useAccount();
let [instances] = useInstances();
const [systemMetrics] = useSystemMetrics();
const metrics = systemMetrics.get(hostId);
const [systems] = useSystems();
const system = systems.get(hostId);
const [host, synced] = useHost(hostId);
const hostInstances = new Map([...instances].filter(([_id, instance]) => instance.assignedHost === hostId));
@@ -78,13 +78,20 @@ export default function HostViewPage() {
{host["connected"] ? "Connected" : "Disconnected"}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="CPU Usage"><MetricCpuRatio metrics={metrics} /></Descriptions.Item>
<Descriptions.Item label="Cores"><MetricCpuUsed metrics={metrics} /></Descriptions.Item>
<Descriptions.Item label="Memory Usage"><MetricMemoryRatio metrics={metrics} /></Descriptions.Item>
<Descriptions.Item label="Memory"><MetricMemoryUsed metrics={metrics} /></Descriptions.Item>
<Descriptions.Item label="Disk Usage"><MetricDiskRatio metrics={metrics} /></Descriptions.Item>
<Descriptions.Item label="Disk"><MetricDiskUsed metrics={metrics} /></Descriptions.Item>
<Descriptions.Item label="Version">{host["version"]}</Descriptions.Item>
<Descriptions.Item label="Version">{host.version}</Descriptions.Item>
<Descriptions.Item label="Node.js">{system?.node}</Descriptions.Item>
<Descriptions.Item label="OS Kernel">{system?.kernel}</Descriptions.Item>
<Descriptions.Item label="Machine">{system?.machine}</Descriptions.Item>
<Descriptions.Item label="Hostname" span={2}>{system?.hostname}</Descriptions.Item>
<Descriptions.Item label="Connected From" span={2}>{host.remoteAddress}</Descriptions.Item>
<Descriptions.Item label="CPU Model" span={2}>{system?.cpuModel}</Descriptions.Item>
<Descriptions.Item label="CPU Usage"><MetricCpuRatio system={system} /></Descriptions.Item>
<Descriptions.Item label="Cores"><MetricCpuUsed system={system} /></Descriptions.Item>
<Descriptions.Item label="Memory Usage"><MetricMemoryRatio system={system} /></Descriptions.Item>
<Descriptions.Item label="Memory"><MetricMemoryUsed system={system} /></Descriptions.Item>
<Descriptions.Item label="Disk Usage"><MetricDiskRatio system={system} /></Descriptions.Item>
<Descriptions.Item label="Disk"><MetricDiskUsed system={system} /></Descriptions.Item>
<Descriptions.Item label="Public Address">{host.publicAddress}</Descriptions.Item>
{
host.tokenValidAfter
? <Descriptions.Item label="Tokens valid after:">
+26 -14
View File
@@ -15,7 +15,7 @@ import {
MetricDiskUsed, MetricDiskRatio,
} from "./system_metrics";
import { useHosts } from "../model/host";
import { useSystemMetrics } from "../model/system_metrics";
import { useSystems } from "../model/system";
import notify, { notifyErrorHandler } from "../util/notify";
const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;
@@ -189,9 +189,10 @@ export default function HostsPage() {
let account = useAccount();
let navigate = useNavigate();
let [hosts] = useHosts();
const [metrics] = useSystemMetrics();
const [systems] = useSystems();
const [showRatios, setShowRatios] = useState(true);
const [showNumbers, setShowNumbers] = useState(false);
const [showCpuModel, setShowCpuModel] = useState(false);
return <PageLayout nav={[{ name: "Hosts" }]}>
<PageHeader
@@ -207,48 +208,55 @@ export default function HostsPage() {
defaultSortOrder: "ascend",
sorter: (a, b) => strcmp(a.name, b.name),
},
{
title: "CPU Model",
dataIndex: "cpuModel",
sorter: (a, b) => strcmp(systems.get(a.id)?.cpuModel ?? "", systems.get(b.id)?.cpuModel ?? ""),
render: (_, host) => systems.get(host.id)?.cpuModel,
hidden: !showCpuModel,
},
{
title: "CPU%",
sorter: (a, b) => (metrics.get(a.id)?.cpuRatio ?? 0) - (metrics.get(b.id)?.cpuRatio ?? 0),
render: (_, host) => <MetricCpuRatio metrics={metrics.get(host.id)} />,
sorter: (a, b) => (systems.get(a.id)?.cpuRatio ?? 0) - (systems.get(b.id)?.cpuRatio ?? 0),
render: (_, host) => <MetricCpuRatio system={systems.get(host.id)} />,
hidden: !showRatios,
},
{
title: "Cores",
sorter: (a, b) => (
(metrics.get(a.id)?.cpuUsed ?? 0) - (metrics.get(b.id)?.cpuUsed ?? 0)
(systems.get(a.id)?.cpuUsed ?? 0) - (systems.get(b.id)?.cpuUsed ?? 0)
),
render: (_, host) => <MetricCpuUsed metrics={metrics.get(host.id)} />,
render: (_, host) => <MetricCpuUsed system={systems.get(host.id)} />,
hidden: !showNumbers,
},
{
title: "Mem%",
sorter: (a, b) => (
(metrics.get(a.id)?.memoryRatio ?? 0) - (metrics.get(b.id)?.memoryRatio ?? 0)
(systems.get(a.id)?.memoryRatio ?? 0) - (systems.get(b.id)?.memoryRatio ?? 0)
),
render: (_, host) => <MetricMemoryRatio metrics={metrics.get(host.id)} />,
render: (_, host) => <MetricMemoryRatio system={systems.get(host.id)} />,
hidden: !showRatios,
},
{
title: "Memory",
sorter: (a, b) => (
(metrics.get(a.id)?.memoryUsed ?? 0) - (metrics.get(b.id)?.memoryUsed ?? 0)
(systems.get(a.id)?.memoryUsed ?? 0) - (systems.get(b.id)?.memoryUsed ?? 0)
),
render: (_, host) => <MetricMemoryUsed metrics={metrics.get(host.id)} />,
render: (_, host) => <MetricMemoryUsed system={systems.get(host.id)} />,
hidden: !showNumbers,
},
{
title: "Disk%",
sorter: (a, b) => (metrics.get(a.id)?.diskAvailable ?? 0) - (metrics.get(b.id)?.diskAvailable ?? 0),
render: (_, host) => <MetricDiskRatio metrics={metrics.get(host.id)} />,
sorter: (a, b) => (systems.get(a.id)?.diskAvailable ?? 0) - (systems.get(b.id)?.diskAvailable ?? 0),
render: (_, host) => <MetricDiskRatio system={systems.get(host.id)} />,
hidden: !showRatios,
},
{
title: "Disk",
sorter: (a, b) => (
(metrics.get(a.id)?.diskUsed ?? 0) - (metrics.get(b.id)?.diskUsed ?? 0)
(systems.get(a.id)?.diskUsed ?? 0) - (systems.get(b.id)?.diskUsed ?? 0)
),
render: (_, host) => <MetricDiskUsed metrics={metrics.get(host.id)} />,
render: (_, host) => <MetricDiskUsed system={systems.get(host.id)} />,
hidden: !showNumbers,
},
{
@@ -290,6 +298,10 @@ export default function HostsPage() {
Show metric numbers:
<Switch checked={showNumbers} onChange={checked => { setShowNumbers(checked); }} />
</label>
<label style={{ width: "14em", display: "flex", justifyContent: "space-between", marginRight: 16 }}>
Show CPU model:
<Switch checked={showCpuModel} onChange={checked => { setShowCpuModel(checked); }} />
</label>
</Flex>
<PluginExtra component="HostsPage" />
</PageLayout>;
@@ -3,53 +3,53 @@ import React from "react";
import * as lib from "@clusterio/lib";
import { Progress } from "antd";
export function MetricCpuRatio(props: { metrics?: lib.SystemMetrics }) {
if (!props.metrics) {
export function MetricCpuRatio(props: { system?: lib.SystemInfo }) {
if (!props.system) {
return "N/A";
}
const ratio = props.metrics.cpuRatio;
const ratio = props.system.cpuRatio;
return <Progress status="normal" percent={Math.ceil(ratio * 100)}/>;
}
export function MetricCpuUsed(props: { metrics?: lib.SystemMetrics }) {
if (!props.metrics) {
export function MetricCpuUsed(props: { system?: lib.SystemInfo }) {
if (!props.system) {
return "N/A";
}
const used = props.metrics.cpuUsed.toLocaleString(undefined, { maximumFractionDigits: 1 });
const capacity = props.metrics.cpuCapacity;
const used = props.system.cpuUsed.toLocaleString(undefined, { maximumFractionDigits: 1 });
const capacity = props.system.cpuCapacity;
return `${used} / ${capacity}`;
}
export function MetricMemoryRatio(props: { metrics?: lib.SystemMetrics }) {
if (!props.metrics) {
export function MetricMemoryRatio(props: { system?: lib.SystemInfo }) {
if (!props.system) {
return "N/A";
}
const ratio = props.metrics.memoryRatio;
const ratio = props.system.memoryRatio;
return <Progress status="normal" percent={Math.ceil(ratio * 100)}/>;
}
export function MetricMemoryUsed(props: { metrics?: lib.SystemMetrics }) {
if (!props.metrics) {
export function MetricMemoryUsed(props: { system?: lib.SystemInfo }) {
if (!props.system) {
return "N/A";
}
const used = lib.formatBytes(props.metrics.memoryUsed, "binary");
const capacity = lib.formatBytes(props.metrics.memoryCapacity, "binary");
const used = lib.formatBytes(props.system.memoryUsed, "binary");
const capacity = lib.formatBytes(props.system.memoryCapacity, "binary");
return `${used} / ${capacity}`;
}
export function MetricDiskRatio(props: { metrics?: lib.SystemMetrics }) {
if (!props.metrics) {
export function MetricDiskRatio(props: { system?: lib.SystemInfo }) {
if (!props.system) {
return "N/A";
}
const ratio = props.metrics.diskRatio;
const ratio = props.system.diskRatio;
return <Progress status="normal" percent={Math.ceil(ratio * 100)}/>;
}
export function MetricDiskUsed(props: { metrics?: lib.SystemMetrics }) {
if (!props.metrics) {
export function MetricDiskUsed(props: { system?: lib.SystemInfo }) {
if (!props.system) {
return "N/A";
}
const used = lib.formatBytes(props.metrics.diskUsed);
const capacity = lib.formatBytes(props.metrics.diskCapacity);
const used = lib.formatBytes(props.system.diskUsed);
const capacity = lib.formatBytes(props.system.diskCapacity);
return `${used} / ${capacity}`;
}
@@ -1,8 +1,8 @@
import { useCallback, useContext, useSyncExternalStore } from "react";
import ControlContext from "../components/ControlContext";
export function useSystemMetrics() {
export function useSystems() {
const control = useContext(ControlContext);
const subscribe = useCallback((callback: () => void) => control.systemMetrics.subscribe(callback), [control]);
return useSyncExternalStore(subscribe, () => control.systemMetrics.getSnapshot());
const subscribe = useCallback((callback: () => void) => control.systems.subscribe(callback), [control]);
return useSyncExternalStore(subscribe, () => control.systems.getSnapshot());
}
+1 -1
View File
@@ -59,7 +59,7 @@ export class Control extends lib.Link {
logHandlers: Map<lib.LogFilter, logHandler[]> = new Map();
/** Updates handled by the subscription service */
systemMetrics = new lib.EventSubscriber(lib.SystemMetricsUpdateEvent, this);
systems = new lib.EventSubscriber(lib.SystemInfoUpdateEvent, this);
hosts = new lib.EventSubscriber(lib.HostUpdatesEvent, this);
instances = new lib.EventSubscriber(lib.InstanceDetailsUpdatesEvent, this);
saves = new lib.EventSubscriber(lib.InstanceSaveDetailsUpdatesEvent, this);
+36
View File
@@ -0,0 +1,36 @@
"use strict";
const assert = require("assert").strict;
const lib = require("@clusterio/lib");
const { SystemInfo } = lib;
describe("lib/data/SystemInfo", function() {
describe("class SystemInfo", function() {
it("correctly calculates derived metrics", function() {
const system = new SystemInfo(
"controller",
"server",
"v18.0.0",
"Linux",
"x86_64",
"x86 CPU",
[0.5, 0.25, 0, 1],
1024,
768,
2048,
512,
0,
false
);
assert.equal(system.cpuCapacity, 4);
assert.equal(system.cpuUsed, 1.75);
assert.equal(system.cpuAvailable, 2.25);
assert.equal(system.cpuRatio, 0.4375);
assert.equal(system.memoryUsed, 256);
assert.equal(system.memoryRatio, 0.25);
assert.equal(system.diskUsed, 1536);
assert.equal(system.diskRatio, 0.75);
});
});
});
-22
View File
@@ -1,22 +0,0 @@
"use strict";
const assert = require("assert").strict;
const lib = require("@clusterio/lib");
const { SystemMetrics } = lib;
describe("lib/data/ModPack", function() {
describe("class SystemMetrics", function() {
it("correctly calculates derived metrics", function() {
const metrics = new SystemMetrics("controller", [0.5, 0.25, 0, 1], 1024, 768, 2048, 512, 0, false);
assert.equal(metrics.cpuCapacity, 4);
assert.equal(metrics.cpuUsed, 1.75);
assert.equal(metrics.cpuAvailable, 2.25);
assert.equal(metrics.cpuRatio, 0.4375);
assert.equal(metrics.memoryUsed, 256);
assert.equal(metrics.memoryRatio, 0.25);
assert.equal(metrics.diskUsed, 1536);
assert.equal(metrics.diskRatio, 0.75);
});
});
});