fix: optimize monitor data fetching for large time ranges (#5)

* fix: adjust cluster monitor (fetch data independently, lazy load, copy request)

Co-authored-by: yaojiping <yaojiping@infini.ltd>
This commit is contained in:
yaojp123 2024-12-06 12:35:13 +08:00 committed by GitHub
parent 4823339a01
commit d6a8e2ff6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1227 additions and 1075 deletions

View File

@ -39,8 +39,11 @@ const formatTimeout = (timeout) => {
return timeout return timeout
} }
const TIMEOUT_CACHE_KEY = "monitor-timeout"
const Monitor = (props) => { const Monitor = (props) => {
const { const {
selectedCluster,
formatState, formatState,
getBreadcrumbList, getBreadcrumbList,
StatisticBar, StatisticBar,
@ -61,7 +64,7 @@ const Monitor = (props) => {
timeFormatter: formatter.dates(1), timeFormatter: formatter.dates(1),
}, },
timeInterval: formatTimeInterval(param?.timeInterval), timeInterval: formatTimeInterval(param?.timeInterval),
timeout: formatTimeout(param?.timeout), timeout: formatTimeout(param?.timeout) || localStorage.getItem(TIMEOUT_CACHE_KEY) || '120s',
param: param, param: param,
}) })
); );
@ -104,6 +107,11 @@ const Monitor = (props) => {
const breadcrumbList = getBreadcrumbList(state); const breadcrumbList = getBreadcrumbList(state);
const isAgent = useMemo(() => {
const { monitor_configs = {} } = selectedCluster || {}
return monitor_configs?.node_stats?.enabled === false && monitor_configs?.index_stats?.enabled === false
}, [JSON.stringify(selectedCluster?.monitor_configs)])
return ( return (
<div> <div>
<BreadcrumbList data={breadcrumbList} /> <BreadcrumbList data={breadcrumbList} />
@ -111,7 +119,7 @@ const Monitor = (props) => {
<Card bodyStyle={{ padding: 15 }}> <Card bodyStyle={{ padding: 15 }}>
<div style={{ marginBottom: 5 }}> <div style={{ marginBottom: 5 }}>
<div style={{ display: 'flex', gap: 8 }}> <div style={{ display: 'flex', gap: 8 }}>
<div style={{ flexGrow: 0, minWidth: 400 }}> <div style={{ flexGrow: 0 }}>
<DatePicker <DatePicker
locale={getLocale()} locale={getLocale()}
start={state.timeRange.min} start={state.timeRange.min}
@ -128,6 +136,7 @@ const Monitor = (props) => {
showTimeout={true} showTimeout={true}
timeout={state.timeout} timeout={state.timeout}
onTimeSettingChange={(timeSetting) => { onTimeSettingChange={(timeSetting) => {
localStorage.setItem(TIMEOUT_CACHE_KEY, timeSetting.timeout)
setState({ setState({
...state, ...state,
timeInterval: timeSetting.timeInterval, timeInterval: timeSetting.timeInterval,
@ -139,16 +148,6 @@ const Monitor = (props) => {
recentlyUsedRangesKey={'monitor'} recentlyUsedRangesKey={'monitor'}
/> />
</div> </div>
<Button
loading={spinning}
icon={"reload"}
type="primary"
onClick={() => {
handleTimeChange({ start: state.timeRange.min, end: state.timeRange.max, timeInterval: state.timeInterval, timeout: state.timeout})
}}
>
{formatMessage({ id: "form.button.refresh"})}
</Button>
</div> </div>
</div> </div>
@ -178,6 +177,8 @@ const Monitor = (props) => {
pane.component pane.component
) : ( ) : (
<pane.component <pane.component
selectedCluster={selectedCluster}
isAgent={isAgent}
{...state} {...state}
handleTimeChange={handleTimeChange} handleTimeChange={handleTimeChange}
setSpinning={setSpinning} setSpinning={setSpinning}

View File

@ -347,4 +347,7 @@ export default {
"cluster.providers.aliyun": "Aliyun", "cluster.providers.aliyun": "Aliyun",
"cluster.providers.tencent-cloud": "Tencent-cloud", "cluster.providers.tencent-cloud": "Tencent-cloud",
"cluster.providers.ecloud": "Ecloud", "cluster.providers.ecloud": "Ecloud",
"cluster.metrics.request.copy": "Copy request",
"cluster.metrics.request.copy.success": "Copy request successfully"
}; };

View File

@ -332,4 +332,7 @@ export default {
"cluster.providers.aliyun": "阿里云", "cluster.providers.aliyun": "阿里云",
"cluster.providers.tencent-cloud": "腾讯云", "cluster.providers.tencent-cloud": "腾讯云",
"cluster.providers.ecloud": "移动云", "cluster.providers.ecloud": "移动云",
"cluster.metrics.request.copy": "复制请求",
"cluster.metrics.request.copy.success": "复制请求成功"
}; };

View File

@ -99,11 +99,7 @@ export default {
.filter((item) => item.enabled) .filter((item) => item.enabled)
.map((item) => { .map((item) => {
return { return {
name: item.name, ...item,
id: item.id,
endpoint: item.endpoint,
host: item.host,
version: item.version,
distribution: item.distribution || "elasticsearch", distribution: item.distribution || "elasticsearch",
cluster_uuid: item.cluster_uuid || "", cluster_uuid: item.cluster_uuid || "",
}; };

View File

@ -1,6 +1,6 @@
import { Tabs } from "antd"; import { Tabs } from "antd";
import { formatMessage } from "umi/locale"; import { formatMessage } from "umi/locale";
import { useState } from "react"; import { useMemo, useState } from "react";
import NodeMetric from "../../components/node_metric"; import NodeMetric from "../../components/node_metric";
import IndexMetric from "../../components/index_metric"; import IndexMetric from "../../components/index_metric";
import ClusterMetric from "../../components/cluster_metric"; import ClusterMetric from "../../components/cluster_metric";
@ -10,11 +10,22 @@ import { ESPrefix } from "@/services/common";
const timezone = "local"; const timezone = "local";
export default ({ export default ({
selectedCluster,
clusterID, clusterID,
timeRange, timeRange,
handleTimeChange, handleTimeChange,
bucketSize, bucketSize,
timeout,
}) => { }) => {
const isVersionGTE6 = useMemo(() => {
const main = selectedCluster?.version?.split('.')[0]
if (main && parseInt(main) >= 6) {
return true
}
return false
}, [selectedCluster?.version])
const [param, setParam] = useState({ const [param, setParam] = useState({
tab: "cluster", tab: "cluster",
}); });
@ -48,6 +59,20 @@ export default ({
handleTimeChange={handleTimeChange} handleTimeChange={handleTimeChange}
fetchUrl={`${ESPrefix}/${clusterID}/cluster_metrics`} fetchUrl={`${ESPrefix}/${clusterID}/cluster_metrics`}
bucketSize={bucketSize} bucketSize={bucketSize}
timeout={timeout}
metrics={[
'cluster_health',
'index_throughput',
'search_throughput',
'index_latency',
'search_latency',
'cluster_documents',
'node_count',
'cluster_indices',
'circuit_breaker',
'shard_count',
'cluster_storage'
]}
/> />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane <Tabs.TabPane
@ -64,6 +89,137 @@ export default ({
param={param} param={param}
setParam={setParam} setParam={setParam}
bucketSize={bucketSize} bucketSize={bucketSize}
timeout={timeout}
metrics={[
[
"operations",
[
"indexing_rate",
"indexing_bytes",
"query_rate",
"fetch_rate",
"scroll_rate",
"refresh_rate",
"flush_rate",
"merges_rate",
"scroll_open_contexts"
]
],
[
"latency",
[
"indexing_latency",
"query_latency",
"fetch_latency",
"scroll_latency",
"refresh_latency",
"flush_latency",
"merge_latency"
]
],
[
"system",
[
"cpu",
"disk",
"open_file",
"open_file_percent",
"os_cpu",
"os_load_average_1m",
"os_used_mem",
"os_used_swap"
]
],
[
"circuit_breaker",
[
"parent_breaker",
"accounting_breaker",
"fielddata_breaker",
"request_breaker",
"in_flight_requests_breaker",
"model_inference_breaker"
]
],
[
"io",
[
"total_io_operations",
"total_read_io_operations",
"total_write_io_operations"
]
],
[
"transport",
[
"transport_rx_bytes",
"transport_rx_rate",
"transport_tx_bytes",
"transport_tx_rate",
"transport_outbound_connections"
]
],
[
"storage",
[
"segment_count",
"index_storage"
]
],
[
"document",
[
"docs_count",
"docs_deleted"
]
],
[
"http",
[
"http_connect_num",
"http_rate"
]
],
[
"JVM",
[
"jvm_heap_used_percent",
"jvm_used_heap",
"jvm_mem_young_peak_used",
"jvm_mem_young_used",
"jvm_young_gc_latency",
"jvm_young_gc_rate",
"jvm_mem_old_peak_used",
"jvm_mem_old_used",
"jvm_old_gc_latency",
"jvm_old_gc_rate"
]
],
[
"memory",
[
"segment_doc_values_memory",
"segment_index_writer_memory",
"segment_memory",
"segment_stored_fields_memory",
"segment_term_vectors_memory",
"segment_terms_memory"
]
],
[
"cache",
[
"query_cache",
"request_cache",
"fielddata_cache",
"query_cache_count",
"query_cache_hit",
"request_cache_hit",
"query_cache_miss",
"request_cache_miss"
]
]
]}
/> />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane <Tabs.TabPane
@ -80,6 +236,73 @@ export default ({
param={param} param={param}
setParam={setParam} setParam={setParam}
bucketSize={bucketSize} bucketSize={bucketSize}
timeout={timeout}
metrics={[
[
"operations",
[
"indexing_rate",
"indexing_bytes",
"query_times",
"fetch_times",
"scroll_times",
"refresh_times",
"flush_times",
"merge_times"
]
],
[
"latency",
[
"indexing_latency",
"query_latency",
"fetch_latency",
"scroll_latency",
"refresh_latency",
"flush_latency",
"merge_latency"
]
],
[
"storage",
[
"index_storage",
"segment_count"
]
],
[
"document",
[
"doc_count",
"docs_deleted",
"doc_percent"
]
],
[
"memory",
[
"segment_doc_values_memory",
"segment_fields_memory",
"segment_memory",
"segment_terms_memory",
"segment_index_writer_memory",
"segment_term_vectors_memory"
]
],
[
"cache",
[
"query_cache",
"request_cache",
"fielddata_cache",
"query_cache_count",
"query_cache_hit",
"request_cache_hit",
"query_cache_miss",
"request_cache_miss"
]
]
]}
/> />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane <Tabs.TabPane
@ -96,6 +319,80 @@ export default ({
param={param} param={param}
setParam={setParam} setParam={setParam}
bucketSize={bucketSize} bucketSize={bucketSize}
timeout={timeout}
metrics={[
isVersionGTE6 ? [
"thread_pool_write",
[
"write_active",
"write_queue",
"write_rejected",
"write_threads"
]
] : [
"thread_pool_index",
[
"index_active",
"index_queue",
"index_rejected",
"index_threads",
]
],
[
"thread_pool_search",
[
"search_active",
"search_queue",
"search_rejected",
"search_threads"
]
],
!isVersionGTE6 ? [
"thread_pool_bulk",
[
"bulk_active",
"bulk_queue",
"bulk_rejected",
"bulk_threads",
]
] : undefined,
[
"thread_pool_get",
[
"get_active",
"get_queue",
"get_rejected",
"get_threads"
]
],
[
"thread_pool_flush",
[
"flush_active",
"flush_queue",
"flush_rejected",
"flush_threads"
]
],
[
"thread_pool_refresh",
[
"refresh_active",
"refresh_queue",
"refresh_rejected",
"refresh_threads"
]
],
[
"thread_pool_force_merge",
[
"force_merge_active",
"force_merge_queue",
"force_merge_rejected",
"force_merge_threads"
]
]
].filter((item) => !!item)}
/> />
</Tabs.TabPane> </Tabs.TabPane>
{/* <Tabs.TabPane {/* <Tabs.TabPane

View File

@ -27,6 +27,7 @@ const Page = (props) => {
return ( return (
<Monitor <Monitor
selectedCluster={selectedCluster}
formatState={(state) => { formatState={(state) => {
let clusterID = props.match.params?.cluster_id; let clusterID = props.match.params?.cluster_id;
if ( if (

View File

@ -8,15 +8,18 @@ export default ({
timeRange, timeRange,
handleTimeChange, handleTimeChange,
bucketSize, bucketSize,
timeout,
}) => { }) => {
return ( return (
<ClusterMetric <ClusterMetric
timezone={timezone} timezone={timezone}
timeRange={timeRange} timeRange={timeRange}
timeout={timeout}
handleTimeChange={handleTimeChange} handleTimeChange={handleTimeChange}
overview={1} overview={1}
fetchUrl={`${ESPrefix}/${clusterID}/cluster_metrics`} fetchUrl={`${ESPrefix}/${clusterID}/cluster_metrics`}
bucketSize={bucketSize} bucketSize={bucketSize}
metrics={['index_throughput', 'search_throughput', 'index_latency', 'search_latency']}
/> />
); );
} }

View File

@ -23,6 +23,7 @@ const StatisticBar = ({
setSpinning, setSpinning,
clusterAvailable, clusterAvailable,
clusterMonitored, clusterMonitored,
onInfoChange
}) => { }) => {
const { loading, error, value } = useFetch( const { loading, error, value } = useFetch(
`${ESPrefix}/${clusterID}/metrics`, `${ESPrefix}/${clusterID}/metrics`,
@ -34,6 +35,12 @@ const StatisticBar = ({
setSpinning(loading); setSpinning(loading);
}, [loading]); }, [loading]);
React.useEffect(() => {
if (onInfoChange) {
onInfoChange(value)
}
}, [JSON.stringify(value)]);
let overviewStatistic = []; let overviewStatistic = [];
if (value?.summary) { if (value?.summary) {
let rawStats = value.summary; let rawStats = value.summary;

View File

@ -10,7 +10,8 @@ export default ({
timeRange, timeRange,
handleTimeChange, handleTimeChange,
shardID, shardID,
bucketSize bucketSize,
timeout
}) => { }) => {
const [param, setParam] = useState({ const [param, setParam] = useState({
show_top: false, show_top: false,
@ -26,6 +27,73 @@ export default ({
setParam={setParam} setParam={setParam}
shardID={shardID} shardID={shardID}
bucketSize={bucketSize} bucketSize={bucketSize}
timeout={timeout}
metrics={[
[
"operations",
[
"indexing_rate",
"indexing_bytes",
"query_times",
"fetch_times",
"scroll_times",
"refresh_times",
"flush_times",
"merge_times"
]
],
[
"latency",
[
"indexing_latency",
"query_latency",
"fetch_latency",
"scroll_latency",
"refresh_latency",
"flush_latency",
"merge_latency"
]
],
[
"storage",
[
"index_storage",
"segment_count"
]
],
[
"document",
[
"doc_count",
"docs_deleted",
"doc_percent"
]
],
[
"memory",
[
"segment_doc_values_memory",
"segment_fields_memory",
"segment_memory",
"segment_terms_memory",
"segment_index_writer_memory",
"segment_term_vectors_memory"
]
],
[
"cache",
[
"query_cache",
"request_cache",
"fielddata_cache",
"query_cache_count",
"query_cache_hit",
"request_cache_hit",
"query_cache_miss",
"request_cache_miss"
]
]
]}
/> />
); );
} }

View File

@ -6,8 +6,10 @@ import { formatMessage } from "umi/locale";
import Monitor from "@/components/Overview/Monitor"; import Monitor from "@/components/Overview/Monitor";
import StatisticBar from "./statistic_bar"; import StatisticBar from "./statistic_bar";
import ShardStatisticBar from "./shard_statistic_bar"; import ShardStatisticBar from "./shard_statistic_bar";
import { connect } from "dva";
export default (props) => { const Page = (props) => {
const { clusterStatus, selectedCluster } = props;
const {shard_id} = props.location.query; const {shard_id} = props.location.query;
const panes = React.useMemo(()=>{ const panes = React.useMemo(()=>{
const panes = [ const panes = [
@ -26,6 +28,7 @@ export default (props) => {
} }
return ( return (
<Monitor <Monitor
selectedCluster={selectedCluster}
formatState={(state) => { formatState={(state) => {
return { return {
...state, ...state,
@ -80,3 +83,8 @@ export default (props) => {
/> />
); );
}; };
export default connect(({ global }) => ({
selectedCluster: global.selectedCluster,
clusterStatus: global.clusterStatus,
}))(Page);

View File

@ -5,12 +5,14 @@ import ClusterMetric from "../../components/cluster_metric";
const timezone = "local"; const timezone = "local";
export default ({ export default ({
isAgent,
clusterID, clusterID,
indexName, indexName,
timeRange, timeRange,
handleTimeChange, handleTimeChange,
shardID, shardID,
bucketSize, bucketSize,
timeout
}) => { }) => {
let url = `${ESPrefix}/${clusterID}/index/${indexName}/metrics`; let url = `${ESPrefix}/${clusterID}/index/${indexName}/metrics`;
if(shardID){ if(shardID){
@ -24,6 +26,15 @@ export default ({
overview={1} overview={1}
fetchUrl={url} fetchUrl={url}
bucketSize={bucketSize} bucketSize={bucketSize}
timeout={timeout}
metrics={[
"index_health",
"index_throughput",
"search_throughput",
"index_latency",
"search_latency",
isAgent ? "shard_state" : undefined,
].filter((item) => !!item)}
/> />
); );
} }

View File

@ -1,4 +1,4 @@
import { useState,useEffect } from "react"; import { useState,useEffect, useMemo } from "react";
import { Tabs } from "antd"; import { Tabs } from "antd";
import NodeMetric from "../../components/node_metric"; import NodeMetric from "../../components/node_metric";
import QueueMetric from "../../components/queue_metric"; import QueueMetric from "../../components/queue_metric";
@ -7,12 +7,23 @@ import { formatMessage } from "umi/locale";
const timezone = "local"; const timezone = "local";
export default ({ export default ({
selectedCluster,
clusterID, clusterID,
nodeID, nodeID,
timeRange, timeRange,
handleTimeChange, handleTimeChange,
bucketSize, bucketSize,
timeout,
}) => { }) => {
const isVersionGTE6 = useMemo(() => {
const main = selectedCluster?.version?.split('.')[0]
if (main && parseInt(main) >= 6) {
return true
}
return false
}, [selectedCluster?.version])
const [param, setParam] = useState({ const [param, setParam] = useState({
show_top: false, show_top: false,
node_name: nodeID, node_name: nodeID,
@ -53,6 +64,137 @@ export default ({
param={param} param={param}
setParam={setParam} setParam={setParam}
bucketSize={bucketSize} bucketSize={bucketSize}
timeout={timeout}
metrics={[
[
"operations",
[
"indexing_rate",
"indexing_bytes",
"query_rate",
"fetch_rate",
"scroll_rate",
"refresh_rate",
"flush_rate",
"merges_rate",
"scroll_open_contexts"
]
],
[
"latency",
[
"indexing_latency",
"query_latency",
"fetch_latency",
"scroll_latency",
"refresh_latency",
"flush_latency",
"merge_latency"
]
],
[
"system",
[
"cpu",
"disk",
"open_file",
"open_file_percent",
"os_cpu",
"os_load_average_1m",
"os_used_mem",
"os_used_swap"
]
],
[
"circuit_breaker",
[
"parent_breaker",
"accounting_breaker",
"fielddata_breaker",
"request_breaker",
"in_flight_requests_breaker",
"model_inference_breaker"
]
],
[
"io",
[
"total_io_operations",
"total_read_io_operations",
"total_write_io_operations"
]
],
[
"transport",
[
"transport_rx_bytes",
"transport_rx_rate",
"transport_tx_bytes",
"transport_tx_rate",
"transport_outbound_connections"
]
],
[
"storage",
[
"segment_count",
"index_storage"
]
],
[
"document",
[
"docs_count",
"docs_deleted"
]
],
[
"http",
[
"http_connect_num",
"http_rate"
]
],
[
"JVM",
[
"jvm_heap_used_percent",
"jvm_used_heap",
"jvm_mem_young_peak_used",
"jvm_mem_young_used",
"jvm_young_gc_latency",
"jvm_young_gc_rate",
"jvm_mem_old_peak_used",
"jvm_mem_old_used",
"jvm_old_gc_latency",
"jvm_old_gc_rate"
]
],
[
"memory",
[
"segment_doc_values_memory",
"segment_index_writer_memory",
"segment_memory",
"segment_stored_fields_memory",
"segment_term_vectors_memory",
"segment_terms_memory"
]
],
[
"cache",
[
"query_cache",
"request_cache",
"fielddata_cache",
"query_cache_count",
"query_cache_hit",
"request_cache_hit",
"query_cache_miss",
"request_cache_miss"
]
]
]}
/> />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane <Tabs.TabPane
@ -69,6 +211,80 @@ export default ({
param={param} param={param}
setParam={setParam} setParam={setParam}
bucketSize={bucketSize} bucketSize={bucketSize}
timeout={timeout}
metrics={[
isVersionGTE6 ? [
"thread_pool_write",
[
"write_active",
"write_queue",
"write_rejected",
"write_threads"
]
] : [
"thread_pool_index",
[
"index_active",
"index_queue",
"index_rejected",
"index_threads",
]
],
[
"thread_pool_search",
[
"search_active",
"search_queue",
"search_rejected",
"search_threads"
]
],
!isVersionGTE6 ? [
"thread_pool_bulk",
[
"bulk_active",
"bulk_queue",
"bulk_rejected",
"bulk_threads",
]
] : undefined,
[
"thread_pool_get",
[
"get_active",
"get_queue",
"get_rejected",
"get_threads"
]
],
[
"thread_pool_flush",
[
"flush_active",
"flush_queue",
"flush_rejected",
"flush_threads"
]
],
[
"thread_pool_refresh",
[
"refresh_active",
"refresh_queue",
"refresh_rejected",
"refresh_threads"
]
],
[
"thread_pool_force_merge",
[
"force_merge_active",
"force_merge_queue",
"force_merge_rejected",
"force_merge_threads"
]
]
].filter((item) => !!item)}
/> />
</Tabs.TabPane> </Tabs.TabPane>
</Tabs> </Tabs>

View File

@ -5,16 +5,18 @@ import Shards from "./shards";
import { formatMessage } from "umi/locale"; import { formatMessage } from "umi/locale";
import Monitor from "@/components/Overview/Monitor"; import Monitor from "@/components/Overview/Monitor";
import StatisticBar from "./statistic_bar"; import StatisticBar from "./statistic_bar";
import { connect } from "dva";
const panes = [ const panes = [
{ title: "Overview", component: Overview, key: "overview" }, { title: "Overview", component: Overview, key: "overview" },
{ title: "Advanced", component: Advanced, key: "advanced" }, { title: "Advanced", component: Advanced, key: "advanced" },
{ title: "Shards", component: Shards, key: "shards" }, { title: "Shards", component: Shards, key: "shards" },
]; ];
const Page = (props) => {
export default (props) => { const { clusterStatus, selectedCluster } = props;
return ( return (
<Monitor <Monitor
selectedCluster={selectedCluster}
formatState={(state) => { formatState={(state) => {
return { return {
...state, ...state,
@ -54,3 +56,8 @@ export default (props) => {
/> />
); );
}; };
export default connect(({ global }) => ({
selectedCluster: global.selectedCluster,
clusterStatus: global.clusterStatus,
}))(Page);

View File

@ -1,24 +1,40 @@
import { ESPrefix } from "@/services/common"; import { ESPrefix } from "@/services/common";
import StatisticBar from "./statistic_bar"; import StatisticBar from "./statistic_bar";
import ClusterMetric from "../../components/cluster_metric"; import ClusterMetric from "../../components/cluster_metric";
import { useMemo } from "react";
const timezone = "local"; const timezone = "local";
export default ({ export default ({
isAgent,
clusterID, clusterID,
nodeID, nodeID,
timeRange, timeRange,
handleTimeChange, handleTimeChange,
bucketSize, bucketSize,
timeout,
}) => { }) => {
return ( return (
<ClusterMetric <ClusterMetric
timezone={timezone} timezone={timezone}
timeRange={timeRange} timeRange={timeRange}
timeout={timeout}
handleTimeChange={handleTimeChange} handleTimeChange={handleTimeChange}
overview={1} overview={1}
fetchUrl={`${ESPrefix}/${clusterID}/node/${nodeID}/metrics`} fetchUrl={`${ESPrefix}/${clusterID}/node/${nodeID}/metrics`}
bucketSize={bucketSize} bucketSize={bucketSize}
metrics={[
"node_health",
"cpu",
"jvm",
"index_throughput",
"search_throughput",
"index_latency",
"search_latency",
"parent_breaker",
isAgent ? "shard_state" : undefined,
].filter((item) => !!item)}
/> />
); );
} }

View File

@ -0,0 +1,319 @@
import request from "@/utils/request";
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 {
Axis,
Chart,
CurveType,
LineSeries,
BarSeries,
niceTimeFormatByDay,
Position,
ScaleType,
Settings,
timeFormatter,
} from "@elastic/charts";
import { formatter, getFormatter, getNumFormatter } from "@/utils/format";
import { CopyToClipboard } from "react-copy-to-clipboard";
import moment from "moment";
import { ESPrefix } from "@/services/common";
export default (props) => {
const {
timezone,
timeRange,
timeout,
handleTimeChange,
fetchUrl,
metricKey,
title,
queryParams,
className,
style,
formatMetric
} = props;
const [loading, setLoading] = useState(false)
const [metric, setMetric] = useState()
const [isInView, setIsInView] = useState(false);
const [error, setError] = useState();
const observerRef = useRef({ isInView: false })
const containerRef = useRef(null)
const firstFetchRef = useRef(true)
const fetchData = async (queryParams, fetchUrl, metricKey, showLoading) => {
if (!observerRef.current.isInView || !fetchUrl) return;
setError()
if (firstFetchRef.current || showLoading) {
setLoading(true)
}
const res = await request(fetchUrl, {
method: 'GET',
queryParams: {
...queryParams,
key: metricKey,
timeout
},
ignoreTimeout: true
}, false, false)
if (res?.error?.reason) {
setError(res.error.reason)
} else if (res && !res.error) {
const { metrics = {} } = res || {};
const metric = metrics[metricKey]
setMetric(formatMetric ? formatMetric(metric) : metric);
}
if (firstFetchRef.current || showLoading) {
setLoading(false)
firstFetchRef.current = false
}
}
useEffect(() => {
observerRef.current.deps = cloneDeep([queryParams, fetchUrl, metricKey])
fetchData(queryParams, fetchUrl, metricKey)
}, [JSON.stringify(queryParams), fetchUrl, metricKey])
useEffect(() => {
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
observerRef.current.isInView = true
if (JSON.stringify(observerRef.current.deps) !== JSON.stringify(observerRef.current.lastDeps)) {
observerRef.current.lastDeps = cloneDeep(observerRef.current.deps)
fetchData(...observerRef.current.deps)
}
} else {
observerRef.current.isInView = false
}
});
},
{
root: null,
threshold: 0,
}
);
if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => {
if (containerRef.current) {
observer.unobserve(containerRef.current);
}
};
}, [isInView]);
const chartRef = useRef();
const pointerUpdate = (event) => {
if (chartRef.current) {
chartRef.current.dispatchExternalPointerEvent(event);
}
};
const handleChartBrush = ({ x }) => {
if (!x) {
return;
}
let [from, to] = x;
if (typeof handleTimeChange == "function") {
if (to - from < 20 * 1000) {
from -= 10 * 1000;
to += 10 * 1000;
}
handleTimeChange({
start: moment(from).toISOString(),
end: moment(to).toISOString(),
});
}
};
const renderChart = () => {
if (loading) return <div style={{ height: 200 }}></div>
if (error) {
return (
<div style={{ height: 200, padding: 10 }}>
<Alert style={{ maxHeight: '100%', overflow: 'auto', wordBreak: 'break-all'}} message={error} type="error"/>
</div>
)
}
const axis = metric?.axis || [];
const lines = metric?.lines || [];
if (lines.every((item) => !item.data || item.data.length === 0)) {
return <Empty style={{ height: 200, margin: 0, paddingTop: 64 }} image={Empty.PRESENTED_IMAGE_SIMPLE} />
}
return (
<Chart
size={[, 200]}
className={styles.vizChartItem}
ref={chartRef}
>
<Settings
pointerUpdateDebounce={0}
pointerUpdateTrigger="x"
onPointerUpdate={pointerUpdate}
showLegend
legendPosition={Position.Bottom}
onBrushEnd={handleChartBrush}
tooltip={{
headerFormatter: ({ value }) =>
`${formatter.full_dates(value)}`,
}}
debug={false}
/>
<Axis
id={`${metricKey}-bottom`}
position={Position.Bottom}
showOverlappingTicks
labelFormat={timeRange.timeFormatter}
tickFormat={timeRange.timeFormatter}
/>
{metricKey == "cluster_health" ? (
<Axis
id="cluster_health"
position={Position.Left}
tickFormat={(d) => Number(d).toFixed(0) + "%"}
/>
) : null}
{axis.map((item) => {
return (
<Axis
key={metricKey + "-" + item.id}
id={metricKey + "-" + item.id}
showGridLines={item.showGridLines}
groupId={item.group}
position={item.position}
ticks={item.ticks}
labelFormat={getFormatter(
item.formatType,
item.labelFormat
)}
tickFormat={getFormatter(
item.formatType,
item.tickFormat
)}
/>
);
})}
{lines.map((item) => {
if (item.type == "Bar") {
return (
<BarSeries
key={item.metric.label}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={["y"]}
stackAccessors={["x"]}
splitSeriesAccessors={["g"]}
data={item.data}
color={({ specId, yAccessor, splitAccessors }) => {
const g = splitAccessors.get("g");
if (
yAccessor === "y"
) {
if( ["red", "yellow", "green"].includes(g)){
return g;
}
if(g == "online" || g == "available"){
return "green";
}
if(g == "offline" || g == "unavailable" || g == "N/A"){
return "gray";
}
}
return null;
}}
/>
);
}
return (
<LineSeries
key={item.metric.label}
id={item.metric.label}
groupId={item.metric.group}
timeZone={timezone}
color={item.color}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor={0}
tickFormat={getFormatter(
item.metric.formatType,
item.metric.tickFormat,
item.metric.units
)}
yAccessors={[1]}
data={item.data}
curve={CurveType.CURVE_MONOTONE_X}
/>
);
})}
</Chart>
)
}
const chartTitle = { title };
const lines = metric?.lines || [];
if (metricKey == "cluster_health") {
chartTitle.units = "%";
} else {
if (lines[0]?.metric) {
if (lines[0].metric.formatType.toLowerCase == "bytes") {
chartTitle.units = lines[0].metric.formatType;
} else {
chartTitle.units = lines[0].metric.units;
}
}
}
return (
<div key={metricKey} ref={containerRef} className={className} style={style}>
<Spin spinning={loading}>
<div className={styles.vizChartItemTitle}>
<span>
{chartTitle.title}
{chartTitle.units ? `(${chartTitle.units})` : ""}
</span>
{
<span>
{
metric?.request && (
<CopyToClipboard text={`GET .infini_metrics/_search\n${metric.request}`}>
<Tooltip title={formatMessage({id: "cluster.metrics.request.copy"})}>
<Icon
className={styles.copy}
type="copy"
onClick={() => message.success(formatMessage({id: "cluster.metrics.request.copy.success"}))}
/>
</Tooltip>
</CopyToClipboard>
)
}
<Tooltip title={formatMessage({id: "form.button.refresh"})}>
<Icon className={styles.copy} style={{ marginLeft: 12 }} type="reload" onClick={() => fetchData(...observerRef.current.deps, true)}/>
</Tooltip>
</span>
}
</div>
{renderChart()}
</Spin>
</div>
);
}

View File

@ -18,6 +18,13 @@
font-weight: 600; font-weight: 600;
} }
.copy {
cursor: pointer;
&:hover {
color: #1890ff;
}
}
.vizChartItem { .vizChartItem {
background: white !important; background: white !important;

View File

@ -1,289 +1,65 @@
import * as React from "react";
import {
Axis,
Chart,
CurveType,
LineSeries,
BarSeries,
niceTimeFormatByDay,
Position,
ScaleType,
Settings,
timeFormatter,
} from "@elastic/charts";
import useFetch from "@/lib/hooks/use_fetch";
import { ESPrefix } from "@/services/common";
import styles from "./Metrics.scss"; import styles from "./Metrics.scss";
import { Spin, Radio, Select, Skeleton } from "antd";
import { formatter, getFormatter, getNumFormatter } from "@/utils/format";
import "./node_metric.scss"; import "./node_metric.scss";
import { calculateBounds } from "@/components/vendor/data/common/query/timefilter"; import { calculateBounds } from "@/components/vendor/data/common/query/timefilter";
import moment from "moment"; import MetricChart from "./MetricChart";
import { useMemo } from "react";
import { formatMessage } from "umi/locale"; import { formatMessage } from "umi/locale";
import _ from "lodash"; import { formatTimeRange } from "@/lib/elasticsearch/util";
export default ({ export default (props) => {
timezone,
timeRange, const { fetchUrl, overview, metrics = [], renderExtra, timeRange, timeout, timezone, bucketSize, handleTimeChange } = props
handleTimeChange,
overview, if (!fetchUrl || metrics.length === 0) {
fetchUrl,
renderExtra,
bucketSize
}) => {
if (!fetchUrl) {
return null; return null;
} }
const queryParams = React.useMemo(() => {
const bounds = calculateBounds({ const queryParams = useMemo(() => {
from: timeRange.min, const newParams = formatTimeRange(timeRange);
to: timeRange.max,
});
let params = {
min: bounds.min.valueOf(),
max: bounds.max.valueOf(),
};
if (overview) { if (overview) {
params.overview = overview; newParams.overview = overview;
} }
if (bucketSize) { if (bucketSize) {
params.bucket_size = bucketSize newParams.bucket_size = bucketSize
} }
return params; return newParams;
}, [timeRange, bucketSize]); }, [timeRange, bucketSize]);
const { loading, error, value } = useFetch(
fetchUrl,
{
queryParams: queryParams,
},
[queryParams, fetchUrl]
);
const metrics = React.useMemo(() => {
const { metrics = {} } = value || {};
return Object.values(metrics)
.sort((a, b) => a.order - b.order)
.map((m) => {
let lines = m.lines || [];
m.lines = lines.map((line) => {
const data = line.data || [];
if (data.length > 1) {
line.data = line.data.slice(0, data.length - 1);
}
return line;
});
return m;
});
}, [value]);
const chartRefs = React.useRef();
React.useEffect(() => {
let refs = [];
Object.keys(metrics).map((m) => {
refs.push(React.createRef());
});
chartRefs.current = refs;
}, [metrics]);
const pointerUpdate = (event) => {
chartRefs.current.forEach((ref) => {
if (ref.current) {
ref.current.dispatchExternalPointerEvent(event);
}
});
};
const handleChartBrush = ({ x }) => {
if (!x) {
return;
}
let [from, to] = x;
if (typeof handleTimeChange == "function") {
if (to - from < 20 * 1000) {
from -= 10 * 1000;
to += 10 * 1000;
}
handleTimeChange({
start: moment(from).toISOString(),
end: moment(to).toISOString(),
});
}
};
let refIdx = 0;
if (Object.keys(metrics).length == 0) {
return null;
}
const extra = renderExtra ? renderExtra() : null; const extra = renderExtra ? renderExtra() : null;
return ( return (
<div id="cluster-metric"> <div id="cluster-metric">
<div className={styles.metricList}> <div className={styles.metricList}>
<Skeleton active loading={!value} paragraph={{ rows: 20 }}> {metrics.map((metricKey, i) => (
{Object.keys(metrics).map((e, i) => { <MetricChart
let axis = metrics[e].axis; key={metricKey}
let lines = metrics[e].lines; timezone={timezone}
if (lines.length == 0 || (lines && lines[0]?.data?.length == 0)) { timeRange={timeRange}
return null; timeout={timeout}
handleTimeChange={handleTimeChange}
fetchUrl={fetchUrl}
metricKey={metricKey}
title={formatMessage({
id: "cluster.metrics.axis." + metricKey + ".title",
})}
queryParams={queryParams}
className={styles.vizChartContainer}
style={{ flex: metricKey == "cluster_health" ? '0 0 calc(100%)' : '0 0 calc(50% - 5px)'}}
formatMetric={(metric) => {
if (metric) {
const lines = metric.lines || []
metric.lines = lines.map((line) => {
const data = line.data || [];
if (data.length > 1) {
line.data = line.data.slice(0, data.length - 1);
} }
let disableHeaderFormat = false; return line;
let headerUnit = "";
let chartTitle = {};
if (metrics[e].key == "cluster_health") {
chartTitle.units = "%";
} else {
if (lines[0].metric.formatType.toLowerCase == "bytes") {
chartTitle.units = lines[0].metric.formatType;
} else {
chartTitle.units = lines[0].metric.units;
}
}
chartTitle.title = formatMessage({
id: "cluster.metrics.axis." + metrics[e].key + ".title",
}); });
return (
<div key={e} className={styles.vizChartContainer} style={{ flex: metrics[e].key == "cluster_health" ? '0 0 calc(100%)' : '0 0 calc(50% - 5px)'}}>
<div className={styles.vizChartItemTitle}>
<span>
{chartTitle.title}
{chartTitle.units ? `(${chartTitle.units})` : ""}
</span>
</div>
<Chart
size={[, 200]}
className={styles.vizChartItem}
ref={chartRefs[i]}
>
<Settings
pointerUpdateDebounce={0}
pointerUpdateTrigger="x"
// externalPointerEvents={{
// tooltip: { visible: true },
// }}
onPointerUpdate={pointerUpdate}
// theme={theme}
showLegend
legendPosition={Position.Bottom}
onBrushEnd={handleChartBrush}
tooltip={{
headerFormatter: disableHeaderFormat
? undefined
: ({ value }) =>
`${formatter.full_dates(value)}${
headerUnit ? ` ${headerUnit}` : ""
}`,
}}
debug={false}
/>
<Axis
id="{e}-bottom"
position={Position.Bottom}
showOverlappingTicks
labelFormat={timeRange.timeFormatter}
tickFormat={timeRange.timeFormatter}
/>
{metrics[e].key == "cluster_health" ? (
<Axis
id="cluster_health"
// title={formatMessage({
// id:
// "dashboard.charts.title." +
// metrics[e].key +
// ".axis.percent",
// })}
position={Position.Left}
tickFormat={(d) => Number(d).toFixed(0) + "%"}
/>
) : null}
{axis.map((item) => {
return (
<Axis
key={e + "-" + item.id}
id={e + "-" + item.id}
showGridLines={item.showGridLines}
groupId={item.group}
// title={formatMessage({
// id:
// "dashboard.charts.title." +
// metrics[e].key +
// ".axis." +
// item.title,
// })}
position={item.position}
ticks={item.ticks}
labelFormat={getFormatter(
item.formatType,
item.labelFormat
)}
tickFormat={getFormatter(
item.formatType,
item.tickFormat
)}
/>
);
})}
{lines.map((item) => {
if (item.type == "Bar") {
return (
<BarSeries
key={item.metric.label}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={["y"]}
stackAccessors={["x"]}
splitSeriesAccessors={["g"]}
data={item.data}
color={({ specId, yAccessor, splitAccessors }) => {
const g = splitAccessors.get("g");
if (
yAccessor === "y"
) {
if( ["red", "yellow", "green"].includes(g)){
return g;
} }
if(g == "online" || g == "available"){ return metric
return "green";
}
if(g == "offline" || g == "unavailable" || g == "N/A"){
return "gray";
}
}
return null;
}} }}
/> />
); ))}
}
return (
<LineSeries
key={item.metric.label}
id={item.metric.label}
groupId={item.metric.group}
timeZone={timezone}
color={item.color}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor={0}
tickFormat={getFormatter(
item.metric.formatType,
item.metric.tickFormat,
item.metric.units
)}
yAccessors={[1]}
data={item.data}
curve={CurveType.CURVE_MONOTONE_X}
/>
);
})}
</Chart>
</div>
);
})}
{ {
extra && ( extra && (
<div key={"metric_extra"} className={styles.vizChartContainer}> <div key={"metric_extra"} className={styles.vizChartContainer}>
@ -291,7 +67,6 @@ export default ({
</div> </div>
) )
} }
</Skeleton>
</div> </div>
</div> </div>
); );

View File

@ -1,39 +1,19 @@
import * as React from "react"; import IndexSelect from "@/components/IndexSelect";
import {
Axis,
Chart,
CurveType,
LineSeries,
BarSeries,
niceTimeFormatByDay,
Position,
ScaleType,
Settings,
timeFormatter,
} from "@elastic/charts";
import useFetch from "@/lib/hooks/use_fetch"; import useFetch from "@/lib/hooks/use_fetch";
import { ESPrefix } from "@/services/common"; import { ESPrefix } from "@/services/common";
import styles from "./Metrics.scss"; import { Radio } from "antd";
import { Spin, Radio, Select, Skeleton } from "antd";
import { formatter, getFormatter, getNumFormatter } from "@/utils/format";
import "./node_metric.scss"; import "./node_metric.scss";
import { calculateBounds } from "@/components/vendor/data/common/query/timefilter";
import moment from "moment";
import { formatMessage } from "umi/locale"; import { formatMessage } from "umi/locale";
import MetricContainer from "./metric_container"; import MetricContainer from "./metric_container";
import _ from "lodash"; import { formatTimeRange } from "@/lib/elasticsearch/util";
import IndexSelect from "@/components/IndexSelect"; import NodeSelect from "@/components/NodeSelect";
import Anchor from "@/components/Anchor"; import Anchor from "@/components/Anchor";
import MetricChart from "./MetricChart";
import { useMemo } from "react";
const gorupOrder = [ export default (props) => {
"operations",
"latency", const {
"storage",
"document",
"memory",
"cache",
];
export default ({
clusterID, clusterID,
timezone, timezone,
timeRange, timeRange,
@ -41,11 +21,15 @@ export default ({
param, param,
setParam, setParam,
shardID, shardID,
bucketSize bucketSize,
}) => { metrics = [],
if (!clusterID) { timeout
} = props
if (!clusterID || metrics.length == 0) {
return null; return null;
} }
const showTop = param.show_top ?? true; const showTop = param.show_top ?? true;
const topChange = (e) => { const topChange = (e) => {
@ -68,16 +52,8 @@ export default ({
}; };
}); });
}; };
const queryParams = React.useMemo(() => { const queryParams = useMemo(() => {
const bounds = calculateBounds({ const newParams = formatTimeRange(timeRange);
from: timeRange.min,
to: timeRange.max,
});
let newParams = {
min: bounds.min.valueOf(),
max: bounds.max.valueOf(),
};
console.log(shardID)
if(shardID){ if(shardID){
newParams.shard_id = shardID; newParams.shard_id = shardID;
} }
@ -92,73 +68,16 @@ export default ({
} }
return newParams; return newParams;
}, [param, timeRange, bucketSize]); }, [param, timeRange, bucketSize]);
const { loading, error, value } = useFetch(
`${ESPrefix}/${clusterID}/index_metrics`,
{
queryParams: queryParams,
},
[clusterID, queryParams]
);
const metrics = React.useMemo(() => {
const grpMetrics = _.groupBy(value?.metrics, "group");
let metrics = {};
Object.keys(grpMetrics).forEach((k) => {
metrics[k] = (grpMetrics[k] || [])
.sort((a, b) => a.order - b.order)
});
return metrics;
}, [value]);
const chartRefs = React.useRef();
React.useEffect(() => {
let refs = [];
Object.values(metrics).map((m) => {
m.forEach(() => {
refs.push(React.createRef());
});
});
chartRefs.current = refs;
}, [metrics]);
const { value: indices } = useFetch( const { value: indices } = useFetch(
`${ESPrefix}/${clusterID}/_cat/indices`, `${ESPrefix}/${clusterID}/_cat/indices`,
{}, {},
[clusterID] [clusterID]
); );
const formatedIndices = React.useMemo(() => { const formatedIndices = useMemo(() => {
return Object.values(indices || []); return Object.values(indices || []);
}, [indices]); }, [indices]);
const pointerUpdate = (event) => {
chartRefs.current.forEach((ref) => {
if (ref.current) {
ref.current.dispatchExternalPointerEvent(event);
}
});
};
const handleChartBrush = ({ x }) => {
if (!x) {
return;
}
let [from, to] = x;
if (typeof handleTimeChange == "function") {
if (to - from < 20 * 1000) {
from -= 10 * 1000;
to += 10 * 1000;
}
handleTimeChange({
start: moment(from).toISOString(),
end: moment(to).toISOString(),
});
}
};
let refIdx = 0;
if (Object.keys(metrics).length == 0) {
return null;
}
return ( return (
<div id="node-metric"> <div id="node-metric">
{showTop ? ( {showTop ? (
@ -189,177 +108,38 @@ export default ({
<div className="px-box"> <div className="px-box">
<div className="px"> <div className="px">
<Skeleton active loading={!value} paragraph={{ rows: 20 }}> {metrics.map((item, i) => {
{gorupOrder.map((e, i) => {
if (!metrics[e]) {
return null;
}
return ( return (
<div key={e} style={{ margin: "8px 0" }}> <div key={item[0]} style={{ margin: "8px 0" }}>
<MetricContainer <MetricContainer
title={formatMessage({ id: `cluster.metrics.group.${e}` })} title={formatMessage({ id: `cluster.metrics.group.${item[0]}` })}
collapsed={false} collapsed={false}
id={e} id={item[0]}
> >
<div className="metric-inner-cnt"> <div className="metric-inner-cnt">
{metrics[e].map((metric) => { {
let axis = metric.axis; item[1].map((metricKey) => (
let lines = metric.lines; <MetricChart
if ( key={metricKey}
lines.length == 0 || timezone={timezone}
(lines && lines[0]?.data?.length == 0) timeRange={timeRange}
) { handleTimeChange={handleTimeChange}
return null; fetchUrl={`${ESPrefix}/${clusterID}/index_metrics`}
metricKey={metricKey}
title={formatMessage({id: "cluster.metrics.index.axis." + metricKey + ".title"})}
queryParams={queryParams}
className={"metric-item"}
timeout={timeout}
/>
))
} }
let disableHeaderFormat = false;
let headerUnit = "";
let chartTitle = {};
if (lines[0].metric.formatType.toLowerCase == "bytes") {
chartTitle.units = lines[0].metric.formatType;
} else {
chartTitle.units = lines[0].metric.units;
}
chartTitle.title = formatMessage({
id:
"cluster.metrics.index.axis." + metric.key + ".title",
});
return (
<div key={metric.key} className="metric-item">
<div className={styles.vizChartItemTitle}>
<span>
{chartTitle.title}
{chartTitle.units ? `(${chartTitle.units})` : ""}
</span>
</div>
<Chart
size={[, 200]}
className={styles.vizChartItem}
ref={chartRefs.current[refIdx++]}
>
<Settings
// theme={theme}
pointerUpdateDebounce={0}
pointerUpdateTrigger="x"
// externalPointerEvents={{
// tooltip: { visible: true },
// }}
onPointerUpdate={pointerUpdate}
showLegend
legendPosition={Position.Bottom}
onBrushEnd={handleChartBrush}
tooltip={{
headerFormatter: disableHeaderFormat
? undefined
: ({ value }) =>
`${formatter.full_dates(value)}${
headerUnit ? ` ${headerUnit}` : ""
}`,
}}
debug={false}
/>
<Axis
id="{e}-bottom"
position={Position.Bottom}
showOverlappingTicks
labelFormat={timeRange.timeFormatter}
tickFormat={timeRange.timeFormatter}
ticks={8}
/>
{lines[0].type == "Bar" ? (
<Axis
id={"axis_left"}
position={Position.Left}
tickFormat={(d) => Number(d).toFixed(0) + "%"}
/>
) : null}
{axis.map((item) => {
return (
<Axis
key={e + "-" + item.id}
id={e + "-" + item.id}
showGridLines={item.showGridLines}
groupId={item.group}
// title={formatMessage({
// id:
// "cluster.metrics.index.axis." +
// metric.key +
// ".title",
// })}
position={item.position}
ticks={item.ticks}
labelFormat={getFormatter(
item.formatType,
item.labelFormat
)}
tickFormat={getFormatter(
item.formatType,
item.tickFormat
)}
/>
);
})}
{lines.map((item) => {
if (item.type == "Bar") {
return (
<BarSeries
key={item.metric.label}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={["y"]}
stackAccessors={["x"]}
splitSeriesAccessors={["g"]}
data={item.data}
color={({
specId,
yAccessor,
splitAccessors,
}) => {
const g = splitAccessors.get("g");
if (
yAccessor === "y" &&
["red", "yellow", "green"].includes(g)
) {
return g;
}
return null;
}}
/>
);
}
return (
<LineSeries
key={item.metric.label}
id={item.metric.label}
groupId={item.metric.group}
timeZone={timezone}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor={0}
tickFormat={getFormatter(
item.metric.formatType,
item.metric.tickFormat,
item.metric.units
)}
yAccessors={[1]}
data={item.data}
curve={CurveType.CURVE_MONOTONE_X}
/>
);
})}
</Chart>
</div>
);
})}
</div> </div>
</MetricContainer> </MetricContainer>
</div> </div>
); );
})} })}
</Skeleton>
</div> </div>
<Anchor links={gorupOrder}></Anchor> <Anchor links={metrics.map((item) => item[0])}></Anchor>
</div> </div>
</div> </div>

View File

@ -1,47 +1,18 @@
import * as React from "react";
import {
Axis,
Chart,
CurveType,
LineSeries,
niceTimeFormatByDay,
Position,
ScaleType,
Settings,
timeFormatter,
} from "@elastic/charts";
import useFetch from "@/lib/hooks/use_fetch"; import useFetch from "@/lib/hooks/use_fetch";
import { ESPrefix } from "@/services/common"; import { ESPrefix } from "@/services/common";
import styles from "./Metrics.scss"; import { Radio } from "antd";
import { Spin, Radio, Select, Skeleton, Row, Col, InputNumber } from "antd";
import { formatter, getFormatter, getNumFormatter } from "@/utils/format";
import "./node_metric.scss"; import "./node_metric.scss";
import { calculateBounds } from "@/components/vendor/data/common/query/timefilter";
import moment from "moment";
import { formatMessage } from "umi/locale"; import { formatMessage } from "umi/locale";
import MetricContainer from "./metric_container"; import MetricContainer from "./metric_container";
import _ from "lodash";
import { formatTimeRange } from "@/lib/elasticsearch/util"; import { formatTimeRange } from "@/lib/elasticsearch/util";
import NodeSelect from "@/components/NodeSelect"; import NodeSelect from "@/components/NodeSelect";
import Anchor from "@/components/Anchor"; import Anchor from "@/components/Anchor";
import MetricChart from "./MetricChart";
import { useCallback, useMemo } from "react";
const gorupOrder = [ export default (props) => {
"operations",
"latency",
"system",
"circuit_breaker",
"io",
"transport",
"storage", const {
"document",
"http",
"JVM",
"memory",
"cache",
];
export default ({
clusterID, clusterID,
timezone, timezone,
timeRange, timeRange,
@ -49,15 +20,17 @@ export default ({
param, param,
setParam, setParam,
bucketSize, bucketSize,
}) => { timeout,
// const [filter, setFilter] = React.useState({ metrics = []
// top: "5", } = props
// node_name: param?.transport,
// }); if (!clusterID || metrics.length == 0) {
return null;
}
const showTop = param.show_top ?? true; const showTop = param.show_top ?? true;
const topChange = React.useCallback( const topChange = useCallback(
(e) => { (e) => {
// setFilter({ // setFilter({
// node_name: undefined, // node_name: undefined,
@ -74,7 +47,7 @@ export default ({
[param] [param]
); );
const nodeValueChange = React.useCallback( const nodeValueChange = useCallback(
(value) => { (value) => {
const nodeNames = value.map(item=>item.host); const nodeNames = value.map(item=>item.host);
setParam((param) => { setParam((param) => {
@ -87,8 +60,15 @@ export default ({
}, },
[param] [param]
); );
const queryParams = React.useMemo(() => {
let newParams = formatTimeRange(timeRange); const { value: nodes } = useFetch(
`${ESPrefix}/${clusterID}/nodes/realtime`,
{},
[clusterID]
);
const queryParams = useMemo(() => {
const newParams = formatTimeRange(timeRange);
if (param.top) { if (param.top) {
newParams.top = param.top; newParams.top = param.top;
} }
@ -100,40 +80,7 @@ export default ({
} }
return newParams; return newParams;
}, [param, timeRange, bucketSize]); }, [param, timeRange, bucketSize]);
const { loading, error, value } = useFetch(
`${ESPrefix}/${clusterID}/node_metrics`,
{
queryParams: queryParams,
},
[clusterID, queryParams]
);
const metrics = React.useMemo(() => {
const grpMetrics = _.groupBy(value?.metrics, "group");
let metrics = {};
Object.keys(grpMetrics).forEach((k) => {
metrics[k] = (grpMetrics[k] || [])
.sort((a, b) => a.order - b.order)
});
return metrics;
}, [value]);
const chartRefs = React.useRef();
React.useEffect(() => {
let refs = [];
Object.values(metrics).map((m) => {
m.forEach(() => {
refs.push(React.createRef());
});
});
chartRefs.current = refs;
}, [metrics]);
const { value: nodes } = useFetch(
`${ESPrefix}/${clusterID}/nodes/realtime`,
{},
[clusterID]
);
const formatedNodes = React.useMemo(() => { const formatedNodes = React.useMemo(() => {
if (!nodes) { if (!nodes) {
return []; return [];
@ -146,36 +93,6 @@ export default ({
}); });
}, [nodes]); }, [nodes]);
const pointerUpdate = (event) => {
chartRefs.current.forEach((ref) => {
if (ref.current) {
ref.current.dispatchExternalPointerEvent(event);
}
});
};
const handleChartBrush = ({ x }) => {
if (!x) {
return;
}
let [from, to] = x;
if (typeof handleTimeChange == "function") {
if (to - from < 20 * 1000) {
from -= 10 * 1000;
to += 10 * 1000;
}
handleTimeChange({
start: moment(from).toISOString(),
end: moment(to).toISOString(),
});
}
};
let refIdx = 0;
if (Object.keys(metrics).length == 0) {
return null;
}
return ( return (
<div id="node-metric"> <div id="node-metric">
{showTop ? ( {showTop ? (
@ -214,146 +131,38 @@ export default ({
<div className="px-box"> <div className="px-box">
<div className="px"> <div className="px">
<Skeleton active loading={!value} paragraph={{ rows: 20 }}> {metrics.map((item, i) => {
{//Object.keys(metrics)
gorupOrder.map((e, i) => {
let hasData = (metrics[e] || []).some(
(m) => m.lines && m.lines[0]?.data.length > 0
);
if (!hasData) {
return null;
}
return ( return (
<div key={e} style={{ margin: "8px 0" }}> <div key={item[0]} style={{ margin: "8px 0" }}>
<MetricContainer <MetricContainer
title={formatMessage({ id: `cluster.metrics.group.${e}` })} title={formatMessage({ id: `cluster.metrics.group.${item[0]}` })}
collapsed={false} collapsed={false}
id={e} id={item[0]}
> >
<div className="metric-inner-cnt"> <div className="metric-inner-cnt">
{metrics[e].map((metric) => { {
let axis = metric.axis; item[1].map((metricKey) => (
let lines = metric.lines; <MetricChart
if ( key={metricKey}
lines.length == 0 || timezone={timezone}
(lines && lines[0]?.data?.length == 0) timeRange={timeRange}
) { handleTimeChange={handleTimeChange}
return null; fetchUrl={`${ESPrefix}/${clusterID}/node_metrics`}
metricKey={metricKey}
title={formatMessage({id:"cluster.metrics.node.axis." + metricKey + ".title"})}
queryParams={queryParams}
className={"metric-item"}
timeout={timeout}
/>
))
} }
let disableHeaderFormat = false;
let headerUnit = "";
let chartTitle = {};
if (lines[0].metric.formatType.toLowerCase == "bytes") {
chartTitle.units = lines[0].metric.formatType;
} else {
chartTitle.units = lines[0].metric.units;
}
chartTitle.title = formatMessage({
id:
"cluster.metrics.node.axis." + metric.key + ".title",
});
return (
<div key={metric.key} className="metric-item">
<div className={styles.vizChartItemTitle}>
<span>
{chartTitle.title}
{chartTitle.units ? `(${chartTitle.units})` : ""}
</span>
</div>
<Chart
size={[, 200]}
className={styles.vizChartItem}
ref={chartRefs.current[refIdx++]}
>
<Settings
// theme={theme}
pointerUpdateDebounce={0}
pointerUpdateTrigger="x"
// externalPointerEvents={{
// tooltip: { visible: true },
// }}
onPointerUpdate={pointerUpdate}
showLegend
legendPosition={Position.Bottom}
onBrushEnd={handleChartBrush}
tooltip={{
headerFormatter: disableHeaderFormat
? undefined
: ({ value }) =>
`${formatter.full_dates(value)}${
headerUnit ? ` ${headerUnit}` : ""
}`,
}}
debug={false}
/>
<Axis
id="{e}-bottom"
position={Position.Bottom}
showOverlappingTicks
labelFormat={timeRange.timeFormatter}
tickFormat={timeRange.timeFormatter}
ticks={8}
/>
{axis.map((item) => {
return (
<Axis
key={e + "-" + item.id}
id={e + "-" + item.id}
showGridLines={item.showGridLines}
groupId={item.group}
// title={formatMessage({
// id:
// "cluster.metrics.node.axis." +
// metric.key +
// ".title",
// })}
position={item.position}
ticks={item.ticks}
labelFormat={getFormatter(
item.formatType,
item.labelFormat
)}
tickFormat={getFormatter(
item.formatType,
item.tickFormat
)}
/>
);
})}
{lines.map((item) => {
return (
<LineSeries
key={item.metric.label}
id={item.metric.label}
groupId={item.metric.group}
timeZone={timezone}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor={0}
tickFormat={getFormatter(
item.metric.formatType,
item.metric.tickFormat,
item.metric.units
)}
yAccessors={[1]}
data={item.data}
curve={CurveType.CURVE_MONOTONE_X}
/>
);
})}
</Chart>
</div>
);
})}
</div> </div>
</MetricContainer> </MetricContainer>
</div> </div>
); );
})} })}
</Skeleton>
</div> </div>
<Anchor links={gorupOrder}></Anchor> <Anchor links={metrics.map((item) => item[0])}></Anchor>
</div> </div>
</div> </div>
); );

View File

@ -1,41 +1,18 @@
import * as React from "react";
import {
Axis,
Chart,
CurveType,
LineSeries,
niceTimeFormatByDay,
Position,
ScaleType,
Settings,
timeFormatter,
} from "@elastic/charts";
import useFetch from "@/lib/hooks/use_fetch"; import useFetch from "@/lib/hooks/use_fetch";
import { ESPrefix } from "@/services/common"; import { ESPrefix } from "@/services/common";
import styles from "./Metrics.scss"; import { Radio } from "antd";
import { Spin, Radio, Select, Skeleton, Row, Col } from "antd";
import { formatter, getFormatter, getNumFormatter } from "@/utils/format";
import "./node_metric.scss"; import "./node_metric.scss";
import { calculateBounds } from "@/components/vendor/data/common/query/timefilter";
import moment from "moment";
import { formatMessage } from "umi/locale"; import { formatMessage } from "umi/locale";
import MetricContainer from "./metric_container"; import MetricContainer from "./metric_container";
import _ from "lodash"; import { formatTimeRange } from "@/lib/elasticsearch/util";
import NodeSelect from "@/components/NodeSelect"; import NodeSelect from "@/components/NodeSelect";
import Anchor from "@/components/Anchor"; import Anchor from "@/components/Anchor";
import MetricChart from "./MetricChart";
import { useCallback, useMemo } from "react";
const gorupOrder = [ export default (props) => {
"thread_pool_write",
"thread_pool_index",
"thread_pool_search",
"thread_pool_bulk",
"thread_pool_get",
"thread_pool_flush",
"thread_pool_refresh",
"thread_pool_force_merge",
];
export default ({ const {
clusterID, clusterID,
timezone, timezone,
timeRange, timeRange,
@ -43,10 +20,22 @@ export default ({
param, param,
setParam, setParam,
bucketSize, bucketSize,
}) => { metrics = [],
timeout
} = props
if (!clusterID || metrics.length == 0) {
return null;
}
const showTop = param.show_top ?? true; const showTop = param.show_top ?? true;
const topChange = React.useCallback(
const topChange = useCallback(
(e) => { (e) => {
// setFilter({
// node_name: undefined,
// top: e.target.value,
// });
setParam((param) => { setParam((param) => {
delete param["node_name"]; delete param["node_name"];
return { return {
@ -58,7 +47,7 @@ export default ({
[param] [param]
); );
const nodeValueChange = React.useCallback( const nodeValueChange = useCallback(
(value) => { (value) => {
const nodeNames = value.map(item=>item.host); const nodeNames = value.map(item=>item.host);
setParam((param) => { setParam((param) => {
@ -71,15 +60,15 @@ export default ({
}, },
[param] [param]
); );
const queryParams = React.useMemo(() => {
const bounds = calculateBounds({ const { value: nodes } = useFetch(
from: timeRange.min, `${ESPrefix}/${clusterID}/nodes/realtime`,
to: timeRange.max, {},
}); [clusterID]
let newParams = { );
min: bounds.min.valueOf(),
max: bounds.max.valueOf(), const queryParams = useMemo(() => {
}; const newParams = formatTimeRange(timeRange);
if (param.top) { if (param.top) {
newParams.top = param.top; newParams.top = param.top;
} }
@ -91,40 +80,7 @@ export default ({
} }
return newParams; return newParams;
}, [param, timeRange, bucketSize]); }, [param, timeRange, bucketSize]);
const { loading, error, value } = useFetch(
`${ESPrefix}/${clusterID}/queue_metrics`,
{
queryParams: queryParams,
},
[clusterID, queryParams]
);
const metrics = React.useMemo(() => {
const grpMetrics = _.groupBy(value?.metrics, "group");
let metrics = {};
Object.keys(grpMetrics).forEach((k) => {
metrics[k] = (grpMetrics[k] || [])
.sort((a, b) => a.order - b.order)
});
return metrics;
}, [value]);
const chartRefs = React.useRef();
React.useEffect(() => {
let refs = [];
Object.values(metrics).map((m) => {
m.forEach(() => {
refs.push(React.createRef());
});
});
chartRefs.current = refs;
}, [metrics]);
const { value: nodes } = useFetch(
`${ESPrefix}/${clusterID}/nodes/realtime`,
{},
[clusterID]
);
const formatedNodes = React.useMemo(() => { const formatedNodes = React.useMemo(() => {
if (!nodes) { if (!nodes) {
return []; return [];
@ -137,38 +93,10 @@ export default ({
}); });
}, [nodes]); }, [nodes]);
const pointerUpdate = (event) => {
chartRefs.current.forEach((ref) => {
if (ref.current) {
ref.current.dispatchExternalPointerEvent(event);
}
});
};
const handleChartBrush = ({ x }) => {
if (!x) {
return;
}
let [from, to] = x;
if (typeof handleTimeChange == "function") {
if (to - from < 20 * 1000) {
from -= 10 * 1000;
to += 10 * 1000;
}
handleTimeChange({
start: moment(from).toISOString(),
end: moment(to).toISOString(),
});
}
};
let refIdx = 0;
if (Object.keys(metrics).length == 0) {
return null;
}
return ( return (
<div id="node-metric"> <div id="node-metric">
{showTop ? <div className="px"> {showTop ? (
<div className="px">
<div className="metric-control"> <div className="metric-control">
<div className="selector"> <div className="selector">
<div className="top_radio"> <div className="top_radio">
@ -196,148 +124,45 @@ export default ({
</div> </div>
</div> </div>
</div> </div>
</div>:null} </div>
) : (
""
)}
<div className="px-box"> <div className="px-box">
<div className="px"> <div className="px">
<Skeleton active loading={!value} paragraph={{ rows: 20 }}> {metrics.map((item, i) => {
{//Object.keys(metrics)
gorupOrder.map((e, i) => {
if (!metrics[e]) {
return null;
}
return ( return (
<div key={e} style={{ margin: "8px 0" }}> <div key={item[0]} style={{ margin: "8px 0" }}>
<MetricContainer <MetricContainer
title={formatMessage({ id: `cluster.metrics.group.${e}` })} title={formatMessage({ id: `cluster.metrics.group.${item[0]}` })}
collapsed={false} collapsed={false}
id={e} id={item[0]}
> >
<div className="metric-inner-cnt"> <div className="metric-inner-cnt">
{metrics[e].map((metric) => { {
let axis = metric.axis; item[1].map((metricKey) => (
let lines = metric.lines; <MetricChart
if ( key={metricKey}
lines.length == 0 || timezone={timezone}
(lines && lines[0]?.data?.length == 0) timeRange={timeRange}
) { handleTimeChange={handleTimeChange}
return null; fetchUrl={`${ESPrefix}/${clusterID}/queue_metrics`}
metricKey={metricKey}
title={formatMessage({id:"cluster.metrics.threadpool.axis." + metricKey + ".title"})}
queryParams={queryParams}
className={"metric-item"}
timeout={timeout}
/>
))
} }
let disableHeaderFormat = false;
let headerUnit = "";
let chartTitle = {};
if (lines[0].metric.formatType.toLowerCase == "bytes") {
chartTitle.units = lines[0].metric.formatType;
} else {
chartTitle.units = lines[0].metric.units;
}
chartTitle.title = formatMessage({
id:
"cluster.metrics.threadpool.axis." +
metric.key +
".title",
});
return (
<div key={metric.key} className="metric-item">
<div className={styles.vizChartItemTitle}>
<span>
{chartTitle.title}
{chartTitle.units ? `(${chartTitle.units})` : ""}
</span>
</div>
<Chart
size={[, 200]}
className={styles.vizChartItem}
ref={chartRefs.current[refIdx++]}
>
<Settings
// theme={theme}
pointerUpdateDebounce={0}
pointerUpdateTrigger="x"
// externalPointerEvents={{
// tooltip: { visible: true },
// }}
onPointerUpdate={pointerUpdate}
showLegend
legendPosition={Position.Bottom}
onBrushEnd={handleChartBrush}
tooltip={{
headerFormatter: disableHeaderFormat
? undefined
: ({ value }) =>
`${formatter.full_dates(value)}${
headerUnit ? ` ${headerUnit}` : ""
}`,
}}
debug={false}
/>
<Axis
id="{e}-bottom"
position={Position.Bottom}
showOverlappingTicks
labelFormat={timeRange.timeFormatter}
tickFormat={timeRange.timeFormatter}
ticks={8}
/>
{axis.map((item) => {
return (
<Axis
key={e + "-" + item.id}
id={e + "-" + item.id}
showGridLines={item.showGridLines}
groupId={item.group}
// title={formatMessage({
// id:
// "cluster.metrics.threadpool.axis." +
// metric.key +
// ".title",
// })}
position={item.position}
ticks={item.ticks}
labelFormat={getFormatter(
item.formatType,
item.labelFormat
)}
tickFormat={getFormatter(
item.formatType,
item.tickFormat
)}
/>
);
})}
{lines.map((item) => {
return (
<LineSeries
key={item.metric.label}
id={item.metric.label}
groupId={item.metric.group}
timeZone={timezone}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor={0}
tickFormat={getFormatter(
item.metric.formatType,
item.metric.tickFormat,
item.metric.units
)}
yAccessors={[1]}
data={item.data}
curve={CurveType.CURVE_MONOTONE_X}
/>
);
})}
</Chart>
</div>
);
})}
</div> </div>
</MetricContainer> </MetricContainer>
</div> </div>
); );
})} })}
</Skeleton>
</div> </div>
<Anchor links={gorupOrder}></Anchor> <Anchor links={metrics.map((item) => item[0])}></Anchor>
</div> </div>
</div> </div>
); );