Merge "feature: Add storage cluster monitor page"

This commit is contained in:
Zuul 2021-12-02 01:24:55 +00:00 committed by Gerrit Code Review
commit c8f9e19e63
7 changed files with 901 additions and 0 deletions

View File

@ -609,6 +609,14 @@ const renderMenu = (t) => {
children: [],
hasBreadcrumb: true,
},
{
path: '/monitor-center/storage-cluster-admin',
name: t('Storage Cluster'),
key: 'monitorStorageClusterAdmin',
level: 1,
children: [],
hasBreadcrumb: true,
},
],
},
{

View File

@ -0,0 +1,377 @@
// Copyright 2021 99cloud
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React, { Component } from 'react';
import { observer } from 'mobx-react';
import { handleResponses } from 'components/PrometheusChart/utils/dataHandler';
import ChartCard from 'components/PrometheusChart/ChartCard';
import { ChartType } from 'components/PrometheusChart/utils/utils';
import { Col, Progress, Row } from 'antd';
import { merge, get } from 'lodash';
import BaseCard from 'components/PrometheusChart/BaseCard';
import {
cephStatusColorMap,
cephStatusMap,
getSuitableValue,
} from 'resources/monitoring';
import { computePercentage } from 'utils/index';
import CircleChart from 'components/PrometheusChart/CircleWithRightLegend';
import RenderTabs from './RenderTabs';
import styles from './index.less';
@observer
class Charts extends Component {
constructor(props) {
super(props);
this.store = props.store;
}
renderTopCards() {
const baseConfig = {
span: 12,
constructorParams: {
requestType: 'current',
formatDataFn: handleResponses,
},
renderContent: (store) => {
const data = get(store, 'data[0].y', 0);
return <div className={styles.topContent}>{data}</div>;
},
visibleHeight: 120,
};
const chartLists = [
{
title: t('Storage Cluster Status'),
span: 6,
constructorParams: {
metricKey: 'storageCluster.cephHealthStatus',
},
renderContent: (store) => {
const data = get(store.data, 'y', 0);
return (
<div
className={styles.topContent}
style={{
fontSize: 28,
fontWeight: 600,
color: cephStatusColorMap[data],
}}
>
{cephStatusMap[data]}
</div>
);
},
},
{
title: 'Monitors',
span: 9,
constructorParams: {
metricKey: 'storageCluster.cephMonitorStatus',
formatDataFn: (...rest) => {
const data = handleResponses(...rest);
const status = [
{
type: 'down',
value: 0,
},
{
type: 'up',
value: 0,
},
];
data.forEach((i) => {
status[i.y].value++;
});
return status;
},
},
renderContent: (store) => (
<div>
<div style={{ height: 120 }}>
<CircleChart data={store.data} />
</div>
</div>
),
},
{
title: 'PGs',
span: 9,
constructorParams: {
metricKey: 'storageCluster.cephPGS',
formatDataFn: (...rest) => {
const data = handleResponses(...rest);
return [
{
type: 'clean',
value: get(data, '[0].y', 0),
},
{
type: 'others',
value: get(data, '[1].y', 0),
},
];
},
},
renderContent: (store) => (
<div>
<div style={{ height: 120 }}>
<CircleChart data={store.data} />
</div>
</div>
),
},
{
title: 'OSDs',
span: 9,
constructorParams: {
metricKey: 'storageCluster.osdData',
formatDataFn: (resps) => {
const [inUp, inDown, outUp, outDown] = resps;
return {
inUp: getValue(inUp),
inDown: getValue(inDown),
outUp: getValue(outUp),
outDown: getValue(outDown),
};
function getValue(d) {
return get(d, 'data.result[0].value[1]', 0);
}
},
},
renderContent: (store) => {
const { data } = store;
return (
<Row className={styles.OSDs}>
<Col span={8} />
<Col span={8} style={{ fontSize: 14, opacity: 0.8 }}>
{t('Up')}
</Col>
<Col span={8} style={{ fontSize: 14, opacity: 0.8 }}>
{t('Down')}
</Col>
<Col span={8} style={{ fontSize: 14, opacity: 0.8 }}>
{t('In Cluster')}
</Col>
<Col span={8} style={{ fontSize: 18 }}>
{data.inUp}
</Col>
<Col span={8} style={{ fontSize: 18 }}>
{data.inDown}
</Col>
<Col span={8} style={{ fontSize: 14, opacity: 0.8 }}>
{t('Out Cluster')}
</Col>
<Col span={8} style={{ fontSize: 18 }}>
{data.outUp}
</Col>
<Col span={8} style={{ fontSize: 18 }}>
{data.outDown}
</Col>
</Row>
);
},
},
{
title: t('Average PGs per OSD'),
span: 5,
constructorParams: {
metricKey: 'storageCluster.avgPerOSD',
},
},
// {
// title: t('Average OSD Apply Latency(ms)'),
// span: 5,
// constructorParams: {
// metricKey: 'storageCluster.avgOSDApplyLatency',
// },
// },
// {
// title: t('Average OSD Commit Latency(ms)'),
// span: 5,
// constructorParams: {
// metricKey: 'storageCluster.avgOSDCommitLatency',
// },
// },
{
title: t('Storage Cluster Usage'),
span: 10,
constructorParams: {
metricKey: 'storageCluster.storageClusterUsage',
},
renderContent: (store) => {
const { data } = store;
const usedValue = get(data[0], 'y', 0);
const totalValue = get(data[1], 'y', 0);
const used = getSuitableValue(usedValue, 'disk');
const total = getSuitableValue(totalValue, 'disk');
const progressPercentage = computePercentage(usedValue, totalValue);
return (
<div className={styles.topContent}>
<div
style={{
width: '100%',
height: '100%',
}}
>
<Row style={{ justifyContent: 'flex-end', height: '50%' }}>
<span
style={{
fontSize: 12,
marginRight: 32,
}}
>
{`${t('Used')} ${used} / ${t('Total')} ${total}`}
</span>
</Row>
<Row style={{ height: '50%' }}>
<Progress
style={{ width: '95%' }}
percent={progressPercentage}
strokeColor={
progressPercentage > 80 ? '#FAAD14' : '#1890FF'
}
showInfo={progressPercentage !== 100}
/>
</Row>
</div>
</div>
);
},
},
];
return (
<Row gutter={[16, 16]}>
{chartLists.map((chartProps) => {
const config = merge({}, baseConfig, chartProps);
const { span, ...rest } = config;
return (
<Col span={span} key={chartProps.constructorParams.metricKey}>
<BaseCard
{...rest}
currentRange={this.store.currentRange}
interval={this.store.interval}
/>
</Col>
);
})}
</Row>
);
}
renderChartCards() {
const baseConfig = {
span: 12,
constructorParams: {
requestType: 'range',
formatDataFn: handleResponses,
},
chartProps: {
height: 250,
scale: {
y: {
nice: true,
},
},
},
};
const chartLists = [
{
title: t('Storage Pool Capacity Usage'),
constructorParams: {
metricKey: 'storageCluster.poolCapacityUsage',
modifyKeys: [t('used'), t('available')],
},
chartProps: {
chartType: ChartType.MULTILINE,
scale: {
y: {
formatter: (d) => getSuitableValue(d, 'disk', 0),
},
},
},
},
{
title: t('Storage Cluster OSD Latency'),
constructorParams: {
metricKey: 'storageCluster.clusterOSDLatency',
modifyKeys: ['apply', 'commit'],
},
chartProps: {
chartType: ChartType.MULTILINE,
},
},
{
title: t('Storage Cluster IOPS'),
constructorParams: {
metricKey: 'storageCluster.clusterIOPS',
modifyKeys: [t('read'), t('write')],
},
chartProps: {
chartType: ChartType.MULTILINE,
},
},
{
title: t('Storage Cluster Bandwidth'),
constructorParams: {
metricKey: 'storageCluster.clusterBandwidth',
modifyKeys: [t('in'), t('out')],
},
chartProps: {
scale: {
y: {
formatter: (d) => getSuitableValue(d, 'bandwidth', 0),
},
},
chartType: ChartType.MULTILINE,
},
},
];
return (
<Row gutter={[16, 16]}>
{chartLists.map((chartProps) => {
const config = merge({}, baseConfig, chartProps);
const { span, ...rest } = config;
return (
<Col span={span} key={chartProps.constructorParams.metricKey}>
<ChartCard
{...rest}
currentRange={this.store.currentRange}
interval={this.store.interval}
BaseContentConfig={this.props.BaseContentConfig}
/>
</Col>
);
})}
</Row>
);
}
render() {
if (this.store.isLoading) {
return null;
}
return (
<Row gutter={[16, 16]} style={{ paddingTop: 16 }}>
<Col span={24}>{this.renderTopCards()}</Col>
<Col span={24}>{this.renderChartCards()}</Col>
<Col span={24}>
<RenderTabs store={this.store} />
</Col>
</Row>
);
}
}
export default Charts;

View File

@ -0,0 +1,285 @@
// Copyright 2021 99cloud
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React, { Component } from 'react';
import { Spin, Tabs } from 'antd';
import { observer } from 'mobx-react';
import { formatSize } from 'utils/index';
import FetchPrometheusStore from 'components/PrometheusChart/store/FetchPrometheusStore';
import { get } from 'lodash';
import metricDict from 'components/PrometheusChart/metricDict';
import { fetchPrometheus } from 'components/PrometheusChart/utils/utils';
import BaseTable from 'components/Tables/Base';
import List from 'stores/base-list';
import styles from './index.less';
const { TabPane } = Tabs;
@observer
class RenderTabs extends Component {
constructor(props) {
super(props);
this.store = new FetchPrometheusStore({
requestType: 'current',
metricKey: 'storageCluster.tabs',
modifyKeys: ['pools', 'osds'],
formatDataFn: this.formatDataFn,
});
this.state = {
filters: {},
};
}
componentDidMount() {
this.getData();
}
async getData() {
await this.store
.fetchData({
currentRange: this.props.store.currentRange,
interval: this.props.store.interval,
})
.then(() => {
this.getListData();
});
}
async getListData() {
const { data } = this.store;
const newData = [...data];
const poolPromises = get(metricDict, 'storageCluster.poolTab.url', []).map(
(item) => fetchPrometheus(item, 'current')
);
const osdPromises = get(metricDict, 'storageCluster.osdTab.url', []).map(
(item) => fetchPrometheus(item, 'current')
);
const poolRets = await Promise.all(poolPromises);
poolRets.forEach((ret, index) => {
handler(ret, index, 'pool_id');
});
const osdRets = await Promise.all(osdPromises);
osdRets.forEach((ret, index) => {
handler(ret, index, 'ceph_daemon');
});
this.store.updateData(newData);
function handler(ret, index, primaryKey) {
ret.data.result.forEach((item) => {
const { metric, value } = item;
const itemIndex = newData.findIndex(
(p) => p[primaryKey] === metric[primaryKey]
);
// metric.__name__
// usagedetails3index===3, Tab3
// console.log(metric.__name__);
if (index === 3) {
newData[itemIndex].usage = parseFloat(
parseFloat(value[1]).toFixed(2)
);
} else if (
[
'ceph_pool_objects',
'ceph_pg_total',
'ceph_pool_max_avail',
'ceph_osd_weight',
'ceph_osd_apply_latency_ms',
'ceph_osd_commit_latency_ms',
'ceph_osd_stat_bytes',
].indexOf(metric.__name__) > -1
) {
newData[itemIndex][metric.__name__] = parseInt(value[1], 10);
} else {
newData[itemIndex][metric.__name__] = value[1];
}
});
}
}
get tableData() {
const { filters } = this.state;
let originData = this.store.data.filter(
(d) => d.type === (this.store.device || this.store.data[0].type)
);
Object.keys(filters).forEach((key) => {
originData = originData.filter((i) => i[key] === filters[key]);
});
return originData;
}
formatDataFn(resps) {
const retData = [];
const [pool, osd] = resps;
get(pool, 'data.result', []).forEach((r) => {
const { metric, value } = r;
retData.push({
type: 'pool',
...metric,
value: parseFloat(value[1]) || 0,
});
});
get(osd, 'data.result', []).forEach((r) => {
const { metric, value } = r;
retData.push({
type: 'osd',
...metric,
value: parseFloat(value[1]) || 0,
});
});
return retData;
}
render() {
const { device = 'pool' } = this.store;
const columns = device === 'pool' ? poolsColumns : osdsColumns;
return (
<>
<Tabs
defaultActiveKey="pool"
onChange={(e) => {
this.setState(
{
filters: {},
},
() => {
this.store.handleDeviceChange(e);
}
);
}}
>
<TabPane tab="Pools" key="pool" />
<TabPane tab="OSDs" key="osd" />
</Tabs>
{this.store.isLoading ? (
<div className={styles.spinContainer}>
<Spin />
</div>
) : (
<BaseTable
resourceName={this.store.device === 'pool' ? t('Pools') : t('OSDs')}
rowKey={this.store.device === 'pool' ? 'pool_id' : 'name'}
columns={columns}
data={this.tableData}
pagination={{
...new List(),
total: this.tableData.length,
}}
hideRefresh
searchFilters={
this.store.device === 'pool'
? [
{
label: t('Pool Name'),
name: 'name',
},
]
: [
{
label: t('Name'),
name: 'ceph_daemon',
},
]
}
itemActions={[]}
onFilterChange={(filters) => {
const { limit, page, sortKey, sortOrder, ...rest } = filters;
this.setState({
filters: rest,
});
}}
onFetchBySort={() => {}}
/>
)}
</>
);
}
}
export default RenderTabs;
const poolsColumns = [
{
title: t('Pool Name'),
dataIndex: 'name',
},
{
title: t('PGs'),
dataIndex: 'ceph_pg_total',
isHideable: true,
},
{
title: t('Objects'),
dataIndex: 'ceph_pool_objects',
isHideable: true,
},
{
title: t('Max Avail'),
dataIndex: 'ceph_pool_max_avail',
render: (text) => formatSize(text),
isHideable: true,
},
{
title: t('Usage'),
dataIndex: 'usage',
render: (text) => `${text}%`,
isHideable: true,
},
];
const osdsColumns = [
{
title: t('Name'),
dataIndex: 'ceph_daemon',
},
{
title: t('Status'),
dataIndex: 'ceph_osd_up',
render: (up) => (up === '1' ? t('Up') : t('Down')),
isHideable: true,
},
{
title: t('Instance Addr'),
dataIndex: 'cluster_addr',
isHideable: true,
},
{
title: t('Weight'),
dataIndex: 'ceph_osd_weight',
isHideable: true,
},
{
title: t('Apply Latency(ms)'),
dataIndex: 'ceph_osd_apply_latency_ms',
isHideable: true,
},
{
title: t('Commit Latency(ms)'),
dataIndex: 'ceph_osd_commit_latency_ms',
isHideable: true,
},
{
title: t('Total Capacity'),
dataIndex: 'ceph_osd_stat_bytes',
render: (text) => formatSize(text),
isHideable: true,
},
{
title: t('Usage'),
dataIndex: 'usage',
render: (text) => `${parseFloat(text).toFixed(2)}%`,
isHideable: true,
},
];

View File

@ -0,0 +1,72 @@
// Copyright 2021 99cloud
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { observer } from 'mobx-react';
import Base from 'containers/List';
@observer
export default class TabTable extends Base {
componentDidMount() {}
getData() {}
getDataSource = () => {
return this.list.data;
};
get params() {
return {};
}
get list() {
return {
data: this.props.store.data.filter(
(d) => d.type === (this.props.store.device || 'pool')
),
filters: [],
};
}
get location() {
return {
search: [],
};
}
get rowKey() {
const { tabKey } = this.store;
return tabKey === 'pools' ? 'pool_id' : 'ceph_daemon';
}
get name() {
return t('tab tables');
}
get actionConfigs() {
return {
rowActions: {},
primaryActions: [],
};
}
getColumns = () => {
const { columns } = this.props;
return columns || [];
};
get searchFilters() {
const { searchFilters } = this.props;
return searchFilters || [];
}
}

View File

@ -0,0 +1,34 @@
// Copyright 2021 99cloud
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { observer } from 'mobx-react';
import BaseContent from 'components/PrometheusChart/component/BaseContent';
import Charts from './Charts';
const StorageCluster = () => {
const BaseContentConfig = {
renderNodeSelect: false,
};
return (
<BaseContent renderChartCards={renderChartCards} {...BaseContentConfig} />
);
function renderChartCards(store) {
return <Charts store={store} BaseContentConfig={BaseContentConfig} />;
}
};
export default observer(StorageCluster);

View File

@ -0,0 +1,119 @@
.OSDs {
height: 100%;
font-size: 16px;
text-align: center;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
.header {
overflow: auto;
padding: 20px;
.range {
:global {
.ant-radio-button-wrapper {
color: rgba(0, 0, 0, 0.65);
}
.ant-radio-button-wrapper-checked {
color: #0068ff;
}
}
}
.download {
float: right;
:global {
.ant-btn-icon-only {
border-radius: 4px;
}
}
}
}
.myCardRow {
.top {
.content {
font-size: 24px;
text-align: center;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
:global {
.ant-card-bordered {
display: flex;
flex-direction: column;
.ant-card-body {
flex-grow: 1;
overflow: hidden;
padding-top: 0;
}
}
}
}
:global {
.ant-card-bordered {
box-shadow: 0px 2px 20px 0px rgba(0, 0, 0, 0.09);
.ant-card-head {
border-bottom: none;
}
}
}
}
.outer {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
font-size: 12px;
.inner {
width: 100%;
height: 100%;
position: absolute;
left: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.inner::-webkit-scrollbar {
display: none;
}
}
.topContent {
font-size: 24px;
font-weight: 500;
height: 120px;
display: flex;
justify-content: center;
align-items: center;
}
.tabs {
:global {
.ant-tabs-tab {
margin-right: 20px;
border-bottom: 1px solid #f0f0f0;
}
.ant-tabs-nav::before {
border-bottom: none;
}
}
}
.spinContainer {
width: 100%;
min-height: 400px;
padding: 30px 50px;
text-align: center;
}

View File

@ -15,6 +15,7 @@
import BaseLayout from 'layouts/Basic';
import E404 from 'pages/base/containers/404';
import PhysicalNode from '../containers/PhysicalNode';
import StorageCluster from '../containers/StorageCluster';
import Overview from '../containers/Overview';
const PATH = '/monitor-center';
@ -29,6 +30,11 @@ export default [
component: PhysicalNode,
exact: true,
},
{
path: `${PATH}/storage-cluster-admin`,
component: StorageCluster,
exact: true,
},
{ path: '*', component: E404 },
],
},