feat: add logs to monitor (#149)
* feat: add logs to cluster's monitor * feat: add logs to node's monitor * chore: update release notes --------- Co-authored-by: yaojiping <yaojiping@infini.ltd>
This commit is contained in:
parent
1e601f259b
commit
8452d8ef3e
|
@ -13,6 +13,8 @@ Information about release notes of INFINI Console is provided here.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- Add Logs to Monitor (cluster, node)
|
||||||
|
|
||||||
### Bug fix
|
### Bug fix
|
||||||
- Fixed the error when querying empty metric data (#144)
|
- Fixed the error when querying empty metric data (#144)
|
||||||
- Fixed empty host when setup step finishes (#147)
|
- Fixed empty host when setup step finishes (#147)
|
||||||
|
|
|
@ -13,6 +13,8 @@ title: "版本历史"
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- 监控(集群、节点)新增日志查询
|
||||||
|
|
||||||
### Bug fix
|
### Bug fix
|
||||||
- 修复指标数据为空时的查询错误 (#144)
|
- 修复指标数据为空时的查询错误 (#144)
|
||||||
- 修复初始化结束步骤中主机显示为错误的问题 (#147)
|
- 修复初始化结束步骤中主机显示为错误的问题 (#147)
|
||||||
|
|
|
@ -119,9 +119,10 @@ const Monitor = (props) => {
|
||||||
setParam({ ...param, timeRange: state.timeRange, timeInterval: state.timeInterval, timeout: state.timeout });
|
setParam({ ...param, timeRange: state.timeRange, timeInterval: state.timeInterval, timeout: state.timeout });
|
||||||
}, [state.timeRange, state.timeInterval, state.timeout]);
|
}, [state.timeRange, state.timeInterval, state.timeout]);
|
||||||
|
|
||||||
const handleTimeChange = useCallback(({ start, end, timeInterval, timeout, refresh }) => {
|
const handleTimeChange = ({ start, end, timeInterval, timeout, refresh }) => {
|
||||||
setState(initState({
|
setState(initState({
|
||||||
...state,
|
...state,
|
||||||
|
param,
|
||||||
timeRange: {
|
timeRange: {
|
||||||
min: start,
|
min: start,
|
||||||
max: end,
|
max: end,
|
||||||
|
@ -130,11 +131,12 @@ const Monitor = (props) => {
|
||||||
timeout: timeout || state.timeout,
|
timeout: timeout || state.timeout,
|
||||||
refresh
|
refresh
|
||||||
}));
|
}));
|
||||||
}, [state])
|
}
|
||||||
|
|
||||||
const onInfoChange = (info) => {
|
const onInfoChange = (info) => {
|
||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
|
param,
|
||||||
info,
|
info,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -180,7 +182,7 @@ const Monitor = (props) => {
|
||||||
onTimeSettingsChange(newRefresh)
|
onTimeSettingsChange(newRefresh)
|
||||||
setRefresh(newRefresh)
|
setRefresh(newRefresh)
|
||||||
}}
|
}}
|
||||||
onRefresh={handleTimeChange}
|
onRefresh={(value) => handleTimeChange({ ...(value || {}), refresh: new Date().valueOf()})}
|
||||||
showTimeSetting={true}
|
showTimeSetting={true}
|
||||||
showTimeInterval={true}
|
showTimeInterval={true}
|
||||||
timeInterval={state.timeInterval}
|
timeInterval={state.timeInterval}
|
||||||
|
@ -194,6 +196,7 @@ const Monitor = (props) => {
|
||||||
})
|
})
|
||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
|
param,
|
||||||
timeInterval: timeSetting.timeInterval,
|
timeInterval: timeSetting.timeInterval,
|
||||||
timeout: timeSetting.timeout
|
timeout: timeSetting.timeout
|
||||||
});
|
});
|
||||||
|
@ -221,7 +224,7 @@ const Monitor = (props) => {
|
||||||
animated={false}
|
animated={false}
|
||||||
>
|
>
|
||||||
{panes.map((pane) => (
|
{panes.map((pane) => (
|
||||||
<TabPane tab={pane.title} key={pane.key}>
|
<TabPane tab={formatMessage({id: `cluster.monitor.tabs.${pane.key}`})} key={pane.key}>
|
||||||
<Spin spinning={spinning && !!state.refresh}>
|
<Spin spinning={spinning && !!state.refresh}>
|
||||||
<StatisticBar
|
<StatisticBar
|
||||||
setSpinning={setSpinning}
|
setSpinning={setSpinning}
|
||||||
|
@ -249,6 +252,7 @@ const Monitor = (props) => {
|
||||||
})
|
})
|
||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
|
param,
|
||||||
timeInterval,
|
timeInterval,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -124,6 +124,21 @@ export default {
|
||||||
"cluster.monitor.topn.color": "Color Metric",
|
"cluster.monitor.topn.color": "Color Metric",
|
||||||
"cluster.monitor.topn.theme": "Theme",
|
"cluster.monitor.topn.theme": "Theme",
|
||||||
|
|
||||||
|
"cluster.monitor.logs.timestamp": "Timestamp",
|
||||||
|
"cluster.monitor.logs.type": "Type",
|
||||||
|
"cluster.monitor.logs.level": "Level",
|
||||||
|
"cluster.monitor.logs.node": "Node",
|
||||||
|
"cluster.monitor.logs.message": "Message",
|
||||||
|
"cluster.monitor.logs.search.placeholder": "Search message",
|
||||||
|
|
||||||
|
"cluster.monitor.tabs.overview": "Overview",
|
||||||
|
"cluster.monitor.tabs.advanced": "Advanced",
|
||||||
|
"cluster.monitor.tabs.topn": "TopN",
|
||||||
|
"cluster.monitor.tabs.logs": "Logs",
|
||||||
|
"cluster.monitor.tabs.nodes": "Nodes",
|
||||||
|
"cluster.monitor.tabs.indices": "Indices",
|
||||||
|
"cluster.monitor.tabs.shards": "Shards",
|
||||||
|
|
||||||
"cluster.metrics.axis.index_throughput.title": "Indexing Rate",
|
"cluster.metrics.axis.index_throughput.title": "Indexing Rate",
|
||||||
"cluster.metrics.axis.search_throughput.title": "Search Rate",
|
"cluster.metrics.axis.search_throughput.title": "Search Rate",
|
||||||
"cluster.metrics.axis.index_latency.title": "Indexing Latency",
|
"cluster.metrics.axis.index_latency.title": "Indexing Latency",
|
||||||
|
@ -371,4 +386,6 @@ export default {
|
||||||
|
|
||||||
"cluster.collect.last_active_at": "Last Active At",
|
"cluster.collect.last_active_at": "Last Active At",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -115,6 +115,21 @@ export default {
|
||||||
"cluster.monitor.topn.color": "颜色指标",
|
"cluster.monitor.topn.color": "颜色指标",
|
||||||
"cluster.monitor.topn.theme": "主题",
|
"cluster.monitor.topn.theme": "主题",
|
||||||
|
|
||||||
|
"cluster.monitor.logs.timestamp": "时间戳",
|
||||||
|
"cluster.monitor.logs.type": "类型",
|
||||||
|
"cluster.monitor.logs.level": "等级",
|
||||||
|
"cluster.monitor.logs.node": "节点",
|
||||||
|
"cluster.monitor.logs.message": "日志",
|
||||||
|
"cluster.monitor.logs.search.placeholder": "搜索日志",
|
||||||
|
|
||||||
|
"cluster.monitor.tabs.overview": "概览",
|
||||||
|
"cluster.monitor.tabs.advanced": "高级",
|
||||||
|
"cluster.monitor.tabs.topn": "TopN",
|
||||||
|
"cluster.monitor.tabs.logs": "日志",
|
||||||
|
"cluster.monitor.tabs.nodes": "节点",
|
||||||
|
"cluster.monitor.tabs.indices": "索引",
|
||||||
|
"cluster.monitor.tabs.shards": "分片",
|
||||||
|
|
||||||
"cluster.metrics.axis.index_throughput.title": "索引吞吐",
|
"cluster.metrics.axis.index_throughput.title": "索引吞吐",
|
||||||
"cluster.metrics.axis.search_throughput.title": "查询吞吐",
|
"cluster.metrics.axis.search_throughput.title": "查询吞吐",
|
||||||
"cluster.metrics.axis.index_latency.title": "索引延迟",
|
"cluster.metrics.axis.index_latency.title": "索引延迟",
|
||||||
|
|
|
@ -666,7 +666,7 @@ const Index = (props) => {
|
||||||
onRangeChange={onTimeChange}
|
onRangeChange={onTimeChange}
|
||||||
{...refresh}
|
{...refresh}
|
||||||
onRefreshChange={setRefresh}
|
onRefreshChange={setRefresh}
|
||||||
onRefresh={({ start, end}) => onTimeChange({ start, end, refresh: new Date().valueOf()})}
|
onRefresh={(value) => onTimeChange({ ...(value || {}), refresh: new Date().valueOf()})}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
onTimeZoneChange={setTimeZone}
|
onTimeZoneChange={setTimeZone}
|
||||||
recentlyUsedRangesKey={'alerting-message'}
|
recentlyUsedRangesKey={'alerting-message'}
|
||||||
|
|
|
@ -333,7 +333,7 @@ const RuleDetail = (props) => {
|
||||||
onRangeChange={handleTimeChange}
|
onRangeChange={handleTimeChange}
|
||||||
{...refresh}
|
{...refresh}
|
||||||
onRefreshChange={setRefresh}
|
onRefreshChange={setRefresh}
|
||||||
onRefresh={({ start, end}) => handleTimeChange({ start, end, refresh: new Date().valueOf()})}
|
onRefresh={(value) => handleTimeChange({ ...(value || {}), refresh: new Date().valueOf()})}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
onTimeZoneChange={setTimeZone}
|
onTimeZoneChange={setTimeZone}
|
||||||
recentlyUsedRangesKey={"rule-detail"}
|
recentlyUsedRangesKey={"rule-detail"}
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default (props) => {
|
||||||
|
|
||||||
const { record, result, options, isGroup, isLock, onReady, bucketSize, isTimeSeries, highlightRange, currentQueries = {}, handleContextMenu, onChartElementClick } = props;
|
const { record, result, options, isGroup, isLock, onReady, bucketSize, isTimeSeries, highlightRange, currentQueries = {}, handleContextMenu, onChartElementClick } = props;
|
||||||
|
|
||||||
const { id, is_stack, is_percent, drilling = {}, series, legend } = record;
|
const { id, is_stack, is_percent, drilling = {}, series, legend, colors } = record;
|
||||||
|
|
||||||
const { metric = {} } = series[0]
|
const { metric = {} } = series[0]
|
||||||
|
|
||||||
|
@ -83,6 +83,13 @@ export default (props) => {
|
||||||
config.yAxis.label.formatter = (value) => `${value * 100}%`
|
config.yAxis.label.formatter = (value) => `${value * 100}%`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (colors) {
|
||||||
|
config.color = Array.isArray(colors) ? colors : (value) => {
|
||||||
|
const { name } = value;
|
||||||
|
return colors[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dataDrillingMenuItem = {
|
const dataDrillingMenuItem = {
|
||||||
type: TYPE_DATA_DRILLING,
|
type: TYPE_DATA_DRILLING,
|
||||||
|
|
|
@ -84,6 +84,7 @@ export const WidgetRender = (props) => {
|
||||||
onResultChange={(res) => {
|
onResultChange={(res) => {
|
||||||
setRequests(Array.isArray(res) ? res.filter((item) => !!item.request).map((item) => item.request) : [])
|
setRequests(Array.isArray(res) ? res.filter((item) => !!item.request).map((item) => item.request) : [])
|
||||||
}}
|
}}
|
||||||
|
refresh={refresh}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
) : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { Empty, Input, Spin, Table } from "antd";
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
import { Link } from "umi";
|
||||||
|
import Logs from "../../components/Logs";
|
||||||
|
|
||||||
|
const AGGS = {
|
||||||
|
"Node":{"terms":{"field":"metadata.labels.node_name", "size": 1000 }},
|
||||||
|
"Type":{"terms":{"field":"metadata.name", "size": 1000 }},
|
||||||
|
"Level":{"terms":{"field":"payload.level", "size": 1000 }},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { clusterID, param = {} } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Logs
|
||||||
|
{...props}
|
||||||
|
aggs={AGGS}
|
||||||
|
queryFilters={[
|
||||||
|
{
|
||||||
|
"term": {
|
||||||
|
"metadata.labels.cluster_id": clusterID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
extraColumns={[
|
||||||
|
{
|
||||||
|
title: formatMessage({ id: "cluster.monitor.logs.node" }),
|
||||||
|
key: "metadata.labels.node_name",
|
||||||
|
dataIndex: "metadata.labels.node_name",
|
||||||
|
width: 88,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (value, record) => {
|
||||||
|
if (!record.metadata?.labels?.node_uuid) return value
|
||||||
|
return (
|
||||||
|
<Link to={`/cluster/monitor/${clusterID}/nodes/${record.metadata.labels.node_uuid}?_g=${encodeURIComponent(JSON.stringify(param))}`}>
|
||||||
|
{value}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -9,11 +9,13 @@ import Monitor from "@/components/Overview/Monitor";
|
||||||
import StatisticBar from "./statistic_bar";
|
import StatisticBar from "./statistic_bar";
|
||||||
import { Empty } from "antd";
|
import { Empty } from "antd";
|
||||||
import TopN from "./TopN";
|
import TopN from "./TopN";
|
||||||
|
import Logs from "./Logs";
|
||||||
|
|
||||||
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: "TopN", component: TopN, key: "topn" },
|
{ title: "TopN", component: TopN, key: "topn" },
|
||||||
|
{ title: "Logs", component: Logs, key: "logs" },
|
||||||
{ title: "Nodes", component: Nodes, key: "nodes" },
|
{ title: "Nodes", component: Nodes, key: "nodes" },
|
||||||
{ title: "Indices", component: Indices, key: "indices" },
|
{ title: "Indices", component: Indices, key: "indices" },
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Empty, Input, Spin, Table } from "antd";
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
import Logs from "../../components/Logs";
|
||||||
|
|
||||||
|
const AGGS = {
|
||||||
|
"Type":{"terms":{"field":"metadata.name", "size": 1000 }},
|
||||||
|
"Level":{"terms":{"field":"payload.level", "size": 1000 }},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { nodeID } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Logs
|
||||||
|
{...props}
|
||||||
|
aggs={AGGS}
|
||||||
|
queryFilters={[
|
||||||
|
{
|
||||||
|
"term": {
|
||||||
|
"metadata.labels.node_uuid": nodeID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -6,10 +6,12 @@ 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";
|
import { connect } from "dva";
|
||||||
|
import Logs from "./Logs";
|
||||||
|
|
||||||
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: "Logs", component: Logs, key: "logs" },
|
||||||
{ title: "Shards", component: Shards, key: "shards" },
|
{ title: "Shards", component: Shards, key: "shards" },
|
||||||
];
|
];
|
||||||
const Page = (props) => {
|
const Page = (props) => {
|
||||||
|
|
|
@ -0,0 +1,323 @@
|
||||||
|
import { Empty, Input, Spin, Table } from "antd";
|
||||||
|
import styles from "./index.less"
|
||||||
|
import DatePicker from "@/common/src/DatePicker";
|
||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { formatESSearchResult, formatTimeRange } from "@/lib/elasticsearch/util";
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
import request from "@/utils/request";
|
||||||
|
import moment from "moment";
|
||||||
|
import { getTimezone } from "@/utils/utils";
|
||||||
|
import ListView from "@/components/ListView";
|
||||||
|
import { ESPrefix } from "@/services/common";
|
||||||
|
import Side from "../Side";
|
||||||
|
import { WidgetRender } from "@/pages/DataManagement/View/WidgetLoader";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import { Link } from "umi";
|
||||||
|
|
||||||
|
const COLORS = {
|
||||||
|
'INFO': '#e8eef2',
|
||||||
|
'WARN': '#e99d43',
|
||||||
|
'ERROR': '#ff3f3f'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { timeRange, isAgent, refresh, aggs, queryFilters = [], extraColumns = [] } = props;
|
||||||
|
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
const timeField = "timestamp";
|
||||||
|
const indexName = ".infini_logs";
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [result, setResult] = useState(false)
|
||||||
|
|
||||||
|
const [queryParams, setQueryParams] = useState({
|
||||||
|
size: 20,
|
||||||
|
from: 0,
|
||||||
|
sort: {
|
||||||
|
[timeField]: 'desc'
|
||||||
|
},
|
||||||
|
keyword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchData = async (queryParams, timeRange, aggs, queryFilters) => {
|
||||||
|
if (!timeRange) return;
|
||||||
|
setLoading(true)
|
||||||
|
const newTimeRange = formatTimeRange(timeRange);
|
||||||
|
const filters = [...queryFilters]
|
||||||
|
if (queryParams?.filters) {
|
||||||
|
Object.keys(queryParams.filters).map((field) => {
|
||||||
|
const values = queryParams.filters[field];
|
||||||
|
if (Array.isArray(values)) {
|
||||||
|
values.forEach((item) => {
|
||||||
|
filters.push({
|
||||||
|
'term': { [field]: item }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (values) {
|
||||||
|
filters.push({
|
||||||
|
'term': { [field]: values }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (queryParams?.keyword) {
|
||||||
|
filters.push({
|
||||||
|
query_string: {
|
||||||
|
query: `*${queryParams.keyword}*`,
|
||||||
|
fields: ['payload.message'],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const res = await request(`${ESPrefix}/infini_default_system_cluster/search/ese?timeout=60m`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
index: indexName,
|
||||||
|
body: {
|
||||||
|
aggs,
|
||||||
|
query: {
|
||||||
|
"bool": {
|
||||||
|
"filter": [
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"timestamp": {
|
||||||
|
"gte": newTimeRange.min,
|
||||||
|
"lte": newTimeRange.max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...filters
|
||||||
|
],
|
||||||
|
"should": [],
|
||||||
|
"must_not": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
size: queryParams.size,
|
||||||
|
from: queryParams.from,
|
||||||
|
sort: queryParams.sort ? Object.keys(queryParams.sort).filter((key) => !!queryParams.sort[key]).map((key) => ({
|
||||||
|
[key]: {
|
||||||
|
"order": queryParams.sort[key]
|
||||||
|
}
|
||||||
|
})) : []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res && !res.error) {
|
||||||
|
const rs = formatESSearchResult(res);
|
||||||
|
setResult(rs)
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTableChange = (pagination, filters, sorter, extra) => {
|
||||||
|
const sortOrder = sorter.order ? sorter.order.replace("end", "") : null;
|
||||||
|
if (queryParams?.sort[sorter.field] !== sortOrder) {
|
||||||
|
setQueryParams((st) => ({ ...st, sort: {
|
||||||
|
...(queryParams.sort || {}),
|
||||||
|
[sorter.field]: sortOrder
|
||||||
|
}}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData(queryParams, timeRange, aggs, queryFilters)
|
||||||
|
}, [JSON.stringify(queryParams), timeRange, JSON.stringify(aggs), JSON.stringify(queryFilters)])
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: formatMessage({ id: "cluster.monitor.logs.timestamp" }),
|
||||||
|
key: timeField,
|
||||||
|
dataIndex: timeField,
|
||||||
|
width: 170,
|
||||||
|
render: (value) =>
|
||||||
|
moment(value)
|
||||||
|
.tz(getTimezone())
|
||||||
|
.format("YYYY-MM-DD HH:mm:ss"),
|
||||||
|
sorter: true,
|
||||||
|
sortOrder: queryParams.sort?.[timeField] ? `${queryParams.sort?.[timeField]}end` : undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: formatMessage({ id: "cluster.monitor.logs.type" }),
|
||||||
|
key: "metadata.name",
|
||||||
|
dataIndex: "metadata.name",
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: formatMessage({ id: "cluster.monitor.logs.level" }),
|
||||||
|
key: "payload.level",
|
||||||
|
dataIndex: "payload.level",
|
||||||
|
width: 88,
|
||||||
|
render: (value, record) => {
|
||||||
|
if (!value) return '-'
|
||||||
|
const colors = {
|
||||||
|
INFO: {
|
||||||
|
background: COLORS.INFO,
|
||||||
|
color: "#959ea0",
|
||||||
|
},
|
||||||
|
WARN: {
|
||||||
|
background: COLORS.WARN,
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
ERROR: {
|
||||||
|
background: COLORS.ERROR,
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 54,
|
||||||
|
height: 18,
|
||||||
|
fontWeight: 600,
|
||||||
|
lineHeight: "18px",
|
||||||
|
textAlign: "center",
|
||||||
|
...(colors[value] || colors[value]),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...extraColumns,
|
||||||
|
{
|
||||||
|
title: formatMessage({ id: "cluster.monitor.logs.message" }),
|
||||||
|
key: "payload.message",
|
||||||
|
dataIndex: "payload.message",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const histogram = {
|
||||||
|
bucket_size: "auto",
|
||||||
|
is_stack: true,
|
||||||
|
format: {
|
||||||
|
type: "number",
|
||||||
|
pattern: "0.00a",
|
||||||
|
},
|
||||||
|
legend: false,
|
||||||
|
colors: COLORS,
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
metric: {
|
||||||
|
formula: "a",
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
field: "payload.level",
|
||||||
|
limit: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
field: "*",
|
||||||
|
name: "a",
|
||||||
|
statistic: "count",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sort: [
|
||||||
|
{
|
||||||
|
direction: "desc",
|
||||||
|
key: "_count",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
queries: {
|
||||||
|
cluster_id: "infini_default_system_cluster",
|
||||||
|
indices: [indexName],
|
||||||
|
time_field: timeField,
|
||||||
|
},
|
||||||
|
type: "date-histogram",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNotEmpty = useMemo(() => {
|
||||||
|
return result?.data?.length > 0
|
||||||
|
}, [result?.data?.length])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<div className={styles.logs} style={isNotEmpty ? {} : { justifyContent: 'center', alignItems: 'center'}}>
|
||||||
|
{
|
||||||
|
isNotEmpty ? (
|
||||||
|
<>
|
||||||
|
<div className={styles.side}>
|
||||||
|
<Side
|
||||||
|
aggs={aggs}
|
||||||
|
data={result?.aggregations || {}}
|
||||||
|
filters={queryParams?.filters || {}}
|
||||||
|
onFacetChange={(v) => {
|
||||||
|
const newFilters = cloneDeep(queryParams.filters) || {}
|
||||||
|
if (!v.value || v.value.length === 0) {
|
||||||
|
delete newFilters[v.field];
|
||||||
|
} else {
|
||||||
|
newFilters[v.field] = v.value;
|
||||||
|
}
|
||||||
|
setQueryParams((st) => ({ ...st, from: 0, filters: newFilters }));
|
||||||
|
}}
|
||||||
|
onReset={() => {
|
||||||
|
setQueryParams((st) => ({ ...st, from: 0, filters: {} }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.result}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<Input.Search
|
||||||
|
style={{ maxWidth: 600 }}
|
||||||
|
placeholder={formatMessage({ id: "cluster.monitor.logs.search.placeholder" })}
|
||||||
|
onSearch={value => {
|
||||||
|
setQueryParams((st) => ({ ...st, from: 0, keyword: value }));
|
||||||
|
}}
|
||||||
|
enterButton
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.histogram}>
|
||||||
|
<WidgetRender
|
||||||
|
widget={histogram}
|
||||||
|
range={{
|
||||||
|
from: timeRange.min,
|
||||||
|
to: timeRange.max,
|
||||||
|
timeField: timeField,
|
||||||
|
}}
|
||||||
|
queryParams={queryParams?.filters || {}}
|
||||||
|
refresh={refresh}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.table}>
|
||||||
|
<Table
|
||||||
|
size={"small"}
|
||||||
|
rowKey={"id"}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={result?.data || []}
|
||||||
|
onChange={onTableChange}
|
||||||
|
pagination={{
|
||||||
|
size: "small",
|
||||||
|
pageSize: queryParams.size,
|
||||||
|
current: Math.ceil(queryParams.from / queryParams.size) + 1,
|
||||||
|
total: result?.total?.value || result?.total || 0,
|
||||||
|
onChange: (page, pageSize) => {
|
||||||
|
setQueryParams((st) => ({
|
||||||
|
...st,
|
||||||
|
from: (page - 1) * st.size,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
showSizeChanger: true,
|
||||||
|
onShowSizeChange: (_, size) => {
|
||||||
|
setQueryParams((st) => ({ ...st, from: 0, size }));
|
||||||
|
},
|
||||||
|
showTotal: (total, range) =>
|
||||||
|
`${range[0]}-${range[1]} of ${total} items`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Spin>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
.logs {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: calc(100vh - 444px);
|
||||||
|
|
||||||
|
.side {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
width: calc(100% - 220px - 12px);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram {
|
||||||
|
width: 100%;
|
||||||
|
height: 140px;
|
||||||
|
border: 1px solid rgb(232, 232, 232);
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { Checkbox, Icon, Input } from "antd";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
import styles from "./SearchFacet.less";
|
||||||
|
|
||||||
|
export default ({ field, label, data = [], onChange, selectedKeys }) => {
|
||||||
|
const [filterData, setFilterData] = useState([]);
|
||||||
|
useEffect(() => {
|
||||||
|
setFilterData(data);
|
||||||
|
}, [data]);
|
||||||
|
const onInputChange = (ev) => {
|
||||||
|
let lowerStr = ev.target.value.toLowerCase();
|
||||||
|
setFilterData(
|
||||||
|
data.filter((item) => {
|
||||||
|
let lowerKey = item.key.toLowerCase();
|
||||||
|
return lowerKey.includes(lowerStr);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectChange = (item, ev) => {
|
||||||
|
if (ev.target.checked) {
|
||||||
|
if (selectedKeys.indexOf(item.key) > -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newKeys = [...selectedKeys, item.key];
|
||||||
|
if (typeof onChange == "function") {
|
||||||
|
onChange({
|
||||||
|
field,
|
||||||
|
value: newKeys,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const newKeys = selectedKeys.filter((key) => key != item.key);
|
||||||
|
if (typeof onChange == "function") {
|
||||||
|
onChange({
|
||||||
|
field,
|
||||||
|
value: newKeys,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
return (
|
||||||
|
<div className={styles.searchFacet}>
|
||||||
|
<div className={`${styles.line} ${styles.label}`}>{label}</div>
|
||||||
|
<div className={styles.line}>
|
||||||
|
<Input
|
||||||
|
placeholder={`${formatMessage({
|
||||||
|
id: "listview.side.filter",
|
||||||
|
})} ${label}`}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.line}>
|
||||||
|
{(showMore ? filterData : filterData.slice(0, 5)).map((item) => {
|
||||||
|
return (
|
||||||
|
<div key={item.key} className={styles.value} title={item.key}>
|
||||||
|
<Checkbox
|
||||||
|
onChange={(v) => {
|
||||||
|
onSelectChange(item, v);
|
||||||
|
}}
|
||||||
|
checked={selectedKeys && selectedKeys.indexOf(item.key) > -1}
|
||||||
|
>
|
||||||
|
{item?.key_as_string ?? item.key}
|
||||||
|
</Checkbox>
|
||||||
|
<div className={styles.count}>{item.doc_count}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!showMore && filterData.length > 5 ? (
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
setShowMore(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type="plus" />
|
||||||
|
more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,30 @@
|
||||||
|
.searchFacet {
|
||||||
|
.line {
|
||||||
|
padding: 4px 0;
|
||||||
|
&.label {
|
||||||
|
text-transform: capitalize;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8b9bad;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
:global {
|
||||||
|
label.ant-checkbox-wrapper {
|
||||||
|
width: 152px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.count {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.85em;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { Icon } from "antd";
|
||||||
|
import { useMemo, useEffect, useState, useCallback } from "react";
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
import SearchFacet from "./SearchFacet";
|
||||||
|
import styles from "./index.less";
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { aggs, data, filters, onFacetChange, onReset } = props;
|
||||||
|
|
||||||
|
const facets = useMemo(() => {
|
||||||
|
const fts = Object.keys(data).map((item) => {
|
||||||
|
return {
|
||||||
|
label: item,
|
||||||
|
field: aggs[item].terms.field,
|
||||||
|
buckets: data[item]?.buckets || [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return fts;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.searchFilter}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
<span>{formatMessage({ id: "listview.side.filter" })}</span>
|
||||||
|
<span
|
||||||
|
className={styles.reload}
|
||||||
|
onClick={onReset}
|
||||||
|
title={formatMessage({ id: "listview.side.filter.reset" })}
|
||||||
|
>
|
||||||
|
<Icon type="reload" style={{ color: "rgba(0, 127, 255, 1)" }} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.facetCnt}>
|
||||||
|
{facets.map((ft) => {
|
||||||
|
if (ft.buckets.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<SearchFacet
|
||||||
|
key={ft.field}
|
||||||
|
label={ft.label}
|
||||||
|
field={ft.field}
|
||||||
|
data={ft.buckets}
|
||||||
|
onChange={onFacetChange}
|
||||||
|
selectedKeys={filters[ft.field] || []}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,18 @@
|
||||||
|
.searchFilter {
|
||||||
|
height: 100%;
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
.reload {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.facetCnt {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue