From 8cdbb3d259fb4acf32acef6a98b234dd5a71f04f Mon Sep 17 00:00:00 2001 From: zhuyue Date: Mon, 29 Nov 2021 15:09:36 +0800 Subject: [PATCH] feature: Add storage cluster monitor page add storage cluster monitor page Change-Id: Ib2eb0e1c711f541713822aae8ec200e9cd0d86f5 --- src/layouts/admin-menu.jsx | 8 + .../containers/StorageCluster/Charts.jsx | 377 ++++++++++++++++++ .../containers/StorageCluster/RenderTabs.jsx | 285 +++++++++++++ .../containers/StorageCluster/TabTable.jsx | 72 ++++ .../containers/StorageCluster/index.jsx | 34 ++ .../containers/StorageCluster/index.less | 119 ++++++ src/pages/monitor/routes/index.js | 6 + 7 files changed, 901 insertions(+) create mode 100644 src/pages/monitor/containers/StorageCluster/Charts.jsx create mode 100644 src/pages/monitor/containers/StorageCluster/RenderTabs.jsx create mode 100644 src/pages/monitor/containers/StorageCluster/TabTable.jsx create mode 100644 src/pages/monitor/containers/StorageCluster/index.jsx create mode 100644 src/pages/monitor/containers/StorageCluster/index.less diff --git a/src/layouts/admin-menu.jsx b/src/layouts/admin-menu.jsx index a6faadeb..f66eee87 100644 --- a/src/layouts/admin-menu.jsx +++ b/src/layouts/admin-menu.jsx @@ -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, + }, ], }, { diff --git a/src/pages/monitor/containers/StorageCluster/Charts.jsx b/src/pages/monitor/containers/StorageCluster/Charts.jsx new file mode 100644 index 00000000..de5cf3c7 --- /dev/null +++ b/src/pages/monitor/containers/StorageCluster/Charts.jsx @@ -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
{data}
; + }, + 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 ( +
+ {cephStatusMap[data]} +
+ ); + }, + }, + { + 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) => ( +
+
+ +
+
+ ), + }, + { + 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) => ( +
+
+ +
+
+ ), + }, + { + 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 ( + + + + {t('Up')} + + + {t('Down')} + + + {t('In Cluster')} + + + {data.inUp} + + + {data.inDown} + + + {t('Out Cluster')} + + + {data.outUp} + + + {data.outDown} + + + ); + }, + }, + { + 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 ( +
+
+ + + {`${t('Used')} ${used} / ${t('Total')} ${total}`} + + + + 80 ? '#FAAD14' : '#1890FF' + } + showInfo={progressPercentage !== 100} + /> + +
+
+ ); + }, + }, + ]; + return ( + + {chartLists.map((chartProps) => { + const config = merge({}, baseConfig, chartProps); + const { span, ...rest } = config; + return ( + + + + ); + })} + + ); + } + + 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 ( + + {chartLists.map((chartProps) => { + const config = merge({}, baseConfig, chartProps); + const { span, ...rest } = config; + return ( + + + + ); + })} + + ); + } + + render() { + if (this.store.isLoading) { + return null; + } + return ( + + {this.renderTopCards()} + {this.renderChartCards()} + + + + + ); + } +} + +export default Charts; diff --git a/src/pages/monitor/containers/StorageCluster/RenderTabs.jsx b/src/pages/monitor/containers/StorageCluster/RenderTabs.jsx new file mode 100644 index 00000000..c90d9a35 --- /dev/null +++ b/src/pages/monitor/containers/StorageCluster/RenderTabs.jsx @@ -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__的情况, + // 此处usage是details的第3个查询表达式,所以判断index===3, 两个Tab放一起处理了,都放在第3个。 + // 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 ( + <> + { + this.setState( + { + filters: {}, + }, + () => { + this.store.handleDeviceChange(e); + } + ); + }} + > + + + + {this.store.isLoading ? ( +
+ +
+ ) : ( + { + 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, + }, +]; diff --git a/src/pages/monitor/containers/StorageCluster/TabTable.jsx b/src/pages/monitor/containers/StorageCluster/TabTable.jsx new file mode 100644 index 00000000..668a71d1 --- /dev/null +++ b/src/pages/monitor/containers/StorageCluster/TabTable.jsx @@ -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 || []; + } +} diff --git a/src/pages/monitor/containers/StorageCluster/index.jsx b/src/pages/monitor/containers/StorageCluster/index.jsx new file mode 100644 index 00000000..d216b106 --- /dev/null +++ b/src/pages/monitor/containers/StorageCluster/index.jsx @@ -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 ( + + ); + + function renderChartCards(store) { + return ; + } +}; + +export default observer(StorageCluster); diff --git a/src/pages/monitor/containers/StorageCluster/index.less b/src/pages/monitor/containers/StorageCluster/index.less new file mode 100644 index 00000000..50ebbda1 --- /dev/null +++ b/src/pages/monitor/containers/StorageCluster/index.less @@ -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; +} diff --git a/src/pages/monitor/routes/index.js b/src/pages/monitor/routes/index.js index 10feacb5..d795dd42 100644 --- a/src/pages/monitor/routes/index.js +++ b/src/pages/monitor/routes/index.js @@ -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 }, ], },