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
}
const TIMEOUT_CACHE_KEY = "monitor-timeout"
const Monitor = (props) => {
const {
selectedCluster,
formatState,
getBreadcrumbList,
StatisticBar,
@ -61,7 +64,7 @@ const Monitor = (props) => {
timeFormatter: formatter.dates(1),
},
timeInterval: formatTimeInterval(param?.timeInterval),
timeout: formatTimeout(param?.timeout),
timeout: formatTimeout(param?.timeout) || localStorage.getItem(TIMEOUT_CACHE_KEY) || '120s',
param: param,
})
);
@ -104,6 +107,11 @@ const Monitor = (props) => {
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 (
<div>
<BreadcrumbList data={breadcrumbList} />
@ -111,7 +119,7 @@ const Monitor = (props) => {
<Card bodyStyle={{ padding: 15 }}>
<div style={{ marginBottom: 5 }}>
<div style={{ display: 'flex', gap: 8 }}>
<div style={{ flexGrow: 0, minWidth: 400 }}>
<div style={{ flexGrow: 0 }}>
<DatePicker
locale={getLocale()}
start={state.timeRange.min}
@ -128,6 +136,7 @@ const Monitor = (props) => {
showTimeout={true}
timeout={state.timeout}
onTimeSettingChange={(timeSetting) => {
localStorage.setItem(TIMEOUT_CACHE_KEY, timeSetting.timeout)
setState({
...state,
timeInterval: timeSetting.timeInterval,
@ -139,16 +148,6 @@ const Monitor = (props) => {
recentlyUsedRangesKey={'monitor'}
/>
</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>
@ -178,6 +177,8 @@ const Monitor = (props) => {
pane.component
) : (
<pane.component
selectedCluster={selectedCluster}
isAgent={isAgent}
{...state}
handleTimeChange={handleTimeChange}
setSpinning={setSpinning}

View File

@ -347,4 +347,7 @@ export default {
"cluster.providers.aliyun": "Aliyun",
"cluster.providers.tencent-cloud": "Tencent-cloud",
"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.tencent-cloud": "腾讯云",
"cluster.providers.ecloud": "移动云",
"cluster.metrics.request.copy": "复制请求",
"cluster.metrics.request.copy.success": "复制请求成功"
};

View File

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

View File

@ -1,6 +1,6 @@
import { Tabs } from "antd";
import { formatMessage } from "umi/locale";
import { useState } from "react";
import { useMemo, useState } from "react";
import NodeMetric from "../../components/node_metric";
import IndexMetric from "../../components/index_metric";
import ClusterMetric from "../../components/cluster_metric";
@ -10,11 +10,22 @@ import { ESPrefix } from "@/services/common";
const timezone = "local";
export default ({
selectedCluster,
clusterID,
timeRange,
handleTimeChange,
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({
tab: "cluster",
});
@ -48,6 +59,20 @@ export default ({
handleTimeChange={handleTimeChange}
fetchUrl={`${ESPrefix}/${clusterID}/cluster_metrics`}
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
@ -64,6 +89,137 @@ export default ({
param={param}
setParam={setParam}
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
@ -80,6 +236,73 @@ export default ({
param={param}
setParam={setParam}
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
@ -96,6 +319,80 @@ export default ({
param={param}
setParam={setParam}
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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,8 @@ export default ({
timeRange,
handleTimeChange,
shardID,
bucketSize
bucketSize,
timeout
}) => {
const [param, setParam] = useState({
show_top: false,
@ -26,6 +27,73 @@ export default ({
setParam={setParam}
shardID={shardID}
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 StatisticBar from "./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 panes = React.useMemo(()=>{
const panes = [
@ -26,6 +28,7 @@ export default (props) => {
}
return (
<Monitor
selectedCluster={selectedCluster}
formatState={(state) => {
return {
...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";
export default ({
isAgent,
clusterID,
indexName,
timeRange,
handleTimeChange,
shardID,
bucketSize,
timeout
}) => {
let url = `${ESPrefix}/${clusterID}/index/${indexName}/metrics`;
if(shardID){
@ -24,6 +26,15 @@ export default ({
overview={1}
fetchUrl={url}
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 NodeMetric from "../../components/node_metric";
import QueueMetric from "../../components/queue_metric";
@ -7,12 +7,23 @@ import { formatMessage } from "umi/locale";
const timezone = "local";
export default ({
selectedCluster,
clusterID,
nodeID,
timeRange,
handleTimeChange,
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({
show_top: false,
node_name: nodeID,
@ -53,6 +64,137 @@ export default ({
param={param}
setParam={setParam}
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
@ -69,6 +211,80 @@ export default ({
param={param}
setParam={setParam}
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>

View File

@ -5,16 +5,18 @@ import Shards from "./shards";
import { formatMessage } from "umi/locale";
import Monitor from "@/components/Overview/Monitor";
import StatisticBar from "./statistic_bar";
import { connect } from "dva";
const panes = [
{ title: "Overview", component: Overview, key: "overview" },
{ title: "Advanced", component: Advanced, key: "advanced" },
{ title: "Shards", component: Shards, key: "shards" },
];
export default (props) => {
const Page = (props) => {
const { clusterStatus, selectedCluster } = props;
return (
<Monitor
selectedCluster={selectedCluster}
formatState={(state) => {
return {
...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 StatisticBar from "./statistic_bar";
import ClusterMetric from "../../components/cluster_metric";
import { useMemo } from "react";
const timezone = "local";
export default ({
isAgent,
clusterID,
nodeID,
timeRange,
handleTimeChange,
bucketSize,
timeout,
}) => {
return (
<ClusterMetric
timezone={timezone}
timeRange={timeRange}
timeout={timeout}
handleTimeChange={handleTimeChange}
overview={1}
fetchUrl={`${ESPrefix}/${clusterID}/node/${nodeID}/metrics`}
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;
}
.copy {
cursor: pointer;
&:hover {
color: #1890ff;
}
}
.vizChartItem {
background: white !important;

View File

@ -1,298 +1,73 @@
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 { Spin, Radio, Select, Skeleton } from "antd";
import { formatter, getFormatter, getNumFormatter } from "@/utils/format";
import "./node_metric.scss";
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 _ from "lodash";
import { formatTimeRange } from "@/lib/elasticsearch/util";
export default ({
timezone,
timeRange,
handleTimeChange,
overview,
fetchUrl,
renderExtra,
bucketSize
}) => {
if (!fetchUrl) {
export default (props) => {
const { fetchUrl, overview, metrics = [], renderExtra, timeRange, timeout, timezone, bucketSize, handleTimeChange } = props
if (!fetchUrl || metrics.length === 0) {
return null;
}
const queryParams = React.useMemo(() => {
const bounds = calculateBounds({
from: timeRange.min,
to: timeRange.max,
});
let params = {
min: bounds.min.valueOf(),
max: bounds.max.valueOf(),
};
const queryParams = useMemo(() => {
const newParams = formatTimeRange(timeRange);
if (overview) {
params.overview = overview;
newParams.overview = overview;
}
if (bucketSize) {
params.bucket_size = bucketSize
newParams.bucket_size = bucketSize
}
return params;
return newParams;
}, [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;
return (
<div id="cluster-metric">
<div className={styles.metricList}>
<Skeleton active loading={!value} paragraph={{ rows: 20 }}>
{Object.keys(metrics).map((e, i) => {
let axis = metrics[e].axis;
let lines = metrics[e].lines;
if (lines.length == 0 || (lines && lines[0]?.data?.length == 0)) {
return null;
}
let disableHeaderFormat = false;
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;
{metrics.map((metricKey, i) => (
<MetricChart
key={metricKey}
timezone={timezone}
timeRange={timeRange}
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);
}
return line;
});
}
}
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 "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 && (
<div key={"metric_extra"} className={styles.vizChartContainer}>
{extra}
</div>
)
}
</Skeleton>
return metric
}}
/>
))}
{
extra && (
<div key={"metric_extra"} className={styles.vizChartContainer}>
{extra}
</div>
)
}
</div>
</div>
);
};
};

View File

@ -1,51 +1,35 @@
import * as React from "react";
import {
Axis,
Chart,
CurveType,
LineSeries,
BarSeries,
niceTimeFormatByDay,
Position,
ScaleType,
Settings,
timeFormatter,
} from "@elastic/charts";
import IndexSelect from "@/components/IndexSelect";
import useFetch from "@/lib/hooks/use_fetch";
import { ESPrefix } from "@/services/common";
import styles from "./Metrics.scss";
import { Spin, Radio, Select, Skeleton } from "antd";
import { formatter, getFormatter, getNumFormatter } from "@/utils/format";
import { Radio } from "antd";
import "./node_metric.scss";
import { calculateBounds } from "@/components/vendor/data/common/query/timefilter";
import moment from "moment";
import { formatMessage } from "umi/locale";
import MetricContainer from "./metric_container";
import _ from "lodash";
import IndexSelect from "@/components/IndexSelect";
import { formatTimeRange } from "@/lib/elasticsearch/util";
import NodeSelect from "@/components/NodeSelect";
import Anchor from "@/components/Anchor";
import MetricChart from "./MetricChart";
import { useMemo } from "react";
const gorupOrder = [
"operations",
"latency",
"storage",
"document",
"memory",
"cache",
];
export default ({
clusterID,
timezone,
timeRange,
handleTimeChange,
param,
setParam,
shardID,
bucketSize
}) => {
if (!clusterID) {
export default (props) => {
const {
clusterID,
timezone,
timeRange,
handleTimeChange,
param,
setParam,
shardID,
bucketSize,
metrics = [],
timeout
} = props
if (!clusterID || metrics.length == 0) {
return null;
}
const showTop = param.show_top ?? true;
const topChange = (e) => {
@ -68,16 +52,8 @@ export default ({
};
});
};
const queryParams = React.useMemo(() => {
const bounds = calculateBounds({
from: timeRange.min,
to: timeRange.max,
});
let newParams = {
min: bounds.min.valueOf(),
max: bounds.max.valueOf(),
};
console.log(shardID)
const queryParams = useMemo(() => {
const newParams = formatTimeRange(timeRange);
if(shardID){
newParams.shard_id = shardID;
}
@ -92,73 +68,16 @@ export default ({
}
return newParams;
}, [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(
`${ESPrefix}/${clusterID}/_cat/indices`,
{},
[clusterID]
);
const formatedIndices = React.useMemo(() => {
const formatedIndices = useMemo(() => {
return Object.values(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 (
<div id="node-metric">
{showTop ? (
@ -189,179 +108,40 @@ export default ({
<div className="px-box">
<div className="px">
<Skeleton active loading={!value} paragraph={{ rows: 20 }}>
{gorupOrder.map((e, i) => {
if (!metrics[e]) {
return null;
}
{metrics.map((item, i) => {
return (
<div key={e} style={{ margin: "8px 0" }}>
<div key={item[0]} style={{ margin: "8px 0" }}>
<MetricContainer
title={formatMessage({ id: `cluster.metrics.group.${e}` })}
title={formatMessage({ id: `cluster.metrics.group.${item[0]}` })}
collapsed={false}
id={e}
id={item[0]}
>
<div className="metric-inner-cnt">
{metrics[e].map((metric) => {
let axis = metric.axis;
let lines = metric.lines;
if (
lines.length == 0 ||
(lines && lines[0]?.data?.length == 0)
) {
return null;
}
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>
);
})}
{
item[1].map((metricKey) => (
<MetricChart
key={metricKey}
timezone={timezone}
timeRange={timeRange}
handleTimeChange={handleTimeChange}
fetchUrl={`${ESPrefix}/${clusterID}/index_metrics`}
metricKey={metricKey}
title={formatMessage({id: "cluster.metrics.index.axis." + metricKey + ".title"})}
queryParams={queryParams}
className={"metric-item"}
timeout={timeout}
/>
))
}
</div>
</MetricContainer>
</div>
);
})}
</Skeleton>
</div>
<Anchor links={gorupOrder}></Anchor>
<Anchor links={metrics.map((item) => item[0])}></Anchor>
</div>
</div>
);
};
};

View File

@ -1,63 +1,36 @@
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 { ESPrefix } from "@/services/common";
import styles from "./Metrics.scss";
import { Spin, Radio, Select, Skeleton, Row, Col, InputNumber } from "antd";
import { formatter, getFormatter, getNumFormatter } from "@/utils/format";
import { Radio } from "antd";
import "./node_metric.scss";
import { calculateBounds } from "@/components/vendor/data/common/query/timefilter";
import moment from "moment";
import { formatMessage } from "umi/locale";
import MetricContainer from "./metric_container";
import _ from "lodash";
import { formatTimeRange } from "@/lib/elasticsearch/util";
import NodeSelect from "@/components/NodeSelect";
import Anchor from "@/components/Anchor";
import MetricChart from "./MetricChart";
import { useCallback, useMemo } from "react";
const gorupOrder = [
"operations",
"latency",
"system",
"circuit_breaker",
"io",
"transport",
export default (props) => {
"storage",
"document",
"http",
"JVM",
"memory",
"cache",
];
const {
clusterID,
timezone,
timeRange,
handleTimeChange,
param,
setParam,
bucketSize,
timeout,
metrics = []
} = props
export default ({
clusterID,
timezone,
timeRange,
handleTimeChange,
param,
setParam,
bucketSize,
}) => {
// const [filter, setFilter] = React.useState({
// top: "5",
// node_name: param?.transport,
// });
if (!clusterID || metrics.length == 0) {
return null;
}
const showTop = param.show_top ?? true;
const topChange = React.useCallback(
const topChange = useCallback(
(e) => {
// setFilter({
// node_name: undefined,
@ -74,7 +47,7 @@ export default ({
[param]
);
const nodeValueChange = React.useCallback(
const nodeValueChange = useCallback(
(value) => {
const nodeNames = value.map(item=>item.host);
setParam((param) => {
@ -87,8 +60,15 @@ export default ({
},
[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) {
newParams.top = param.top;
}
@ -100,40 +80,7 @@ export default ({
}
return newParams;
}, [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(() => {
if (!nodes) {
return [];
@ -146,36 +93,6 @@ export default ({
});
}, [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 (
<div id="node-metric">
{showTop ? (
@ -214,147 +131,39 @@ export default ({
<div className="px-box">
<div className="px">
<Skeleton active loading={!value} paragraph={{ rows: 20 }}>
{//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;
}
{metrics.map((item, i) => {
return (
<div key={e} style={{ margin: "8px 0" }}>
<div key={item[0]} style={{ margin: "8px 0" }}>
<MetricContainer
title={formatMessage({ id: `cluster.metrics.group.${e}` })}
title={formatMessage({ id: `cluster.metrics.group.${item[0]}` })}
collapsed={false}
id={e}
id={item[0]}
>
<div className="metric-inner-cnt">
{metrics[e].map((metric) => {
let axis = metric.axis;
let lines = metric.lines;
if (
lines.length == 0 ||
(lines && lines[0]?.data?.length == 0)
) {
return null;
}
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>
);
})}
{
item[1].map((metricKey) => (
<MetricChart
key={metricKey}
timezone={timezone}
timeRange={timeRange}
handleTimeChange={handleTimeChange}
fetchUrl={`${ESPrefix}/${clusterID}/node_metrics`}
metricKey={metricKey}
title={formatMessage({id:"cluster.metrics.node.axis." + metricKey + ".title"})}
queryParams={queryParams}
className={"metric-item"}
timeout={timeout}
/>
))
}
</div>
</MetricContainer>
</div>
);
})}
</Skeleton>
</div>
<Anchor links={gorupOrder}></Anchor>
<Anchor links={metrics.map((item) => item[0])}></Anchor>
</div>
</div>
);
};
};

View File

@ -1,52 +1,41 @@
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 { ESPrefix } from "@/services/common";
import styles from "./Metrics.scss";
import { Spin, Radio, Select, Skeleton, Row, Col } from "antd";
import { formatter, getFormatter, getNumFormatter } from "@/utils/format";
import { Radio } from "antd";
import "./node_metric.scss";
import { calculateBounds } from "@/components/vendor/data/common/query/timefilter";
import moment from "moment";
import { formatMessage } from "umi/locale";
import MetricContainer from "./metric_container";
import _ from "lodash";
import { formatTimeRange } from "@/lib/elasticsearch/util";
import NodeSelect from "@/components/NodeSelect";
import Anchor from "@/components/Anchor";
import MetricChart from "./MetricChart";
import { useCallback, useMemo } from "react";
const gorupOrder = [
"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 (props) => {
const {
clusterID,
timezone,
timeRange,
handleTimeChange,
param,
setParam,
bucketSize,
metrics = [],
timeout
} = props
if (!clusterID || metrics.length == 0) {
return null;
}
export default ({
clusterID,
timezone,
timeRange,
handleTimeChange,
param,
setParam,
bucketSize,
}) => {
const showTop = param.show_top ?? true;
const topChange = React.useCallback(
const topChange = useCallback(
(e) => {
// setFilter({
// node_name: undefined,
// top: e.target.value,
// });
setParam((param) => {
delete param["node_name"];
return {
@ -58,7 +47,7 @@ export default ({
[param]
);
const nodeValueChange = React.useCallback(
const nodeValueChange = useCallback(
(value) => {
const nodeNames = value.map(item=>item.host);
setParam((param) => {
@ -71,15 +60,15 @@ export default ({
},
[param]
);
const queryParams = React.useMemo(() => {
const bounds = calculateBounds({
from: timeRange.min,
to: timeRange.max,
});
let newParams = {
min: bounds.min.valueOf(),
max: bounds.max.valueOf(),
};
const { value: nodes } = useFetch(
`${ESPrefix}/${clusterID}/nodes/realtime`,
{},
[clusterID]
);
const queryParams = useMemo(() => {
const newParams = formatTimeRange(timeRange);
if (param.top) {
newParams.top = param.top;
}
@ -91,40 +80,7 @@ export default ({
}
return newParams;
}, [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(() => {
if (!nodes) {
return [];
@ -137,208 +93,77 @@ export default ({
});
}, [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 (
<div id="node-metric">
{showTop ? <div className="px">
<div className="metric-control">
<div className="selector">
<div className="top_radio">
<Radio.Group onChange={topChange} value={param.top}>
<Radio.Button key="5" value="5">
Top5
</Radio.Button>
<Radio.Button key="10" value="10">
Top10
</Radio.Button>
<Radio.Button key="15" value="15">
Top15
</Radio.Button>
<Radio.Button key="20" value="20">
Top20
</Radio.Button>
</Radio.Group>
</div>
<div className="value-selector">
<NodeSelect nodes={formatedNodes}
mode="multiple"
placeholder="Select node"
allowClear
onChange={nodeValueChange}/>
{showTop ? (
<div className="px">
<div className="metric-control">
<div className="selector">
<div className="top_radio">
<Radio.Group onChange={topChange} value={param.top}>
<Radio.Button key="5" value="5">
Top5
</Radio.Button>
<Radio.Button key="10" value="10">
Top10
</Radio.Button>
<Radio.Button key="15" value="15">
Top15
</Radio.Button>
<Radio.Button key="20" value="20">
Top20
</Radio.Button>
</Radio.Group>
</div>
<div className="value-selector">
<NodeSelect nodes={formatedNodes}
mode="multiple"
placeholder="Select node"
allowClear
onChange={nodeValueChange}/>
</div>
</div>
</div>
</div>
</div>:null}
) : (
""
)}
<div className="px-box">
<div className="px">
<Skeleton active loading={!value} paragraph={{ rows: 20 }}>
{//Object.keys(metrics)
gorupOrder.map((e, i) => {
if (!metrics[e]) {
return null;
}
{metrics.map((item, i) => {
return (
<div key={e} style={{ margin: "8px 0" }}>
<div key={item[0]} style={{ margin: "8px 0" }}>
<MetricContainer
title={formatMessage({ id: `cluster.metrics.group.${e}` })}
title={formatMessage({ id: `cluster.metrics.group.${item[0]}` })}
collapsed={false}
id={e}
id={item[0]}
>
<div className="metric-inner-cnt">
{metrics[e].map((metric) => {
let axis = metric.axis;
let lines = metric.lines;
if (
lines.length == 0 ||
(lines && lines[0]?.data?.length == 0)
) {
return null;
}
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>
);
})}
{
item[1].map((metricKey) => (
<MetricChart
key={metricKey}
timezone={timezone}
timeRange={timeRange}
handleTimeChange={handleTimeChange}
fetchUrl={`${ESPrefix}/${clusterID}/queue_metrics`}
metricKey={metricKey}
title={formatMessage({id:"cluster.metrics.threadpool.axis." + metricKey + ".title"})}
queryParams={queryParams}
className={"metric-item"}
timeout={timeout}
/>
))
}
</div>
</MetricContainer>
</div>
);
})}
</Skeleton>
</div>
<Anchor links={gorupOrder}></Anchor>
<Anchor links={metrics.map((item) => item[0])}></Anchor>
</div>
</div>
);
};
};