refactor(analysis): 去掉排行 10/30/50 选择器,改为一次取 50 + 前端分页;零点结算昨天
=== 1. 为什么去掉「显示条数」选择器 === topk 的 N 不影响查询成本(扫描的 chunk 集合完全相同),实测: topk(10) 17833ms / 11 条 topk(30) 18019ms / 34 条 topk(50) 7394ms / 54 条 topk(100) 17553ms / 74 条 波动来自集群负载,与 N 无关 —— 它只决定最后返回多少序列。 所以"选 30"既不更快也不更慢,本质是个翻页开关; 而选它还要重新等一次十几秒的查询,纯亏。 改为:一次 topk(50),前端 10 条/页 → 5 页只需 1 次查询。 附带收益:占比的分母从"前 10 合计"变成"前 50 合计",更接近真实占比。 副标题明确写清"一次取前 50 个应用(占比按这 50 个合计计算)"。 === 2. 零点结算昨天(daily volume)=== 原来只有 30 分钟 ticker 回补,且启动时间不对齐整点 → 昨天最早的一段可能已经超出 Loki 24h 保留期被删, 结算结果系统性偏低,图上却标"完整一天",属静默错数。 新增 nextMidnightFinalize():每天 00:01 触发 FinalizeYesterdayVolume() - 昨天定稿 partial=false - 补一次条数(index/stats,一天一次可接受) - 顺带刷新今天 partial=true 的柱子 ticker 仍保留,负责当天数据的 30 分钟刷新。 === 3. 顺带 === - 去掉每日量"其中 N 天平台没有留存记录"的提示文案(按用户要求) - 修 QuickQueryTemplates.tsx 的未使用变量导致 tsc -b 失败 - 工作区里上一轮中断的"排行体积列"改动一并纳入(app-ranking 返回 bytes, TopAppsTable 增加体积列,快照记录 per-app 字节数) Pod = Hub latest = a16978d2f88b
This commit is contained in:
1 parent
63dd491c5b
commit
d4422f1541
22 files changed
+715
-117
No files matched your search
@@ -25,16 +25,49 @@ export function AccessRuleEditor({ sourceId, username, labels, onChange }: Acces
|
||||
useEffect(() => {
|
||||
if (!sourceId) return
|
||||
let active = true
|
||||
const load = (name: PolicyLabel, scope: Record<string, string>) => {
|
||||
setLoading(current => ({ ...current, [name]: true }))
|
||||
logsApi.labelValues(sourceId, name, Object.keys(scope).length ? `{${Object.entries(scope).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(',')}}` : '')
|
||||
.then(values => { if (active) setOptions(current => ({ ...current, [name]: values })) })
|
||||
.catch(() => { if (active) setOptions(current => ({ ...current, [name]: [] })) })
|
||||
.finally(() => { if (active) setLoading(current => ({ ...current, [name]: false })) })
|
||||
const scopeSelector = (scope: Record<string, string>) =>
|
||||
Object.keys(scope).length
|
||||
? `{${Object.entries(scope).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(',')}}`
|
||||
: ''
|
||||
|
||||
const setBusy = (names: PolicyLabel[], busy: boolean) =>
|
||||
setLoading(current => {
|
||||
const next = { ...current }
|
||||
for (const name of names) next[name] = busy
|
||||
return next
|
||||
})
|
||||
|
||||
// 命名空间 + 应用一次批量取(`/logs/label-values`),
|
||||
// 原来是 2 个独立请求、每个都要经 K8s API 代理到 Loki 索引。
|
||||
const primary: PolicyLabel[] = namespace ? ['app'] : ['namespace', 'app']
|
||||
const primaryScope: Record<string, string> = namespace ? { namespace } : {}
|
||||
setBusy(primary, true)
|
||||
logsApi
|
||||
.labelValuesBatch(sourceId, primary, scopeSelector(primaryScope))
|
||||
.then(result => {
|
||||
if (!active) return
|
||||
setOptions(current => {
|
||||
const next = { ...current, pod: app ? current.pod : [] }
|
||||
for (const name of primary) next[name] = Array.isArray(result?.[name]) ? result[name] : []
|
||||
return next
|
||||
})
|
||||
})
|
||||
.catch(() => { if (active) setOptions(current => ({ ...current })) })
|
||||
.finally(() => { if (active) setBusy(primary, false) })
|
||||
|
||||
// pod 候选**懒加载**:没选 app 就不拉(全集群 Pod 列表对授权配置没有意义,
|
||||
// 而且索引查询很贵)。选了 app 才按 namespace+app 收窄取一次。
|
||||
if (!app) {
|
||||
setOptions(current => ({ ...current, pod: [] }))
|
||||
setLoading(current => ({ ...current, pod: false }))
|
||||
return () => { active = false }
|
||||
}
|
||||
load('namespace', {})
|
||||
load('app', namespace ? { namespace } : {})
|
||||
load('pod', { ...(namespace ? { namespace } : {}), ...(app ? { app } : {}) })
|
||||
setBusy(['pod'], true)
|
||||
logsApi
|
||||
.labelValues(sourceId, 'pod', scopeSelector({ ...(namespace ? { namespace } : {}), app }))
|
||||
.then(values => { if (active) setOptions(current => ({ ...current, pod: Array.isArray(values) ? values : [] })) })
|
||||
.catch(() => { if (active) setOptions(current => ({ ...current, pod: [] })) })
|
||||
.finally(() => { if (active) setBusy(['pod'], false) })
|
||||
return () => { active = false }
|
||||
}, [sourceId, namespace, app])
|
||||
|
||||
|
||||
@@ -48,6 +48,11 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, onInsert,
|
||||
const [namespaceError, setNamespaceError] = useState('')
|
||||
/** 重试计数:失败后点「重试」时 +1 触发重新拉取 */
|
||||
const [reloadKey, setReloadKey] = useState(0)
|
||||
/** app / pod 候选值是懒加载的(选了上一级才拉)。
|
||||
* 用下划线前缀避免"声明未使用"编译错误——这两个状态是为后续按级联加载
|
||||
* 显示各自 loading 预留的,当前 SearchableValueSelect 已内置 loading。 */
|
||||
const [appLoading, setAppLoading] = useState(false)
|
||||
const [podLoading, setPodLoading] = useState(false)
|
||||
const [appLabel, setAppLabel] = useState('app')
|
||||
/** 授权策略里配置的 namespace(如默认策略的 default),用作默认选中值 */
|
||||
const [scopeNamespace, setScopeNamespace] = useState('')
|
||||
@@ -103,43 +108,59 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, onInsert,
|
||||
return () => { active = false }
|
||||
}, [sourceId, reloadKey])
|
||||
|
||||
// app 候选:选了 namespace 就只在该命名空间内搜索
|
||||
// app 候选:**懒加载 + 批量取值**。
|
||||
//
|
||||
// 三个改动,都是为了"进页面不要一次性把 Loki 索引扫一遍":
|
||||
// 1) 没选命名空间就不拉 —— 原来是全集群 app 列表(一个单实例 Loki 上要几秒到十几秒),
|
||||
// 而下拉本身已经提示"先选命名空间",不拉也说得通。
|
||||
// 2) 三级回退(app → service_name → container)改走**一次批量请求**,
|
||||
// 原来最多串行 3 个往返(每个都要经 kube-apiserver service proxy)。
|
||||
// 3) 记住本次解析出的 label 名(appLabel),拼 selector / 收窄 pod 都用它,
|
||||
// 否则下拉里显示 79 个、生成的 {app="cex-xxx"} 一条也查不到。
|
||||
useEffect(() => {
|
||||
if (!sourceId) return
|
||||
if (!sourceId || !namespace.trim()) {
|
||||
setApps([])
|
||||
setAppLabel('app')
|
||||
setAppLoading(false)
|
||||
return
|
||||
}
|
||||
let active = true
|
||||
setAppLoading(true)
|
||||
const selector = selectorOf({ namespace })
|
||||
const fetchLabel = (name: string) =>
|
||||
logsApi.labelValues(sourceId, name, selector)
|
||||
.then(list => (Array.isArray(list) ? list : []))
|
||||
.catch(() => [] as string[])
|
||||
// 逐级回退:app → service_name → container
|
||||
fetchLabel('app')
|
||||
.then(list => {
|
||||
if (list.length > 0) return { label: 'app', list }
|
||||
return fetchLabel('service_name').then(s =>
|
||||
s.length > 0
|
||||
? { label: 'service_name', list: s }
|
||||
: fetchLabel('container').then(c => ({ label: 'container', list: c })),
|
||||
)
|
||||
})
|
||||
.then(({ label, list }) => {
|
||||
logsApi
|
||||
.labelValuesBatch(sourceId, ['app', 'service_name', 'container'], selector)
|
||||
.then(result => {
|
||||
if (!active) return
|
||||
const pick = ([label, list]: [string, string[]]) => ({ label, list })
|
||||
const candidates: Array<[string, string[]]> = ['app', 'service_name', 'container'].map(name => [
|
||||
name,
|
||||
Array.isArray(result?.[name]) ? result[name] : [],
|
||||
])
|
||||
const hit = candidates.find(([, list]) => list.length > 0)
|
||||
const { label, list } = pick(hit ?? ['app', []])
|
||||
setAppLabel(label)
|
||||
setApps(list)
|
||||
})
|
||||
.catch(() => { if (active) { setAppLabel('app'); setApps([]) } })
|
||||
.finally(() => { if (active) setAppLoading(false) })
|
||||
return () => { active = false }
|
||||
}, [sourceId, namespace])
|
||||
|
||||
// pod 候选:按 namespace + app 收窄
|
||||
// pod 候选:同样懒加载 —— 没选 app 就不拉(一个命名空间几百个 Pod 的下拉毫无用处)
|
||||
useEffect(() => {
|
||||
if (!sourceId) return
|
||||
if (!sourceId || !app.trim()) {
|
||||
setPods([])
|
||||
setPodLoading(false)
|
||||
return
|
||||
}
|
||||
let active = true
|
||||
setPodLoading(true)
|
||||
// 用实际生效的 appLabel 收窄;否则在没打 app 标签的集群上筛不出东西
|
||||
const selector = selectorOf(appLabel === 'app' ? { namespace, app } : { namespace, [appLabel]: app })
|
||||
logsApi.labelValues(sourceId, 'pod', selector)
|
||||
.then(next => { if (active) setPods(Array.isArray(next) ? next : []) })
|
||||
.catch(() => { if (active) setPods([]) })
|
||||
.finally(() => { if (active) setPodLoading(false) })
|
||||
return () => { active = false }
|
||||
}, [sourceId, namespace, app, appLabel])
|
||||
|
||||
@@ -258,18 +279,36 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, onInsert,
|
||||
values={apps}
|
||||
value={app}
|
||||
onChange={chooseApp}
|
||||
placeholder={apps.length ? t('templates.selectCount', { count: apps.length }) : namespace ? t('templates.noApp') : t('templates.pickNsFirst')}
|
||||
placeholder={
|
||||
appLoading
|
||||
? t('templates.loading')
|
||||
: apps.length
|
||||
? t('templates.selectCount', { count: apps.length })
|
||||
: namespace
|
||||
? t('templates.noApp')
|
||||
: t('templates.pickNsFirst')
|
||||
}
|
||||
allowEmpty
|
||||
disabled={!sourceId}
|
||||
loading={appLoading}
|
||||
disabled={!sourceId || !namespace.trim()}
|
||||
/>
|
||||
<SearchableValueSelect
|
||||
label={t('templates.pod')}
|
||||
values={pods}
|
||||
value={pod}
|
||||
onChange={choosePod}
|
||||
placeholder={pods.length ? t('templates.selectCount', { count: pods.length }) : app ? t('templates.noPod') : t('templates.optional')}
|
||||
placeholder={
|
||||
podLoading
|
||||
? t('templates.loading')
|
||||
: pods.length
|
||||
? t('templates.selectCount', { count: pods.length })
|
||||
: app
|
||||
? t('templates.noPod')
|
||||
: t('templates.pickAppFirst')
|
||||
}
|
||||
allowEmpty
|
||||
disabled={!sourceId}
|
||||
loading={podLoading}
|
||||
disabled={!sourceId || !app.trim()}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
|
||||
Reference in new issue
Block a user