Files
LokiStack/backend/services/log_policy.go
T
opsx 4dda6e5ba8 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
2026-10-27 01:19:06 +00:00

178 lines
4.5 KiB
Go

package services
import (
"fmt"
"strings"
"time"
"opsxloki/models"
"opsxloki/repository"
)
func ListLogPolicyGroups() []models.LogPolicyGroup {
db := repository.GetDB()
db.RLock()
defer db.RUnlock()
groups := make([]models.LogPolicyGroup, len(db.LogPolicyGroups))
for i, group := range db.LogPolicyGroups {
groups[i] = group
groups[i].UserIDs = append([]uint(nil), group.UserIDs...)
groups[i].Labels = make(map[string]string, len(group.Labels))
for key, value := range group.Labels {
groups[i].Labels[key] = value
}
}
return groups
}
func validatePolicyGroup(db *repository.JsonDB, group models.LogPolicyGroup, excludeID uint) error {
group.Name = strings.TrimSpace(group.Name)
if group.Name == "" {
return fmt.Errorf("策略组名称必填")
}
if group.SourceID == 0 {
return fmt.Errorf("请选择数据源")
}
foundSource := false
for _, source := range db.LogSources {
if source.ID == group.SourceID {
foundSource = true
break
}
}
if !foundSource {
return fmt.Errorf("数据源不存在")
}
if len(group.UserIDs) == 0 {
return fmt.Errorf("请选择至少一个用户")
}
for key := range group.Labels {
if !ValidLogLabelName(key) {
return fmt.Errorf("无效标签名: %s", key)
}
}
seen := map[uint]bool{}
for _, id := range group.UserIDs {
if id == 0 || seen[id] {
return fmt.Errorf("用户 ID 无效或重复")
}
seen[id] = true
found := false
for _, user := range db.Users {
if user.ID == id && user.Role != "admin" {
found = true
break
}
}
if !found {
return fmt.Errorf("用户 %d 不存在或不是普通用户", id)
}
}
for _, other := range db.LogPolicyGroups {
if other.ID == excludeID || other.SourceID != group.SourceID {
continue
}
if other.Name == group.Name {
return fmt.Errorf("此数据源已有同名策略组")
}
for _, id := range other.UserIDs {
if seen[id] {
return fmt.Errorf("用户 %d 已属于此数据源的另一个策略组", id)
}
}
}
return nil
}
// Group membership replaces legacy per-user grants for the same source.
func removeDirectGrants(db *repository.JsonDB, sourceID uint, userIDs []uint) {
members := map[uint]bool{}
for _, id := range userIDs {
members[id] = true
}
for i := range db.LogSources {
if db.LogSources[i].ID != sourceID {
continue
}
kept := db.LogSources[i].Access[:0]
for _, rule := range db.LogSources[i].Access {
if !members[rule.UserID] {
kept = append(kept, rule)
}
}
db.LogSources[i].Access = kept
return
}
}
func CreateLogPolicyGroup(group models.LogPolicyGroup) (*models.LogPolicyGroup, error) {
db := repository.GetDB()
id := db.NextID("logPolicyGroups")
db.Lock()
defer db.Unlock()
if err := validatePolicyGroup(db, group, 0); err != nil {
return nil, err
}
group.ID = id
group.Name = strings.TrimSpace(group.Name)
group.CreatedAt = time.Now()
group.UpdatedAt = group.CreatedAt
removeDirectGrants(db, group.SourceID, group.UserIDs)
db.LogPolicyGroups = append(db.LogPolicyGroups, group)
db.RequestSave()
return &group, nil
}
func UpdateLogPolicyGroup(id uint, group models.LogPolicyGroup) (*models.LogPolicyGroup, error) {
db := repository.GetDB()
db.Lock()
defer db.Unlock()
for i := range db.LogPolicyGroups {
if db.LogPolicyGroups[i].ID != id {
continue
}
if err := validatePolicyGroup(db, group, id); err != nil {
return nil, err
}
group.ID = id
group.Name = strings.TrimSpace(group.Name)
group.CreatedAt = db.LogPolicyGroups[i].CreatedAt
group.UpdatedAt = time.Now()
removeDirectGrants(db, group.SourceID, group.UserIDs)
db.LogPolicyGroups[i] = group
db.RequestSave()
return &group, nil
}
return nil, fmt.Errorf("策略组不存在")
}
func DeleteLogPolicyGroup(id uint) error {
db := repository.GetDB()
db.Lock()
defer db.Unlock()
for i := range db.LogPolicyGroups {
if db.LogPolicyGroups[i].ID == id {
db.LogPolicyGroups = append(db.LogPolicyGroups[:i], db.LogPolicyGroups[i+1:]...)
db.RequestSave()
return nil
}
}
return fmt.Errorf("策略组不存在")
}
func policyGroupAccess(groups []models.LogPolicyGroup, sourceID, userID uint) *models.LogSourceAccess {
for _, group := range groups {
if group.SourceID != sourceID {
continue
}
for _, id := range group.UserIDs {
// 0 是哨兵值:表示「所有人」(anyone / *),匹配任意用户。
// 用于默认策略——让所有账号开箱即可查询业务命名空间。
if id == 0 || id == userID {
return &models.LogSourceAccess{UserID: userID, Labels: group.Labels}
}
}
}
return nil
}