feature: Add prometheus base component

add prometheus base component

Change-Id: I4c3c646ddf94ae3c03e98601dbe301fa2ca9e933
This commit is contained in:
zhuyue 2021-11-29 14:30:58 +08:00
parent 5c3c89c78a
commit e21ddcf1bd
20 changed files with 2752 additions and 0 deletions

View File

@ -119,6 +119,13 @@ class SkylineClient extends Base {
}, },
], ],
}, },
{
key: 'query',
},
{
name: 'queryRange',
key: 'query_range',
},
]; ];
} }
} }

View File

@ -0,0 +1,113 @@
// 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 { Card, Select } from 'antd';
import VisibleObserver from 'components/VisibleObserver';
import PropTypes from 'prop-types';
import { observer } from 'mobx-react';
import isEqual from 'react-fast-compare';
import FetchPrometheusStore from './store/FetchPrometheusStore';
import style from './style.less';
@observer
class BaseFetch extends Component {
static propTypes = {
constructorParams: PropTypes.shape({
requestType: PropTypes.oneOf(['current', 'range']).isRequired,
metricKey: PropTypes.string.isRequired,
formatDataFn: PropTypes.func.isRequired,
typeKey: PropTypes.string,
deviceKey: PropTypes.string,
}).isRequired,
params: PropTypes.object,
currentRange: PropTypes.array.isRequired,
interval: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
extra: PropTypes.func,
renderContent: PropTypes.func.isRequired,
visibleHeight: PropTypes.number,
};
static defaultProps = {
// src/pages/monitor/containers/PhysicalNode/index.less:91
//
visibleHeight: 100,
};
constructor(props) {
super(props);
const { constructorParams } = this.props;
this.store = new FetchPrometheusStore(constructorParams);
}
componentDidMount() {
this.getData();
}
componentDidUpdate(prevProps) {
if (!isEqual(prevProps, this.props)) {
this.getData();
}
}
getData() {
const { params, currentRange, interval } = this.props;
this.store.fetchData({ params, currentRange, interval });
}
renderExtra() {
const { extra } = this.props;
return (
<>
{this.store.device && this.store.devices.length !== 0 && (
<Select
defaultValue={this.store.device}
style={{ width: 150, marginRight: 16 }}
options={this.store.devices.map((i) => ({
label: i,
value: i,
}))}
onChange={(e) => this.store.handleDeviceChange.call(this.store, e)}
/>
)}
{extra && extra(this.store)}
</>
);
}
render() {
const { isLoading } = this.store;
const { visibleHeight } = this.props;
return (
<Card
className={style.remove_extra_padding}
bodyStyle={{
// padding 24
minHeight: visibleHeight + 48,
}}
title={this.props.title}
extra={this.renderExtra()}
loading={isLoading}
>
<VisibleObserver style={{ width: '100%', height: visibleHeight }}>
{(visible) => (visible ? this.props.renderContent(this.store) : null)}
</VisibleObserver>
</Card>
);
}
}
export default BaseFetch;

View File

@ -0,0 +1,137 @@
// 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 { Chart, Line, Tooltip } from 'bizcharts';
import BaseCard from 'components/PrometheusChart/BaseCard';
import React from 'react';
import { ChartType, getXScale } from 'components/PrometheusChart/utils/utils';
import {
baseLineProps,
baseToolTipProps,
multilineProps,
} from 'components/PrometheusChart/utils/baseProps';
import { toJS } from 'mobx';
import { observer } from 'mobx-react';
import { merge } from 'lodash';
import ArrowsAltOutlined from '@ant-design/icons/lib/icons/ArrowsAltOutlined';
import { Button, Modal } from 'antd';
import BaseContent from 'components/PrometheusChart/component/BaseContent';
const ChartCard = (props) => {
const {
constructorParams,
params,
currentRange,
interval,
chartProps,
title,
extra,
isModal = false,
BaseContentConfig = {},
} = props;
const {
height,
scale,
chartType,
toolTipProps = baseToolTipProps,
} = chartProps;
let lineProps;
switch (chartType) {
case ChartType.ONELINE:
case ChartType.ONELINEDEVICES:
lineProps = baseLineProps;
break;
case ChartType.MULTILINE:
case ChartType.MULTILINEDEVICES:
lineProps = multilineProps;
break;
default:
lineProps = baseLineProps;
}
const renderContent = (store) => {
let data = toJS(store.data);
if (store.device) {
data = data.filter((d) => d.device === store.device);
}
scale.x = merge({}, getXScale(props.currentRange), scale.x || {});
return (
<Chart autoFit padding="auto" data={data} height={height} scale={scale}>
<Line {...lineProps} />
<Tooltip {...toolTipProps} />
</Chart>
);
};
const getModalCardsParams = (store) => {
const pa = {};
if (params && Object.keys(params).includes('hostname')) {
pa.hostname = store.node.metric.hostname;
}
return pa;
};
const ModalContent = observer(() => (
<div style={{ height: 520 }}>
<BaseContent
renderChartCards={(store) => (
<ChartCard
{...props}
currentRange={store.currentRange}
interval={store.interval}
params={getModalCardsParams(store)}
isModal
/>
)}
{...BaseContentConfig}
/>
</div>
));
return (
<BaseCard
constructorParams={constructorParams}
params={params}
currentRange={currentRange}
interval={interval}
title={title}
extra={(s) => (
<>
{extra && extra(s)}
{!isModal && (
<Button
type="text"
icon={<ArrowsAltOutlined />}
onClick={() => {
Modal.info({
icon: null,
content: <ModalContent />,
width: 1200,
okText: t('OK'),
});
}}
/>
)}
</>
)}
renderContent={renderContent}
visibleHeight={height}
/>
);
};
export default observer(ChartCard);

View File

@ -0,0 +1,100 @@
// 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 {
Annotation,
Axis,
Chart,
Coordinate,
Interaction,
Interval,
Legend,
registerShape,
Tooltip,
} from 'bizcharts';
import React from 'react';
import PropTypes from 'prop-types';
export default class CircleChart extends React.Component {
static propTypes = {
data: PropTypes.array,
legendFontSize: PropTypes.number,
legendOffsetX: PropTypes.number,
middleFontSize: PropTypes.number,
};
static defaultProps = {
legendFontSize: 16,
legendOffsetX: -40,
middleFontSize: 30,
};
render() {
const { data, legendFontSize, legendOffsetX, middleFontSize } = this.props;
const sliceNumber = 0.01; // other 线
registerShape('interval', 'sliceShape', {
draw(cfg, container) {
const { points } = cfg;
let path = [];
path.push(['M', points[0].x, points[0].y]);
path.push(['L', points[1].x, points[1].y - sliceNumber]);
path.push(['L', points[2].x, points[2].y - sliceNumber]);
path.push(['L', points[3].x, points[3].y]);
path.push('Z');
// eslint-disable-next-line react/no-this-in-sfc
path = this.parsePath(path);
return container.addShape('path', {
attrs: {
fill: cfg.color,
path,
},
});
},
});
return (
<Chart data={data} autoFit padding="auto" appendPadding={[0, 20, 0, 0]}>
<Coordinate type="theta" radius={0.8} innerRadius={0.75} />
<Axis visible={false} />
<Tooltip showTitle={false} />
<Interval
adjust="stack"
position="value"
color="type"
shape="sliceShape"
/>
<Annotation.Text
position={['50%', '50%']}
content={data.reduce((a, b) => a + b.value, 0)}
style={{
lineHeight: 240,
fontSize: middleFontSize,
fill: '#262626',
textAlign: 'center',
}}
/>
<Legend
position="right"
offsetX={legendOffsetX}
itemName={{
style: {
fontSize: legendFontSize,
},
}}
/>
<Interaction type="element-single-selected" />
</Chart>
);
}
}

View File

@ -0,0 +1,74 @@
// 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 PropTypes from 'prop-types';
import { observer } from 'mobx-react';
import NodeSelect from 'components/PrometheusChart/component/NodeSelect';
import TimeRangeSelect from 'components/PrometheusChart/component/TimeRangeSelect';
import styles from './index.less';
import BaseMonitorStore from '../store/BaseMonitorStore';
@observer
class BaseContent extends Component {
static propTypes = {
renderChartCards: PropTypes.func.isRequired,
renderTimeRangeSelect: PropTypes.bool,
renderNodeSelect: PropTypes.bool,
fetchNodesFunc: PropTypes.func,
};
static defaultProps = {
renderTimeRangeSelect: true,
renderNodeSelect: true,
};
constructor(props) {
super(props);
this.store = new BaseMonitorStore({
fetchNodesFunc: this.props.fetchNodesFunc,
});
}
componentDidMount() {
const { renderNodeSelect } = this.props;
if (renderNodeSelect) {
this.store.getNodes();
} else {
this.store.setLoading(false);
}
}
render() {
const { renderChartCards, renderTimeRangeSelect, renderNodeSelect } =
this.props;
return (
<div className={styles.header}>
{renderTimeRangeSelect && (
<TimeRangeSelect
style={{ marginBottom: 24 }}
store={this.store}
renderNodeSelect={renderNodeSelect}
/>
)}
{renderNodeSelect && (
<NodeSelect style={{ marginBottom: 24 }} store={this.store} />
)}
{renderChartCards(this.store)}
</div>
);
}
}
export default BaseContent;

View File

@ -0,0 +1,54 @@
// 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 { Select, Spin } from 'antd';
import { observer } from 'mobx-react';
const { Option } = Select;
@observer
class NodeSelect extends Component {
render() {
const { style } = this.props;
const { node, nodes, isLoading, handleNodeChange } = this.props.store;
return (
<div style={style}>
{isLoading ? (
<Spin />
) : (
<>
<span style={{ color: 'black', fontSize: 14, fontWeight: 400 }}>
Node:{' '}
</span>
<Select
value={node.metric.instance}
onChange={handleNodeChange}
getPopupContainer={(triggerNode) => triggerNode.parentNode}
style={{ minWidth: 150 }}
>
{nodes.map((item) => (
<Option key={item.metric.instance} value={item.metric.instance}>
{item.metric.instance}
</Option>
))}
</Select>
</>
)}
</div>
);
}
}
export default NodeSelect;

View File

@ -0,0 +1,181 @@
// 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 { Button, ConfigProvider, DatePicker, Radio, Select } from 'antd';
import { SyncOutlined } from '@ant-design/icons';
import moment from 'moment';
import { observer } from 'mobx-react';
import i18n from 'core/i18n';
import enUS from 'antd/es/locale/en_US';
import zhCN from 'antd/es/locale/zh_CN';
import { getRange } from 'components/PrometheusChart/utils/utils';
import styles from './index.less';
const { RangePicker } = DatePicker;
const { Option } = Select;
@observer
class TimeRangeSelect extends Component {
constructor(props) {
super(props);
// const { store } = props;
this.state = {
filterValue: 0,
datePickerRange: null,
};
}
handleRefresh = () => {
const { filterValue } = this.state;
const { store, renderNodeSelect } = this.props;
if (filterValue > -1) {
const currentRange = getRange(filterValue);
store.handleRangePickerChange(currentRange, true);
} else {
const { datePickerRange } = this.state;
store.handleRangePickerChange(datePickerRange, true);
}
if (renderNodeSelect) {
store.getNodes();
}
};
handleFilterChange = (e) => {
const { store } = this.props;
const currentRange = getRange(e.target.value);
this.setState({
filterValue: e.target.value,
datePickerRange: null,
});
store.handleRangePickerChange(currentRange);
};
handleRangePickerChange = (dates) => {
const { store } = this.props;
this.setState({
datePickerRange: dates || [moment().subtract(1, 'hours'), moment()],
filterValue: 4,
});
store.handleRangePickerChange(dates);
};
disableTime = (pickRange) => {
const now = moment();
if (now.isSame(pickRange, 'day')) {
if (now.isSame(pickRange, 'hour')) {
if (now.isSame(pickRange, 'minutes')) {
return {
disabledHours: () => filterRange(now.hour() + 1, 24),
disabledMinutes: () => filterRange(now.minute() + 1, 60),
disabledSeconds: () => filterRange(now.second() + 1, 60),
};
}
return {
disabledHours: () => filterRange(now.hour() + 1, 24),
disabledMinutes: () => filterRange(now.minute() + 1, 60),
};
}
return {
disabledHours: () => filterRange(now.hour() + 1, 24),
};
}
};
disabledDate(current) {
// Can not select days after today
return current > moment().endOf('day');
}
render() {
const { store } = this.props;
const { intervals, interval } = store;
const { filterValue, datePickerRange } = this.state;
const lang = i18n.getLocale();
const localeProvider = lang === 'en' ? enUS : zhCN;
return (
<div style={this.props.style}>
<Button
type="default"
icon={<SyncOutlined />}
onClick={this.handleRefresh}
/>
<Radio.Group
value={filterValue}
onChange={this.handleFilterChange}
className={styles.range}
style={{
marginLeft: 20,
}}
>
<Radio.Button value={0}>{t('Last Hour')}</Radio.Button>
<Radio.Button value={1}>{t('Last Day')}</Radio.Button>
<Radio.Button value={2}>{t('Last 7 Days')}</Radio.Button>
<Radio.Button value={3}>{t('Last 2 Weeks')}</Radio.Button>
<Radio.Button value={4} style={{ float: 'right', padding: 0 }}>
<ConfigProvider locale={localeProvider}>
<RangePicker
showTime={{
hideDisabledOptions: true,
defaultValue: [
moment('00:00:00', 'HH:mm:ss'),
moment('00:00:00', 'HH:mm:ss'),
],
}}
disabledDate={this.disabledDate}
disabledTime={this.disableTime}
onChange={this.handleRangePickerChange}
value={datePickerRange}
bordered={false}
allowClear={false}
/>
</ConfigProvider>
</Radio.Button>
</Radio.Group>
<span
style={{
marginLeft: 20,
fontSize: 14,
fontWeight: 400,
color: 'rgba(0,0,0,.85)',
}}
>
{t('Time Interval: ')}
</span>
<Select
value={interval}
style={{ width: 120 }}
onChange={store.handleIntervalChange}
>
{intervals.map((d) => (
<Option key={d.value} value={d.value}>
{d.text}
</Option>
))}
</Select>
</div>
);
}
}
function filterRange(start, end) {
const result = [];
for (let i = start; i < end; i++) {
result.push(i);
}
return result;
}
export default TimeRangeSelect;

View File

@ -0,0 +1,58 @@
.header {
width: 100%;
height: 100%;
overflow-y: scroll;
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;
}
}
}
}
.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: 100px;
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -0,0 +1,517 @@
// 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.
const metricDict = {
monitorOverview: {
alertInfo: {
url: [
'node_cpu_seconds_total',
'node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes',
],
baseParams: [
{
mode: 'idle',
node: '',
},
{},
],
finalFormatFunc: [
(url) => `1 - (avg by(instance) (irate(${url}[5m]))) > 0.8`,
(url) => `1 - (${url}) > 0.8`,
],
},
physicalCPUUsage: {
url: ['openstack_nova_vcpus_used', 'openstack_nova_vcpus_available'],
finalFormatFunc: [(url) => `sum(${url})`, (url) => `sum(${url})`],
},
physicalMemoryUsage: {
url: [
'openstack_nova_memory_used_bytes',
'openstack_nova_memory_available_bytes',
],
finalFormatFunc: [(url) => `sum(${url})`, (url) => `sum(${url})`],
},
physicalStorageUsage: {
url: ['ceph_cluster_total_used_bytes', 'ceph_cluster_total_bytes'],
},
computeNodeStatus: {
url: ['openstack_nova_agent_state'],
baseParams: [
{
service: 'nova-compute',
},
],
finalFormatFunc: [(url) => `sum(${url})by(services_state)`],
},
topHostCPUUsage: {
url: ['node_cpu_seconds_total'],
baseParams: [
{
mode: 'idle',
},
],
finalFormatFunc: [
(url) => `topk(5, 100 - (avg(irate(${url}[30m])) by (instance) * 100))`,
],
},
topHostDiskIOPS: {
url: [
'node_disk_reads_completed_total',
'node_disk_writes_completed_total',
],
finalFormatFunc: [
(url) => `topk(5, avg(irate(${url}[10m])) by (instance))`,
(url) => `topk(5, avg(irate(${url}[10m])) by (instance))`,
],
},
topHostMemoryUsage: {
url: ['node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes'],
finalFormatFunc: [(url) => `topk(5, (1 - ${url}) * 100)`],
},
topHostInterface: {
url: [
'node_network_receive_bytes_total',
'node_network_transmit_bytes_total',
],
finalFormatFunc: [
(url) => `topk(5, avg(irate(${url}[5m])) by (instance))`,
(url) => `topk(5, avg(irate(${url}[5m])) by (instance))`,
],
},
cephHealthStatus: {
url: ['ceph_health_status'],
},
cephStorageUsage: {
url: ['ceph_cluster_total_used_bytes', 'ceph_cluster_total_bytes'],
},
cephStorageAllocate: {
url: [
'os_cinder_volume_pools_free_capacity_gb',
'os_cinder_volume_pools_total_capacity_gb',
],
finalFormatFunc: [(url) => `sum(${url})`, (url) => `sum(${url})`],
},
cephStorageClusterIOPS: {
url: ['ceph_osd_op_r', 'ceph_osd_op_w'],
finalFormatFunc: [
(url) => `sum(irate(${url}[5m]))`,
(url) => `sum(irate(${url}[5m]))`,
],
},
},
physicalNode: {
cpuCores: {
url: ['node_cpu_seconds_total'],
finalFormatFunc: [(url) => `count(${url}) by (cpu)`],
},
totalMem: {
url: ['node_memory_MemTotal_bytes'],
},
systemRunningTime: {
url: ['node_boot_time_seconds'],
},
fileSystemFreeSpace: {
url: ['node_filesystem_avail_bytes', 'node_filesystem_size_bytes'],
baseParams: [
{
fstype: ['ext4', 'xfs'],
},
{
fstype: ['ext4', 'xfs'],
},
],
},
cpuUsage: {
url: ['node_cpu_seconds_total'],
finalFormatFunc: [(url) => `avg by (mode)(irate(${url}[30m])) * 100`],
baseParams: [
{
mode: ['idle', 'system', 'user', 'iowait'],
},
],
},
memUsage: {
url: [
'node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes',
'node_memory_MemAvailable_bytes',
],
},
diskIOPS: {
url: [
'node_disk_reads_completed_total',
'node_disk_writes_completed_total',
],
finalFormatFunc: [
(url) => `irate(${url}[5m])`,
(url) => `irate(${url}[5m])`,
],
},
diskUsage: {
url: ['node_filesystem_free_bytes / node_filesystem_size_bytes'],
finalFormatFunc: [(url) => `(1 - ${url}) * 100`],
baseParams: [
{
device: ['/dev/.*'],
},
],
},
systemLoad: {
url: ['node_load1', 'node_load5', 'node_load15'],
},
networkTraffic: {
url: [
'node_network_receive_bytes_total',
'node_network_transmit_bytes_total',
],
finalFormatFunc: [
(url) => `sum(irate(${url}[10m]))`,
(url) => `sum(irate(${url}[10m]))`,
],
},
tcpConnections: {
url: ['node_netstat_Tcp_CurrEstab'],
},
networkErrors: {
url: [
'node_network_receive_errs_total',
'node_network_transmit_errs_total',
],
},
networkDroppedPackets: {
url: [
'node_network_receive_drop_total',
'node_network_transmit_drop_total',
],
finalFormatFunc: [
(url) => `irate(${url}[5m])`,
(url) => `irate(${url}[5m])`,
],
},
},
storageCluster: {
cephHealthStatus: {
url: ['ceph_health_status'],
},
cephMonitorStatus: {
url: ['ceph_mon_quorum_status'],
},
cephPGS: {
url: ['ceph_pg_clean', 'ceph_pg_total-ceph_pg_clean'],
finalFormatFunc: [(url) => `sum(${url})`, (url) => `sum(${url})`],
},
storageClusterUsage: {
url: ['ceph_cluster_total_used_bytes', 'ceph_cluster_total_bytes'],
},
osdData: {
url: [
'ceph_osd_in == 1 and ceph_osd_up == 1',
'ceph_osd_in == 1 and ceph_osd_up == 0',
'ceph_osd_in == 0 and ceph_osd_up == 1',
'ceph_osd_in == 0 and ceph_osd_up == 0',
],
finalFormatFunc: [
(url) => `count(${url})`,
(url) => `count(${url})`,
(url) => `count(${url})`,
(url) => `count(${url})`,
],
},
avgPerOSD: {
url: ['ceph_osd_numpg'],
finalFormatFunc: [(url) => `avg(${url})`],
},
// avgOSDApplyLatency: {
// url: ['ceph_osd_apply_latency_ms'],
// finalFormatFunc: [
// url => `avg(${url})`,
// ],
// },
// avgOSDCommitLatency: {
// url: ['ceph_osd_commit_latency_ms'],
// finalFormatFunc: [
// url => `avg(${url})`,
// ],
// },
poolCapacityUsage: {
url: [
'ceph_cluster_total_used_bytes',
'ceph_cluster_total_bytes-ceph_cluster_total_used_bytes',
],
},
clusterOSDLatency: {
url: ['ceph_osd_apply_latency_ms', 'ceph_osd_commit_latency_ms'],
finalFormatFunc: [(url) => `avg(${url})`, (url) => `avg(${url})`],
},
clusterIOPS: {
url: ['ceph_osd_op_r', 'ceph_osd_op_w'],
finalFormatFunc: [
(url) => `sum(irate(${url}[5m]))`,
(url) => `sum(irate(${url}[5m]))`,
],
},
clusterBandwidth: {
url: ['ceph_osd_op_rw_in_bytes', 'ceph_osd_op_rw_out_bytes'],
finalFormatFunc: [
(url) => `sum(irate(${url}[5m]))`,
(url) => `sum(irate(${url}[5m]))`,
],
},
tabs: {
url: ['ceph_pool_metadata', 'ceph_osd_metadata'],
},
poolTab: {
url: [
'ceph_pg_total',
'ceph_pool_objects',
'ceph_pool_max_avail',
'(ceph_pool_stored/ceph_pool_max_avail)*100',
],
},
osdTab: {
url: [
'ceph_osd_weight',
'ceph_osd_apply_latency_ms',
'ceph_osd_commit_latency_ms',
'(ceph_osd_stat_bytes_used/ceph_osd_stat_bytes)*100',
'ceph_osd_up',
'ceph_osd_stat_bytes',
],
},
},
openstackService: {
novaService: {
url: [
'openstack_nova_agent_state',
'openstack_nova_agent_state',
'node_process_total',
'node_process_total',
],
baseParams: [
{},
{
adminState: 'disabled',
},
{
name: 'libvirtd',
},
{
name: 'libvirtd',
},
],
finalFormatFunc: [
(url) => url,
(url) => `sum_over_time(${url}[24h]) > 0`,
(url) => url,
(url) => `min_over_time(${url}[24h]) == 0`,
],
},
networkService: {
url: ['openstack_neutron_agent_state', 'openstack_neutron_agent_state'],
baseParams: [
{},
{
adminState: 'down',
},
],
finalFormatFunc: [
(url) => url,
(url) => `sum_over_time(${url}[24h]) > 0`,
],
},
cinderService: {
url: ['openstack_cinder_agent_state', 'openstack_cinder_agent_state'],
baseParams: [
{},
{
service_state: 'down',
},
],
finalFormatFunc: [
(url) => url,
(url) => `sum_over_time(${url}[24h]) > 0`,
],
},
otherService: {
url: ['mysql_up', 'rabbitmq_identity_info', 'memcached_up'],
},
otherServiceMinOverTime: {
url: ['mysql_up', 'rabbitmq_identity_info', 'memcached_up'],
finalFormatFunc: [
(url) => `min_over_time(${url}[24h]) == 0`,
(url) => `min_over_time(${url}[24h]) == 0`,
(url) => `min_over_time(${url}[24h]) == 0`,
],
},
// heatMinOverTime: {
// url: ['os_heat_services_status', 'os_heat_services_status'],
// finalFormatFunc: [
// (url) => url,
// (url) => `min_over_time(${url}[24h]) == 0`,
// ],
// },
},
mysqlService: {
runningTime: {
url: ['mysql_global_status_uptime'],
},
connectedThreads: {
url: ['mysql_global_status_threads_connected'],
},
runningThreads: {
url: ['mysql_global_status_threads_running'],
},
slowQuery: {
url: ['mysql_global_status_slow_queries'],
},
threadsActivityTrends_connected: {
url: ['mysql_global_status_threads_connected'],
},
mysqlActions: {
url: [
'mysql_global_status_commands_total',
'mysql_global_status_commands_total',
'mysql_global_status_commands_total',
],
baseParams: [
{ command: 'delete' },
{ command: 'insert' },
{ command: 'update' },
],
},
slowQueryChart: {
url: ['mysql_global_status_slow_queries'],
},
},
memcacheService: {
currentConnections: {
url: ['memcached_current_connections'],
},
totalConnections: {
url: ['memcached_connections_total'],
},
readWriteBytesTotal: {
url: ['memcached_read_bytes_total', 'memcached_written_bytes_total'],
finalFormatFunc: [
(url) => `irate(${url}[20m])`,
(url) => `irate(${url}[20m])`,
],
},
evictions: {
url: ['memcached_slab_items_evicted_unfetched_total'],
},
itemsInCache: {
url: ['memcached_items_total'],
},
},
rabbitMQService: {
serviceStatus: {
url: ['rabbitmq_identity_info'],
},
totalConnections: {
url: ['rabbitmq_connections_opened_total'],
},
totalQueues: {
url: ['rabbitmq_queues_created_total'],
},
totalExchanges: {
url: ['erlang_mnesia_tablewise_size'],
},
totalConsumers: {
url: ['rabbitmq_queue_consumers'],
},
publishedOut: {
url: ['rabbitmq_channel_messages_published_total'],
finalFormatFunc: [(url) => `sum(irate(${url}[20m]))`],
},
publishedIn: {
url: ['rabbitmq_channel_messages_confirmed_total'],
finalFormatFunc: [(url) => `sum(irate(${url}[20m]))`],
},
// totalMessage: {
// url: ['rabbitmq_overview_messages'],
// },
channel: {
url: ['rabbitmq_channels'],
},
},
haProxyService: {
backendStatus: {
url: ['haproxy_backend_up'],
},
connections: {
url: [
'haproxy_frontend_current_sessions',
'haproxy_frontend_current_session_rate',
],
finalFormatFunc: [(url) => `sum(${url})`, (url) => `sum(${url})`],
},
httpResponse: {
url: [
'haproxy_frontend_http_responses_total',
'haproxy_backend_http_responses_total',
],
finalFormatFunc: [
(url) => `sum(irate(${url}[5m])) by (code)`,
(url) => `sum(irate(${url}[5m])) by (code)`,
],
},
session: {
url: [
'haproxy_backend_current_sessions',
'haproxy_backend_current_session_rate',
],
finalFormatFunc: [(url) => `sum(${url})`, (url) => `sum(${url})`],
},
bytes: {
url: [
'haproxy_frontend_bytes_in_total',
'haproxy_backend_bytes_in_total',
'haproxy_frontend_bytes_out_total',
'haproxy_backend_bytes_out_total',
],
finalFormatFunc: [
(url) => `sum(irate(${url}[5m]))`,
(url) => `sum(irate(${url}[5m]))`,
(url) => `sum(irate(${url}[5m]))`,
(url) => `sum(irate(${url}[5m]))`,
],
},
},
instanceMonitor: {
cpu: {
url: ['virtual:kvm:cpu:usage'],
},
memory: {
url: ['virtual:kvm:memory:used'],
},
network: {
url: [
'virtual:kvm:network:receive:rate',
'virtual:kvm:network:transmit:rate',
],
},
disk: {
url: ['virtual:kvm:disk:read:kbps', 'virtual:kvm:disk:write:kbps'],
},
disk_iops: {
url: ['virtual:kvm:disk:read:iops', 'virtual:kvm:disk:write:iops'],
},
disk_usage: {
url: ['vm_disk_fs_used_pcent'],
finalFormatFunc: [(url) => `avg(${url}) without(hostname)`],
},
},
};
export default metricDict;

View File

@ -0,0 +1,116 @@
// 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 { action, computed, observable } from 'mobx';
import {
defaultOneHourAgo,
fetchPrometheus,
getInterval,
} from 'components/PrometheusChart/utils/utils';
import { get } from 'lodash';
import { getTimestamp } from 'utils/time';
export default class BaseMonitorStore {
constructor(props) {
const { fetchNodesFunc } = props || {};
if (fetchNodesFunc) {
this.fetchNodesFunc = fetchNodesFunc;
}
}
@observable
nodes = [];
@observable
node = {
metric: {
hostname: '',
},
};
@observable
currentRange = defaultOneHourAgo;
@observable
interval = 10;
@observable
isLoading = true;
@action
handleRangePickerChange = async (dates, refresh = false) => {
if (
!refresh &&
!(
getTimestamp(this.currentRange[0]) === getTimestamp(dates[0]) &&
getTimestamp(this.currentRange[1]) === getTimestamp(dates[1])
)
) {
// do not extract, see @compute get intervals functions
this.currentRange = dates;
this.interval = this.intervals[0].value;
} else {
// do not extract, see @compute get intervals functions
this.currentRange = dates;
}
// await this.getChartData();
};
@computed get intervals() {
return getInterval(this.currentRange);
}
@action
handleIntervalChange = async (interval) => {
this.interval = interval;
// await this.getChartData();
};
@action
getNodes = async () => {
this.isLoading = true;
let result = [];
try {
if (this.fetchNodesFunc) {
result = await this.fetchNodesFunc();
} else {
const query = 'node_load1';
const res = await fetchPrometheus(query, 'current', this.currentRange);
result = get(res, 'data.result', []);
}
} finally {
this.nodes = result;
this.node = this.nodes[0] || {
metric: {
hostname: '',
},
};
this.isLoading = false;
}
};
@action
handleNodeChange = async (instance) => {
const newNode = this.nodes.find(
(item) => item.metric.instance === instance
);
this.node = newNode;
};
@action
setLoading(flag) {
this.isLoading = flag;
}
}

View File

@ -0,0 +1,126 @@
// 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 { observable, action } from 'mobx';
import { get, isArray } from 'lodash';
import DataSet from '@antv/data-set';
import {
getRequestUrl,
fetchPrometheus,
} from 'components/PrometheusChart/utils/utils';
import metricDict from '../metricDict';
export default class FetchPrometheusStore {
type = '';
@observable
data = [];
@observable
isLoading = true;
@observable
device;
@observable
devices = [];
constructor({
requestType,
metricKey,
formatDataFn,
typeKey,
deviceKey,
modifyKeys,
}) {
// 保存类型是range的还是current的
this.requestType = requestType;
// 获取初始串
this.queryParams = get(metricDict, metricKey);
// 格式化返回数据的方法
this.formatDataFn = formatDataFn || this.baseReturnFunc;
// 设置type的key
this.typeKey = typeKey;
// 设置device的key
this.deviceKey = deviceKey;
// 自定义type的值用于tooltip的展示否则一直会是y方便处理
this.modifyKeys = modifyKeys;
}
baseReturnFunc = (d) => d;
@action
async fetchData({ params = {}, currentRange, interval }) {
this.isLoading = true;
this.device = undefined;
const promises = this.queryParams.url.map((u, idx) => {
// 按顺序取聚合函数
const finalFormatFunc =
(this.queryParams.finalFormatFunc || [])[idx] || this.baseReturnFunc;
// 按顺序获取基础参数
const baseParams = (this.queryParams.baseParams || [])[idx] || {};
const finalUrl = getRequestUrl(u, params, finalFormatFunc, baseParams);
return fetchPrometheus(
finalUrl,
this.requestType,
currentRange,
interval
);
});
const res = await Promise.all(promises).catch((e) => {
// eslint-disable-next-line no-console
console.log(e);
return this.data;
});
this.formatData(res);
this.isLoading = false;
return this.data;
}
formatData(data) {
this.data = this.formatDataFn(
data,
this.typeKey,
this.deviceKey,
this.modifyKeys
);
if (isArray(this.data) && this.data.length !== 0 && this.data[0].device) {
const dv = new DataSet()
.createView()
.source(this.data)
.transform({
type: 'partition',
groupBy: ['device'],
});
this.devices = Object.keys(dv.rows).map((device) =>
device.slice(1, device.length)
);
this.device = this.devices[0];
}
}
@action
handleDeviceChange = (device) => {
this.isLoading = true;
this.device = device;
setTimeout(() => {
this.isLoading = false;
}, 200);
};
@action
updateData = (data) => {
this.data = data;
};
}

View File

@ -0,0 +1,21 @@
.remove_extra_padding {
:global{
.ant-card-extra {
padding: 0
}
.ant-card-head {
border-bottom: none;
}
.ant-card-body {
display: flex;
justify-content: center;
align-items:center;
.ant-card-loading-content {
width: 100%
}
}
}
}

View File

@ -0,0 +1,27 @@
// 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.
export const baseLineProps = {
position: 'x*y',
};
export const multilineProps = {
position: 'x*y',
color: 'type',
};
export const baseToolTipProps = {
showCrosshairs: true,
shared: true,
};

View File

@ -0,0 +1,61 @@
// 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 { get } from 'lodash';
export function baseFixToChart(value) {
return {
x: value[0],
y: parseFloat(parseFloat(value[1]).toFixed(2)),
};
}
// eslint-disable-next-line import/prefer-default-export
export function handleResponses(
responses,
typeKey,
deviceKey,
modifyKeys = []
) {
const ret = [];
responses.forEach((response, idx) => {
ret.push(...handleResponse(response, typeKey, deviceKey, modifyKeys[idx]));
});
return ret;
}
export function handleResponse(response, typeKey, deviceKey, modifyType) {
const { data } = response;
const ret = [];
data.result.forEach((result) => {
// values for range type & value for current type
const values = result.values || [result.value] || [];
values.forEach((value) => {
const item = {
...baseFixToChart(value),
};
if (typeKey) {
item.type = get(result.metric, typeKey);
}
if (deviceKey) {
item.device = get(result.metric, deviceKey);
}
if (modifyType) {
item.type = modifyType;
}
ret.push(item);
});
});
return ret;
}

View File

@ -0,0 +1,217 @@
// 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 { get, isArray } from 'lodash';
import moment from 'moment';
import { getStrFromTimestamp, getTimestamp } from 'utils/time';
import client from 'client';
import metricDict from '../metricDict';
// 给query串增加数据如hostname等。
export function getRequestUrl(url, params, finalFormatFunc, baseParams) {
const totalParams = { ...params, ...baseParams };
return finalFormatFunc(
Object.keys(totalParams).length === 0 ? url : addParams(url, totalParams)
);
}
export function addParams(query, params) {
let addStr = '';
Object.keys(params).forEach((key) => {
if (isArray(params[key])) {
addStr += `${key}=~"${params[key].join('|')}",`;
} else {
addStr += `${key}="${params[key]}",`;
}
});
return `${query}{${addStr.substring(0, addStr.length - 1)}}`;
}
export function fetchPrometheus(
query,
getRangeType = 'range',
currentRange,
interval
) {
if (getRangeType === 'current') {
return client.skyline.query.list({
query,
});
}
if (getRangeType === 'range') {
return client.skyline.queryRange.list({
query,
start: getTimestamp(currentRange[0]),
end: getTimestamp(currentRange[1]),
step: interval,
});
}
}
export function getBaseQuery(metricKey) {
let query = metricDict;
metricKey.split('.').forEach((key) => {
query = query[key];
});
return query;
}
export const ChartType = {
ONELINE: 'oneline',
MULTILINE: 'multiline',
ONELINEDEVICES: 'oneline_devices',
MULTILINEDEVICES: 'multiline_devices',
};
export const getXScale = (timeRange) => {
const rangeMinutes = moment(timeRange[1]).diff(
moment(timeRange[0]),
'minutes',
true
);
const index =
(rangeMinutes > 20160 && 4) ||
(rangeMinutes > 10080 && rangeMinutes <= 20160 && 3) ||
(rangeMinutes > 1440 && rangeMinutes <= 10080 && 2) ||
(rangeMinutes > 60 && rangeMinutes <= 1440 && 1) ||
(rangeMinutes > 0 && rangeMinutes <= 60 && 0) ||
0;
return {
type: 'time',
...maskAndTicketCountDict[index],
};
};
export const baseReturnFunc = (d) => d;
export const getPromises = (metricKey) => {
const queries = get(metricDict, metricKey);
return queries.url.map((u, idx) => {
// 按顺序取聚合函数
const finalFormatFunc =
(queries.finalFormatFunc || [])[idx] || baseReturnFunc;
// 按顺序获取基础参数
const baseParams = (queries.baseParams || [])[idx] || {};
const finalUrl = getRequestUrl(u, {}, finalFormatFunc, baseParams);
return fetchPrometheus(finalUrl, 'current');
});
};
const maskAndTicketCountDict = [
{
// 一小时内的
// mask: 'HH:mm:ss',
formatter: (d) => getStrFromTimestamp(d, 'HH:mm:ss'),
ticketCount: 6,
},
{
// 一天内的
// mask: 'HH:mm:ss',
formatter: (d) => getStrFromTimestamp(d, 'HH:mm:ss'),
ticketCount: 6,
},
{
// 7天内的
// mask: 'MM-DD HH:mm',
formatter: (d) => getStrFromTimestamp(d, 'MM-DD HH:mm'),
ticketCount: 3,
},
{
// 14天内的
// mask: 'MM-DD HH:mm',
formatter: (d) => getStrFromTimestamp(d, 'MM-DD HH:mm'),
ticketCount: 6,
},
{
// 以上
// mask: 'MM-DD HH:mm',
formatter: (d) => getStrFromTimestamp(d, 'MM-DD HH:mm'),
ticketCount: 6,
},
];
export const range2IntervalsDict = [
[
{
text: t('10s'),
value: 10,
},
{
text: t('1min'),
value: 60,
},
{
text: t('5min'),
value: 300,
},
],
[
{
text: t('1min'),
value: 60,
},
{
text: t('5min'),
value: 300,
},
{
text: t('1H'),
value: 3600,
},
],
[
{
text: t('1H'),
value: 3600,
},
{
text: t('1D'),
value: 86400,
},
],
[
{
text: t('1D'),
value: 86400,
},
],
];
export const getRange = (type) =>
({
// last 2 weeks
3: [moment().subtract(2, 'weeks'), moment()],
// last 7 days
2: [moment().subtract(1, 'weeks'), moment()],
// last day
1: [moment().subtract(1, 'days'), moment()],
// last hour
0: [moment().subtract(1, 'hours'), moment()],
}[type] || [moment().subtract(1, 'hours'), moment()]);
export function getInterval(currentRange) {
const start = (currentRange || getRange(0))[0];
const end = (currentRange || getRange(0))[1];
const rangeMinutes = end.diff(start, 'minutes');
const index =
(rangeMinutes > 44640 && 3) ||
(rangeMinutes > 1440 && rangeMinutes <= 44640 && 2) ||
(rangeMinutes > 60 && rangeMinutes <= 1440 && 1) ||
(rangeMinutes > 0 && rangeMinutes <= 60 && 0) ||
0;
return range2IntervalsDict[index];
}
// 1 hour ago - now
export const defaultOneHourAgo = [moment().subtract(1, 'hours'), moment()];

View File

@ -0,0 +1,62 @@
// 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';
@observer
export default class Observer extends Component {
constructor(props) {
super(props);
this.state = { visible: !window.IntersectionObserver };
this.io = null;
this.container = null;
}
componentDidMount() {
(window.IntersectionObserver
? Promise.resolve()
: import('intersection-observer')
).then(() => {
this.io = new window.IntersectionObserver((entries) => {
entries.forEach((entry) => {
this.setState({ visible: entry.isIntersecting });
});
}, {});
this.io.observe(this.container);
});
}
componentWillUnmount() {
if (this.io) {
this.io.disconnect();
}
}
render() {
return (
// 使 findDOMNode
<div
ref={(div) => {
this.container = div;
}}
{...this.props}
>
{Array.isArray(this.props.children)
? this.props.children.map((child) => child(this.state.visible))
: this.props.children(this.state.visible)}
</div>
);
}
}

393
src/resources/monitoring.js Normal file
View File

@ -0,0 +1,393 @@
// 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 {
isEmpty,
isArray,
isNaN,
isUndefined,
isNumber,
isString,
get,
set,
last,
flatten,
min,
max,
} from 'lodash';
import { COLORS_MAP, MILLISECOND_IN_TIME_UNIT } from 'utils/constants';
import { getLocalTimeStr, getStrFromTimestamp } from 'utils/time';
const UnitTypes = {
second: {
conditions: [0.01, 0],
units: ['s', 'ms'],
},
cpu: {
conditions: [0.1, 0],
units: ['core', 'm'],
},
memory: {
conditions: [1024 ** 4, 1024 ** 3, 1024 ** 2, 1024, 0],
units: ['TiB', 'GiB', 'MiB', 'KiB', 'Bytes'],
},
disk: {
conditions: [1000 ** 4, 1000 ** 3, 1000 ** 2, 1000, 0],
units: ['TB', 'GB', 'MB', 'KB', 'Bytes'],
},
throughput: {
conditions: [1000 ** 4, 1000 ** 3, 1000 ** 2, 1000, 0],
units: ['TB/s', 'GB/s', 'MB/s', 'KB/s', 'B/s'],
},
traffic: {
conditions: [1000 ** 4, 1000 ** 3, 1000 ** 2, 1000, 0],
units: ['TB/s', 'GB/s', 'MB/s', 'KB/s', 'B/s'],
},
bandwidth: {
conditions: [1024 ** 2 / 8, 1024 / 8, 0],
units: ['Mbps', 'Kbps', 'bps'],
},
};
export const getSuitableUnit = (value, unitType) => {
const config = UnitTypes[unitType];
if (isEmpty(config)) return '';
// value can be an array or a single value
const values = isArray(value) ? value : [[0, Number(value)]];
let result = last(config.units);
config.conditions.some((condition, index) => {
const triggered = values.some(
(_value) =>
((isArray(_value) ? get(_value, '[1]') : Number(_value)) || 0) >=
condition
);
if (triggered) {
result = config.units[index];
}
return triggered;
});
return result;
};
export const getSuitableValue = (
value,
unitType = 'default',
defaultValue = 0
) => {
if ((!isNumber(value) && !isString(value)) || isNaN(Number(value))) {
return defaultValue;
}
const unit = getSuitableUnit(value, unitType);
const unitText = unit ? ` ${t(unit)}` : '';
const count = getValueByUnit(value, unit || unitType);
return `${count}${unitText}`;
};
export const getValueByUnit = (num, unit) => {
let value = parseFloat(num);
switch (unit) {
default:
break;
case '':
case 'default':
return value;
case 'iops':
return Math.round(value);
case '%':
value *= 100;
break;
case 'm':
value *= 1000;
if (value < 1) return 0;
break;
case 'KiB':
value /= 1024;
break;
case 'MiB':
value /= 1024 ** 2;
break;
case 'GiB':
value /= 1024 ** 3;
break;
case 'TiB':
value /= 1024 ** 4;
break;
case 'Bytes':
case 'B':
case 'B/s':
break;
case 'KB':
case 'KB/s':
value /= 1000;
break;
case 'MB':
case 'MB/s':
value /= 1000 ** 2;
break;
case 'GB':
case 'GB/s':
value /= 1000 ** 3;
break;
case 'TB':
case 'TB/s':
value /= 1000 ** 4;
break;
case 'bps':
value *= 8;
break;
case 'Kbps':
value = (value * 8) / 1024;
break;
case 'Mbps':
value = (value * 8) / 1024 / 1024;
break;
case 'ms':
value *= 1000;
break;
}
return Number(value) === 0 ? 0 : Number(value.toFixed(2));
};
export const getFormatTime = (ms) =>
getStrFromTimestamp(ms).replace(/:00$/g, '');
export const getChartData = ({
type,
unit,
xKey = 'time',
legend = [],
valuesData = [],
xFormatter,
}) => {
/*
build a value map => { 1566289260: {...} }
e.g. { 1566289260: { 'utilisation': 30.2 } }
*/
const valueMap = {};
valuesData.forEach((values, index) => {
values.forEach((item) => {
const time = parseInt(get(item, [0], 0), 10);
const value = get(item, [1]);
const key = get(legend, [index]);
if (time && !valueMap[time]) {
valueMap[time] = legend.reduce((obj, xAxisKey) => {
if (!obj[xAxisKey]) obj[xAxisKey] = null;
return obj;
}, {});
}
if (key && valueMap[time]) {
valueMap[time][key] =
value === '-1'
? null
: getValueByUnit(value, isUndefined(unit) ? type : unit);
}
});
});
const formatter = (key) => (xKey === 'time' ? getFormatTime(key) : key);
// generate the chart data
const chartData = Object.entries(valueMap).map(([key, value]) => ({
[xKey]: (xFormatter || formatter)(key),
...value,
}));
return chartData;
};
export const getAreaChartOps = ({
type,
title,
unitType,
xKey = 'time',
legend = [],
data = [],
xFormatter,
...rest
}) => {
const seriesData = isArray(data) ? data : [];
const valuesData = seriesData.map((result) => get(result, 'values') || []);
const unit = unitType
? getSuitableUnit(flatten(valuesData), unitType)
: rest.unit;
const chartData = getChartData({
type,
unit,
xKey,
legend,
valuesData,
xFormatter,
});
const xAxisTickFormatter =
xKey === 'time' ? getXAxisTickFormatter(chartData) : (value) => value;
return {
...rest,
title,
unit,
xAxisTickFormatter,
data: chartData,
};
};
export const getXAxisTickFormatter = (chartValus = []) => {
const timeList = chartValus.map(({ time }) => +new Date(time));
const minTime = min(timeList);
const maxTime = max(timeList);
if (maxTime - minTime > 8640000) {
return (time) => getLocalTimeStr(time, t('Do HH:mm'));
}
return (time) => getLocalTimeStr(time, 'HH:mm:ss');
};
export const getLastMonitoringData = (data) => {
const result = {};
Object.entries(data).forEach(([key, value]) => {
const values = get(value, 'data.result[0].values', []) || [];
const _value = isEmpty(values)
? get(value, 'data.result[0].value', []) || []
: last(values);
set(result, `[${key}].value`, _value);
});
return result;
};
export const getTimesData = (data) => {
const result = [];
data.forEach((record) => {
const values = get(record, 'values') || [];
values.forEach((value) => {
const time = get(value, '[0]', 0);
if (!result.includes(time)) {
result.push(time);
}
});
});
return result.sort();
};
export const getZeroValues = () => {
const values = [];
let time = parseInt(Date.now() / 1000, 10) - 6000;
for (let i = 0; i < 10; i++) {
values[i] = [time, 0];
time += 600;
}
return values;
};
export const getColorByName = (colorName = '#fff') =>
COLORS_MAP[colorName] || colorName;
export const startAutoRefresh = (context, options = {}) => {
const params = {
method: 'fetchData',
interval: 5000, // milliseconds
leading: true,
...options,
};
if (context && context[params.method]) {
const fetch = context[params.method];
if (params.leading) {
fetch({ autoRefresh: true });
}
context.timer = setInterval(() => {
fetch({ autoRefresh: true });
}, params.interval);
}
};
export const stopAutoRefresh = (context) => {
if (context && context.timer) {
clearInterval(context.timer);
context.timer = null;
}
};
export const isSameDay = (preTime, nextTime) =>
Math.floor(preTime / 86400000) === Math.floor(nextTime / 86400000);
export const timeAliasReg = /(\d+)(\w+)/;
export const timestampify = (timeAlias) => {
const [, count = 0, unit] = timeAlias.match(timeAliasReg) || [];
return Number(count) * (MILLISECOND_IN_TIME_UNIT[unit] || 0);
};
export const fillEmptyMetrics = (params, result) => {
if (!params.times || !params.start || !params.end) {
return result;
}
const format = (num) => String(num).replace(/\..*$/, '');
const step = Math.floor((params.end - params.start) / params.times);
const correctCount = params.times + 1;
Object.values(result).forEach((item) => {
const _result = get(item, 'data.result');
if (!isEmpty(_result)) {
_result.forEach((resultItem) => {
const curValues = resultItem.values || [];
const curValuesMap = curValues.reduce(
(prev, cur) => ({
...prev,
[format(cur[0])]: cur[1],
}),
{}
);
if (curValues.length < correctCount) {
const newValues = [];
for (let index = 0; index < correctCount; index++) {
const time = format(params.start + index * step);
newValues.push([time, curValuesMap[time] || '0']);
}
resultItem.values = newValues;
}
});
}
});
return result;
};
export const cephStatusMap = {
0: t('Healthy'),
1: t('Warning'),
2: t('Error'),
};
export const cephStatusColorMap = {
0: '#379738',
1: '#FAAD14',
2: '#D93126',
};

View File

@ -0,0 +1,113 @@
// 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 { action, computed, observable, set } from 'mobx';
import {
addParams,
defaultOneHourAgo,
getInterval,
} from 'components/PrometheusChart/utils/utils';
import { getTimestamp } from 'utils/time';
import Base from '../base';
export default class MonitorBase extends Base {
get responseKey() {
return '';
}
@observable
currentRange = defaultOneHourAgo;
@observable
interval = 10;
@observable
loading = true;
@action
handleRangePickerChange = async (dates, refresh = false) => {
if (
!refresh &&
!(
getTimestamp(this.currentRange[0]) === getTimestamp(dates[0]) &&
getTimestamp(this.currentRange[1]) === getTimestamp(dates[1])
)
) {
// do not extract, see @compute get intervals functions
this.currentRange = dates;
this.interval = this.intervals[0].value;
} else {
// do not extract, see @compute get intervals functions
this.currentRange = dates;
}
await this.getChartData();
};
@computed get intervals() {
return getInterval(this.currentRange);
}
// getRange = type => ({
// // last 2 weeks
// 3: [moment().subtract(2, 'weeks'), moment()],
// // last 7 days
// 2: [moment().subtract(1, 'weeks'), moment()],
// // last day
// 1: [moment().subtract(1, 'days'), moment()],
// // last hour
// 0: [moment().subtract(1, 'hours'), moment()],
// }[type] || [moment().subtract(1, 'hours'), moment()]);
@action
handleIntervalChange = async (interval) => {
this.interval = interval;
await this.getChartData();
};
@action
handleDeviceChange = (device, type) => {
const source = this[type];
set(source, {
isLoading: true,
});
const data = source.data.filter((item) => item.device === device);
setTimeout(() => {
set(source, {
currentDevice: device,
currentShowData: data,
isLoading: false,
});
}, 200);
};
formatToGB(str) {
return parseFloat((parseInt(str, 10) / 1073741824).toFixed(2));
}
buildRequest(query, getRangeType = 'range', params = {}) {
const newQueryStr =
Object.keys(params).length === 0 ? query : addParams(query, params);
if (getRangeType === 'current') {
return this.skylineClient.query.list({
query: newQueryStr,
});
}
return this.skylineClient.queryRange.list({
query: newQueryStr,
start: getTimestamp(this.currentRange[0]),
end: getTimestamp(this.currentRange[1]),
step: this.interval,
});
}
}

View File

@ -0,0 +1,314 @@
// 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 { action, observable, set } from 'mobx';
import { getPromises } from 'components/PrometheusChart/utils/utils';
import MonitorBase from './monitor-base';
const serviceNameMap = {
mysql_up: t('Database Service'),
rabbitmq_identity_info: t('Message Queue Service'),
memcached_up: t('Cache Service'),
};
const indexToServiceName = [
t('Database Service'),
t('Message Queue Service'),
t('Cache Service'),
];
export class OpenstackServiceStore extends MonitorBase {
// @observable
// nodes = [];
// @observable
// node = {};
@observable
nova_service = {
isLoading: false,
data: [],
};
@observable
network_service = {
isLoading: false,
data: [],
};
@observable
cinder_service = {
isLoading: false,
data: [],
};
@observable
other_service = {
isLoading: false,
data: [],
};
@action
getChartData = async () => {
// const { hostname } = this.node.metric;
const defaultPromises = [
this.getNovaService(),
this.getNetworkService(),
this.getCinderService(),
this.getOtherService(),
];
await Promise.all(defaultPromises);
};
@action
getNovaService = async () => {
set(this.nova_service, {
isLoading: true,
data: [],
});
const [currentState, last24State, libvirtdState, libvirtd24State] =
await Promise.all(getPromises.call(this, 'openstackService.novaService'));
const {
data: { result: currentStateResult },
} = currentState;
const tmp = [];
currentStateResult.forEach((service) => {
const {
metric: {
service: serviceName = '',
adminState = '',
hostname = '',
} = {},
} = service;
tmp.push({
hostname,
serviceName,
state: adminState === 'enabled' ? 'up' : 'down',
});
});
const {
data: { result: last24HResult },
} = last24State;
last24HResult.forEach((service) => {
const { metric: { service: serviceName = '', hostname = '' } = {} } =
service;
const idx = tmp.findIndex(
(item) => item.serviceName === serviceName && item.hostname === hostname
);
tmp[idx][`${serviceName}24`] = 'down';
});
const {
data: { result: data },
} = libvirtdState;
data.forEach((item) => {
const { metric, value } = item;
tmp.push({
// hard code
serviceName: 'nova_libvirt',
hostname: metric.hostname,
state: value[1] === 'enabled' ? 'up' : 'down',
});
});
const {
data: { result: libvirtd24Result },
} = libvirtd24State;
libvirtd24Result.forEach((service) => {
const { metric: { hostname = '' } = {} } = service;
const idx = tmp.findIndex(
(item) =>
item.serviceName === 'nova_libvirt' && item.hostname === hostname
);
tmp[idx].nova_libvirt24 = 'down';
});
set(this.nova_service, {
isLoading: false,
data: tmp,
});
};
@action
getNetworkService = async () => {
set(this.network_service, {
isLoading: true,
data: [],
});
const [currentState, last24State] = await Promise.all(
getPromises.call(this, 'openstackService.networkService')
);
const {
data: { result: currentStateResult },
} = currentState;
const tmp = [];
currentStateResult.forEach((service) => {
const {
metric: {
service: serviceName = '',
adminState = '',
hostname = '',
} = {},
} = service;
tmp.push({
serviceName,
hostname,
state: adminState,
});
});
const {
data: { result: last24HResult },
} = last24State;
last24HResult.forEach((service) => {
const { metric: { service: serviceName = '', hostname = '' } = {} } =
service;
const idx = tmp.findIndex(
(item) => item.serviceName === serviceName && item.hostname === hostname
);
tmp[idx][`${serviceName}24`] = 'down';
});
set(this.network_service, {
isLoading: false,
data: tmp,
});
};
@action
getCinderService = async () => {
set(this.cinder_service, {
isLoading: true,
data: [],
});
const [currentState, last24State] = await Promise.all(
getPromises.call(this, 'openstackService.cinderService')
);
const {
data: { result: currentStateResult },
} = currentState;
const tmp = [];
currentStateResult.forEach((service) => {
const {
metric: {
service: serviceName = '',
adminState = '',
hostname = '',
} = {},
} = service;
tmp.push({
serviceName,
hostname,
state: adminState === 'enabled' ? 'up' : 'down',
});
});
const {
data: { result: last24HResult },
} = last24State;
last24HResult.forEach((service) => {
const { metric: { service: serviceName = '', hostname = '' } = {} } =
service;
const idx = tmp.findIndex(
(item) => item.serviceName === serviceName && item.hostname === hostname
);
tmp[idx][`${serviceName}24`] = 'down';
});
set(this.cinder_service, {
isLoading: false,
data: tmp,
});
};
@action
getOtherService = async () => {
set(this.other_service, {
isLoading: true,
data: [],
});
const tmp = [];
let results = await Promise.all(
getPromises.call(this, 'openstackService.otherService')
);
results.forEach((result) => {
const {
data: { result: data },
} = result;
data.forEach((d) => {
const { metric, value } = d;
tmp.push({
serviceName: serviceNameMap[metric.__name__],
hostname: metric.instance,
state: value[1] === '1' ? 'up' : 'down',
});
});
});
results = await Promise.all(
getPromises.call(this, 'openstackService.otherServiceMinOverTime')
);
results.forEach((result, index) => {
const {
data: { result: last24HResult },
} = result;
last24HResult.forEach((service) => {
const { metric: { instance = '' } = {} } = service;
const idx = tmp.findIndex(
(item) =>
item.serviceName === indexToServiceName[index] &&
item.hostname === instance
);
tmp[idx][`${indexToServiceName[index]}24`] = 'down';
});
});
// const [heatResponse, heat24Response] = await Promise.all(
// getPromises.call(this, 'openstackService.heatMinOverTime')
// );
// const {
// data: { result: heatResults },
// } = heatResponse;
// heatResults.forEach((item) => {
// const {
// metric: {
// host = '',
// binary = '',
// engine_id = '',
// services_status = '',
// } = {},
// } = item;
// tmp.push({
// serviceName: binary,
// host,
// state: services_status,
// engine_id,
// });
// });
// const {
// data: { result: heat24Results },
// } = heat24Response;
// heat24Results.forEach((result) => {
// const { metric: { binary = '', engine_id = '', host = '' } = {} } =
// result;
// const idx = tmp.findIndex(
// (item) =>
// item.serviceName === binary &&
// item.host === host &&
// item.engine_id === engine_id
// );
// tmp[idx][`${binary}24`] = 'down';
// });
set(this.other_service, {
isLoading: false,
data: tmp,
});
};
}
const globalOpenstackServiceStore = new OpenstackServiceStore();
export default globalOpenstackServiceStore;

View File

@ -0,0 +1,61 @@
// 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 { action, observable, set } from 'mobx';
import MonitorBase from 'stores/prometheus/monitor-base';
export class StorageClusterStore extends MonitorBase {
@observable
storageClusterUsage = {
isLoading: false,
data: {
used: 0,
total: 0,
},
};
@action
getStorageClusterUsage = async (query = '') => {
set(this.storageClusterUsage, {
isLoading: true,
data: {
used: 0,
total: 0,
},
});
const query1 = 'ceph_cluster_total_used_bytes';
const query2 = 'ceph_cluster_total_bytes';
const [used, total] = await Promise.all([
await this.buildRequest(query1 + query, 'current'),
await this.buildRequest(query2 + query, 'current'),
]);
set(this.storageClusterUsage, {
isLoading: false,
data: {
used:
used.data.result.length === 0
? 0
: this.formatToGB(used.data.result[0].value[1]),
total:
total.data.result.length === 0
? 0
: this.formatToGB(total.data.result[0].value[1]),
},
});
};
}
const globalStorageClusterStore = new StorageClusterStore();
export default globalStorageClusterStore;