fix: finish 2.9.0 plugin moves and get every check green

This commit is contained in:
LukeGus committed 2026-09-24 23:35:18 -05:00
1 parent 8c1e02af89
commit 6489b62ece
335 files changed
+29878 -17749

No files matched your search

+12 -2
View File
@@ -98,7 +98,12 @@ export async function collectMetrics(
onAuthenticated?.();
const platform = await detectHostPlatform(client);
const result: CollectedMetrics = {
cpu: await collectCpuMetrics(client, platform, host.id),
cpu: await collectCpuMetrics(
client,
platform,
host.id,
state.rateSamples.cpu,
),
memory: await collectMemoryMetrics(client, platform),
disk: await collectDiskMetrics(
client,
@@ -106,7 +111,12 @@ export async function collectMetrics(
mounts.monitoredMounts,
platform,
),
network: await collectNetworkMetrics(client, platform, host.id),
network: await collectNetworkMetrics(
client,
platform,
host.id,
state.rateSamples.network,
),
uptime: await collectUptimeMetrics(client, platform),
processes: await collectProcessesMetrics(client),
system: await collectSystemMetrics(client, platform),
@@ -30,3 +30,32 @@ export function hostImportNormalizer(
});
return { ...settings };
}
/**
* Registered as "host-metrics.hostPayloadLegacy": the 2.8 statsConfig shape
* Termix-Mobile still reads. Remove once the mobile app reads
* pluginSettings["host-metrics"] and GET /host/status.
*/
export function hostPayloadLegacy(
values: Record<string, unknown>,
host: Record<string, unknown>,
): Record<string, unknown> {
const settings = readHostMetricsSettings(values);
const statusInterval =
typeof host.statusCheckInterval === "number"
? host.statusCheckInterval
: null;
return {
statsConfig: {
enabledWidgets: settings.enabledWidgets,
statusCheckEnabled: host.statusCheckEnabled !== false,
statusCheckInterval: statusInterval ?? 30,
useGlobalStatusInterval: statusInterval === null,
metricsEnabled: settings.metricsEnabled,
metricsInterval: settings.metricsInterval ?? 30,
useGlobalMetricsInterval: settings.metricsInterval == null,
excludedMounts: settings.excludedMounts,
monitoredMounts: settings.monitoredMounts,
},
};
}
+2 -1
View File
@@ -7,7 +7,7 @@ import { MetricsSessions } from "./sessions.js";
import { CollectorRegistry } from "./collectors.js";
import { MetricsPoller, TOPIC_HEALTH_CHECK } from "./poller.js";
import { registerRoutes } from "./routes.js";
import { hostImportNormalizer } from "./host-import.js";
import { hostImportNormalizer, hostPayloadLegacy } from "./host-import.js";
import { newSessionId, supportsMetrics } from "./helpers.js";
export type { MetricsCollectorV1 } from "./collectors.js";
@@ -101,6 +101,7 @@ export async function activate(ctx: PluginContext) {
"host-metrics.hostImportNormalizer",
hostImportNormalizer,
);
ctx.registry.provide("host-metrics.hostPayloadLegacy", hostPayloadLegacy);
ctx.log.info("Host Metrics mounted at /plugin-api/host-metrics");
}
+2 -6
View File
@@ -18,8 +18,6 @@ import {
metricsConcurrencyFor,
type MetricsState,
} from "./state.js";
import { clearCpuSampleCache } from "./widgets/cpu-collector.js";
import { clearNetworkSampleCache } from "./widgets/network-collector.js";
export const DEFAULT_METRICS_INTERVAL = 30;
export const DEFAULT_RETENTION_DAYS = 7;
@@ -201,8 +199,7 @@ export class MetricsPoller {
for (const hostId of [...this.polled.keys()]) this.stop(hostId, true);
this.viewers.clear();
this.viewerDetails.clear();
clearCpuSampleCache();
clearNetworkSampleCache();
this.state.rateSamples.clear();
}
/** The host as `userId` may reach it, cached for a few minutes. */
@@ -274,8 +271,7 @@ export class MetricsPoller {
if (clearData) {
this.metrics.delete(hostId);
this.state.hostCache.invalidate(hostId);
clearCpuSampleCache(hostId);
clearNetworkSampleCache(hostId);
this.state.rateSamples.clear(hostId);
}
}
+25
View File
@@ -1,3 +1,6 @@
import type { CpuSamples } from "./widgets/cpu-collector.js";
import { createNetworkSamples } from "./widgets/network-collector.js";
export type HostStatus = "online" | "reachable" | "offline";
export class RequestQueue {
@@ -422,8 +425,30 @@ export function canStartInitialMetrics(
}
/** Everything one activation of the plugin polls with. */
/** The last CPU and network sample per host, for rates between polls. */
export function createRateSamples() {
const cpu: CpuSamples = new Map();
const network = createNetworkSamples();
return {
cpu,
network,
clear(hostId?: number) {
if (hostId === undefined) {
cpu.clear();
network.counters.clear();
network.windows.clear();
return;
}
cpu.delete(hostId);
network.counters.delete(hostId);
network.windows.delete(hostId);
},
};
}
export function createMetricsState() {
return {
rateSamples: createRateSamples(),
/** SSH metrics execs are expensive; resized with the fleet. */
metricsLimiter: new ConcurrentLimiter(5),
/** Viewer registration is bursty; admit only two first samples at a time. */
@@ -9,20 +9,13 @@ import type { Client } from "ssh2";
/**
* CPU percent is derived from the delta against the previous poll's
* /proc/stat sample rather than a same-poll double-read, so a poll no longer
* has to block for a fixed settle window. Keyed by host.
* has to block for a fixed settle window. Keyed by host, and owned by the
* poller so it goes away with the plugin.
*/
const previousStat = new Map<
export type CpuSamples = Map<
number,
{ total: number; idle: number; timestamp: number }
>();
export function clearCpuSampleCache(hostId?: number): void {
if (hostId === undefined) {
previousStat.clear();
return;
}
previousStat.delete(hostId);
}
>;
export function parseCpuLine(
cpuLine: string,
@@ -149,6 +142,7 @@ export async function collectCpuMetrics(
client: Client,
platform?: HostPlatform,
hostId?: number,
previousStat: CpuSamples = new Map(),
): Promise<{
percent: number | null;
cores: number | null;
@@ -49,24 +49,16 @@ export function counterRate(
* double-sample, so a poll no longer has to block for a fixed settle window.
* Keyed by host, since counters from one host are meaningless against another.
*/
const previousSamples = new Map<
number,
{ counters: Map<string, NetworkCounters>; timestamp: number }
>();
export interface NetworkSamples {
counters: Map<
number,
{ counters: Map<string, NetworkCounters>; timestamp: number }
>;
windows: Map<number, { rows: WindowsAdapterRow[]; timestamp: number }>;
}
const previousWindowsSamples = new Map<
number,
{ rows: WindowsAdapterRow[]; timestamp: number }
>();
export function clearNetworkSampleCache(hostId?: number): void {
if (hostId === undefined) {
previousSamples.clear();
previousWindowsSamples.clear();
return;
}
previousSamples.delete(hostId);
previousWindowsSamples.delete(hostId);
export function createNetworkSamples(): NetworkSamples {
return { counters: new Map(), windows: new Map() };
}
export function parseDarwinIfconfig(
@@ -112,7 +104,8 @@ export function parseDarwinNetstat(
async function collectDarwinNetworkMetrics(
client: Client,
hostId?: number,
hostId: number | undefined,
samples: NetworkSamples,
): Promise<{
interfaces: Array<{
name: string;
@@ -143,7 +136,7 @@ async function collectDarwinNetworkMetrics(
const netstatOut = await execCommand(client, "netstat -ib 2>/dev/null");
const current = parseDarwinNetstat(netstatOut.stdout);
const previous =
hostId !== undefined ? previousSamples.get(hostId) : undefined;
hostId !== undefined ? samples.counters.get(hostId) : undefined;
const elapsedSeconds = previous
? (readAt - previous.timestamp) / 1000
: 0;
@@ -166,7 +159,7 @@ async function collectDarwinNetworkMetrics(
});
}
if (hostId !== undefined) {
previousSamples.set(hostId, { counters: current, timestamp: readAt });
samples.counters.set(hostId, { counters: current, timestamp: readAt });
}
} catch {
for (const [name, data] of ifMap.entries()) {
@@ -225,7 +218,8 @@ export function parseWindowsAdapterJson(output: string): WindowsAdapterRow[] {
async function collectWindowsNetworkMetrics(
client: Client,
hostId?: number,
hostId: number | undefined,
samples: NetworkSamples,
): Promise<{
interfaces: Array<{
name: string;
@@ -252,7 +246,7 @@ async function collectWindowsNetworkMetrics(
const result = await execPowerShell(client, WINDOWS_ADAPTER_SCRIPT);
const currentRows = parseWindowsAdapterJson(result.stdout);
const previous =
hostId !== undefined ? previousWindowsSamples.get(hostId) : undefined;
hostId !== undefined ? samples.windows.get(hostId) : undefined;
const elapsedSeconds = previous ? (readAt - previous.timestamp) / 1000 : 0;
const previousMap = new Map(
(previous?.rows ?? []).map((row) => [row.name, row]),
@@ -276,7 +270,7 @@ async function collectWindowsNetworkMetrics(
}
if (hostId !== undefined) {
previousWindowsSamples.set(hostId, {
samples.windows.set(hostId, {
rows: currentRows,
timestamp: readAt,
});
@@ -292,6 +286,7 @@ export async function collectNetworkMetrics(
client: Client,
platform?: HostPlatform,
hostId?: number,
samples: NetworkSamples = createNetworkSamples(),
): Promise<{
interfaces: Array<{
name: string;
@@ -304,10 +299,10 @@ export async function collectNetworkMetrics(
}>;
}> {
if (platform === "darwin") {
return collectDarwinNetworkMetrics(client, hostId);
return collectDarwinNetworkMetrics(client, hostId, samples);
}
if (platform === "windows") {
return collectWindowsNetworkMetrics(client, hostId);
return collectWindowsNetworkMetrics(client, hostId, samples);
}
const interfaces: Array<{
@@ -368,7 +363,7 @@ export async function collectNetworkMetrics(
const procNet = await execCommand(client, "cat /proc/net/dev");
const rxTxMap = parseNetworkCounters(procNet.stdout);
const previous =
hostId !== undefined ? previousSamples.get(hostId) : undefined;
hostId !== undefined ? samples.counters.get(hostId) : undefined;
const elapsedSeconds = previous
? (readAt - previous.timestamp) / 1000
: 0;
@@ -395,7 +390,7 @@ export async function collectNetworkMetrics(
});
}
if (hostId !== undefined) {
previousSamples.set(hostId, {
samples.counters.set(hostId, {
counters: rxTxMap,
timestamp: readAt,
});