diff --git a/src/pages/alertRules/List/EvalRecordsDrawer/index.tsx b/src/pages/alertRules/List/EvalRecordsDrawer/index.tsx new file mode 100644 index 000000000..a300759ec --- /dev/null +++ b/src/pages/alertRules/List/EvalRecordsDrawer/index.tsx @@ -0,0 +1,477 @@ +import React, { useState, useContext, useEffect, useRef } from 'react'; +import { Drawer, Table, Tag, Space, Tooltip, Empty, Button, message, Alert, Typography } from 'antd'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import _ from 'lodash'; +import moment from 'moment'; +import { useTranslation } from 'react-i18next'; +import type { TFunction } from 'i18next'; +import type { ColumnsType } from 'antd/lib/table'; + +import { CommonStateContext, basePrefix } from '@/App'; +import TimeRangePicker, { parseRange, IRawTimeRange } from '@/components/TimeRangePicker'; +import RefreshIcon from '@/components/RefreshIcon'; +import { getAlertRuleEvalRecords, EvalRecord, EvalQueryRecord, EvalRecordsNodeErr } from '@/pages/alertRules/services'; + +import './style.less'; + +export interface Props { + title?: string; + rid?: number; + visible: boolean; + onClose: () => void; +} + +const FETCH_LIMIT = 1000; +const SeverityColor = ['red', 'orange', 'yellow', 'green']; + +// 事件裁决阶段的着色:触发红、等待黄、恢复绿,未产生通知的各类拦截为灰 +const STAGE_COLOR: Record = { + fired: 'red', + pending: 'gold', + recovered: 'green', + push_queue_failed: 'red', + stalled: undefined, + notify_muted: undefined, + muted: undefined, + muted_notify_only: undefined, + muted_by_hook: undefined, + drop_by_pipeline: undefined, + inhibited: undefined, +}; + +// 这些阶段的事件确定已落库到 alert_his_event,/event-detail 才查得到。 +// 其余阶段(pending / muted / drop_by_pipeline / inhibited / push_queue_failed 等) +// 事件从未入队持久化,点进去后端会以 no such alert event 返回 500, +// 所以只展示 hash 文本,不给链接。 +const PERSISTED_STAGES = ['fired', 'recovered', 'stalled', 'notify_muted']; + +function formatValue(v: number) { + if (!_.isFinite(v)) return String(v); + return Math.abs(v) >= 1e6 || (v !== 0 && Math.abs(v) < 1e-4) ? v.toExponential(4) : _.round(v, 4).toString(); +} + +function QueryDetail({ query, t }: { query: EvalQueryRecord; t: TFunction }) { + return ( +
+
+ + {query.ref || '-'} + {query.var_query && {t('eval_records.var_query')}} + {t('eval_records.series_total', { count: query.series_total })} + {query.duration_ms}ms + +
+
{query.query}
+ {query.error &&
{query.error}
} + {_.map(query.warnings, (w, i) => ( +
+ {w} +
+ ))} + {!_.isEmpty(query.series) && ( + JSON.stringify(r.labels)} + pagination={query.series!.length > 10 ? { pageSize: 10, size: 'small', showSizeChanger: false } : false} + dataSource={query.series} + columns={[ + { + title: t('eval_records.labels'), + dataIndex: 'labels', + render: (labels: Record) => ( + + {_.map(labels, (v, k) => ( + + {k}={v} + + ))} + + ), + }, + { + title: t('eval_records.value'), + width: 140, + render: (record: any) => { + const last = _.last(record.points as [number, number][]); + if (!last) return '-'; + return ( + `${moment.unix(p[0]).format('MM-DD HH:mm:ss')} → ${formatValue(p[1])}`).join('\n')} + overlayClassName='eval-records-points-tooltip' + > + {formatValue(last[1])} + + ); + }, + }, + { + title: t('eval_records.point_time'), + width: 160, + render: (record: any) => { + const last = _.last(record.points as [number, number][]); + return last ? moment.unix(last[0]).format('YYYY-MM-DD HH:mm:ss') : '-'; + }, + }, + ]} + /> + )} + + ); +} + +function RecordDetail({ record, t }: { record: EvalRecord; t: TFunction }) { + return ( +
+ {record.error &&
{record.error}
} + {record.truncated && ( +
+ {t('eval_records.truncated')} +
+ )} +
{t('eval_records.detail_query')}
+ {_.isEmpty(record.queries) ? : _.map(record.queries, (q, i) => )} + {!_.isEmpty(record.anomalies) && ( + <> +
{t('eval_records.detail_anomalies')}
+
`${r.key}_${r.severity}_${r.recover ? 1 : 0}_${r.value}`} + pagination={record.anomalies!.length > 10 ? { pageSize: 10, size: 'small', showSizeChanger: false } : false} + dataSource={record.anomalies} + columns={[ + { + title: t('eval_records.labels'), + dataIndex: 'key', + render: (val, r: any) => ( + + S{r.severity} + {r.recover && {t('eval_records.recover')}} + {r.trigger_type === 'nodata' && nodata} + {val} + + ), + }, + { + title: t('eval_records.value'), + dataIndex: 'value', + width: 140, + render: (val) => formatValue(val), + }, + ]} + /> + + )} + {!_.isEmpty(record.events) && ( + <> +
{t('eval_records.detail_events')}
+
`${r.hash}_${r.stage}_${r.detail || ''}`} + pagination={record.events!.length > 10 ? { pageSize: 10, size: 'small', showSizeChanger: false } : false} + dataSource={record.events} + columns={[ + { + title: t('eval_records.event_hash'), + dataIndex: 'hash', + width: 130, + render: (hash: string, r: any) => ( + + {_.includes(PERSISTED_STAGES, r.stage) ? ( + + + {_.truncate(hash, { length: 10, omission: '…' })} + + + ) : ( + {_.truncate(hash, { length: 10, omission: '…' })} + )} + + + ), + }, + { + title: t('eval_records.labels'), + dataIndex: 'tags', + render: (tags: string | undefined, r: any) => ( + + {!!r.severity && S{r.severity}} + {_.map(_.compact(_.split(tags || '', ',,')), (tag, i) => ( + {tag} + ))} + + ), + }, + { + title: t('eval_records.stage'), + dataIndex: 'stage', + width: 120, + render: (stage: string) => ( + {t(`eval_records.stage_${stage}`, { defaultValue: stage })} + ), + }, + { + title: t('eval_records.stage_detail'), + dataIndex: 'detail', + render: (detail: string | undefined) => (detail ? {detail} : -), + }, + ]} + /> + + )} + + ); +} + +export default function EvalRecordsDrawer(props: Props) { + const { t } = useTranslation('alertRules'); + const { datasourceList } = useContext(CommonStateContext); + const { title, rid, visible, onClose } = props; + const [range, setRange] = useState({ start: 'now-1h', end: 'now' }); + const [refreshFlag, setRefreshFlag] = useState(_.uniqueId('refresh_')); + const [loading, setLoading] = useState(false); + const [records, setRecords] = useState([]); + const [hasMore, setHasMore] = useState(false); + const [nodeErrs, setNodeErrs] = useState([]); + const [queriedRange, setQueriedRange] = useState<{ from: number; to: number }>(); + // 请求代次:抽屉常驻挂载,切换规则/时间范围/关闭都可能留下在途请求, + // 慢节点(后端对 edge 节点有 5s 超时)的旧响应回来晚了会覆盖新结果 + const reqIdRef = useRef(0); + + const resetState = () => { + setRecords([]); + setNodeErrs([]); + setHasMore(false); + setQueriedRange(undefined); + }; + + const fetchData = (before?: number) => { + if (!rid) return; + const parsedRange = parseRange(range); + const from = moment(parsedRange.start).unix(); + const to = moment(parsedRange.end).unix(); + const reqId = ++reqIdRef.current; + // 首屏请求先清空上一次的结果,避免请求返回前回显上一条规则的数据 + if (!before) resetState(); + setLoading(true); + getAlertRuleEvalRecords(rid, { + from, + to, + limit: FETCH_LIMIT, + before, + }) + .then((dat) => { + if (reqId !== reqIdRef.current) return; + const list = dat?.list || []; + // 后端游标语义是 ts < before,翻页时传的是 last.ts + 1,因此本页会与上页 + // 边界记录重叠,这里按 rowKey 同款的组合键去重 + setRecords((prev) => (before ? _.uniqBy(_.concat(prev, list), (r) => `${r.datasource_id}_${r.ts}`) : list)); + setHasMore(list.length >= FETCH_LIMIT); + setNodeErrs(dat?.errors || []); + setQueriedRange({ from, to }); + }) + .catch(() => { + if (reqId !== reqIdRef.current) return; + if (!before) resetState(); + }) + .finally(() => { + if (reqId !== reqIdRef.current) return; + setLoading(false); + }); + }; + + useEffect(() => { + if (visible) { + fetchData(); + } else { + // 关闭时让在途请求失效并清理本地状态,避免下次打开先闪一下上一条规则的记录与节点错误 + reqIdRef.current += 1; + resetState(); + setLoading(false); + } + }, [rid, visible, JSON.stringify(range), refreshFlag]); + + const showDatasourceCol = _.uniqBy(records, 'datasource_id').length > 1; + + const columns: ColumnsType = _.compact([ + { + title: t('eval_records.time'), + dataIndex: 'ts', + width: 170, + render: (val) => moment(val).format('YYYY-MM-DD HH:mm:ss'), + }, + showDatasourceCol && { + title: t('eval_records.datasource'), + dataIndex: 'datasource_id', + width: 120, + render: (val) => _.find(datasourceList, { id: val })?.name || (val === 0 ? 'host' : val), + }, + { + title: t('eval_records.queries'), + dataIndex: 'queries', + render: (queries: EvalQueryRecord[] | undefined, record) => { + if (record.error) { + return ( + + {t('eval_records.query_error')} + + ); + } + if (_.isEmpty(queries)) return '-'; + return ( + + {_.map(queries, (q, i) => { + if (q.error) { + return ( + + {q.ref || i}: {t('eval_records.query_error')} + + ); + } + return ( + + {q.ref || i}: {q.series_total === 0 ? t('eval_records.no_series') : t('eval_records.series_total', { count: q.series_total })} + + ); + })} + + ); + }, + }, + { + title: t('eval_records.anomalies'), + dataIndex: 'anomaly_total', + width: 90, + render: (val, record) => { + if (val === 0 && record.recover_total === 0) return 0; + return ( + + {val > 0 && {val}} + {record.recover_total > 0 && ↓{record.recover_total}} + + ); + }, + }, + { + title: t('eval_records.funnel'), + key: 'funnel', + render: (record: EvalRecord) => { + const items = _.compact([ + record.fired > 0 && { label: t('eval_records.funnel_fired'), value: record.fired, color: 'red' }, + record.pending > 0 && { label: t('eval_records.funnel_pending'), value: record.pending, color: 'gold' }, + record.muted > 0 && { label: t('eval_records.funnel_muted'), value: record.muted, color: 'default' }, + record.drop_by_pipeline > 0 && { label: t('eval_records.funnel_dropped'), value: record.drop_by_pipeline, color: 'default' }, + record.inhibited > 0 && { label: t('eval_records.funnel_inhibited'), value: record.inhibited, color: 'default' }, + ]) as { label: string; value: number; color: string }[]; + if (_.isEmpty(items)) return -; + return ( + + {_.map(items, (item, i) => ( + + {item.label} {item.value} + + ))} + + ); + }, + }, + { + title: t('eval_records.duration'), + dataIndex: 'duration_ms', + width: 90, + render: (val) => `${val}ms`, + }, + ]) as ColumnsType; + + return ( + + {t('eval_records.title')} + {title && {title}} + + } + width='75%' + placement='right' + visible={visible} + onClose={onClose} + destroyOnClose + > +
+ + { + setRefreshFlag(_.uniqueId('refresh_')); + }} + /> + + + + + +
+ {!_.isEmpty(nodeErrs) && ( + + {_.map(nodeErrs, (e, i) => ( +
+
+ {e.instance} + {e.datasource_id > 0 ? `(datasource ${e.datasource_id})` : ''}: {e.error} +
+
+ ))} +
{t('eval_records.node_error_hint')}
+ {_.map(_.uniqBy(nodeErrs, 'instance'), (e, i) => ( +
+                  {`curl -u : 'http://${e.instance}/v1/n9e/eval-records?rule_id=${rid}&datasource_id=${e.datasource_id}&from=${queriedRange?.from || ''}&to=${
+                    queriedRange?.to || ''
+                  }'`}
+                
+ ))} + + } + /> + )} +
`${record.datasource_id}_${record.ts}`} + loading={loading} + dataSource={records} + columns={columns} + pagination={{ pageSize: 30, showSizeChanger: false, showTotal: (total) => t('common:table.total', { total }) }} + expandable={{ + expandedRowRender: (record) => , + rowExpandable: () => true, + }} + locale={{ + emptyText: {t('eval_records.empty')}} />, + }} + /> + {hasMore && ( +
+ +
+ )} + + ); +} diff --git a/src/pages/alertRules/List/EvalRecordsDrawer/style.less b/src/pages/alertRules/List/EvalRecordsDrawer/style.less new file mode 100644 index 000000000..769c6b167 --- /dev/null +++ b/src/pages/alertRules/List/EvalRecordsDrawer/style.less @@ -0,0 +1,93 @@ +.eval-records-drawer-subtitle { + font-size: 12px; + font-weight: normal; +} + +.eval-records-detail { + padding: 4px 8px; + + .eval-records-section-title { + font-weight: 600; + margin: 8px 0 4px; + } +} + +.eval-records-query-detail { + margin-bottom: 12px; + + .eval-records-query-header { + margin-bottom: 4px; + } + + .eval-records-query-ql { + background: var(--fc-fill-3); + border-radius: 4px; + padding: 6px 8px; + margin: 0; + font-size: 12px; + white-space: pre-wrap; + word-break: break-all; + } +} + +.eval-records-error { + color: var(--fc-red-11); + background: var(--fc-red-2); + border-radius: 4px; + padding: 4px 8px; + margin: 4px 0; + word-break: break-all; +} + +.eval-records-warning { + color: var(--fc-orange-11); + background: var(--fc-orange-2); + border-radius: 4px; + padding: 4px 8px; + margin: 4px 0; + word-break: break-all; +} + +.eval-records-labels { + .ant-tag { + margin-bottom: 2px; + } +} + +.eval-records-anomaly-key { + word-break: break-all; +} + +.eval-records-points-tooltip { + .ant-tooltip-inner { + white-space: pre-line; + max-height: 300px; + overflow-y: auto; + } +} + +.eval-records-empty-desc { + white-space: pre-line; +} + +.eval-records-node-err { + word-break: break-all; +} + +.eval-records-hash { + font-family: monospace; +} + +.eval-records-stage-detail { + word-break: break-all; +} + +.eval-records-node-err-url { + background: var(--fc-fill-3); + border-radius: 4px; + padding: 4px 8px; + margin: 4px 0 0; + font-size: 12px; + white-space: pre-wrap; + word-break: break-all; +} diff --git a/src/pages/alertRules/List/ListNG.tsx b/src/pages/alertRules/List/ListNG.tsx index 40924d045..dac6a105a 100644 --- a/src/pages/alertRules/List/ListNG.tsx +++ b/src/pages/alertRules/List/ListNG.tsx @@ -24,6 +24,7 @@ import { NS as notificationRulesNS } from '@/pages/notificationRules/constants'; import { AlertRuleType, AlertRuleStatus } from '@/pages/alertRules/types'; import { defaultColumnsConfigs, LOCAL_STORAGE_KEY } from '@/pages/alertRules/List/constants'; import EventsDrawer, { Props as EventsDrawerProps } from '@/pages/alertRules/List/EventsDrawer'; +import EvalRecordsDrawer, { Props as EvalRecordsDrawerProps } from '@/pages/alertRules/List/EvalRecordsDrawer'; import { matchTriggerType, TRIGGER_TYPE_OPTIONS, TriggerType } from '@/pages/alertRules/List/utils'; interface Filter { @@ -92,6 +93,15 @@ export default function AlertRules(props: Props) { }, }); const [notificationRules, setNotificationRules] = useState(); + const [evalRecordsDrawerProps, setEvalRecordsDrawerProps] = useState({ + visible: false, + onClose: () => { + setEvalRecordsDrawerProps((prev) => ({ + ...prev, + visible: false, + })); + }, + }); const columns: ColumnType>[] = _.concat( [ { @@ -520,6 +530,19 @@ export default function AlertRules(props: Props) { window.open(`/alert-rules/edit/${record.id}?mode=clone`, '_blank'); }, }, + { + key: 'eval_records', + icon: 'view', + text: t('eval_records.btn'), + onClick: () => { + setEvalRecordsDrawerProps((prev) => ({ + ...prev, + visible: true, + title: record.name, + rid: record.id, + })); + }, + }, record.cate === 'prometheus' && anomalyEnabled === true ? { key: 'brain', @@ -557,6 +580,7 @@ export default function AlertRules(props: Props) { actionColumn={{ title: t('common:table.operations'), width: 64 }} /> + ); } diff --git a/src/pages/alertRules/locale/en_US.ts b/src/pages/alertRules/locale/en_US.ts index 97fedc8da..9bd2d1674 100644 --- a/src/pages/alertRules/locale/en_US.ts +++ b/src/pages/alertRules/locale/en_US.ts @@ -15,6 +15,54 @@ const en_US = { status_triggered: 'Triggered', status_normal: 'Normal', notify_rule_not_found: 'Corresponding notification rule not found', + eval_records: { + btn: 'Eval records', + title: 'Evaluation records', + tip: 'Raw query data and evaluation results of each evaluation cycle, stored on the local disk of the alert engine, kept for 8 days by default', + time: 'Eval time', + duration: 'Duration', + datasource: 'Data source', + queries: 'Query results', + anomalies: 'Anomalies', + funnel: 'Event handling', + funnel_fired: 'Fired', + funnel_pending: 'Pending (for duration)', + funnel_muted: 'Muted', + funnel_dropped: 'Dropped by pipeline', + funnel_inhibited: 'Inhibited', + series_total: '{{count}} series', + no_series: 'No data', + query_error: 'Query error', + detail_query: 'Queries', + detail_anomalies: 'Evaluation results', + detail_events: 'Event handling details', + event_hash: 'Event', + event_hash_tip: 'View the notification logs after this event was queued (subject to the engine log retention)', + stage: 'Stage', + stage_detail: 'Decision', + stage_fired: 'Fired', + stage_stalled: 'Repeat stalled', + stage_notify_muted: 'Notify muted', + stage_pending: 'Pending (for)', + stage_muted: 'Muted', + stage_muted_notify_only: 'Notify-only muted', + stage_muted_by_hook: 'Muted by hook', + stage_drop_by_pipeline: 'Dropped by pipeline', + stage_inhibited: 'Inhibited', + stage_recovered: 'Recovered', + stage_push_queue_failed: 'Enqueue failed', + labels: 'Labels', + value: 'Value', + point_time: 'Data time', + recover: 'Recovered', + truncated: 'Content exceeded capacity limits, raw series partially truncated', + var_query: 'Variable-expanded query', + empty: 'No evaluation records in this time range.\nRecords are stored on the local disk of the alert engine and kept for 8 days by default; no records for older engines or when EvalLog is disabled.', + load_more: 'Load earlier records', + node_error_title: 'Some alert engine nodes failed to respond; their records are not included in the results', + node_error_hint: + 'Records are still stored on the local disk of the corresponding engine node (e.g. an edge site). Log in to that node and run the following command locally (authenticate with the BasicAuth account configured in HTTP.APIForService on that node):', + }, prod: 'Type', severity: 'Severity', notify_groups: 'Notify groups', diff --git a/src/pages/alertRules/locale/ja_JP.ts b/src/pages/alertRules/locale/ja_JP.ts index abddc21b3..054aff1bf 100644 --- a/src/pages/alertRules/locale/ja_JP.ts +++ b/src/pages/alertRules/locale/ja_JP.ts @@ -15,6 +15,54 @@ const ja_JP = { status_triggered: 'アラート中', status_normal: '正常', notify_rule_not_found: '対応する通知ルールが見つかりません', + eval_records: { + btn: '実行記録', + title: '評価実行記録', + tip: '各評価サイクルで取得した生データと判定結果。アラートエンジンのローカルディスクに保存され、デフォルトで 8 日間保持されます', + time: '評価時刻', + duration: '所要時間', + datasource: 'データソース', + queries: 'クエリ結果', + anomalies: '異常ポイント', + funnel: 'イベント処理', + funnel_fired: '発火', + funnel_pending: '継続時間待ち', + funnel_muted: 'ミュート', + funnel_dropped: 'パイプライン破棄', + funnel_inhibited: '抑制', + series_total: '{{count}} 系列', + no_series: 'データなし', + query_error: 'クエリエラー', + detail_query: 'クエリ詳細', + detail_anomalies: '判定結果', + detail_events: 'イベント処理の詳細', + event_hash: 'イベント', + event_hash_tip: 'このイベントがキュー投入された後の通知処理ログを表示(エンジンのログ保持期間に依存)', + stage: '段階', + stage_detail: '判定内容', + stage_fired: '発火', + stage_stalled: '再通知抑止', + stage_notify_muted: '通知ミュート', + stage_pending: '継続時間待ち', + stage_muted: 'ミュート', + stage_muted_notify_only: '通知のみミュート', + stage_muted_by_hook: 'hook によるミュート', + stage_drop_by_pipeline: 'パイプライン破棄', + stage_inhibited: '抑制', + stage_recovered: '回復', + stage_push_queue_failed: 'キュー投入失敗', + labels: 'ラベル', + value: '値', + point_time: 'データ時刻', + recover: '回復', + truncated: '容量上限を超えたため、生データは一部切り捨てられました', + var_query: '変数展開クエリ', + empty: 'この時間範囲に実行記録はありません。\n記録はアラートエンジンのローカルディスクに保存され、デフォルトで 8 日間保持されます。旧バージョンのエンジンや EvalLog が無効の場合、記録はありません。', + load_more: 'さらに古い記録を読み込む', + node_error_title: '一部のアラートエンジンノードへの照会に失敗したため、以下のノードの記録は結果に含まれていません', + node_error_hint: + '記録は該当エンジンノード(edge 拠点など)のローカルディスクに保存されています。そのノードにログインし、以下のコマンドをローカルで実行して確認できます(認証はそのノードの HTTP.APIForService に設定された BasicAuth アカウント):', + }, prod: '監視タイプ', severity: 'レベル', notify_groups: 'アラート受信グループ', diff --git a/src/pages/alertRules/locale/ru_RU.ts b/src/pages/alertRules/locale/ru_RU.ts index 892d38acf..6973796f1 100644 --- a/src/pages/alertRules/locale/ru_RU.ts +++ b/src/pages/alertRules/locale/ru_RU.ts @@ -15,6 +15,54 @@ const ru_RU = { status_triggered: 'Сработало', status_normal: 'Нет срабатываний', notify_rule_not_found: 'Соответствующее правило уведомления не найдено', + eval_records: { + btn: 'Записи выполнения', + title: 'Записи выполнения оценки', + tip: 'Исходные данные и результаты оценки каждого цикла, хранятся на локальном диске движка алертинга, по умолчанию 8 дней', + time: 'Время оценки', + duration: 'Длительность', + datasource: 'Источник данных', + queries: 'Результаты запросов', + anomalies: 'Аномалии', + funnel: 'Обработка событий', + funnel_fired: 'Сработало', + funnel_pending: 'Ожидание длительности', + funnel_muted: 'Заглушено', + funnel_dropped: 'Отброшено конвейером', + funnel_inhibited: 'Подавлено', + series_total: '{{count}} серий', + no_series: 'Нет данных', + query_error: 'Ошибка запроса', + detail_query: 'Запросы', + detail_anomalies: 'Результаты оценки', + detail_events: 'Детали обработки событий', + event_hash: 'Событие', + event_hash_tip: 'Посмотреть логи обработки уведомлений после постановки события в очередь (в пределах срока хранения логов движка)', + stage: 'Этап', + stage_detail: 'Решение', + stage_fired: 'Сработало', + stage_stalled: 'Повтор отложен', + stage_notify_muted: 'Уведомление заглушено', + stage_pending: 'Ожидание длительности', + stage_muted: 'Заглушено', + stage_muted_notify_only: 'Заглушено только уведомление', + stage_muted_by_hook: 'Заглушено хуком', + stage_drop_by_pipeline: 'Отброшено конвейером', + stage_inhibited: 'Подавлено', + stage_recovered: 'Восстановлено', + stage_push_queue_failed: 'Ошибка постановки в очередь', + labels: 'Метки', + value: 'Значение', + point_time: 'Время данных', + recover: 'Восстановлено', + truncated: 'Содержимое превысило лимит, исходные серии частично усечены', + var_query: 'Запрос с раскрытием переменных', + empty: 'Нет записей выполнения в этом диапазоне времени.\nЗаписи хранятся на локальном диске движка алертинга, по умолчанию 8 дней; для старых версий движка или при отключённом EvalLog записей нет.', + load_more: 'Загрузить более ранние записи', + node_error_title: 'Некоторые узлы движка алертинга не ответили; их записи не включены в результаты', + node_error_hint: + 'Записи по-прежнему хранятся на локальном диске соответствующего узла (например, на площадке edge). Войдите на этот узел и выполните локально следующую команду (аутентификация — учётная запись BasicAuth из HTTP.APIForService этого узла):', + }, prod: 'Тип мониторинга', severity: 'Уровень', notify_groups: 'Группы получателей оповещений', diff --git a/src/pages/alertRules/locale/zh_CN.ts b/src/pages/alertRules/locale/zh_CN.ts index 93101e192..9ab0a26b5 100644 --- a/src/pages/alertRules/locale/zh_CN.ts +++ b/src/pages/alertRules/locale/zh_CN.ts @@ -15,6 +15,53 @@ const zh_CN = { status_triggered: '告警中', status_normal: '无告警', notify_rule_not_found: '未找到对应的通知规则', + eval_records: { + btn: '执行记录', + title: '评估执行记录', + tip: '每个评估周期查到的原始数据与判定结果,存储在告警引擎本地磁盘,默认保留 8 天', + time: '评估时间', + duration: '耗时', + datasource: '数据源', + queries: '查询结果', + anomalies: '异常点', + funnel: '事件处理', + funnel_fired: '触发', + funnel_pending: '等待持续时长', + funnel_muted: '屏蔽', + funnel_dropped: '流水线丢弃', + funnel_inhibited: '抑制', + series_total: '{{count}} 条曲线', + no_series: '无数据', + query_error: '查询错误', + detail_query: '查询现场', + detail_anomalies: '判定结果', + detail_events: '事件处理明细', + event_hash: '事件', + event_hash_tip: '点击查看该事件入队之后的通知处理日志(受引擎日志保留时长限制)', + stage: '阶段', + stage_detail: '裁决说明', + stage_fired: '已触发', + stage_stalled: '重复静默', + stage_notify_muted: '通知屏蔽', + stage_pending: '等待持续时长', + stage_muted: '已屏蔽', + stage_muted_notify_only: '仅屏蔽通知', + stage_muted_by_hook: '被 hook 屏蔽', + stage_drop_by_pipeline: '流水线丢弃', + stage_inhibited: '被抑制', + stage_recovered: '已恢复', + stage_push_queue_failed: '入队失败', + labels: '标签', + value: '值', + point_time: '数据时间', + recover: '恢复', + truncated: '内容超出容量上限,原始曲线已部分截断', + var_query: '变量展开查询', + empty: '该时间范围内没有执行记录。\n记录存储在告警引擎本地磁盘,默认保留 8 天;旧版本引擎或未开启 EvalLog 时无记录。', + load_more: '加载更早的记录', + node_error_title: '部分告警引擎节点查询失败,以下节点的记录未包含在结果中', + node_error_hint: '记录仍保存在对应引擎节点(如 edge 机房)的本地磁盘上。可登录该节点,在本机执行以下命令查看(认证账号为该节点 HTTP.APIForService 配置的 BasicAuth):', + }, prod: '监控类型', severity: '级别', notify_groups: '告警接收组', diff --git a/src/pages/alertRules/locale/zh_HK.ts b/src/pages/alertRules/locale/zh_HK.ts index b68bac9df..5dff0d3e8 100644 --- a/src/pages/alertRules/locale/zh_HK.ts +++ b/src/pages/alertRules/locale/zh_HK.ts @@ -15,6 +15,53 @@ const zh_HK = { status_triggered: '告警中', status_normal: '無告警', notify_rule_not_found: '未找到對應的通知規則', + eval_records: { + btn: '執行記錄', + title: '評估執行記錄', + tip: '每個評估週期查到的原始數據與判定結果,存儲在告警引擎本地磁盤,默認保留 8 天', + time: '評估時間', + duration: '耗時', + datasource: '數據源', + queries: '查詢結果', + anomalies: '異常點', + funnel: '事件處理', + funnel_fired: '觸發', + funnel_pending: '等待持續時長', + funnel_muted: '屏蔽', + funnel_dropped: '流水線丟棄', + funnel_inhibited: '抑制', + series_total: '{{count}} 條曲線', + no_series: '無數據', + query_error: '查詢錯誤', + detail_query: '查詢現場', + detail_anomalies: '判定結果', + detail_events: '事件處理明細', + event_hash: '事件', + event_hash_tip: '點擊查看該事件入隊之後的通知處理日誌(受引擎日誌保留時長限制)', + stage: '階段', + stage_detail: '裁決說明', + stage_fired: '已觸發', + stage_stalled: '重複靜默', + stage_notify_muted: '通知屏蔽', + stage_pending: '等待持續時長', + stage_muted: '已屏蔽', + stage_muted_notify_only: '僅屏蔽通知', + stage_muted_by_hook: '被 hook 屏蔽', + stage_drop_by_pipeline: '流水線丟棄', + stage_inhibited: '被抑制', + stage_recovered: '已恢復', + stage_push_queue_failed: '入隊失敗', + labels: '標籤', + value: '值', + point_time: '數據時間', + recover: '恢復', + truncated: '內容超出容量上限,原始曲線已部分截斷', + var_query: '變量展開查詢', + empty: '該時間範圍內沒有執行記錄。\n記錄存儲在告警引擎本地磁盤,默認保留 8 天;舊版本引擎或未開啟 EvalLog 時無記錄。', + load_more: '加載更早的記錄', + node_error_title: '部分告警引擎節點查詢失敗,以下節點的記錄未包含在結果中', + node_error_hint: '記錄仍保存在對應引擎節點(如 edge 機房)的本地磁盤上。可登錄該節點,在本機執行以下命令查看(認證賬號為該節點 HTTP.APIForService 配置的 BasicAuth):', + }, prod: '監控類型', severity: '級別', notify_groups: '告警接收組', diff --git a/src/pages/alertRules/services.ts b/src/pages/alertRules/services.ts index ece1fd84a..b5270adb1 100644 --- a/src/pages/alertRules/services.ts +++ b/src/pages/alertRules/services.ts @@ -65,3 +65,75 @@ export const getTimezones = (): Promise => { method: RequestMethod.Get, }).then((res) => res.dat); }; + +export interface EvalSeriesSample { + labels: Record; + points: [number, number][]; // [ts(秒), value] +} + +export interface EvalQueryRecord { + ref: string; + query: string; + duration_ms: number; + error?: string; + warnings?: string[]; + series_total: number; + series?: EvalSeriesSample[]; + var_query?: boolean; +} + +export interface EvalAnomalyBrief { + key: string; + value: number; + severity: number; + trigger_type?: string; + recover?: boolean; +} + +// 事件在一个裁决点的结论;同一 hash 同周期可能有多条,按顺序构成处理轨迹 +export interface EvalEventTrail { + hash: string; + tags?: string; + severity?: number; + stage: string; // drop_by_pipeline/muted/muted_notify_only/muted_by_hook/pending/inhibited/fired/stalled/notify_muted/recovered/push_queue_failed + detail?: string; +} + +export interface EvalRecord { + ts: number; // 毫秒 + rule_id: number; + datasource_id: number; + duration_ms: number; + error?: string; + queries?: EvalQueryRecord[]; + anomalies?: EvalAnomalyBrief[]; + events?: EvalEventTrail[]; + anomaly_total: number; + recover_total: number; + fired: number; + muted: number; + drop_by_pipeline: number; + pending: number; + inhibited: number; + truncated?: boolean; +} + +// 单个引擎节点查询失败信息(如 edge 节点不可达),可登录该节点本机访问 /v1/n9e/eval-records 查看 +export interface EvalRecordsNodeErr { + instance: string; + datasource_id: number; + error: string; +} + +// 查询告警规则的评估执行记录(存储于告警引擎本地磁盘,默认保留 8 天) +export const getAlertRuleEvalRecords = ( + id: number, + params: { from?: number; to?: number; before?: number; limit?: number; datasource_id?: number }, + // disabled_instances:未开启 evallog 的引擎节点,后端同时会在 errors 里给出可读原因, + // 便于区分「该节点没开这个功能」与「该时间段确实没有记录」 +): Promise<{ list: EvalRecord[]; instances?: string[]; errors?: EvalRecordsNodeErr[]; disabled_instances?: string[] }> => { + return request(`/api/n9e/alert-rule/${id}/eval-records`, { + method: RequestMethod.Get, + params, + }).then((res) => res.dat); +};