1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

improve how metrics are queried & displayed

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2021-01-12 12:12:48 +02:00
parent 11595abc93
commit 029694a3ca
18 changed files with 173 additions and 72 deletions

View File

@ -3,6 +3,7 @@ import { IPodContainer } from "./pods.api";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { autobind } from "../../utils";
import { KubeApi } from "../kube-api";
import { IResourceMetrics, metricsApi } from "./metrics.api";
@autobind()
export class DaemonSet extends WorkloadKubeObject {
@ -71,6 +72,24 @@ export class DaemonSet extends WorkloadKubeObject {
}
}
export const daemonSetApi = new KubeApi({
export class DaemonSetApi extends KubeApi<DaemonSet> {
getMetrics(daemonsets: DaemonSet[], namespace: string, selector = ""): Promise<IResourceMetrics> {
const podSelector = daemonsets.map(daemonset => `${daemonset.getName()}-[[:alnum:]]{5}`).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector };
return metricsApi.getMetrics({
cpuUsage: opts,
memoryUsage: opts,
fsUsage: opts,
networkReceive: opts,
networkTransmit: opts,
}, {
namespace,
});
}
}
export const daemonSetApi = new DaemonSetApi({
objectConstructor: DaemonSet,
});

View File

@ -3,6 +3,7 @@ import moment from "moment";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { autobind } from "../../utils";
import { KubeApi } from "../kube-api";
import { IResourceMetrics, metricsApi } from "./metrics.api";
export class DeploymentApi extends KubeApi<Deployment> {
protected getScaleApiUrl(params: { namespace: string; name: string }) {
@ -44,6 +45,21 @@ export class DeploymentApi extends KubeApi<Deployment> {
}
});
}
getMetrics(deployments: Deployment[], namespace: string, selector = ""): Promise<IResourceMetrics> {
const podSelector = deployments.map(deployment => `${deployment.getName()}-[[:alnum:]]{9,}-[[:alnum:]]{5}`).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector };
return metricsApi.getMetrics({
cpuUsage: opts,
memoryUsage: opts,
fsUsage: opts,
networkReceive: opts,
networkTransmit: opts,
}, {
namespace,
});
}
}
interface IContainerProbe {

View File

@ -4,6 +4,7 @@ import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { IPodContainer } from "./pods.api";
import { KubeApi } from "../kube-api";
import { JsonApiParams } from "../json-api";
import { IResourceMetrics, metricsApi } from "./metrics.api";
@autobind()
export class Job extends WorkloadKubeObject {
@ -107,6 +108,24 @@ export class Job extends WorkloadKubeObject {
}
}
export const jobApi = new KubeApi({
export class JobApi extends KubeApi<Job> {
getMetrics(jobs: Job[], namespace: string, selector = ""): Promise<IResourceMetrics> {
const podSelector = jobs.map(job => `${job.getName()}-[[:alnum:]]{5}`).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector };
return metricsApi.getMetrics({
cpuUsage: opts,
memoryUsage: opts,
fsUsage: opts,
networkReceive: opts,
networkTransmit: opts,
}, {
namespace,
});
}
}
export const jobApi = new JobApi({
objectConstructor: Job,
});

View File

@ -33,6 +33,15 @@ export interface IMetricsReqParams {
namespace?: string; // rbac-proxy validation param
}
export interface IResourceMetrics<T = IMetrics> {
[metric: string]: T;
cpuUsage: T;
memoryUsage: T;
fsUsage: T;
networkReceive: T;
networkTransmit: T;
}
export const metricsApi = {
async getMetrics<T = IMetricsQuery>(query: T, reqParams: IMetricsReqParams = {}): Promise<T extends object ? { [K in keyof T]: IMetrics } : IMetrics> {
const { range = 3600, step = 60, namespace } = reqParams;

View File

@ -1,6 +1,7 @@
import { KubeApi } from "../kube-api";
import { KubeObject } from "../kube-object";
import { autobind } from "../../utils";
import { IResourceMetrics, metricsApi } from "./metrics.api";
export enum NamespaceStatus {
ACTIVE = "Active",
@ -22,6 +23,22 @@ export class Namespace extends KubeObject {
}
}
export const namespacesApi = new KubeApi({
export class NamespaceApi extends KubeApi<Namespace> {
getMetrics(namespace: string, selector = ""): Promise<IResourceMetrics> {
const opts = { category: "pods", pods: ".*", namespace, selector };
return metricsApi.getMetrics({
cpuUsage: opts,
memoryUsage: opts,
fsUsage: opts,
networkReceive: opts,
networkTransmit: opts,
}, {
namespace,
});
}
}
export const namespacesApi = new NamespaceApi({
objectConstructor: Namespace,
});

View File

@ -3,6 +3,7 @@ import { autobind } from "../../utils";
import { WorkloadKubeObject } from "../workload-kube-object";
import { IPodContainer, Pod } from "./pods.api";
import { KubeApi } from "../kube-api";
import { IResourceMetrics, metricsApi } from "./metrics.api";
export class ReplicaSetApi extends KubeApi<ReplicaSet> {
protected getScaleApiUrl(params: { namespace: string; name: string }) {
@ -25,6 +26,21 @@ export class ReplicaSetApi extends KubeApi<ReplicaSet> {
}
});
}
getMetrics(replicasets: ReplicaSet[], namespace: string, selector = ""): Promise<IResourceMetrics> {
const podSelector = replicasets.map(replicaset => `${replicaset.getName()}-[[:alnum:]]{5}`).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector };
return metricsApi.getMetrics({
cpuUsage: opts,
memoryUsage: opts,
fsUsage: opts,
networkReceive: opts,
networkTransmit: opts,
}, {
namespace,
});
}
}
@autobind()

View File

@ -3,6 +3,7 @@ import { IPodContainer } from "./pods.api";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { autobind } from "../../utils";
import { KubeApi } from "../kube-api";
import { IResourceMetrics, metricsApi } from "./metrics.api";
export class StatefulSetApi extends KubeApi<StatefulSet> {
protected getScaleApiUrl(params: { namespace: string; name: string }) {
@ -25,6 +26,21 @@ export class StatefulSetApi extends KubeApi<StatefulSet> {
}
});
}
getMetrics(statefulSets: StatefulSet[], namespace: string, selector = ""): Promise<IResourceMetrics> {
const podSelector = statefulSets.map(statefulset => `${statefulset.getName()}-[[:digit:]]+`).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector };
return metricsApi.getMetrics({
cpuUsage: opts,
memoryUsage: opts,
fsUsage: opts,
networkReceive: opts,
networkTransmit: opts,
}, {
namespace,
});
}
}
@autobind()

View File

@ -12,6 +12,9 @@ import { Spinner } from "../spinner";
import { resourceQuotaStore } from "../+config-resource-quotas/resource-quotas.store";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { namespaceStore } from "./namespace.store";
import { ResourceMetrics } from "../resource-metrics";
import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts";
interface Props extends KubeObjectDetailsProps<Namespace> {
}
@ -33,9 +36,18 @@ export class NamespaceDetails extends React.Component<Props> {
if (!namespace) return;
const status = namespace.getStatus();
const metrics = namespaceStore.metrics;
return (
<div className="NamespaceDetails">
{namespaceStore.isLoaded && (
<ResourceMetrics
loader={() => namespaceStore.loadMetrics(namespace)}
tabs={podMetricTabs} object={namespace} params={{ metrics }}
>
<PodCharts />
</ResourceMetrics>
)}
<KubeObjectMeta object={namespace}/>
<DrawerItem name="Status">

View File

@ -1,7 +1,7 @@
import { action, comparer, observable, reaction } from "mobx";
import { autobind, createStorage } from "../../utils";
import { KubeObjectStore } from "../../kube-object.store";
import { Namespace, namespacesApi } from "../../api/endpoints";
import { INamespaceMetrics, Namespace, namespacesApi } from "../../api/endpoints";
import { createPageParam } from "../../navigation";
import { apiManager } from "../../api/api-manager";
import { isAllowedResource } from "../../../common/rbac";
@ -22,12 +22,17 @@ export const namespaceUrlParam = createPageParam<string[]>({
export class NamespaceStore extends KubeObjectStore<Namespace> {
api = namespacesApi;
contextNs = observable.array<string>();
@observable metrics: INamespaceMetrics = null;
constructor() {
super();
this.init();
}
async loadMetrics(namespace: Namespace) {
this.metrics = await namespacesApi.getMetrics(namespace.getName(), "");
}
private init() {
this.setContext(this.initNamespaces);

View File

@ -1,7 +1,7 @@
import { observable } from "mobx";
import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils";
import { DaemonSet, daemonSetApi, IPodMetrics, Pod, podsApi, PodStatus } from "../../api/endpoints";
import { DaemonSet, daemonSetApi, IDaemonSetMetrics, Pod, PodStatus } from "../../api/endpoints";
import { podsStore } from "../+workloads-pods/pods.store";
import { apiManager } from "../../api/api-manager";
@ -9,12 +9,10 @@ import { apiManager } from "../../api/api-manager";
export class DaemonSetStore extends KubeObjectStore<DaemonSet> {
api = daemonSetApi;
@observable metrics: IPodMetrics = null;
@observable metrics: IDaemonSetMetrics = null;
async loadMetrics(daemonSet: DaemonSet) {
const pods = this.getChildPods(daemonSet);
this.metrics = await podsApi.getMetrics(pods, daemonSet.getNs(), "");
this.metrics = await daemonSetApi.getMetrics([daemonSet], daemonSet.getNs(), "");
}
getChildPods(daemonSet: DaemonSet): Pod[] {

View File

@ -1,5 +1,5 @@
import { observable } from "mobx";
import { Deployment, deploymentApi, IPodMetrics, podsApi, PodStatus } from "../../api/endpoints";
import { Deployment, deploymentApi, IDeploymentMetrics, PodStatus } from "../../api/endpoints";
import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils";
import { podsStore } from "../+workloads-pods/pods.store";
@ -8,7 +8,7 @@ import { apiManager } from "../../api/api-manager";
@autobind()
export class DeploymentStore extends KubeObjectStore<Deployment> {
api = deploymentApi;
@observable metrics: IPodMetrics = null;
@observable metrics: IDeploymentMetrics = null;
protected sortItems(items: Deployment[]) {
return super.sortItems(items, [
@ -17,9 +17,7 @@ export class DeploymentStore extends KubeObjectStore<Deployment> {
}
async loadMetrics(deployment: Deployment) {
const pods = this.getChildPods(deployment);
this.metrics = await podsApi.getMetrics(pods, deployment.getNs(), "");
this.metrics = await deploymentApi.getMetrics([deployment], deployment.getNs(), "");
}
getStatuses(deployments?: Deployment[]) {

View File

@ -18,6 +18,8 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { lookupApiLink } from "../../api/kube-api";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceMetrics } from "../resource-metrics";
import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts";
interface Props extends KubeObjectDetailsProps<Job> {
}
@ -40,9 +42,18 @@ export class JobDetails extends React.Component<Props> {
const childPods = jobStore.getChildPods(job);
const ownerRefs = job.getOwnerRefs();
const condition = job.getCondition();
const metrics = jobStore.metrics;
return (
<div className="JobDetails">
{jobStore.isLoaded && (
<ResourceMetrics
loader={() => jobStore.loadMetrics(job)}
tabs={podMetricTabs} object={job} params={{ metrics }}
>
<PodCharts />
</ResourceMetrics>
)}
<KubeObjectMeta object={job}/>
<DrawerItem name="Selector" labelsOnly>
{

View File

@ -1,14 +1,21 @@
import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils";
import { Job, jobApi } from "../../api/endpoints/job.api";
import { IJobMetrics, Job, jobApi } from "../../api/endpoints/job.api";
import { CronJob, Pod, PodStatus } from "../../api/endpoints";
import { podsStore } from "../+workloads-pods/pods.store";
import { apiManager } from "../../api/api-manager";
import { observable } from "mobx";
@autobind()
export class JobStore extends KubeObjectStore<Job> {
api = jobApi;
@observable metrics: IJobMetrics = null;
async loadMetrics(job: Job) {
this.metrics = await jobApi.getMetrics([job], job.getNs(), "");
}
getChildPods(job: Job): Pod[] {
return podsStore.getPodsByOwner(job);
}

View File

@ -6,6 +6,7 @@ import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.ap
import { NoMetrics } from "../resource-metrics/no-metrics";
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
import { themeStore } from "../../theme.store";
import { mapValues } from "lodash";
type IContext = IResourceMetricsValue<any, { metrics: IPodMetrics }>;
@ -16,10 +17,7 @@ export const ContainerCharts = observer(() => {
if (!metrics) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const values = Object.values(metrics)
.map(normalizeMetrics)
.map(({ data }) => data.result[0].values);
const [
const {
cpuUsage,
cpuRequests,
cpuLimits,
@ -27,7 +25,7 @@ export const ContainerCharts = observer(() => {
memoryRequests,
memoryLimits,
fsUsage
] = values;
} = mapValues(metrics, metric => normalizeMetrics(metric).data.result[0].values);
const datasets = [
// CPU

View File

@ -6,7 +6,7 @@ import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.ap
import { NoMetrics } from "../resource-metrics/no-metrics";
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
import { WorkloadKubeObject } from "../../api/workload-kube-object";
import { themeStore } from "../../theme.store";
import { mapValues } from "lodash";
export const podMetricTabs = [
"CPU",
@ -19,27 +19,20 @@ type IContext = IResourceMetricsValue<WorkloadKubeObject, { metrics: IPodMetrics
export const PodCharts = observer(() => {
const { params: { metrics }, tabId, object } = useContext<IContext>(ResourceMetricsContext);
const { chartCapacityColor } = themeStore.activeTheme.colors;
const id = object.getId();
if (!metrics) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const options = tabId == 0 ? cpuOptions : memoryOptions;
const values = Object.values(metrics)
.map(normalizeMetrics)
.map(({ data }) => data.result[0].values);
const [
const {
cpuUsage,
cpuRequests,
cpuLimits,
memoryUsage,
memoryRequests,
memoryLimits,
fsUsage,
networkReceive,
networkTransmit
] = values;
} = mapValues(metrics, metric => normalizeMetrics(metric).data.result[0].values);
const datasets = [
// CPU
@ -50,20 +43,6 @@ export const PodCharts = observer(() => {
tooltip: `Container CPU cores usage`,
borderColor: "#3D90CE",
data: cpuUsage.map(([x, y]) => ({ x, y }))
},
{
id: `${id}-cpuRequests`,
label: `Requests`,
tooltip: `Container CPU requests`,
borderColor: "#30b24d",
data: cpuRequests.map(([x, y]) => ({ x, y }))
},
{
id: `${id}-cpuLimits`,
label: `Limits`,
tooltip: `CPU limits`,
borderColor: chartCapacityColor,
data: cpuLimits.map(([x, y]) => ({ x, y }))
}
],
// Memory
@ -74,20 +53,6 @@ export const PodCharts = observer(() => {
tooltip: `Container memory usage`,
borderColor: "#c93dce",
data: memoryUsage.map(([x, y]) => ({ x, y }))
},
{
id: `${id}-memoryRequests`,
label: `Requests`,
tooltip: `Container memory requests`,
borderColor: "#30b24d",
data: memoryRequests.map(([x, y]) => ({ x, y }))
},
{
id: `${id}-memoryLimits`,
label: `Limits`,
tooltip: `Container memory limits`,
borderColor: chartCapacityColor,
data: memoryLimits.map(([x, y]) => ({ x, y }))
}
],
// Network

View File

@ -1,7 +1,7 @@
import { observable } from "mobx";
import { autobind } from "../../utils";
import { KubeObjectStore } from "../../kube-object.store";
import { Deployment, IPodMetrics, podsApi, ReplicaSet, replicaSetApi } from "../../api/endpoints";
import { Deployment, IReplicaSetMetrics, ReplicaSet, replicaSetApi } from "../../api/endpoints";
import { podsStore } from "../+workloads-pods/pods.store";
import { apiManager } from "../../api/api-manager";
import { PodStatus } from "../../api/endpoints/pods.api";
@ -9,12 +9,10 @@ import { PodStatus } from "../../api/endpoints/pods.api";
@autobind()
export class ReplicaSetStore extends KubeObjectStore<ReplicaSet> {
api = replicaSetApi;
@observable metrics: IPodMetrics = null;
@observable metrics: IReplicaSetMetrics = null;
async loadMetrics(replicaSet: ReplicaSet) {
const pods = this.getChildPods(replicaSet);
this.metrics = await podsApi.getMetrics(pods, replicaSet.getNs(), "");
this.metrics = await replicaSetApi.getMetrics([replicaSet], replicaSet.getNs(), "");
}
getChildPods(replicaSet: ReplicaSet) {

View File

@ -1,19 +1,17 @@
import { observable } from "mobx";
import { autobind } from "../../utils";
import { KubeObjectStore } from "../../kube-object.store";
import { IPodMetrics, podsApi, PodStatus, StatefulSet, statefulSetApi } from "../../api/endpoints";
import { IStatefulSetMetrics, PodStatus, StatefulSet, statefulSetApi } from "../../api/endpoints";
import { podsStore } from "../+workloads-pods/pods.store";
import { apiManager } from "../../api/api-manager";
@autobind()
export class StatefulSetStore extends KubeObjectStore<StatefulSet> {
api = statefulSetApi;
@observable metrics: IPodMetrics = null;
@observable metrics: IStatefulSetMetrics = null;
async loadMetrics(statefulSet: StatefulSet) {
const pods = this.getChildPods(statefulSet);
this.metrics = await podsApi.getMetrics(pods, statefulSet.getNs(), "");
this.metrics = await statefulSetApi.getMetrics([statefulSet], statefulSet.getNs(), "");
}
getChildPods(statefulSet: StatefulSet) {

View File

@ -1,12 +1,11 @@
import React from "react";
import { IPodMetrics } from "../../api/endpoints";
import { getMetricLastPoints, IMetrics } from "../../api/endpoints/metrics.api";
import { getMetricLastPoints, IResourceMetrics, IMetrics } from "../../api/endpoints/metrics.api";
import { bytesToUnits } from "../../utils";
import { Badge } from "../badge";
import { DrawerItem } from "../drawer";
interface Props {
metrics: IPodMetrics<IMetrics>;
metrics: IResourceMetrics<IMetrics>;
}
export function ResourceMetricsText(props: Props) {