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
@@ -42,6 +42,12 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, onInsert,
|
||||
* Promtail 会从容器名生成 service_name / container,覆盖度更高,
|
||||
* 因此按 app → service_name → container 逐级回退。
|
||||
*/
|
||||
/** 命名空间加载中(与"加载失败"要分开,否则空列表会一直显示"加载中") */
|
||||
const [namespaceLoading, setNamespaceLoading] = useState(false)
|
||||
/** 命名空间加载失败原因(如数据源地址不可达 connection refused) */
|
||||
const [namespaceError, setNamespaceError] = useState('')
|
||||
/** 重试计数:失败后点「重试」时 +1 触发重新拉取 */
|
||||
const [reloadKey, setReloadKey] = useState(0)
|
||||
const [appLabel, setAppLabel] = useState('app')
|
||||
/** 授权策略里配置的 namespace(如默认策略的 default),用作默认选中值 */
|
||||
const [scopeNamespace, setScopeNamespace] = useState('')
|
||||
@@ -61,18 +67,41 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, onInsert,
|
||||
return () => { active = false }
|
||||
}, [sourceId])
|
||||
|
||||
// namespace 列表只加载一次
|
||||
// namespace 列表:**每次切换数据源都重新加载**。
|
||||
//
|
||||
// 三个必须点:
|
||||
// 1) 先清空旧值:否则切换数据源后仍显示上一个源的命名空间(看着像"没反应")
|
||||
// 2) 记录 loading 与 error 两个状态:原来用 !options.length 表示 loading,
|
||||
// 数据源不可达时列表永远为空 → 界面永远"加载中…",把真实错误藏起来了
|
||||
// (典型错误:主机模式数据源地址是 0.0.0.0 → connection refused)
|
||||
// 3) 失败后可点重试,不用刷新整页
|
||||
useEffect(() => {
|
||||
if (!sourceId) return
|
||||
if (!sourceId) {
|
||||
setNamespaces([])
|
||||
setApps([])
|
||||
setPods([])
|
||||
setNamespaceError('')
|
||||
return
|
||||
}
|
||||
let active = true
|
||||
setNamespaceLoading(true)
|
||||
setNamespaceError('')
|
||||
setNamespaces([])
|
||||
setApps([])
|
||||
setPods([])
|
||||
logsApi.labelValues(sourceId, 'namespace')
|
||||
// 后端历史版本可能返回 null(Loki 无值时 data:null),这里兜底成数组,
|
||||
// 否则 setState(null) 后 datalist 的 .map() 会抛
|
||||
// "Cannot read properties of null (reading 'map')"
|
||||
.then(next => { if (active) setNamespaces(Array.isArray(next) ? next : []) })
|
||||
.catch(() => { if (active) setNamespaces([]) })
|
||||
.catch(err => {
|
||||
if (!active) return
|
||||
setNamespaces([])
|
||||
setNamespaceError(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
.finally(() => { if (active) setNamespaceLoading(false) })
|
||||
return () => { active = false }
|
||||
}, [sourceId])
|
||||
}, [sourceId, reloadKey])
|
||||
|
||||
// app 候选:选了 namespace 就只在该命名空间内搜索
|
||||
useEffect(() => {
|
||||
@@ -204,10 +233,26 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, onInsert,
|
||||
values={namespaceOptions}
|
||||
value={namespace}
|
||||
onChange={chooseNamespace}
|
||||
placeholder={t('templates.selectCount', { count: namespaceOptions.length })}
|
||||
placeholder={
|
||||
namespaceError
|
||||
? t('templates.loadFailed')
|
||||
: namespaceLoading
|
||||
? t('templates.loading')
|
||||
: t('templates.selectCount', { count: namespaceOptions.length })
|
||||
}
|
||||
allowEmpty
|
||||
loading={!namespaceOptions.length}
|
||||
loading={namespaceLoading}
|
||||
/>
|
||||
{namespaceError && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setReloadKey(k => k + 1)}
|
||||
className="self-center text-xs text-destructive underline underline-offset-2"
|
||||
title={namespaceError}
|
||||
>
|
||||
{t('templates.retry')}
|
||||
</button>
|
||||
)}
|
||||
<SearchableValueSelect
|
||||
label={appLabel === 'app' ? t('templates.app') : t('templates.appWithKey', { key: appLabel })}
|
||||
values={apps}
|
||||
|
||||
Reference in new issue
Block a user