Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added top consumer to ceph storage plugin #2040

Merged
merged 1 commit into from Jul 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,119 @@
import * as React from 'react';
import * as _ from 'lodash';
import { ChartLineIcon } from '@patternfly/react-icons';
import {
Chart,
ChartAxis,
ChartGroup,
ChartLegend,
ChartLine,
ChartThemeColor,
ChartTooltip,
ChartVoronoiContainer,
} from '@patternfly/react-charts';
import { DataPoint, PrometheusResponse } from '@console/internal/components/graphs';
import { EmptyState, EmptyStateIcon, EmptyStateVariant, Title } from '@patternfly/react-core';
import { humanizeBinaryBytesWithoutB, LoadingInline } from '@console/internal/components/utils';
import { twentyFourHourTime } from '@console/internal/components/utils/datetime';
import { getGraphVectorStats, getMetricType } from './utils';

const chartPropsValue = {
chartHeight: 175,
};

const chartLegendPropsValue = {
x: 10,
y: 5,
symbolSpacer: 7,
height: 30,
gutter: 10,
};

const getYTickValues = (value: number): number[] => [
Math.floor(value / 4),
Math.floor(value / 2),
Math.floor((3 * value) / 4),
Math.floor(value),
Math.floor((5 * value) / 4),
Math.floor((6 * value) / 4),
];

export const TopConsumersBody: React.FC<TopConsumerBodyProps> = React.memo(
({ topConsumerStats, metricType, sortByOption }) => {
if (!topConsumerStats) {
return <LoadingInline />;
}
const topConsumerStatsResult = _.get(topConsumerStats, 'data.result', []);
if (topConsumerStatsResult.length) {
const legends = topConsumerStatsResult.map((resource) => ({
name: getMetricType(resource, metricType),
}));
const resourceValues = _.flatMap(topConsumerStatsResult, (resource) => resource.values);
const maxCapacity = _.maxBy(resourceValues, (value) => Number(value[1]));
const maxCapacityConverted = humanizeBinaryBytesWithoutB(Number(maxCapacity[1]));
const chartData = getGraphVectorStats(
topConsumerStats,
metricType,
maxCapacityConverted.unit,
);

const chartLineList = chartData.map((data, i) => (
<ChartLine key={i} data={data as DataPoint[]} /> // eslint-disable-line react/no-array-index-key
));
return (
<>
<span className="text-secondary">{`${sortByOption}(${maxCapacityConverted.unit})`}</span>
<Chart
domain={{ y: [0, 1.5 * maxCapacityConverted.value] }}
height={chartPropsValue.chartHeight}
padding={{ top: 20, bottom: 20, left: 30, right: 20 }}
containerComponent={
<ChartVoronoiContainer
labels={(datum) => `${datum.y} ${maxCapacityConverted.unit}`}
labelComponent={<ChartTooltip style={{ fontSize: 8, padding: 5 }} />}
/>
}
themeColor={ChartThemeColor.multi}
scale={{ x: 'time' }}
>
<ChartAxis
tickFormat={(x) => twentyFourHourTime(x)}
style={{ tickLabels: { fontSize: 8, padding: 5 } }}
/>
<ChartAxis
dependentAxis
tickValues={getYTickValues(maxCapacityConverted.value)}
style={{ tickLabels: { fontSize: 8, padding: 5 }, grid: { stroke: '#4d525840' } }}
/>
<ChartGroup>{chartLineList}</ChartGroup>
</Chart>
<ChartLegend
data={legends}
themeColor={ChartThemeColor.multi}
x={chartLegendPropsValue.x}
y={chartLegendPropsValue.y}
symbolSpacer={chartLegendPropsValue.symbolSpacer}
height={chartLegendPropsValue.height}
gutter={chartLegendPropsValue.gutter}
orientation="horizontal"
style={{
labels: { fontSize: 8 },
}}
/>
</>
);
}
return (
<EmptyState className="graph-empty-state" variant={EmptyStateVariant.full}>
<EmptyStateIcon size="sm" icon={ChartLineIcon} />
<Title size="sm">No Prometheus datapoints found.</Title>
</EmptyState>
);
},
);

type TopConsumerBodyProps = {
topConsumerStats: PrometheusResponse[];
metricType?: string;
sortByOption?: string;
};
@@ -0,0 +1,15 @@
@import '~@patternfly/patternfly/sass-utilities/colors';

.ceph-top-consumer-card__dropdown {
border-bottom: 1px solid $pf-color-black-300;
display: flex;
padding-bottom: 0.6em;
}

.ceph-top-consumer-card__dropdown--left {
padding-left: 0.3em;
}

.ceph-top-consumer-card__dropdown--right {
padding-right: 0.3em;
}
@@ -0,0 +1,97 @@
import * as React from 'react';
import * as _ from 'lodash';
import { DashboardCard } from '@console/internal/components/dashboard/dashboard-card/card';
import { DashboardCardBody } from '@console/internal/components/dashboard/dashboard-card/card-body';
import { DashboardCardHeader } from '@console/internal/components/dashboard/dashboard-card/card-header';
import { DashboardCardTitle } from '@console/internal/components/dashboard/dashboard-card/card-title';
import { Dropdown } from '@console/internal/components/utils/dropdown';
import {
DashboardItemProps,
withDashboardResources,
} from '@console/internal/components/dashboards-page/with-dashboard-resources';
import { BY_REQUESTED, BY_USED, PODS, PROJECTS, STORAGE_CLASSES, VMS } from '../../../../constants';
import { TOP_CONSUMER_QUERIES, StorageDashboardQuery } from '../../../../constants/queries';
import { TopConsumersBody } from './top-consumers-card-body';
import './top-consumers-card.scss';

const TopConsumerResourceValue = {
[PROJECTS]: 'PROJECTS_',
[STORAGE_CLASSES]: 'STORAGE_CLASSES_',
[PODS]: 'PODS_',
[VMS]: 'VMS_',
};
const TopConsumerSortByValue = {
[BY_USED]: 'BY_USED',
[BY_REQUESTED]: 'BY_REQUESTED',
};

const TopConsumerResourceValueMapping = {
Projects: 'namespace',
'Storage Classes': 'storageclass',
Pods: 'pod',
};

const metricTypes = _.keys(TopConsumerResourceValue);
const sortByTypes = _.keys(TopConsumerSortByValue);

const metricTypesOptions = _.zipObject(metricTypes, metricTypes);
const sortByOptions = _.zipObject(sortByTypes, sortByTypes);

const TopConsumerCard: React.FC<DashboardItemProps> = ({
watchPrometheus,
stopWatchPrometheusQuery,
prometheusResults,
}) => {
const [metricType, setMetricType] = React.useState(metricTypes[0]);
const [sortBy, setSortBy] = React.useState(sortByTypes[0]);
React.useEffect(() => {
const query =
TOP_CONSUMER_QUERIES[
StorageDashboardQuery[TopConsumerResourceValue[metricType] + TopConsumerSortByValue[sortBy]]
];
watchPrometheus(query);
return () => stopWatchPrometheusQuery(query);
}, [watchPrometheus, stopWatchPrometheusQuery, metricType, sortBy]);

const topConsumerstats = prometheusResults.getIn([
TOP_CONSUMER_QUERIES[
StorageDashboardQuery[TopConsumerResourceValue[metricType] + TopConsumerSortByValue[sortBy]]
],
'result',
]);

return (
<DashboardCard>
<DashboardCardHeader>
<DashboardCardTitle>Top Consumers</DashboardCardTitle>
<div className="ceph-top-consumer-card__dropdown">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

height of the card header is inconsistent with others. Take a look at these styles https://github.com/openshift/console/pull/1722/files#diff-163991d4c119db922a729100ce2bea7fR91

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, those dropdowns are actually not in header as yours are.
@andybraren @matthewcarleton any help on how to include these dropdowns into PF4 Card Header and keep header height consistent ?

dropdowns-header

See the difference between Capacity and Top Consumers header

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @rawagner I believe you can use the actions area for the card header provided. That should fix the spacing isssues. If not we may need to investigate further if there is unnecessary spacing added to other elements in there.

<Dropdown
className="btn-group ceph-top-consumer-card__dropdown--right"
id="metric-type"
items={metricTypesOptions}
onChange={setMetricType}
selectedKey={metricType}
title={metricType}
/>
<Dropdown
className="btn-group ceph-top-consumer-card__dropdown--left"
id="sort-by"
items={sortByOptions}
onChange={setSortBy}
selectedKey={sortBy}
title={sortBy}
/>
</div>
</DashboardCardHeader>
<DashboardCardBody>
<TopConsumersBody
topConsumerStats={topConsumerstats}
metricType={TopConsumerResourceValueMapping[metricType]}
sortByOption={sortBy}
/>
</DashboardCardBody>
</DashboardCard>
);
};

export default withDashboardResources(TopConsumerCard);
@@ -0,0 +1,18 @@
import * as _ from 'lodash';
import { humanizeBinaryBytesWithoutB } from '@console/internal/components/utils';
import { PrometheusResponse, DataPoint } from '@console/internal/components/graphs';

export const getMetricType = (resource, metricType) => _.get(resource, ['metric', metricType], '');

export const getGraphVectorStats: GetStats = (response, metricType, unit) => {
const result = _.get(response, 'data.result', []);
return result.map((r) => {
return r.values.map((arr) => ({
name: getMetricType(r, metricType),
x: new Date(arr[0] * 1000),
y: Number(humanizeBinaryBytesWithoutB(arr[1], unit).value),
}));
});
};

type GetStats = (response: PrometheusResponse[], metric?: string, unit?: string) => DataPoint[];
6 changes: 6 additions & 0 deletions frontend/packages/ceph-storage-plugin/src/constants/index.ts
Expand Up @@ -7,3 +7,9 @@ export const CEPH_CLUSTER_NAME = 'rook-ceph';
export const ONE_HR = '1 Hour';
export const SIX_HR = '6 Hours';
export const TWENTY_FOUR_HR = '24 Hours';
export const PROJECTS = 'Projects';
export const STORAGE_CLASSES = 'Storage Classes';
export const PODS = 'Pods';
export const VMS = 'VMs';
export const BY_USED = 'By Used Capacity';
export const BY_REQUESTED = 'By Requested Capacity';
27 changes: 27 additions & 0 deletions frontend/packages/ceph-storage-plugin/src/constants/queries.ts
Expand Up @@ -10,6 +10,14 @@ export enum StorageDashboardQuery {
UTILIZATION_RECOVERY_RATE_QUERY = 'UTILIZATION_RECOVERY_RATE_QUERY',
CEPH_CAPACITY_TOTAL = 'CAPACITY_TOTAL',
CEPH_CAPACITY_USED = 'CAPACITY_USED',
PODS_BY_REQUESTED = 'PODS_BY_REQUESTED',
PODS_BY_USED = 'PODS_BY_USED',
PROJECTS_BY_REQUESTED = 'PROJECTS_BY_REQUESTED',
PROJECTS_BY_USED = 'PROJECTS_BY_USED',
STORAGE_CLASSES_BY_REQUESTED = 'STORAGE_CLASSES_BY_REQUESTED',
STORAGE_CLASSES_BY_USED = 'STORAGE_CLASSES_BY_USED',
VMS_BY_REQUESTED = 'VMS_BY_REQUESTED',
VMS_BY_USED = 'VMS_BY_USED',
}

export const STORAGE_HEALTH_QUERIES = {
Expand Down Expand Up @@ -42,3 +50,22 @@ export const CAPACITY_USAGE_QUERIES = {
[StorageDashboardQuery.CEPH_CAPACITY_TOTAL]: 'ceph_cluster_total_bytes',
[StorageDashboardQuery.CEPH_CAPACITY_USED]: 'ceph_cluster_total_used_bytes[60m:5m]',
};

export const TOP_CONSUMER_QUERIES = {
[StorageDashboardQuery.PODS_BY_REQUESTED]:
'(sort(topk(5, sum(avg_over_time(kube_persistentvolumeclaim_resource_requests_storage_bytes[1h]) * on (namespace,persistentvolumeclaim) group_left(pod) kube_pod_spec_volumes_persistentvolumeclaims_info) by (pod))))[10m:1m]',
[StorageDashboardQuery.PODS_BY_USED]:
'(sort(topk(5, sum(avg_over_time(kubelet_volume_stats_used_bytes[1h]) * on (namespace,persistentvolumeclaim) group_left(pod) kube_pod_spec_volumes_persistentvolumeclaims_info) by (pod))))[10m:1m]',
[StorageDashboardQuery.PROJECTS_BY_REQUESTED]:
'(sort(topk(5, sum(avg_over_time(kube_persistentvolumeclaim_resource_requests_storage_bytes[1h]) * on (namespace,persistentvolumeclaim) group_left(storageclass) kube_persistentvolumeclaim_info) by (namespace))))[10m:1m]',
[StorageDashboardQuery.PROJECTS_BY_USED]:
'(sort(topk(5, sum(avg_over_time(kubelet_volume_stats_used_bytes[1h]) * on (namespace,persistentvolumeclaim) group_left(storageclass) kube_persistentvolumeclaim_info) by (namespace))))[10m:1m]',
[StorageDashboardQuery.STORAGE_CLASSES_BY_REQUESTED]:
'(sort(topk(5, sum(avg_over_time(kube_persistentvolumeclaim_resource_requests_storage_bytes[1h]) * on (namespace,persistentvolumeclaim) group_left(storageclass) kube_persistentvolumeclaim_info) by (storageclass))))[10m:1m]',
[StorageDashboardQuery.STORAGE_CLASSES_BY_USED]:
'(sort(topk(5, sum(avg_over_time(kubelet_volume_stats_used_bytes[1h]) * on (namespace,persistentvolumeclaim) group_left(storageclass) kube_persistentvolumeclaim_info) by (storageclass))))[10m:1m]',
[StorageDashboardQuery.VMS_BY_REQUESTED]:
'(sort(topk(5, sum(avg_over_time(kube_persistentvolumeclaim_resource_requests_storage_bytes[1h]) * on (namespace,persistentvolumeclaim) group_left(pod) kube_pod_spec_volumes_persistentvolumeclaims_info{pod=~"virt-launcher-.*"}) by (pod))))[10m:1m]',
[StorageDashboardQuery.VMS_BY_USED]:
'(sort(topk(5, sum(max(avg_over_time(kubelet_volume_stats_used_bytes[1h]) * on (namespace,persistentvolumeclaim) group_left(pod) kube_pod_spec_volumes_persistentvolumeclaims_info{pod=~"virt-launcher-.*"}) by (pod,persistentvolumeclaim)) by (pod))))[10m:1m]',
};
11 changes: 11 additions & 0 deletions frontend/packages/ceph-storage-plugin/src/plugin.ts
Expand Up @@ -128,6 +128,17 @@ const plugin: Plugin<ConsumedExtensions> = [
).then((m) => m.default),
},
},
{
type: 'Dashboards/Card',
properties: {
tab: 'persistent-storage',
position: GridPosition.MAIN,
loader: () =>
import(
'./components/dashboard-page/storage-dashboard/top-consumers-card/top-consumers-card' /* webpackChunkName: "ceph-storage-top-consumers-card" */
).then((m) => m.default),
},
},
{
type: 'Dashboards/Overview/Health/Prometheus',
properties: {
Expand Down