fix(logs,analysis,alerts): 数据源不可达、分析页超时、告警规则混入基础设施项
=== 1. 主机模式数据源地址 0.0.0.0 → 主机真实 IP(查询页"永远加载中"的根因)===
现象:切换数据源后命名空间/App 下拉不动,一直显示"加载中…",控制台报
Get "http://0.0.0.0:3100/loki/api/v1/labels": dial tcp 0.0.0.0:3100: connect: connection refused
根因:主机模式部署的 endpoint 记的是**容器监听地址** 0.0.0.0:3100,
而平台进程不在被采集主机上,0.0.0.0 只在本机有意义。
EnsureLogSourceForDeployment 把它原样写成了数据源 URL。
修法:
- 新增 reachableEndpoint():把 0.0.0.0 / [::] 换成关联主机的真实 IP
- schemaVersion v3→v4 迁移 fixUnreachableLogSourceURLs(),修正已有脏数据
- 实测:http://0.0.0.0:3100 → http://172.19.16.8:3100,该源 label 查询由 502 变 200
=== 2. 前端:区分"加载中"与"加载失败" ===
原来用 `loading={!options.length}` 表示加载中 —— 数据源不可达时列表永远为空,
于是界面永远转圈,把真实错误完全藏起来了(这次的 0.0.0.0 就是这样被掩盖的)。
改为独立的 namespaceLoading / namespaceError 两个状态:
- 切换数据源先清空旧 options(否则显示上一个源的命名空间,看着像"没反应")
- 失败显示"加载失败"并在旁边给「重试」按钮,不用刷新整页
=== 3. 日志分析页:主图改走索引接口(原来 5 条 count_over_time 全超时)===
实测 astralx(约 370GB/天)15 分钟窗口:
count_over_time → exec 29s + 排队 95s,超过 30s 护栏 → 502
index/volume_range → 0s
改为两阶段渲染:
- 总量趋势改用 /logs/histogram(内部 index/volume_range),**0s 出图**
- 错误/警告/app 维度仍需读日志内容,串行执行,先渲染主图再更新
- heavyQueryTimeout 30s → 90s(实测 error 维度查询 14s 可完成)
=== 4. 告警规则按日志平台定位收敛 ===
- seed 里删掉「Kubernetes 节点 NotReady」「容器 OOMKilled」:
这类属于 kube-state-metrics / Prometheus 的职责,用日志字符串匹配不可靠
- 迁移里清掉线上已存在的同类规则(3 条)
- 修 AlertRule / NotifyChannel 重复 ID(同 logSources 一样漏在 sequences 重建外)
=== 5. 告警页 UI ===
- 规则列表由"每条一张卡"改为紧凑表格(一屏看全,阈值可对齐比较)
- 通知通道(飞书/Lark/Webhook)从「系统设置」搬到告警页折叠区:
规则和"发到哪"是一件事的两半,分两页配容易漏配导致告警发不出去
=== 6. 权限收敛 ===
- /alerts 加 adminOnly:普通用户按查询授权只看「日志查询」,不再看到告警菜单
- 系统设置从侧栏移到右上角齿轮(低频平台配置,不占侧栏一位)
=== 7. 迁移设计教训(写进代码注释)===
v2 时把"告警去重 + 清基础设施规则"塞进 case 1,但线上库已到 v2,
那段代码永远不执行。**结构变更只要逻辑变了、目标库已达该版本,就必须再抬一版**,
不能改历史迁移分支。
=== 实测(v4-1028-2210 / 979bdb4a48e8)===
v4 迁移:数据源 URL 修正 http://0.0.0.0:3100 → http://172.19.16.8:3100
source 1 label:200,0s(astralx / log)
source 2 label:200,0s(不再 connection refused)
分析页 histogram astralx 15m:0s
分析页 metrics error astralx 15m:14s
告警规则:10 条(已无基础设施类)、ID 唯一
另清理 172.19.16.8 上 13 个残留容器,回收 6.1GB
This commit is contained in:
1 parent
1a4c75e28c
commit
95dcd9408c
20 files changed
+941
-87
No files matched your search
@@ -39,7 +39,9 @@ const AnalysisPage = memo(function AnalysisPage() {
|
||||
|
||||
const [sources, setSources] = useState<LogSource[]>([])
|
||||
const [sourceId, setSourceId] = useState(0)
|
||||
const [start, setStart] = useState(() => toLocal(new Date(Date.now() - 3600000)))
|
||||
// 默认 15 分钟:分析页一次性要跑 5 条 count_over_time,
|
||||
// 1 小时窗口在日志量大的命名空间要十几秒;15 分钟既能看出趋势又能快速出结果。
|
||||
const [start, setStart] = useState(() => toLocal(new Date(Date.now() - 15 * 60 * 1000)))
|
||||
const [end, setEnd] = useState(() => toLocal(new Date()))
|
||||
|
||||
/**
|
||||
@@ -49,7 +51,9 @@ const AnalysisPage = memo(function AnalysisPage() {
|
||||
* 现在默认只查业务命名空间,用户可下拉切换;"全部命名空间"作为显式选项并列提示较慢。
|
||||
*/
|
||||
const [namespaces, setNamespaces] = useState<string[]>([])
|
||||
const [namespace, setNamespace] = useState<string>(() => localStorage.getItem('analysisNamespace') ?? '')
|
||||
// 初始为空,等策略返回后再定(不要用 localStorage 覆盖策略:
|
||||
// 策略是服务端下发的权威值,本地记忆会把管理员改过的策略"顶掉")。
|
||||
const [namespace, setNamespace] = useState<string>('')
|
||||
const [namespaceReady, setNamespaceReady] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -65,20 +69,35 @@ const AnalysisPage = memo(function AnalysisPage() {
|
||||
.catch(() => setSources([]))
|
||||
}, [])
|
||||
|
||||
// 拉取可用命名空间;默认优先业务命名空间 astralx,其次列表第一个
|
||||
// 拉取可用命名空间,并把**查询授权策略里配置的命名空间**作为默认值。
|
||||
//
|
||||
// 之前这里硬编码"优先 astralx",等于把环境特征写进代码:
|
||||
// 换环境(策略配的是 default / 别的 ns)就默认选错,用户还得手动改。
|
||||
// 现在默认值来源依次为:URL 记忆 → 授权策略 labels.namespace → default → 列表第一个。
|
||||
useEffect(() => {
|
||||
if (!sourceId) return
|
||||
let cancelled = false
|
||||
logsApi.labelValues(sourceId, 'namespace')
|
||||
.then(list => {
|
||||
Promise.all([
|
||||
logsApi.labelValues(sourceId, 'namespace'),
|
||||
// 策略里配的命名空间(后台「查询授权」可改);失败不影响主流程
|
||||
logsApi.myScope(sourceId).catch(() => null),
|
||||
])
|
||||
.then(([list, scope]) => {
|
||||
if (cancelled) return
|
||||
const values = (list || []).filter(Boolean).sort()
|
||||
const scopeNs = scope?.labels?.namespace ?? ''
|
||||
setNamespaces(values)
|
||||
setNamespace(current => {
|
||||
if (current && (current === ALL_NAMESPACES || values.includes(current))) return current
|
||||
const saved = localStorage.getItem('analysisNamespace')
|
||||
if (saved && (saved === ALL_NAMESPACES || values.includes(saved))) return saved
|
||||
return values.includes('astralx') ? 'astralx' : values[0] ?? ''
|
||||
// 优先级:用户当前已选(同一数据源内不跳变)→ 授权策略 → default → 第一个
|
||||
const candidates = [current, scopeNs, 'default', values[0]]
|
||||
for (const candidate of candidates) {
|
||||
if (!candidate) continue
|
||||
if (candidate === ALL_NAMESPACES) return candidate
|
||||
if (values.includes(candidate)) return candidate
|
||||
}
|
||||
// 策略里配了但 Loki 里暂时没数据时,仍按策略显示(用户能看到"为什么没数据")
|
||||
if (scopeNs) return scopeNs
|
||||
return ''
|
||||
})
|
||||
})
|
||||
.catch(() => { if (!cancelled) setNamespaces([]) })
|
||||
@@ -86,6 +105,11 @@ const AnalysisPage = memo(function AnalysisPage() {
|
||||
return () => { cancelled = true }
|
||||
}, [sourceId])
|
||||
|
||||
// 记住用户手选,下次进入沿用(但不会覆盖策略下发的值)
|
||||
useEffect(() => {
|
||||
if (namespace) localStorage.setItem('analysisNamespace', namespace)
|
||||
}, [namespace])
|
||||
|
||||
// hook 内部把空串当作 {namespace=~".+"}(全部),这里做哨兵转换
|
||||
const scopedNamespace = namespace === ALL_NAMESPACES ? '' : namespace
|
||||
const { data, loading, error, appLabel, refresh } = useAnalysisMetrics({ sourceId, start, end, namespace: scopedNamespace })
|
||||
@@ -106,7 +130,7 @@ const AnalysisPage = memo(function AnalysisPage() {
|
||||
title={t('title')}
|
||||
description={t('subtitle')}
|
||||
action={
|
||||
<Button variant="outline" onClick={refresh} disabled={loading || !sourceId}>
|
||||
<Button variant="outline" onClick={refresh} disabled={!sourceId}>
|
||||
{loading ? <Loader2 className="mr-1 size-4 animate-spin" /> : <RefreshCw className="mr-1 size-4" />}
|
||||
{t('filter.refresh')}
|
||||
</Button>
|
||||
|
||||
Reference in new issue
Block a user