mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 07:24:52 +09:00
Merge pull request #552 from Hornwitser/additional-system-info
Additional system info
This commit is contained in:
18 files changed
+206
-134
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:">
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user