Files
LokiStack/backend/seed/seed.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

148 lines
4.6 KiB
Go

package seed
import (
"crypto/rand"
"log"
"os"
"path/filepath"
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
)
// InitPasswordFile 首次启动生成的随机管理员密码,存放于数据卷根目录。
// 用户名 admin 首次登录并修改密码后,该文件会被自动删除(见 CleanupInitPassword)。
const InitPasswordFile = "initial-admin-password.txt"
// randomPassword 生成 n 位随机密码(大小写字母 + 数字,排除 0/O/1/l/I 等易混淆字符)
func randomPassword(n int) (string, error) {
const charset = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
buf := make([]byte, n)
if _, err := rand.Read(buf); err != nil {
return "", err
}
for i := range buf {
buf[i] = charset[int(buf[i])%len(charset)]
}
return string(buf), nil
}
func dataDir() string {
if d := os.Getenv("DATA_DIR"); d != "" {
return d
}
return "/app/data"
}
func Seed() {
db := repository.GetDB()
db.RLock()
hasUsers := len(db.Users) > 0
db.RUnlock()
if !hasUsers {
// 优先级:环境变量 ADMIN_INIT_PASSWORD > 随机生成
pwd := os.Getenv("ADMIN_INIT_PASSWORD")
generated := false
if pwd == "" {
var err error
if pwd, err = randomPassword(16); err != nil {
// 不降级到默认口令:宁可启动失败,也不留公开默认密码
log.Fatalf("[SEED] 生成随机初始密码失败: %v;请通过 ADMIN_INIT_PASSWORD 环境变量指定初始密码", err)
}
generated = true
}
hash, _ := crypto.HashPassword(pwd)
repository.GetDB().CreateUser(&models.User{
Username: "admin",
Password: hash,
Nickname: "Administrator",
Role: "admin",
})
if generated {
path := filepath.Join(dataDir(), InitPasswordFile)
if err := os.WriteFile(path, []byte(pwd+"\n"), 0600); err != nil {
log.Printf("[SEED] 写入初始密码文件失败: %v", err)
}
log.Printf("==================================================")
log.Printf(" 首次启动:已生成管理员账号")
log.Printf(" 用户名 : admin")
log.Printf(" 密 码 : %s", pwd)
log.Printf(" 已保存 : %s", path)
log.Printf(" 获取方式:")
log.Printf(" docker compose exec <服务名> cat %s", path)
log.Printf(" kubectl -n <ns> exec deploy/opsx-loki -- cat %s", path)
log.Printf(" ⚠️ 首次登录后请立即修改密码(改密后本文件自动删除)")
log.Printf("==================================================")
} else {
log.Printf("[SEED] 管理员初始密码来自环境变量 ADMIN_INIT_PASSWORD")
}
}
// 内置默认查询策略:所有人可查询 default 命名空间。
// 目的:新环境/新账号开箱即有可查范围,快捷 LogQL 的 namespace 下拉不会空。
seedDefaultPolicy()
// 内置告警规则:有 managed 数据源(部署过 Loki)时补齐通用模板(默认禁用)
SeedAlertRules()
}
// seedDefaultPolicy 创建「默认策略」:所有用户可查询 default 命名空间。
//
// 设计要点:
// - UserIDs 里的 0 是哨兵值,表示「所有人」(anyone / *),见 policyGroupAccess
// - 只在当前还没有任何策略组时创建,避免覆盖管理员的自定义配置
// - 命名空间取 default 作为占位;管理员可在「查询授权」里改成实际业务命名空间
// (如 astralx / prod),改动对该策略组所有用户立即生效
func seedDefaultPolicy() {
db := repository.GetDB()
if db == nil {
return
}
db.RLock()
hasGroup := len(db.LogPolicyGroups) > 0
var sourceID uint
if len(db.LogSources) > 0 {
sourceID = db.LogSources[0].ID
}
db.RUnlock()
if hasGroup || sourceID == 0 {
return
}
id := db.NextID("logPolicyGroups")
now := time.Now()
group := models.LogPolicyGroup{
ID: id,
Name: "默认策略(所有人 · default 命名空间)",
SourceID: sourceID,
Labels: map[string]string{"namespace": "default"},
UserIDs: []uint{0}, // 0 = 所有人
CreatedAt: now,
UpdatedAt: now,
}
db.Lock()
db.LogPolicyGroups = append(db.LogPolicyGroups, group)
db.Unlock()
db.RequestSave()
log.Printf("[SEED] 已创建默认查询策略:所有人可查 default 命名空间(可在「查询授权」改为实际业务命名空间)")
}
// CleanupInitPassword 在 admin 首次修改密码后调用,删除明文密码文件。
// 幂等:文件不存在时静默返回。
func CleanupInitPassword() {
path := filepath.Join(dataDir(), InitPasswordFile)
if _, err := os.Stat(path); err != nil {
return
}
if err := os.Remove(path); err != nil {
log.Printf("[SEED] 删除初始密码文件失败: %v", err)
return
}
log.Printf("[SEED] 初始密码文件已删除: %s", path)
}