fix: add suggestion to chart in monitor if is no data because the time interval is less than the collection interval (#58)

* fix: add suggestion to chart in monitor if is no data because the time interval is less than the collection interval

* chore: update release notes

---------

Co-authored-by: yaojiping <yaojiping@infini.ltd>
This commit is contained in:
yaojp123 2024-12-26 12:52:38 +08:00 committed by GitHub
parent 53178fa869
commit b8b24f8fab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 214 additions and 151 deletions

View File

@ -21,6 +21,7 @@ Information about release notes of INFINI Console is provided here.
- Optimize UI of agent list when its columns are overflow.
- Add loading to each row in overview table.
- Adapter metrics query with cluster id and cluster uuid
- Add suggestion to chart in monitor if is no data because the time interval is less than the collection interval.
## 1.27.0 (2024-12-09)

View File

@ -8,7 +8,7 @@ import request from "@/utils/request";
import MetricChart from "@/pages/Platform/Overview/components/MetricChart";
export default (props) => {
const { action, timeRange, timezone, timeout, overview, renderExtraMetric, metrics = [], queryParams } = props
const { action, timeRange, timezone, timeout, overview, renderExtraMetric, metrics = [], queryParams, handleTimeIntervalChange } = props
return (
<div className={styles.metricChart}>
@ -60,6 +60,7 @@ export default (props) => {
};
return <MetricLine {...config} key={metric.key} />
}}
handleTimeIntervalChange={handleTimeIntervalChange}
/>
)
})}

View File

@ -1,4 +1,4 @@
import React, { useState, useMemo } from "react";
import React, { useState, useMemo, useEffect } from "react";
import { Tabs, Button } from "antd";
import { ESPrefix } from "@/services/common";
import useFetch from "@/lib/hooks/use_fetch";
@ -15,7 +15,7 @@ import { formatMessage } from "umi/locale";
import DatePicker from "@/common/src/DatePicker";
import { getLocale } from "umi/locale";
import { getTimezone } from "@/utils/utils";
import { TIMEOUT_CACHE_KEY } from "../../Monitor";
import { getAllTimeSettingsCache, TIME_SETTINGS_KEY } from "../../Monitor";
const { TabPane } = Tabs;
@ -30,6 +30,8 @@ export default (props) => {
metrics = [],
} = props;
const allTimeSettingsCache = getAllTimeSettingsCache() || {}
const [spinning, setSpinning] = useState(false);
const [state, setState] = useState({
timeRange: {
@ -37,12 +39,12 @@ export default (props) => {
max: "now",
timeFormatter: formatter.dates(1),
},
timeInterval: '',
timeout: localStorage.getItem(TIMEOUT_CACHE_KEY) || '120s',
timeInterval: allTimeSettingsCache.timeInterval,
timeout: allTimeSettingsCache.timeout || '120s',
});
const [refresh, setRefresh] = useState({ isRefreshPaused: false });
const [timeZone, setTimeZone] = useState(() => getTimezone());
const [refresh, setRefresh] = useState({ isRefreshPaused: allTimeSettingsCache.isRefreshPaused || false, refreshInterval: allTimeSettingsCache.refreshInterval || 30000 });
const [timeZone, setTimeZone] = useState(() => allTimeSettingsCache.timeZone || getTimezone());
const handleTimeChange = ({ start, end, timeInterval, timeout }) => {
const bounds = calculateBounds({
@ -65,6 +67,15 @@ export default (props) => {
setSpinning(true);
};
const onTimeSettingsChange = (timeSettings) => {
let allTimeSettings = getAllTimeSettingsCache();
allTimeSettings = {
...(allTimeSettings || {}),
...(timeSettings || {})
}
localStorage.setItem(TIME_SETTINGS_KEY, JSON.stringify(allTimeSettings))
}
const [linkMoreNew] = useMemo(() => {
let urlObj = parseUrl(linkMore);
let query = urlObj.query;
@ -94,14 +105,21 @@ export default (props) => {
end={state.timeRange.max}
onRangeChange={handleTimeChange}
{...refresh}
onRefreshChange={setRefresh}
onRefreshChange={(newRefresh) => {
onTimeSettingsChange(newRefresh)
setRefresh(newRefresh)
}}
onRefresh={handleTimeChange}
showTimeSetting={true}
showTimeInterval={true}
showTimeout={true}
timeout={state.timeout}
timeInterval={state.timeInterval}
onTimeSettingChange={(timeSetting) => {
localStorage.setItem(TIMEOUT_CACHE_KEY, timeSetting.timeout)
onTimeSettingsChange({
timeInterval: timeSetting.timeInterval,
timeout: timeSetting.timeout
})
setState({
...state,
timeInterval: timeSetting.timeInterval,
@ -109,7 +127,12 @@ export default (props) => {
});
}}
timeZone={timeZone}
onTimeZoneChange={setTimeZone}
onTimeZoneChange={(timeZone) => {
onTimeSettingsChange({
timeZone,
})
setTimeZone(timeZone)
}}
recentlyUsedRangesKey={'overview-detail'}
/>
</div>
@ -124,6 +147,15 @@ export default (props) => {
renderExtraMetric={renderExtraMetric}
metrics={metrics}
{...state}
handleTimeIntervalChange={(timeInterval) => {
onTimeSettingsChange({
timeInterval,
})
setState({
...state,
timeInterval,
});
}}
bucketSize={state.timeInterval}
/>
</div>

View File

@ -32,7 +32,7 @@
flex-wrap: wrap;
justify-content: space-between;
.lineWrapper {
height: 150px;
height: 180px;
width: 48.8%;
border: 1px solid #e8e8e8;
border-radius: 2px;
@ -45,7 +45,7 @@
white-space: nowrap;
}
.chartBody {
height: 120px;
height: 150px;
padding: 0 2px 2px 2px;
}
}

View File

@ -41,7 +41,17 @@ const formatTimeout = (timeout) => {
return timeout
}
export const TIMEOUT_CACHE_KEY = "monitor-timeout"
export const TIME_SETTINGS_KEY = "monitor-time-settings"
export const getAllTimeSettingsCache = () => {
const allTimeSettings = localStorage.getItem(TIME_SETTINGS_KEY) || `{}`
try {
const object = JSON.parse(allTimeSettings);
return object || {}
} catch (error) {
return {}
}
}
const Monitor = (props) => {
const {
@ -54,6 +64,8 @@ const Monitor = (props) => {
checkPaneParams,
} = props;
const allTimeSettingsCache = getAllTimeSettingsCache()
const [param, setParam] = useQueryParam("_g", JsonParam);
const [spinning, setSpinning] = useState(false);
@ -65,15 +77,15 @@ const Monitor = (props) => {
max: param?.timeRange?.max || "now",
timeFormatter: formatter.dates(1),
},
timeInterval: formatTimeInterval(param?.timeInterval),
timeout: formatTimeout(param?.timeout) || localStorage.getItem(TIMEOUT_CACHE_KEY) || '120s',
timeInterval: formatTimeInterval(param?.timeInterval) || allTimeSettingsCache.timeInterval,
timeout: formatTimeout(param?.timeout) || allTimeSettingsCache.timeout || '120s',
param: param,
refresh: true
refresh: true,
})
);
const [refresh, setRefresh] = useState({ isRefreshPaused: false, refreshInterval: 30000 });
const [timeZone, setTimeZone] = useState(() => getTimezone());
const [refresh, setRefresh] = useState({ isRefreshPaused: allTimeSettingsCache.isRefreshPaused || false, refreshInterval: allTimeSettingsCache.refreshInterval || 30000 });
const [timeZone, setTimeZone] = useState(() => allTimeSettingsCache.timeZone || getTimezone());
useEffect(() => {
setParam({ ...param, timeRange: state.timeRange, timeInterval: state.timeInterval, timeout: state.timeout });
@ -108,6 +120,15 @@ const Monitor = (props) => {
});
};
const onTimeSettingsChange = (timeSettings) => {
let allTimeSettings = getAllTimeSettingsCache();
allTimeSettings = {
...(allTimeSettings || {}),
...(timeSettings || {})
}
localStorage.setItem(TIME_SETTINGS_KEY, JSON.stringify(allTimeSettings))
}
const breadcrumbList = getBreadcrumbList(state);
const isAgent = useMemo(() => {
@ -133,7 +154,10 @@ const Monitor = (props) => {
handleTimeChange({ start, end })
}}
{...refresh}
onRefreshChange={setRefresh}
onRefreshChange={(newRefresh) => {
onTimeSettingsChange(newRefresh)
setRefresh(newRefresh)
}}
onRefresh={handleTimeChange}
showTimeSetting={true}
showTimeInterval={true}
@ -141,7 +165,10 @@ const Monitor = (props) => {
showTimeout={true}
timeout={state.timeout}
onTimeSettingChange={(timeSetting) => {
localStorage.setItem(TIMEOUT_CACHE_KEY, timeSetting.timeout)
onTimeSettingsChange({
timeInterval: timeSetting.timeInterval,
timeout: timeSetting.timeout
})
setState({
...state,
timeInterval: timeSetting.timeInterval,
@ -149,7 +176,12 @@ const Monitor = (props) => {
});
}}
timeZone={timeZone}
onTimeZoneChange={setTimeZone}
onTimeZoneChange={(timeZone) => {
onTimeSettingsChange({
timeZone,
})
setTimeZone(timeZone)
}}
recentlyUsedRangesKey={'monitor'}
/>
<CollectStatus fetchUrl={`${ESPrefix}/${selectedCluster?.id}/_collection_stats`}/>
@ -188,6 +220,15 @@ const Monitor = (props) => {
isAgent={isAgent}
{...state}
handleTimeChange={handleTimeChange}
handleTimeIntervalChange={(timeInterval) => {
onTimeSettingsChange({
timeInterval,
})
setState({
...state,
timeInterval,
});
}}
setSpinning={setSpinning}
{...extraParams}
bucketSize={state.timeInterval}

View File

@ -274,7 +274,7 @@ export default forwardRef((props: IProps, ref: any) => {
<listItemConfig.component
data={item}
id={infoField}
isActive={selectedItem?._id == item?._id}
isActive={listItemConfig.getId(selectedItem?._id) == infoField}
onSelect={() => {
setSelectedItem(item);
drawRef.current?.open();

View File

@ -89,6 +89,7 @@ export default {
"cluster.monitor.node.title": "Node",
"cluster.monitor.index.title": "Index",
"cluster.monitor.queue.title": "Thread Pool",
"cluster.monitor.shard.title": "Shard",
"cluster.monitor.summary.name": "Cluster Name",
"cluster.monitor.summary.online_time": "Uptime",
"cluster.monitor.summary.version": "Version",
@ -352,6 +353,12 @@ export default {
"cluster.metrics.request.copy": "Copy request",
"cluster.metrics.request.copy.success": "Copy request successfully",
"cluster.metrics.time_interval.reload": "Apply global time interval({time_interval})",
"cluster.metrics.time_interval.set.global": "Global",
"cluster.metrics.time_interval.set.current": "Current",
"cluster.metrics.time_interval.empty": "No data, the time interval is less than the collection interval, suggest to set the time interval to {min_bucket_size} seconds.",
"cluster.metrics.time_interval.apply": "Apply suggestion",
"cluster.collect.last_active_at": "Last Active At",
};

View File

@ -80,6 +80,7 @@ export default {
"cluster.monitor.node.title": "节点",
"cluster.monitor.index.title": "索引",
"cluster.monitor.queue.title": "线程池",
"cluster.monitor.shard.title": "分片",
"cluster.monitor.summary.name": "集群名称",
"cluster.monitor.summary.online_time": "在线时长",
"cluster.monitor.summary.version": "集群版本",
@ -337,6 +338,12 @@ export default {
"cluster.metrics.request.copy": "复制请求",
"cluster.metrics.request.copy.success": "复制请求成功",
"cluster.metrics.time_interval.reload": "设置为全局时间间隔({time_interval})",
"cluster.metrics.time_interval.set.global": "全局",
"cluster.metrics.time_interval.set.current": "当前",
"cluster.metrics.time_interval.empty": "暂无数据,当前时间间隔小于采集间隔,建议设置时间间隔为{min_bucket_size}秒",
"cluster.metrics.time_interval.apply": "应用建议",
"cluster.collect.last_active_at": "最后活动时间",
};

View File

@ -26,26 +26,9 @@ export const isVersionGTE6 = (cluster) => {
return false
}
export default ({
selectedCluster,
clusterID,
timeRange,
handleTimeChange,
timezone,
bucketSize,
timeout,
refresh,
}) => {
export default (props) => {
const tabProps = {
clusterID,
timeRange,
handleTimeChange,
timezone,
bucketSize,
timeout,
refresh
}
const { selectedCluster, clusterID } = props
const isVersionGTE8_6 = useMemo(() => {
return shouldHaveModelInferenceBreaker(selectedCluster)
@ -83,7 +66,7 @@ export default ({
})}
>
<ClusterMetric
{...tabProps}
{...props}
fetchUrl={`${ESPrefix}/${clusterID}/cluster_metrics`}
metrics={[
'cluster_health',
@ -107,7 +90,7 @@ export default ({
})}
>
<NodeMetric
{...tabProps}
{...props}
param={param}
setParam={setParam}
metrics={[
@ -249,7 +232,7 @@ export default ({
})}
>
<IndexMetric
{...tabProps}
{...props}
param={param}
setParam={setParam}
metrics={[
@ -327,7 +310,7 @@ export default ({
})}
>
<QueueMetric
{...tabProps}
{...props}
param={param}
setParam={setParam}
metrics={[

View File

@ -1,25 +1,15 @@
import ClusterMetric from "../../components/cluster_metric";
import { ESPrefix } from "@/services/common";
export default ({
clusterID,
timeRange,
handleTimeChange,
bucketSize,
timeout,
timezone,
refresh
}) => {
export default (props) => {
const { clusterID } = props
return (
<ClusterMetric
timezone={timezone}
timeRange={timeRange}
timeout={timeout}
refresh={refresh}
handleTimeChange={handleTimeChange}
{...props}
overview={1}
fetchUrl={`${ESPrefix}/${clusterID}/cluster_metrics`}
bucketSize={bucketSize}
metrics={['index_throughput', 'search_throughput', 'index_latency', 'search_latency']}
/>
);

View File

@ -2,33 +2,19 @@ import { useState } from "react";
import StatisticBar from "./statistic_bar";
import IndexMetric from "../../components/index_metric";
export default ({
clusterID,
indexName,
timeRange,
handleTimeChange,
shardID,
bucketSize,
timezone,
timeout,
refresh,
}) => {
export default (props) => {
const { indexName } = props
const [param, setParam] = useState({
show_top: false,
index_name: indexName,
});
return (
<IndexMetric
clusterID={clusterID}
timezone={timezone}
timeRange={timeRange}
handleTimeChange={handleTimeChange}
{...props}
param={param}
setParam={setParam}
shardID={shardID}
bucketSize={bucketSize}
timeout={timeout}
refresh={refresh}
metrics={[
[
"operations",

View File

@ -2,32 +2,24 @@ import { ESPrefix } from "@/services/common";
import StatisticBar from "./statistic_bar";
import ClusterMetric from "../../components/cluster_metric";
export default ({
export default (props) => {
const {
isAgent,
clusterID,
indexName,
timeRange,
handleTimeChange,
shardID,
bucketSize,
timezone,
timeout,
refresh
}) => {
} = props
let url = `${ESPrefix}/${clusterID}/index/${indexName}/metrics`;
if(shardID){
url += `?shard_id=${shardID}`
}
return (
<ClusterMetric
timezone={timezone}
timeRange={timeRange}
handleTimeChange={handleTimeChange}
{...props}
overview={1}
fetchUrl={url}
bucketSize={bucketSize}
timeout={timeout}
refresh={refresh}
metrics={[
isAgent && shardID ? 'shard_state' : "index_health",
"index_throughput",

View File

@ -6,27 +6,13 @@ import { formatMessage } from "umi/locale";
import { SearchEngines } from "@/lib/search_engines";
import { isVersionGTE6, shouldHaveModelInferenceBreaker } from "../../Cluster/Monitor/advanced";
export default ({
export default (props) => {
const {
selectedCluster,
clusterID,
nodeID,
timeRange,
handleTimeChange,
timezone,
bucketSize,
timeout,
refresh,
}) => {
const tabProps = {
clusterID,
timeRange,
handleTimeChange,
timezone,
bucketSize,
timeout,
refresh
}
} = props
const isVersionGTE8_6 = useMemo(() => {
return shouldHaveModelInferenceBreaker(selectedCluster)
@ -69,7 +55,7 @@ export default ({
})}
>
<NodeMetric
{...tabProps}
{...props}
param={param}
setParam={setParam}
metrics={[
@ -211,7 +197,7 @@ export default ({
})}
>
<QueueMetric
{...tabProps}
{...props}
param={param}
setParam={setParam}
metrics={[

View File

@ -3,28 +3,19 @@ import StatisticBar from "./statistic_bar";
import ClusterMetric from "../../components/cluster_metric";
import { useMemo } from "react";
export default ({
export default (props) => {
const {
isAgent,
clusterID,
nodeID,
timeRange,
handleTimeChange,
bucketSize,
timezone,
timeout,
refresh
}) => {
} = props
return (
<ClusterMetric
timezone={timezone}
timeRange={timeRange}
timeout={timeout}
refresh={refresh}
handleTimeChange={handleTimeChange}
{...props}
overview={1}
fetchUrl={`${ESPrefix}/${clusterID}/node/${nodeID}/metrics`}
bucketSize={bucketSize}
metrics={[
"node_health",
"cpu",

View File

@ -3,7 +3,7 @@ import { cloneDeep } from "lodash";
import { useEffect, useRef, useState } from "react";
import { formatMessage } from "umi/locale";
import styles from "./Metrics.scss";
import { Alert, Empty, Icon, message, Spin, Tooltip } from "antd";
import { Alert, Dropdown, Empty, Icon, Menu, message, Spin, Tooltip } from "antd";
import {
Axis,
Chart,
@ -39,7 +39,8 @@ export default (props) => {
height = 200,
customRenderChart,
instance,
pointerUpdate
pointerUpdate,
handleTimeIntervalChange
} = props;
const [loading, setLoading] = useState(false)
@ -56,19 +57,25 @@ export default (props) => {
const firstFetchRef = useRef(true)
const fetchData = async (queryParams, fetchUrl, metricKey, showLoading) => {
const [timeInterval, setTimeInterval] = useState()
const fetchData = async (queryParams, fetchUrl, metricKey, timeInterval, showLoading) => {
if (!observerRef.current.isInView || !fetchUrl) return;
setError()
if (firstFetchRef.current || showLoading) {
setLoading(true)
}
const res = await request(fetchUrl, {
method: 'GET',
queryParams: {
const newQueryParams = {
...queryParams,
key: metricKey,
timeout
},
}
if (timeInterval) {
newQueryParams.bucket_size = timeInterval
}
const res = await request(fetchUrl, {
method: 'GET',
queryParams: newQueryParams,
ignoreTimeout: true
}, false, false)
if (res?.error) {
@ -86,9 +93,9 @@ export default (props) => {
}
useEffect(() => {
observerRef.current.deps = cloneDeep([queryParams, fetchUrl, metricKey, refresh])
fetchData(queryParams, fetchUrl, metricKey, refresh)
}, [JSON.stringify(queryParams), fetchUrl, metricKey, refresh])
observerRef.current.deps = cloneDeep([queryParams, fetchUrl, metricKey, timeInterval, refresh])
fetchData(queryParams, fetchUrl, metricKey, timeInterval, refresh)
}, [JSON.stringify(queryParams), fetchUrl, metricKey, timeInterval, refresh])
useEffect(() => {
const observer = new IntersectionObserver(
@ -150,9 +157,33 @@ export default (props) => {
const axis = metric?.axis || [];
const lines = metric?.lines || [];
if (lines.every((item) => !item.data || item.data.length === 0)) {
const emptyProps = {}
if (metric?.min_bucket_size > 0 && metric?.hits_total > 0) {
emptyProps.description = (
<>
<div style={{ wordBreak: 'break-all', textAlign: 'left', marginBotton: 2 }} >
{formatMessage({ id: "cluster.metrics.time_interval.empty" }, { min_bucket_size: metric.min_bucket_size})}
</div>
<Dropdown overlay={(
<Menu>
<Menu.Item onClick={() => handleTimeIntervalChange(`${metric?.min_bucket_size}s`)}>
{formatMessage({ id: `cluster.metrics.time_interval.set.global`})}
</Menu.Item>
<Menu.Item onClick={() => setTimeInterval(`${metric?.min_bucket_size}s`)}>
{formatMessage({ id: `cluster.metrics.time_interval.set.current`})}
</Menu.Item>
</Menu>
)}>
<a onClick={e => e.preventDefault()}>
{formatMessage({ id: `cluster.metrics.time_interval.apply`})} <Icon type="down" />
</a>
</Dropdown>
</>
)
}
return (
<div style={{ height, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Empty style={{ margin: 0}} image={Empty.PRESENTED_IMAGE_SIMPLE} />
<Empty style={{ margin: 0}} image={Empty.PRESENTED_IMAGE_SIMPLE} {...emptyProps} />
</div>
)
}
@ -297,12 +328,20 @@ export default (props) => {
</span>
{
<span>
{
timeInterval && (
<Tooltip title={formatMessage({id: "cluster.metrics.time_interval.reload"}, { time_interval: queryParams.bucket_size })}>
<Icon className={styles.copy} style={{ marginRight: 12 }} type="history" onClick={() => setTimeInterval()}/>
</Tooltip>
)
}
{
metric?.request && (
<CopyToClipboard text={`GET .infini_metrics/_search\n${metric.request}`}>
<Tooltip title={formatMessage({id: "cluster.metrics.request.copy"})}>
<Icon
className={styles.copy}
style={{ marginRight: 12 }}
type="copy"
onClick={() => message.success(formatMessage({id: "cluster.metrics.request.copy.success"}))}
/>
@ -311,7 +350,7 @@ export default (props) => {
)
}
<Tooltip title={formatMessage({id: "form.button.refresh"})}>
<Icon className={styles.copy} style={{ marginLeft: 12 }} type="reload" onClick={() => fetchData(...observerRef.current.deps, true)}/>
<Icon className={styles.copy} type="sync" onClick={() => fetchData(...observerRef.current.deps, true)}/>
</Tooltip>
</span>
}

View File

@ -8,7 +8,7 @@ import { formatTimeRange } from "@/lib/elasticsearch/util";
export default (props) => {
const { fetchUrl, overview, metrics = [], renderExtra, timeRange, timeout, timezone, refresh, bucketSize, handleTimeChange } = props
const { fetchUrl, overview, metrics = [], renderExtra, timeRange, timeout, timezone, refresh, bucketSize, handleTimeChange, handleTimeIntervalChange } = props
if (!fetchUrl || metrics.length === 0) {
return null;
@ -81,6 +81,7 @@ export default (props) => {
}
return metric
}}
handleTimeIntervalChange={handleTimeIntervalChange}
/>
))}
{

View File

@ -25,6 +25,7 @@ export default (props) => {
metrics = [],
timeout,
refresh,
handleTimeIntervalChange
} = props
if (!clusterID || metrics.length == 0) {
@ -158,6 +159,7 @@ export default (props) => {
className={"metric-item"}
timeout={timeout}
refresh={refresh}
handleTimeIntervalChange={handleTimeIntervalChange}
/>
))
}

View File

@ -22,7 +22,8 @@ export default (props) => {
bucketSize,
timeout,
refresh,
metrics = []
metrics = [],
handleTimeIntervalChange
} = props
if (!clusterID || metrics.length == 0) {
@ -181,6 +182,7 @@ export default (props) => {
className={"metric-item"}
timeout={timeout}
refresh={refresh}
handleTimeIntervalChange={handleTimeIntervalChange}
/>
))
}

View File

@ -22,7 +22,8 @@ export default (props) => {
bucketSize,
metrics = [],
timeout,
refresh
refresh,
handleTimeIntervalChange
} = props
if (!clusterID || metrics.length == 0) {
@ -181,6 +182,7 @@ export default (props) => {
className={"metric-item"}
timeout={timeout}
refresh={refresh}
handleTimeIntervalChange={handleTimeIntervalChange}
/>
))
}