From 160b29c004f0000395f79b49a4748b2a6dd46bb2 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 13 Oct 2021 11:13:26 -0400 Subject: [PATCH] work Signed-off-by: Sebastian Malton --- src/common/k8s-api/endpoints/metrics.api.ts | 101 ++++++++------- src/main/routes/metrics-route.ts | 6 +- .../+cluster/cluster-metric-switchers.tsx | 8 +- .../components/+cluster/cluster-metrics.tsx | 33 +---- .../+cluster/cluster-overview.store.ts | 7 +- .../components/+cluster/cluster-overview.tsx | 33 ++++- .../+cluster/cluster-pie-charts.tsx | 8 +- .../+workloads-pods/pod-details.tsx | 121 +++++++++--------- 8 files changed, 169 insertions(+), 148 deletions(-) diff --git a/src/common/k8s-api/endpoints/metrics.api.ts b/src/common/k8s-api/endpoints/metrics.api.ts index b0e3c0bf9d..cb86383c9c 100644 --- a/src/common/k8s-api/endpoints/metrics.api.ts +++ b/src/common/k8s-api/endpoints/metrics.api.ts @@ -24,6 +24,7 @@ import moment from "moment"; import { apiBase } from "../index"; import type { IMetricsQuery } from "../../../main/routes/metrics-route"; +import { iter, toJS } from "../../utils"; export interface IMetrics { status: string; @@ -36,7 +37,7 @@ export interface IMetrics { export interface IMetricsResult { metric: { [name: string]: string; - instance: string; + instance?: string; node?: string; pod?: string; kubernetes?: string; @@ -104,7 +105,7 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics { result: [{ metric: {}, values: [] - } as IMetricsResult], + }], }, status: "", }; @@ -112,18 +113,23 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics { const { result } = metrics.data; + console.log(toJS(result)); + if (result.length) { if (frames > 0) { // fill the gaps - result.forEach(res => { - if (!res.values || !res.values.length) return; + for (const res of result) { + if (!res.values || !res.values.length) { + continue; + } - let now = moment().startOf("minute").subtract(1, "minute").unix(); - let timestamp = res.values[0][0]; - - while (timestamp <= now) { - timestamp = moment.unix(timestamp).add(1, "minute").unix(); + const now = moment().startOf("minute").subtract(1, "minute").unix(); + for ( + let timestamp = res.values[0][0]; + timestamp <= now; + timestamp = moment.unix(timestamp).add(1, "minute").unix() + ) { if (!res.values.find((value) => value[0] === timestamp)) { res.values.push([timestamp, "0"]); } @@ -135,58 +141,67 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics { if (!res.values.find((value) => value[0] === timestamp)) { res.values.unshift([timestamp, "0"]); } - now = timestamp; } - }); + } } - } - else { + } else { // always return at least empty values array result.push({ metric: {}, values: [] - } as IMetricsResult); + }); } return metrics; } export function isMetricsEmpty(metrics: Record) { - return Object.values(metrics).every(metric => !metric?.data?.result?.length); + return !Object.values(metrics).some(metric => metric?.data?.result?.length); } -export function getItemMetrics(metrics: Record, itemName: string): Record | void { - if (!metrics) return; - const itemMetrics = { ...metrics }; - - for (const metric in metrics) { - if (!metrics[metric]?.data?.result) { - continue; - } - const results = metrics[metric].data.result; - const result = results.find(res => Object.values(res.metric)[0] == itemName); - - itemMetrics[metric].data.result = result ? [result] : []; +export function getItemMetrics(metrics: Record, itemName: string): Record { + if (!metrics) { + return {}; } - return itemMetrics; -} + return Object.fromEntries( + iter.filterMap( + Object.entries(metrics), + ([key, value]) => { + if (value?.data?.result) { + const result = value.data.result.find(res => res.metric.container === itemName); -export function getMetricLastPoints(metrics: Record) { - const result: Partial<{ [metric: string]: number }> = {}; + value.data.result = [result].filter(Boolean); - Object.keys(metrics).forEach(metricName => { - try { - const metric = metrics[metricName]; + return [key, value]; + } - if (metric.data.result.length) { - result[metricName] = +metric.data.result[0].values.slice(-1)[0][1]; + return undefined; } - } catch (e) { - } - - return result; - }, {}); - - return result; + ) + ); +} + +export function getMetricLastPoints(metrics: Record): Record { + return Object.fromEntries( + iter.filterMap( + Object.entries(metrics), + ([key, value]) => { + if (value.data.result.length > 0) { + const [{ values }] = value.data.result; + + if (values.length > 0) { + const [[, value]] = values.slice(-1); + const parsed = +value; + + if (!isNaN(parsed)) { + return [key, parsed]; + } + } + } + + return undefined; + }, + ), + ); } diff --git a/src/main/routes/metrics-route.ts b/src/main/routes/metrics-route.ts index 2b8aa0f921..894428098f 100644 --- a/src/main/routes/metrics-route.ts +++ b/src/main/routes/metrics-route.ts @@ -44,11 +44,13 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa async function loadMetric(query: string): Promise { async function loadMetricHelper(): Promise { for (const [attempt, lastAttempt] of ATTEMPTS.entries()) { // retry + const reqParams = { query, ...queryParams }; + try { - return await getMetrics(cluster, prometheusPath, { query, ...queryParams }); + return await getMetrics(cluster, prometheusPath, reqParams); } catch (error) { if (lastAttempt || (error?.statusCode >= 400 && error?.statusCode < 500)) { - logger.error("[Metrics]: metrics not available", { error }); + logger.error("[Metrics]: metrics not available", { ...error.error, reqParams }); throw new Error("Metrics not available"); } diff --git a/src/renderer/components/+cluster/cluster-metric-switchers.tsx b/src/renderer/components/+cluster/cluster-metric-switchers.tsx index 3be4ae903f..f7577a4e19 100644 --- a/src/renderer/components/+cluster/cluster-metric-switchers.tsx +++ b/src/renderer/components/+cluster/cluster-metric-switchers.tsx @@ -26,10 +26,10 @@ import { observer } from "mobx-react"; import { nodesStore } from "../+nodes/nodes.store"; import { cssNames } from "../../utils"; import { Radio, RadioGroup } from "../radio"; -import { clusterApiStore, MetricNodeRole, MetricType } from "./cluster-overview.store"; +import { kubeClusterStore, MetricNodeRole, MetricType } from "./cluster-overview.store"; export const ClusterMetricSwitchers = observer(() => { - const { metricType, metricNodeRole, getMetricsValues, metrics } = clusterApiStore; + const { metricType, metricNodeRole, getMetricsValues, metrics } = kubeClusterStore; const { masterNodes, workerNodes } = nodesStore; const metricsValues = getMetricsValues(metrics); const disableRoles = !masterNodes.length || !workerNodes.length; @@ -42,7 +42,7 @@ export const ClusterMetricSwitchers = observer(() => { asButtons className={cssNames("RadioGroup flex gaps", { disabled: disableRoles })} value={metricNodeRole} - onChange={(metric: MetricNodeRole) => clusterApiStore.metricNodeRole = metric} + onChange={(metric: MetricNodeRole) => kubeClusterStore.metricNodeRole = metric} > @@ -53,7 +53,7 @@ export const ClusterMetricSwitchers = observer(() => { asButtons className={cssNames("RadioGroup flex gaps", { disabled: disableMetrics })} value={metricType} - onChange={(value: MetricType) => clusterApiStore.metricType = value} + onChange={(value: MetricType) => kubeClusterStore.metricType = value} > diff --git a/src/renderer/components/+cluster/cluster-metrics.tsx b/src/renderer/components/+cluster/cluster-metrics.tsx index 17221c3944..83ed4ea5c6 100644 --- a/src/renderer/components/+cluster/cluster-metrics.tsx +++ b/src/renderer/components/+cluster/cluster-metrics.tsx @@ -24,7 +24,7 @@ import "./cluster-metrics.scss"; import React from "react"; import { observer } from "mobx-react"; import type { ChartOptions, ChartPoint } from "chart.js"; -import { clusterApiStore } from "./cluster-overview.store"; +import { kubeClusterStore } from "./cluster-overview.store"; import { BarChart } from "../chart"; import { bytesToUnits, createStorage } from "../../utils"; import { Spinner } from "../spinner"; @@ -33,31 +33,10 @@ import { ClusterNoMetrics } from "./cluster-no-metrics"; import { ClusterMetricSwitchers } from "./cluster-metric-switchers"; import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api"; -export enum MetricType { - MEMORY = "memory", - CPU = "cpu" -} - -export enum MetricNodeRole { - MASTER = "master", - WORKER = "worker" -} - -export interface ClusterMetricsStorageState { - metricType: MetricType; - metricNodeRole: MetricNodeRole, -} - -const storage = createStorage("cluster_overview", { - metricType: MetricType.CPU, // setup defaults - metricNodeRole: MetricNodeRole.WORKER, -}); - export const ClusterMetrics = observer(() => { - const { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics } = clusterApiStore; - const { memoryCapacity, cpuCapacity } = getMetricLastPoints(clusterApiStore.metrics); + const { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics } = kubeClusterStore; + const { memoryCapacity, cpuCapacity } = getMetricLastPoints(metrics); const metricValues = getMetricsValues(metrics); - const colors = { cpu: "#3D90CE", memory: "#C93DCE" }; const data = metricValues.map(value => ({ x: value[0], y: parseFloat(value[1]).toFixed(3) @@ -66,7 +45,7 @@ export const ClusterMetrics = observer(() => { const datasets = [{ id: metricType + metricNodeRole, label: `${metricType.toUpperCase()} usage`, - borderColor: colors[metricType], + borderColor: metricType === MetricType.CPU ? "#3D90CE" : "#C93DCE", data }]; const cpuOptions: ChartOptions = { @@ -104,11 +83,11 @@ export const ClusterMetrics = observer(() => { const options = metricType === MetricType.CPU ? cpuOptions : memoryOptions; const renderMetrics = () => { - if (!metricValues.length && !metricsLoaded) { + if (!metricsLoaded) { return ; } - if (!memoryCapacity || !cpuCapacity) { + if (!memoryCapacity && !cpuCapacity) { return ; } diff --git a/src/renderer/components/+cluster/cluster-overview.store.ts b/src/renderer/components/+cluster/cluster-overview.store.ts index f4312aa25a..59c8dca37e 100644 --- a/src/renderer/components/+cluster/cluster-overview.store.ts +++ b/src/renderer/components/+cluster/cluster-overview.store.ts @@ -27,7 +27,7 @@ import { IMetricsReqParams, normalizeMetrics } from "../../../common/k8s-api/end import { nodesStore } from "../+nodes/nodes.store"; import { apiManager } from "../../../common/k8s-api/api-manager"; -export class ClusterOverviewStore extends KubeObjectStore { +export class KubeClusterStore extends KubeObjectStore { api = clusterApi; @@ -80,6 +80,7 @@ export class ClusterOverviewStore extends KubeObjectStore { const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes; this.metrics = await getMetricsByNodeNames(nodes.map(node => node.getName()), params); + console.log(toJS(this.metrics)); this.metricsLoaded = true; } @@ -107,5 +108,5 @@ export class ClusterOverviewStore extends KubeObjectStore { } } -export const clusterApiStore = new ClusterOverviewStore(); -apiManager.registerStore(clusterApiStore); +export const kubeClusterStore = new KubeClusterStore(); +apiManager.registerStore(kubeClusterStore); diff --git a/src/renderer/components/+cluster/cluster-overview.tsx b/src/renderer/components/+cluster/cluster-overview.tsx index ca59d49456..7ecb94ad08 100644 --- a/src/renderer/components/+cluster/cluster-overview.tsx +++ b/src/renderer/components/+cluster/cluster-overview.tsx @@ -22,39 +22,62 @@ import "./cluster-overview.scss"; import React from "react"; -import { reaction } from "mobx"; +import { observable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { nodesStore } from "../+nodes/nodes.store"; import { podsStore } from "../+workloads-pods/pods.store"; -import { getHostedClusterId, interval } from "../../utils"; +import { createStorage, getHostedClusterId, interval } from "../../utils"; import { TabLayout } from "../layout/tab-layout"; import { Spinner } from "../spinner"; import { ClusterIssues } from "./cluster-issues"; import { ClusterMetrics } from "./cluster-metrics"; -import { clusterApiStore } from "./cluster-overview.store"; +import { kubeClusterStore } from "./cluster-overview.store"; import { ClusterPieCharts } from "./cluster-pie-charts"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { ClusterStore } from "../../../common/cluster-store"; +import type { IClusterMetrics } from "../../../common/k8s-api/endpoints"; + +export enum MetricType { + MEMORY = "memory", + CPU = "cpu" +} + +export enum MetricNodeRole { + MASTER = "master", + WORKER = "worker" +} + +const storage = createStorage("cluster_overview", { + metricType: MetricType.CPU, // setup defaults + metricNodeRole: MetricNodeRole.WORKER, +}); @observer export class ClusterOverview extends React.Component { + @observable metrics?: IClusterMetrics = undefined; private metricPoller = interval(60, () => this.loadMetrics()); loadMetrics() { const cluster = ClusterStore.getInstance().getById(getHostedClusterId()); if (cluster.available) { - clusterApiStore.loadMetrics(); + kubeClusterStore.loadMetrics(); } } + async loadMetrics() { + const { object: pod } = this.props; + + this.metrics = await getMetricsForPods([pod], pod.getNs()); + this.containerMetrics = await getMetricsForPods([pod], pod.getNs(), "container, namespace"); + } componentDidMount() { this.metricPoller.start(true); disposeOnUnmount(this, [ reaction( - () => clusterApiStore.metricNodeRole, // Toggle Master/Worker node switcher + () => kubeClusterStore.metricNodeRole, // Toggle Master/Worker node switcher () => this.metricPoller.restart(true) ), ]); diff --git a/src/renderer/components/+cluster/cluster-pie-charts.tsx b/src/renderer/components/+cluster/cluster-pie-charts.tsx index da622bcc45..37c8726cdb 100644 --- a/src/renderer/components/+cluster/cluster-pie-charts.tsx +++ b/src/renderer/components/+cluster/cluster-pie-charts.tsx @@ -23,7 +23,7 @@ import "./cluster-pie-charts.scss"; import React from "react"; import { observer } from "mobx-react"; -import { clusterApiStore, MetricNodeRole } from "./cluster-overview.store"; +import { kubeClusterStore, MetricNodeRole } from "./cluster-overview.store"; import { Spinner } from "../spinner"; import { Icon } from "../icon"; import { nodesStore } from "../+nodes/nodes.store"; @@ -48,7 +48,7 @@ export const ClusterPieCharts = observer(() => { }; const renderCharts = () => { - const data = getMetricLastPoints(clusterApiStore.metrics); + const data = getMetricLastPoints(kubeClusterStore.metrics); const { memoryUsage, memoryRequests, memoryAllocatableCapacity, memoryCapacity, memoryLimits } = data; const { cpuUsage, cpuRequests, cpuAllocatableCapacity, cpuCapacity, cpuLimits } = data; const { podUsage, podAllocatableCapacity, podCapacity } = data; @@ -215,7 +215,7 @@ export const ClusterPieCharts = observer(() => { const renderContent = () => { const { masterNodes, workerNodes } = nodesStore; - const { metricNodeRole, metricsLoaded } = clusterApiStore; + const { metricNodeRole, metricsLoaded } = kubeClusterStore; const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes; if (!nodes.length) { @@ -234,7 +234,7 @@ export const ClusterPieCharts = observer(() => { ); } - const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterApiStore.metrics); + const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(kubeClusterStore.metrics); if (!memoryCapacity || !cpuCapacity || !podCapacity) { return ; diff --git a/src/renderer/components/+workloads-pods/pod-details.tsx b/src/renderer/components/+workloads-pods/pod-details.tsx index 9f95c5289f..08f7dab3ce 100644 --- a/src/renderer/components/+workloads-pods/pod-details.tsx +++ b/src/renderer/components/+workloads-pods/pod-details.tsx @@ -77,13 +77,16 @@ export class PodDetails extends React.Component { render() { const { object: pod } = this.props; - if (!pod) return null; - const { status, spec } = pod; - const { conditions, podIP } = status; + if (!pod) { + return null; + } + + const { status: { conditions, podIP }, spec: { nodeName } } = pod; + const { metrics } = this; const podIPs = pod.getIPs(); - const { nodeName } = spec; const nodeSelector = pod.getNodeSelectors(); const volumes = pod.getVolumes(); + const initContainers = pod.getInitContainers(); const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Pod); return ( @@ -91,7 +94,9 @@ export class PodDetails extends React.Component { {!isMetricHidden && ( @@ -111,11 +116,7 @@ export class PodDetails extends React.Component { {podIP} {pod.getPriorityClassName()} @@ -123,65 +124,65 @@ export class PodDetails extends React.Component { {pod.getQosClass()} - {conditions && - - { - conditions.map(condition => { - const { type, status, lastTransitionTime } = condition; - - return ( - - ); - }) - } - + { + conditions && ( + + { + conditions + .map(({ type, status, lastTransitionTime }) => ( + + )) + } + + ) } - {nodeSelector.length > 0 && - - { - nodeSelector.map(label => ( - - )) - } - + { + nodeSelector.length > 0 && ( + + {nodeSelector.map(label => )} + + ) } - - {pod.getSecrets().length > 0 && ( - - - - )} - - {pod.getInitContainers() && pod.getInitContainers().length > 0 && - + { + pod.getSecrets().length > 0 && ( + + + + ) } { - pod.getInitContainers() && pod.getInitContainers().map(container => { - return ; - }) + initContainers.length && ( + <> + + { + initContainers.map(container => ( + + )) + } + + ) } { - pod.getContainers().map(container => { - const { name } = container; - const metrics = getItemMetrics(toJS(this.containerMetrics), name); - - return ( - - ); - }) + pod.getContainers().map(container => ( + + )) } {volumes.length > 0 && (