refine dashboard infrastructure and resource status
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-09 11:56:05 +09:00
parent c1d424b13c
commit b191dc9f03
2 changed files with 138 additions and 0 deletions
+4
View File
@@ -23,6 +23,10 @@ export class RuntimeController {
return this.runtimeService.listPodMetrics(namespace);
}
@Get('metrics/cluster')
getClusterMetrics() {
return this.runtimeService.getClusterMetrics();
}
@Get('workloads/:namespace/:kind/:name/env')
getWorkloadEnv(
+134
View File
@@ -72,6 +72,64 @@ export class RuntimeService {
return parsePodMetrics(output, Boolean(namespace), namespace);
}
async getClusterMetrics() {
const nodes = await this.kubectlJson(['get', 'nodes', '-o', 'json']);
const topOutput = await this.kubectl(['top', 'nodes', '--no-headers']).catch(() => '');
const usageByNode = parseNodeMetrics(topOutput);
const nodeMetrics = await Promise.all((nodes.items ?? []).map(async (node: KubectlJson) => {
const name = node.metadata?.name ?? '-';
const capacity = node.status?.capacity ?? {};
const usage = usageByNode.get(name);
const storage = await this.getNodeStorageMetric(name).catch(() => null);
return {
name,
status: node.status?.conditions?.find((condition: KubectlJson) => condition.type === 'Ready')?.status === 'True' ? 'Ready' : 'NotReady',
cpu: {
usage: usage?.cpu ?? null,
capacity: capacity.cpu ?? null,
percent: usage?.cpuMillicores !== undefined ? percentage(usage.cpuMillicores, parseCpuToMillicores(capacity.cpu)) : null,
},
memory: {
usage: usage?.memory ?? null,
capacity: capacity.memory ?? null,
percent: usage?.memoryBytes !== undefined ? percentage(usage.memoryBytes, parseMemoryToBytes(capacity.memory)) : null,
},
storage: storage ?? {
usage: null,
capacity: capacity['ephemeral-storage'] ?? null,
percent: null,
},
};
}));
return {
nodes: nodeMetrics,
summary: {
cpuPercent: averagePercent(nodeMetrics.map((node: any) => node.cpu.percent)),
memoryPercent: averagePercent(nodeMetrics.map((node: any) => node.memory.percent)),
storagePercent: averagePercent(nodeMetrics.map((node: any) => node.storage.percent)),
},
};
}
private async getNodeStorageMetric(nodeName: string) {
const stats = await this.kubectlJson(['get', '--raw', `/api/v1/nodes/${nodeName}/proxy/stats/summary`]);
const fsStats = stats.node?.fs;
const usedBytes = Number(fsStats?.usedBytes);
const capacityBytes = Number(fsStats?.capacityBytes);
if (!Number.isFinite(usedBytes) || !Number.isFinite(capacityBytes) || capacityBytes <= 0) {
return null;
}
return {
usage: formatBytes(usedBytes),
capacity: formatBytes(capacityBytes),
percent: percentage(usedBytes, capacityBytes),
};
}
async getWorkloadEnv(namespace: string, kind: string, name: string) {
const normalizedKind = this.normalizeEnvWorkloadKind(kind);
@@ -642,3 +700,79 @@ function parsePodMetrics(output: string, namespacedOutput: boolean, namespace?:
})
.filter((metric) => metric.namespace && metric.pod);
}
function parseNodeMetrics(output: string) {
const metrics = new Map<string, { cpu: string; cpuMillicores: number; memory: string; memoryBytes: number }>();
output
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.forEach((line) => {
const columns = line.split(/\s+/);
const [node, cpu, memory] = [columns[0], columns[1], columns[3]];
if (!node) return;
metrics.set(node, {
cpu,
cpuMillicores: parseCpuToMillicores(cpu),
memory,
memoryBytes: parseMemoryToBytes(memory),
});
});
return metrics;
}
function parseCpuToMillicores(value?: string | null) {
if (!value) return 0;
const normalized = value.trim();
if (normalized.endsWith('m')) return Number(normalized.slice(0, -1)) || 0;
return (Number(normalized) || 0) * 1000;
}
function parseMemoryToBytes(value?: string | null) {
if (!value) return 0;
const normalized = value.trim();
const matched = normalized.match(/^([0-9.]+)([A-Za-z]+)?$/);
if (!matched) return 0;
const amount = Number(matched[1]);
if (!Number.isFinite(amount)) return 0;
const unit = matched[2] ?? '';
const multipliers: Record<string, number> = {
Ki: 1024,
Mi: 1024 ** 2,
Gi: 1024 ** 3,
Ti: 1024 ** 4,
K: 1000,
M: 1000 ** 2,
G: 1000 ** 3,
T: 1000 ** 4,
};
return amount * (multipliers[unit] ?? 1);
}
function percentage(used: number, capacity: number) {
if (!Number.isFinite(used) || !Number.isFinite(capacity) || capacity <= 0) return null;
return Math.max(0, Math.min(100, Math.round((used / capacity) * 100)));
}
function averagePercent(values: Array<number | null | undefined>) {
const validValues = values.filter((value): value is number => typeof value === 'number' && Number.isFinite(value));
if (!validValues.length) return null;
return Math.round(validValues.reduce((sum, value) => sum + value, 0) / validValues.length);
}
function formatBytes(value: number) {
const units = ['B', 'Ki', 'Mi', 'Gi', 'Ti'];
let amount = value;
let unitIndex = 0;
while (amount >= 1024 && unitIndex < units.length - 1) {
amount /= 1024;
unitIndex += 1;
}
return `${amount >= 10 ? amount.toFixed(0) : amount.toFixed(1)}${units[unitIndex]}`;
}