fix(logs): 快捷 LogQL null 崩溃 + app 标签错位 + 查询授权「所有人」与默认策略
=== 一、快捷 LogQL 报 Cannot read properties of null (reading 'map') ===
根因:Loki 对无值的 label 返回 {"data":null},Go nil slice 序列化成 JSON null,
前端 setState(null) 后再 .map() / [...null] / .filter() 直接抛错。
表现为「不稳定」——切到某个没有该 label 值的范围就崩。
修复(前后端双保险):
后端 LokiLabels / LokiLabelValues 归一化 nil → []string{}
(LokiHistogram 原本就返回空切片,无需改)
前端 6 处调用点加 Array.isArray 防御:
QuickQueryTemplates ×3、QueryInput ×2、LokiLabelBrowser ×2
=== 二、选 astralx 只显示 2 个 app(不是权限问题)===
实测:
astralx 117 个 Deployment / 497 个 Pod
带 app 标签的 Pod 只有 2 个(cex-promotion-web / cex-web-h5)
其余 115 个 Deployment 的 Pod 模板没打 app,Kuboard 打的是 k8s.kuboard.cn/name
Loki 里 service_name / container 各有 79 个值(Promtail 从容器名生成)
修复:QuickQueryTemplates 的「app」候选改为三级回退 app → service_name → container,
并用 appLabel 状态记住实际生效的标签名,pod 收窄与 generate() 都用它,
保证生成的 LogQL selector 真能查到数据。
=== 三、查询授权:支持「所有人」+ 内置默认策略 ===
1. 后端 policyGroupAccess:userIds 里的 0 作为哨兵值表示「所有人」(anyone / *),
匹配任意用户(原来只能逐个指定用户 ID)
2. 种子 seedDefaultPolicy():首次启动且尚无任何策略组时,创建
「默认策略(所有人 · default 命名空间)」(labels: namespace=default,userIds: [0])。
管理员可在「查询授权」里把命名空间改成实际业务命名空间(如 astralx),
对该组所有用户立即生效。
3. 前端 PolicyGroupsPage:新增「所有人(anyone)」一键按钮(与具体用户互斥),
列表与编辑区显示「所有人」,校验文案同步更新。
验证:
GET /logs/label/nonexistent_xyz/values → {"data":[]}
GET /logs/label/service_name/values?query={namespace="astralx"} → 79 个
GET /logs/policy-groups → 1 条:默认策略(所有人 · default 命名空间)所有人
镜像:lghost/lokistack:latest + v0.1 = sha256:88a4ba626f62
This commit is contained in:
1 parent
46e73d3d17
commit
4dda6e5ba8
7 files changed
+119
-16
No files matched your search
@@ -24,13 +24,25 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, compact }:
|
||||
const [namespaces, setNamespaces] = useState<string[]>([])
|
||||
const [apps, setApps] = useState<string[]>([])
|
||||
const [pods, setPods] = useState<string[]>([])
|
||||
/**
|
||||
* 实际用来标识「应用」的标签名。
|
||||
*
|
||||
* K8s 惯例是 app 标签,但很多集群(如本环境)Deployment 模板没打 app,
|
||||
* 只有 Kuboard 注入的 k8s.kuboard.cn/name,此时 Loki 里 app 只剩零星几个值。
|
||||
* Promtail 会从容器名生成 service_name / container,覆盖度更高,
|
||||
* 因此按 app → service_name → container 逐级回退。
|
||||
*/
|
||||
const [appLabel, setAppLabel] = useState('app')
|
||||
|
||||
// namespace 列表只加载一次
|
||||
useEffect(() => {
|
||||
if (!sourceId) return
|
||||
let active = true
|
||||
logsApi.labelValues(sourceId, 'namespace')
|
||||
.then(next => { if (active) setNamespaces(next) })
|
||||
// 后端历史版本可能返回 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([]) })
|
||||
return () => { active = false }
|
||||
}, [sourceId])
|
||||
@@ -40,9 +52,26 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, compact }:
|
||||
if (!sourceId) return
|
||||
let active = true
|
||||
const selector = selectorOf({ namespace })
|
||||
logsApi.labelValues(sourceId, 'app', selector)
|
||||
.then(next => { if (active) setApps(next) })
|
||||
.catch(() => { if (active) setApps([]) })
|
||||
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 }) => {
|
||||
if (!active) return
|
||||
setAppLabel(label)
|
||||
setApps(list)
|
||||
})
|
||||
.catch(() => { if (active) { setAppLabel('app'); setApps([]) } })
|
||||
return () => { active = false }
|
||||
}, [sourceId, namespace])
|
||||
|
||||
@@ -50,12 +79,13 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, compact }:
|
||||
useEffect(() => {
|
||||
if (!sourceId) return
|
||||
let active = true
|
||||
const selector = selectorOf({ namespace, app })
|
||||
// 用实际生效的 appLabel 收窄;否则在没打 app 标签的集群上筛不出东西
|
||||
const selector = selectorOf(appLabel === 'app' ? { namespace, app } : { namespace, [appLabel]: app })
|
||||
logsApi.labelValues(sourceId, 'pod', selector)
|
||||
.then(next => { if (active) setPods(next) })
|
||||
.then(next => { if (active) setPods(Array.isArray(next) ? next : []) })
|
||||
.catch(() => { if (active) setPods([]) })
|
||||
return () => { active = false }
|
||||
}, [sourceId, namespace, app])
|
||||
}, [sourceId, namespace, app, appLabel])
|
||||
|
||||
function chooseNamespace(value: string) {
|
||||
setNamespace(value)
|
||||
@@ -73,7 +103,7 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, compact }:
|
||||
let next = extractSelector(query) || '{}'
|
||||
if (template === 'ingress') next = applyLabelFilter(next, 'namespace', namespace.trim() || 'ingress-nginx')
|
||||
else if (namespace.trim()) next = applyLabelFilter(next, 'namespace', namespace.trim())
|
||||
if (app.trim()) next = applyLabelFilter(next, 'app', app.trim())
|
||||
if (app.trim()) next = applyLabelFilter(next, appLabel, app.trim())
|
||||
if (pod.trim()) next = applyLabelFilter(next, 'pod', pod.trim())
|
||||
if (next === '{}') return
|
||||
if (template === 'error') next += ' |= "error"'
|
||||
|
||||
Reference in new issue
Block a user