refine dashboard infrastructure and resource status
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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]}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user