diff --git a/docs/content.en/docs/release-notes/_index.md b/docs/content.en/docs/release-notes/_index.md index 17a9e2a0..a4683cf4 100644 --- a/docs/content.en/docs/release-notes/_index.md +++ b/docs/content.en/docs/release-notes/_index.md @@ -13,6 +13,8 @@ Information about release notes of INFINI Console is provided here. ### Features +- Add Logs to Monitor (cluster, node) + ### Bug fix - Fixed the error when querying empty metric data (#144) - Fixed empty host when setup step finishes (#147) diff --git a/docs/content.zh/docs/release-notes/_index.md b/docs/content.zh/docs/release-notes/_index.md index 50e53813..36481fc2 100644 --- a/docs/content.zh/docs/release-notes/_index.md +++ b/docs/content.zh/docs/release-notes/_index.md @@ -13,6 +13,8 @@ title: "版本历史" ### Features +- 监控(集群、节点)新增日志查询 + ### Bug fix - 修复指标数据为空时的查询错误 (#144) - 修复初始化结束步骤中主机显示为错误的问题 (#147) diff --git a/web/src/components/Overview/Monitor/index.jsx b/web/src/components/Overview/Monitor/index.jsx index c34b6af5..c1b006b3 100644 --- a/web/src/components/Overview/Monitor/index.jsx +++ b/web/src/components/Overview/Monitor/index.jsx @@ -119,9 +119,10 @@ const Monitor = (props) => { setParam({ ...param, timeRange: state.timeRange, timeInterval: state.timeInterval, timeout: 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({ ...state, + param, timeRange: { min: start, max: end, @@ -130,11 +131,12 @@ const Monitor = (props) => { timeout: timeout || state.timeout, refresh })); - }, [state]) + } const onInfoChange = (info) => { setState({ ...state, + param, info, }); }; @@ -180,7 +182,7 @@ const Monitor = (props) => { onTimeSettingsChange(newRefresh) setRefresh(newRefresh) }} - onRefresh={handleTimeChange} + onRefresh={(value) => handleTimeChange({ ...(value || {}), refresh: new Date().valueOf()})} showTimeSetting={true} showTimeInterval={true} timeInterval={state.timeInterval} @@ -194,6 +196,7 @@ const Monitor = (props) => { }) setState({ ...state, + param, timeInterval: timeSetting.timeInterval, timeout: timeSetting.timeout }); @@ -221,7 +224,7 @@ const Monitor = (props) => { animated={false} > {panes.map((pane) => ( - + { }) setState({ ...state, + param, timeInterval, }); }} diff --git a/web/src/locales/en-US/cluster.js b/web/src/locales/en-US/cluster.js index a115bb6e..bc6f8c54 100644 --- a/web/src/locales/en-US/cluster.js +++ b/web/src/locales/en-US/cluster.js @@ -124,6 +124,21 @@ export default { "cluster.monitor.topn.color": "Color Metric", "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.search_throughput.title": "Search Rate", "cluster.metrics.axis.index_latency.title": "Indexing Latency", @@ -371,4 +386,6 @@ export default { "cluster.collect.last_active_at": "Last Active At", + + }; diff --git a/web/src/locales/zh-CN/cluster.js b/web/src/locales/zh-CN/cluster.js index a4f4b249..fae3064f 100644 --- a/web/src/locales/zh-CN/cluster.js +++ b/web/src/locales/zh-CN/cluster.js @@ -115,6 +115,21 @@ export default { "cluster.monitor.topn.color": "颜色指标", "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.search_throughput.title": "查询吞吐", "cluster.metrics.axis.index_latency.title": "索引延迟", diff --git a/web/src/pages/Alerting/Message/Index.jsx b/web/src/pages/Alerting/Message/Index.jsx index 812a5f56..beeb6db3 100644 --- a/web/src/pages/Alerting/Message/Index.jsx +++ b/web/src/pages/Alerting/Message/Index.jsx @@ -666,7 +666,7 @@ const Index = (props) => { onRangeChange={onTimeChange} {...refresh} onRefreshChange={setRefresh} - onRefresh={({ start, end}) => onTimeChange({ start, end, refresh: new Date().valueOf()})} + onRefresh={(value) => onTimeChange({ ...(value || {}), refresh: new Date().valueOf()})} timeZone={timeZone} onTimeZoneChange={setTimeZone} recentlyUsedRangesKey={'alerting-message'} diff --git a/web/src/pages/Alerting/Rule/components/RuleDetail.jsx b/web/src/pages/Alerting/Rule/components/RuleDetail.jsx index 5430d2b7..8375809d 100644 --- a/web/src/pages/Alerting/Rule/components/RuleDetail.jsx +++ b/web/src/pages/Alerting/Rule/components/RuleDetail.jsx @@ -333,7 +333,7 @@ const RuleDetail = (props) => { onRangeChange={handleTimeChange} {...refresh} onRefreshChange={setRefresh} - onRefresh={({ start, end}) => handleTimeChange({ start, end, refresh: new Date().valueOf()})} + onRefresh={(value) => handleTimeChange({ ...(value || {}), refresh: new Date().valueOf()})} timeZone={timeZone} onTimeZoneChange={setTimeZone} recentlyUsedRangesKey={"rule-detail"} diff --git a/web/src/pages/DataManagement/View/Widget/widgets/date-histogram/Visualization.jsx b/web/src/pages/DataManagement/View/Widget/widgets/date-histogram/Visualization.jsx index 62b8cbd6..99649992 100644 --- a/web/src/pages/DataManagement/View/Widget/widgets/date-histogram/Visualization.jsx +++ b/web/src/pages/DataManagement/View/Widget/widgets/date-histogram/Visualization.jsx @@ -14,7 +14,7 @@ export default (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] @@ -83,6 +83,13 @@ export default (props) => { config.yAxis.label.formatter = (value) => `${value * 100}%` } } + + if (colors) { + config.color = Array.isArray(colors) ? colors : (value) => { + const { name } = value; + return colors[name]; + } + } const dataDrillingMenuItem = { type: TYPE_DATA_DRILLING, diff --git a/web/src/pages/DataManagement/View/WidgetLoader.jsx b/web/src/pages/DataManagement/View/WidgetLoader.jsx index cd30ef16..dc8b31d5 100644 --- a/web/src/pages/DataManagement/View/WidgetLoader.jsx +++ b/web/src/pages/DataManagement/View/WidgetLoader.jsx @@ -84,6 +84,7 @@ export const WidgetRender = (props) => { onResultChange={(res) => { setRequests(Array.isArray(res) ? res.filter((item) => !!item.request).map((item) => item.request) : []) }} + refresh={refresh} /> ) : diff --git a/web/src/pages/Platform/Overview/Cluster/Monitor/Logs.jsx b/web/src/pages/Platform/Overview/Cluster/Monitor/Logs.jsx new file mode 100644 index 00000000..5cf38e97 --- /dev/null +++ b/web/src/pages/Platform/Overview/Cluster/Monitor/Logs.jsx @@ -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 ( + { + if (!record.metadata?.labels?.node_uuid) return value + return ( + + {value} + + ) + } + }, + ]} + /> + ); +} \ No newline at end of file diff --git a/web/src/pages/Platform/Overview/Cluster/Monitor/index.jsx b/web/src/pages/Platform/Overview/Cluster/Monitor/index.jsx index 1ee45594..fcd3514d 100644 --- a/web/src/pages/Platform/Overview/Cluster/Monitor/index.jsx +++ b/web/src/pages/Platform/Overview/Cluster/Monitor/index.jsx @@ -9,11 +9,13 @@ import Monitor from "@/components/Overview/Monitor"; import StatisticBar from "./statistic_bar"; import { Empty } from "antd"; import TopN from "./TopN"; +import Logs from "./Logs"; const panes = [ { title: "Overview", component: Overview, key: "overview" }, { title: "Advanced", component: Advanced, key: "advanced" }, { title: "TopN", component: TopN, key: "topn" }, + { title: "Logs", component: Logs, key: "logs" }, { title: "Nodes", component: Nodes, key: "nodes" }, { title: "Indices", component: Indices, key: "indices" }, ]; diff --git a/web/src/pages/Platform/Overview/Node/Monitor/Logs.jsx b/web/src/pages/Platform/Overview/Node/Monitor/Logs.jsx new file mode 100644 index 00000000..2efaad62 --- /dev/null +++ b/web/src/pages/Platform/Overview/Node/Monitor/Logs.jsx @@ -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 ( + + ); +} \ No newline at end of file diff --git a/web/src/pages/Platform/Overview/Node/Monitor/index.jsx b/web/src/pages/Platform/Overview/Node/Monitor/index.jsx index 56bb5a0d..8df49f59 100644 --- a/web/src/pages/Platform/Overview/Node/Monitor/index.jsx +++ b/web/src/pages/Platform/Overview/Node/Monitor/index.jsx @@ -6,10 +6,12 @@ import { formatMessage } from "umi/locale"; import Monitor from "@/components/Overview/Monitor"; import StatisticBar from "./statistic_bar"; import { connect } from "dva"; +import Logs from "./Logs"; const panes = [ { title: "Overview", component: Overview, key: "overview" }, { title: "Advanced", component: Advanced, key: "advanced" }, + { title: "Logs", component: Logs, key: "logs" }, { title: "Shards", component: Shards, key: "shards" }, ]; const Page = (props) => { diff --git a/web/src/pages/Platform/Overview/components/Logs/index.jsx b/web/src/pages/Platform/Overview/components/Logs/index.jsx new file mode 100644 index 00000000..022eaa76 --- /dev/null +++ b/web/src/pages/Platform/Overview/components/Logs/index.jsx @@ -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 ( +
+ {value} +
+ ); + }, + }, + ...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 ( + +
+ { + isNotEmpty ? ( + <> +
+ { + 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: {} })); + }} + /> +
+
+
+ { + setQueryParams((st) => ({ ...st, from: 0, keyword: value })); + }} + enterButton + /> +
+
+ +
+
+ { + 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`, + }} + /> + + + + ) : ( + + ) + } + + + ); +} \ No newline at end of file diff --git a/web/src/pages/Platform/Overview/components/Logs/index.less b/web/src/pages/Platform/Overview/components/Logs/index.less new file mode 100644 index 00000000..475def79 --- /dev/null +++ b/web/src/pages/Platform/Overview/components/Logs/index.less @@ -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; + } + } +} \ No newline at end of file diff --git a/web/src/pages/Platform/Overview/components/Side/SearchFacet.jsx b/web/src/pages/Platform/Overview/components/Side/SearchFacet.jsx new file mode 100644 index 00000000..4e807a35 --- /dev/null +++ b/web/src/pages/Platform/Overview/components/Side/SearchFacet.jsx @@ -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 ( +
+
{label}
+
+ +
+
+ {(showMore ? filterData : filterData.slice(0, 5)).map((item) => { + return ( +
+ { + onSelectChange(item, v); + }} + checked={selectedKeys && selectedKeys.indexOf(item.key) > -1} + > + {item?.key_as_string ?? item.key} + +
{item.doc_count}
+
+ ); + })} + {!showMore && filterData.length > 5 ? ( + + ) : null} +
+
+ ); +}; diff --git a/web/src/pages/Platform/Overview/components/Side/SearchFacet.less b/web/src/pages/Platform/Overview/components/Side/SearchFacet.less new file mode 100644 index 00000000..be367094 --- /dev/null +++ b/web/src/pages/Platform/Overview/components/Side/SearchFacet.less @@ -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; + } + } + } +} diff --git a/web/src/pages/Platform/Overview/components/Side/index.jsx b/web/src/pages/Platform/Overview/components/Side/index.jsx new file mode 100644 index 00000000..e20ecddd --- /dev/null +++ b/web/src/pages/Platform/Overview/components/Side/index.jsx @@ -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 ( +
+
+ {formatMessage({ id: "listview.side.filter" })} + + + +
+
+ {facets.map((ft) => { + if (ft.buckets.length == 0) { + return null; + } + return ( + + ); + })} +
+
+ ); +}; diff --git a/web/src/pages/Platform/Overview/components/Side/index.less b/web/src/pages/Platform/Overview/components/Side/index.less new file mode 100644 index 00000000..5ea2c2a7 --- /dev/null +++ b/web/src/pages/Platform/Overview/components/Side/index.less @@ -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; + } +}