81 Commits
Author SHA1 Message Date
opsx b8eca87a89 fix(alerts): Pro 到期后告警引擎停止评估 + 规则/通道写操作挂 RequirePro
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
用户指出的漏洞:免费期配完规则,到期后若引擎继续跑,告警等于免费功能,Pro 形同虚设;
重装又拿新试用继续白用。修复:
- 引擎每轮先查 IsPro(),非 Pro 整轮跳过(规则与投递历史保留,重新授权即恢复)
- 规则建/改、通道保存、立即检查挂 RequirePro 闸门(读列表/历史保持可见,升级引导有据可依)
- 试用期内 IsPro=true 照常工作(实测建规则 200)
2026-11-01 06:06:10 +08:00
opsx 9a477e9c9b feat(license): 首次安装 14 天 Pro 试用(无需授权码)
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
- 首次启动落试用起点(DATA_DIR/.trial-start 0600 + 库内 settings 双写,取最早防分次续命);
  窗口内 IsPro() 等效 Pro——日志分析/告警/AI 助手/审计全解锁,到期自动回免费版
- 防刷三层:时间戳双写(删任一不重发)+ 机器级指纹(machine-id / hostname+cpu,
  故意不含 DATA_DIR 特征——目录特征会因删除重建漂移等于后门);指纹命中则落已过期起点
- LicenseStatusPayload 返回 trial{active,daysLeft} 与 plan=trial;
  设置页授权区显示「Pro 试用中 · 剩余 N 天」提示条
- 单测:全新安装 14 天/到期回落/删时间戳重装不重发(指纹防刷)三用例
- 滥用评估:删库重装同机器被指纹拦截;换机器/VM 必然可重试(接受,Pro 功能零边际成本,
  追杀到硬件级得不偿失);正式授权随时激活覆盖试用
2026-11-01 05:53:55 +08:00
opsx 8423192719 test(license): 退出授权降级语义测试同步——远端失败清本机+标记而非报错保留
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
2026-11-01 04:08:12 +08:00
opsx daa531f4ed fix(license): 退出授权在拿不到解绑凭证时降级清本机,不再卡死报错
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
根因(实测确认):授权服务 activate 响应不含 activation_token 字段,本机存证里
token 为空;deactivate 端要求该凭证 → 远端永远拒绝 → 用户点退出只会报
「远端解绑失败,本机授权未清除」且无法自愈。
修复:远端解绑失败时降级——清本机授权(回免费版,用户诉求达成),
返回 offlineLocalOnly 提示远端席位需服务端释放;服务端将来补发 token 后
此路径自动回到完整解绑。
2026-11-01 04:06:56 +08:00
opsx cec7c6eeaf feat(license): 退出授权 / 释放本实例(确认后 deactivate)
设置→授权 Pro 态管理员可退出:在线 LIC- 先调 opsx deactivate(含 activation_token),成功才清本地;失败保留现状。LICOFF- 仅解本机并提示后台名额可能仍占。禁静默清码。
2026-11-01 03:16:05 +08:00
opsx f3d27ceb04 feat(license): show Instance ID with copy on settings + ProGate
Always return machine fingerprint instanceId from license status for
offline LICOFF- binding. Shared InstanceIdRow (zh/en) above activate.
2026-11-01 03:12:53 +08:00
opsx 62d9b81a70 fix(license): shared invalid/grace banners + instance copy
Surface expired/over_instance/revoked red banners with CTAs via a
shared LicenseStatusBanners used by settings and ProGate. Hide seats
in product UI; keep HMAC dual-path and never silent-clear keys.
2026-11-01 03:08:37 +08:00
opsx af0298e994 feat(license): dual-path opsx-licence client alongside HMAC
Wire LIC-/LICOFF- activate/verify/heartbeat to licence.rlwj.cn/api,
keep legacy HMAC accepted with deprecation banner, add instance_id
fingerprint and licensing docs (product lokistack still needs admin create).
2026-11-01 02:55:37 +08:00
opsx 5f42d9f696 perf(logs): label 缓存 TTL 30s + 平台变更即时失效(回答"新 app 会不会延迟")
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
问题:加了 60 秒缓存后,新增 app / 新副本 / 新主机采集器要等最久 60 秒
才出现在下拉里,抢修时"刚上线的服务查不到"是不可接受的。

两层处理:

1) TTL 60s → 30s(前后端一致)
   纯 Loki 侧的变更(新 Pod 开始打日志、新副本、主机上新进程写日志)
   最长 30 秒可见。这类变化平台无从感知,只能靠 TTL 兜住。

2) 平台侧变更**立即失效**(不等 TTL)
   新增 InvalidateLabelCache(),在下列入口 defer 调用:
     CollectorService.Create / Update        (主机模式 app 标签来源)
     DeploymentService.Delete                (连带清理托管数据源)
     CreateLogSource / UpdateLogSource / DeleteLogSource
   也就是"用户在平台上做了什么导致候选值集合变化",缓存当场清掉,
   下一个请求必然回源。这正好覆盖"主机采集的 app 标签"这个场景 ——
   采集器一改,下拉里马上就有。

关于「要不要用定时任务预热」:
   不做。预热只能省掉一次 25~40ms 的网络往返,却要多一个常驻任务和
   一份额外的失败路径;而"时效性"问题已经由上面的即时失效解决了。
   如果将来真要削掉这 40ms,更划算的做法是把 label 值随部署一起下发缓存,
   而不是定时轮询 Loki。
2026-10-31 18:27:17 +00:00
opsx 5f42ac5486 perf(logs): 快捷 LogQL 下拉加前后端双层缓存,重复打开不再转圈
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
=== 先测清楚瓶颈在哪 ===
后端单点实测(都很快,不是问题所在):
  /logs/my-scope                    10ms
  /logs/label/namespace/values      25ms
  /logs/label-values?scoped astralx 23ms
  /logs/label/app/values scoped     18ms
结论:慢的是**前端每次都重新走一遍请求链**(切数据源 / 切命名空间 / 重开面板
都会触发),三四次往返串起来 + 每次都有 loading 态,用户看到的就是转圈。

=== 后端:label 候选值短缓存 ===
services/log_query.go:60 秒 TTL,key = sourceId|label|selector|时间窗(按 5 分钟取整),
上限 500 条并做过期清理。理由:下拉值的时效性要求很低——
新上线的 app 晚 1 分钟出现在候选里完全可以接受。
LokiLabelValuesBatch 内部逐个走 LokiLabelValues,所以批量接口自动受益。

=== 前端:内存缓存 + 飞行中请求合并 ===
api/logs/index.ts:60 秒 TTL 的 Map,**缓存 Promise 而不是结果**,
所以并发的相同请求共享同一次飞行(去重);失败立刻剔除缓存,避免缓存错误。
带 AbortSignal 的调用不参与缓存(用户主动取消的请求不应共享给别人)。
额外导出 invalidateLabelCache(),数据源变更时可手动清。

=== 预期效果 ===
  首次打开:与之前相同(~100ms 量级)
  再次切换 / 重开面板:直接命中内存,0 请求
  服务端重复请求:25~40ms → 微秒级
2026-10-31 18:22:38 +00:00
opsx 550d8f4967 refactor(ai): 助手按 AI-AGENT 开发指南对齐(Ask 模式)
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
对照指南逐条评估后落地 4 项真实差距(MCP/Agent Loop/白名单等重型章节不适用,明确排除):
- 上下文预算(指南 07 TrimMessages):历史按 24k 字符预算从最旧裁剪,system 标注
  「更早的对话已省略」;抽 trimHistory 独立函数+单测(超预算丢最旧/小历史不裁)
- 配额超限诚实终态(指南 05.3/06 禁止伪装 completed):不再 400 报错,返回提示
  文案;前端渲染为 warning 提示条样式,对话上下文保留
- 回复脱敏(指南 08):assistant 回复同样过 RedactTextOnly,堵住模型复述样本
  带回敏感串的通道
- 退避重试(指南 05.2):429/5xx/网络错误指数退避 1s→2s 最多 2 次(计入总超时),
  鉴权/参数错不重试
2026-11-01 01:59:17 +08:00
opsx 448f695c28 fix(ai): 推理模型 content 为空时回退 reasoning_content
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
根因(带响应体日志实测确认):deepseek-v4-pro 等推理系模型把输出写在
reasoning_content,MaxTokens 较小时最终 content 为空串,被误判为「模型返回了
空内容(检查 baseURL)」——与 key/端点无关,保存的 key 解密与沿用逻辑均正常
(空参/掩码传参两路均能取到库内真值)。
修复:content 空白时回退使用 reasoning_content;移除调试日志。
2026-11-01 01:50:44 +08:00
opsx fbbd3a3c27 refactor(ai): 砍掉 /aiops 整页,模型配置迁入系统设置,浮窗助手保留
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
评估结论落地:整页分析只喂聚合统计(模型看不到日志原文),结论价值低于 /analysis
图表自身;浮窗助手携带真实查询样本,价值成立。
- 删 /aiops 页面与路由(菜单去项);llmProviders 迁 lib 共用
- 模型配置重做为设置页「AI 模型接入」区:状态严谨映射——供应商/模型按已存
  baseUrl/model 正确回显(自定义供应商显示 BaseURL 与自定义模型输入,不再重置),
  API Key 掩码占位、留空或掩码保存均沿用旧值(不回传掩码)
- 后端删 /ai/analyze 与信号采集链路;/ai/chat 保留(上下文由前端日志样本注入)
2026-11-01 00:46:38 +08:00
opsx c216bdfdd4 feat(ai): 助手浮窗聚焦日志查询页,携带当前查询结果对话
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
- 浮球只在 /logs 渲染(产品定位:辅助分析正在查看的查询结果,其它页面不打扰)
- /logs 查询成功后写入 AI 上下文:LogQL、时间范围、结果条数、error 行数、
  最新 20 条样本(去 CRI 包装);对话请求自动携带
- 后端 /ai/chat 接收 context:截断 6000 字 + RedactTextOnly 统一脱敏后并入 system
  (ECHO 假模型实测注入完整);空态文案改为查询导向
2026-11-01 00:31:48 +08:00
opsx 5d898e3592 fix(ai): 测试连接改为结构化结果,不再倾倒模型原始回复
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
返回 {model, latencyMs, tokensUsed, replyLength};前端提示「连接正常:模型 · 耗时 · tokens」。
模型原始回复不回传——测试要的是通没通/多快,不是看模型说了什么(ECHO 假模型下尤其明显)。
空回复时明确报错提示检查 baseURL 是否 OpenAI 兼容。
2026-11-01 00:21:59 +08:00
opsx f1d9480caf fix(ai): 浮窗助手提示词调优 — 身份固定/风格收敛/话题引导/禁编数据
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
实测(经 ECHO 假模型检验注入):被问模型身份时答固定一句(底层模型由管理员配置指定,
不自行猜测 DeepSeek 版本),并引导回排查;回答限 5 句、优先动作;无关话题礼貌拒绝;
引用数字仅限注入的日志概况。
2026-11-01 00:18:39 +08:00
opsx f457d6da09 feat(ai): 全站 AI 助手浮窗(参考 FlowStack 右下角浮空设计)
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
- POST /ai/chat:多轮对话(messages 透传)+ 页面上下文注入 —— 前端带当前路径,
  后端自动附加该数据源近 15 分钟轻量日志摘要(总量/Top3 应用/告警命中),
  追问「这个报错什么原因」有数据依据;脱敏/每日配额/Pro 闸门与整页分析共用
- AiAssistant 浮球+浮窗挂 Layout:仅 admin+Pro 渲染;消息气泡、Enter 发送、
  上下文随路由自动采集;免费版不显示
2026-10-31 23:46:33 +08:00
opsx 9641503007 feat(license): Pro 授权机制 — 离线许可码 + 免费版功能闸门
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
免费版:日志查询/部署管理/采集/用户管理/LDAP/设置;Pro(¥199 个人单实例永久 / ¥499 团队多实例永久):
日志分析、日志告警、智能运维(AI)、审计日志。
- 后端:HMAC-SHA256 离线许可码(签发 CLI / license --plan --edition --seats --customer --days;
  永久=days 0),GET /system/license + POST activate;RequirePro 闸门挂 metrics/ai/audit;
  过期自动回落免费;测试覆盖签发-校验/篡改/免费拒签
- 前端:菜单 Pro 项带锁标;四个 Pro 页面免费版渲染升级引导卡(功能清单+定价+页内激活);
  设置页新增授权区(版本/席位/到期/激活入口);license 词典中英
- domains 注册提升到 main.tsx 全局(修复 license 命名空间在未引 domains 的页面显示裸键)
2026-10-31 23:08:06 +08:00
opsx c090ba0564 feat(aiops): 长窗口(>1h)分析必须指定应用
全命名空间 6h/24h 聚合数据量过大会拖垮 Loki:前端时间范围旁新增应用范围选择器
(labelValues 候选),>1h 未选应用禁用开始分析并提示;后端同样强制 400(闸门在服务端);
信号采集选择器按 app 收窄后 histogram/stats 从全量变为单应用流,秒级返回。
2026-10-31 22:51:55 +08:00
opsx 7be41f0a6b feat(aiops): 新增「智能运维」——AI 日志分析(P0,不扫日志内容)
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
=== 为什么这样设计(成本优先)===
全集群 370GB/天,把日志原文丢给模型是不可能的(一次几百万 token)。
所以整条链路只做一件事:**把 GB 级日志压成 KB 级信号**。

P0 的信号来源全部是索引/快照,**零日志内容扫描**:
  体积 + 趋势   → index/volume_range(实测 24h 仅 54ms)
  条数/流数     → index/stats,且只在 ≤2 小时窗口用
                  (全天 index/stats 实测 20s,绝不能进 AI 路径)
  应用排行      → 平台小时级预聚合快照(毫秒)
  基线对比      → 上一个等长窗口的 volume_range
  告警          → 平台投递记录
实测一次分析:3~6 秒、输入 600~1400 tokens(约 ¥0.005/次)。

=== 后端 ===
models/ai.go            AIConfig(APIKey 加密存储)+ AIAnalysisRun(审计)
services/ai_config.go   读写配置、掩码返回、每日 token 用量统计
services/ai_provider.go OpenAI 兼容客户端 + 连通性测试
                        (DeepSeek/通义/内网 vLLM/Ollama 通用)
services/ai_redact.go   脱敏:凭据/邮箱/手机号/身份证 → 占位符,
                        **保留内网 IP 与端口**(排查必需)
services/ai_signal.go   信号采集 + 渲染成紧凑文本(省 token)
services/ai_analyze.go  结构化输出解析、配额闸门、审计落库
controllers/ai.go       /ai/config /ai/config/test /ai/analyze /ai/runs

成本闸门(三层):
  1) 单次:时间范围 ≤24h、maxTokens ≤1500
  2) 每用户每日 token 上限(默认 50 万,可配,超限直接拒绝并说明)
  3) 审计只留最近 500 条,避免表膨胀

=== 前端 ===
新页面 /aiops(侧栏「智能运维」,管理员),页内单层 tab:
  日志分析 / 模型配置 / 分析历史
- 分析:数据源 + 时间预设(15m/1h/6h/24h) + 可选提问 → 结论/证据/建议/不确定 四区
       顶部显示本次消耗(in/out tokens、耗时),可查看「发给模型的完整内容」
- 配置:启用开关、Base URL、模型、API Key(掩码)、温度、输出上限、超时、
       每日配额、额外脱敏正则;按钮顺序改为「测试连接 → 保存配置」
- 历史:服务端分页(20/页),点行看详情与 payload

=== 两个真实踩坑(已修)===
1) 密码含 @ 导致脱敏漏掉(mysql://user:P@ssw0rd@host)
   → URL basic-auth 规则改为贪婪匹配到最后一个 @,由单测锁定
2) 结论里的时间是 UTC(容器无 tzdata,time.Local=UTC),
   模型写"凌晨 6 点下跌"而运维当时是下午 2 点
   → 显式用东八区展示(LOKISTACK_DISPLAY_TZ=UTC 可覆盖)

=== 实测(69147008fcf4)===
  配置保存 → key 掩码 sk-***e286;连通性测试返回「正常」
  分析 15 分钟窗口:4.1s,结论识别出"窗口末段体积断崖约 -60%"
  信号时间 14:23~14:38(本地时间正确)
  历史分页 total=3,tokens/耗时字段完整
  单测:脱敏 2 个用例 + 模型输出解析 4 个用例 + 6 条 AI 路由断言
2026-10-31 06:39:01 +00:00
opsx 9448868a71 feat(collectors): Promtail DaemonSet 的部署命名空间固定为 log,表单隐藏该字段
CI / Backend (go vet + test) (push) Waiting to run
CI / Frontend (lint + build + test) (push) Waiting to run
=== 需求 ===
创建 Loki 部署后添加采集策略(Promtail DaemonSet)时,"部署节点命名空间"
不该让用户选 —— 采集器必须和 LokiStack 放在一起(log)。

=== 前端 ===
CollectorFormDialog:选择 k8s-daemonset 时整块隐藏 namespace 字段
(连同分类型提示、手动输入开关一起),改为一条只读说明:
  「采集器部署命名空间固定为 Loki 所在命名空间(通常 log),与 LokiStack
    放在一起,无需选择。要限制采集范围请用下面的『只采集这些命名空间』。」
提交时 DaemonSet 不传 namespace(原来会传用户填的值)。
顺带修掉隐藏后暴露的 tsc 报错:placeholder 里 `collectorType === 'k8s-daemonset'`
在收窄后的分支里已成不可能比较,改为常量 'default'。

=== 后端(强制约束,不只靠前端)===
services/collector.go:
  Create:type == k8s-daemonset 时强制清空 req.Namespace
          → 渲染与部署两侧都会回退到 d.loki.Namespace
  Update:DaemonSet 忽略 updates["namespace"]
原始渲染逻辑本就有"为空则用 LokiDeployment.Namespace,再兜底 log"。

为什么后端也要拦:API 直接调用(脚本/其他客户端)不该能把采集器
部署到任意命名空间——那样会出现"平台显示在 log、实际 DaemonSet 在别处"
的状态漂移。

=== 实测(0108617e4180)===
  现有采集器 promtail-pre: type=k8s-daemonset namespace='log'
  PUT {"namespace":"evil-ns"} → 返回 success,但读回仍是 'log' ✅ 被忽略
  tsc 通过;Pod = Hub latest = 0108617e4180
2026-10-31 01:56:15 +00:00
logan 2e97c04f84 fix(db): 修复 NextID 与写锁重入死锁
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
- NextID 改用独立 seqMu,不再依赖数据写锁,杜绝 Lock 内调用 NextID 的死锁
- CaptureAppVolumeForHour 先取 ID 再 Lock
- 新增写锁看门狗:长时间占用时转储 goroutine 栈
- 新增 TestNextIDWhileWriteLocked 回归测试
2026-10-31 02:20:34 +08:00
opsx d4422f1541 refactor(analysis): 去掉排行 10/30/50 选择器,改为一次取 50 + 前端分页;零点结算昨天
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
=== 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
2026-10-30 17:13:18 +00:00
opsx 63dd491c5b fix(daily-volume): 历史快照的 partial 自动修正 + 三个回归测试
=== 现象 ===
仓库里 6 条快照 partial 全是 true(10-27 到 10-30),前端把昨天的柱子
也画成虚线空心,看着像"每天都在采样中"。

=== 根因 ===
早期实现把两件事都塞进 partial:「今天还没过完」和「起点超出 24h」。
10-27/10-28 的数据现在已经被 retention 删掉、永远不会再被抓一次,
所以这两条错误标记会永久留在库里。

=== 修法 ===
prune 里加幂等修正:过去的天,只有快照是**在那天结束之后**才抓的
(说明抓到的是完整一天)才改成 partial=false;当天中途抓的继续保持 partial
(那时确实只有半天,不能伪装成完整数据)。

=== 测试(3/3 PASS)===
TestPruneDailyVolumeNormalizesPartialAndDropsExpired
  · 昨天结束之后抓的 → partial=false
  · 前天中途抓的     → 保持 partial=true
  · 今天             → 保持 partial=true
  · 超留存期         → 被清理
  · 条数不被清理流程清空
TestUpsertVolumeSnapshotKeepsEntries  刷新体积不清空已抓到的条数
TestGetDailyVolumeHistoryReturnsAllDaysWithMissing  始终返回 days 个点,缺的标 Missing 不伪造 0

go vet ./services/ ./models/ 干净;go test ./services/ 全绿。
2026-10-30 16:56:39 +00:00
opsx b7de337c9e perf(daily-volume): 每日量抓取拆成「体积每 30 分钟 / 条数每天最多 3 次」
=== 现象 ===
用户问:每日日志量不是有定时任务记录吗?为什么还要每次查询?
盘了一遍发现:定时任务确实有(每 30 分钟),接口也确实只读库(实测 0.1ms),
但每轮抓取都顺手跑一次**整天 index/stats** 拿条数(实测 6ms ~ 20s,随负载波动),
2 天 × 多个数据源 × 每 30 分钟 = 一天几十次重查询,纯浪费单实例 Loki 的算力,
还会和用户页面上的查询抢 CPU。

第二条更糟:仓库里所有快照的 partial 全是 true。
根因:partial 里混了两种语义 ——「今天还没过完」和「起点超出 24h」。
后者让昨天/前天的柱子也画成虚线空心,看起来「每天都在进行中」。

=== 修法 ===
1. 抓取按成本拆成两条路径(体积走 volume_range,条数走 index/stats):
   - 体积:今天 + 昨天,每 30 分钟刷,54ms~4s,无感
   - 条数:只对**已结束**且 entries==0 的天补,每天最多 3 次、间隔≥1h;
     尝试次数**先写库再查**,进程中途被杀也不会退化成每 30 分钟重试
2. partial 只表示「今天还没过完」,昨天起是实心柱
3. 0 与「未留存」分开:起点超出 24h 且查不到数据 → **跳过落库**保持 Missing。
   否则「数据被 retention 删掉」会被画成「那天没有日志」,比灰色柱误导得多
4. 每次抓取打日志:`[daily-volume] 2026-10-30 体积 291.37 GB 已落库(volume_range 4.27s,34 个桶)`
   —— 以后不用猜「到底抓没抓、走的哪条路径」

=== 实测取证(本集群)===
Loki 直接查(port-forward :13100):
  10-30(16.8h) volume_range 291.37GB/34桶   index/stats entries=2.21B
  10-29(24h)   volume_range 121.99GB/19桶   index/stats entries=927.9M
  10-28/10-27  volume_range 0 桶、index/stats 0 —— 已被 retention 删掉
  同范围 count_over_time 两次 60s 超时(永远别用在每日量上)
平台库里的快照与上述数字一致(288.32 / 121.75 GB),说明落库值可信;
10-27/10-28 的 34.87/25.65 GB 是当时抓的 —— 这正是「Loki 只留 1~3 天,
历史必须自己落库」的实证。

go vet ./services/ ./models/ 干净;go build 通过;go test ./services/ 通过。
2026-10-30 16:52:55 +00:00
opsx 7b1ffd0274 fix(daily-volume): 30/90/365 天切换"没反应";/logs/stats 加短缓存
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
=== 1. 切换范围没反应(真 bug)===
现象:7/30/90/365 天切换后图完全一样。
根因:GetDailyVolumeHistory 只返回**已抓到的快照**。平台只有 4 天数据,
      所以任何 days 都只返回 4 个点 → 图表一模一样。
修复:始终返回 days 个点,缺失的天用 Missing=true 标记。
      实测:days=7→7 点(缺3)、30→30 点(缺26)、90→90(缺86)、365→365(缺361)。

为什么必须区分 Missing 和"0 字节":
  前者是"平台没留存",后者是"那天确实没日志"。
  混在一起会让 30/90 天的图上出现大片 0,被误读成"日志量暴跌/系统停摆"。
前端:缺失的天画灰色浅柱,tooltip 显示"未留存",图下方提示
      "其中 N 天平台没有留存记录(灰色柱),不是当天没有日志"。

=== 2. /logs/stats 加 90 秒缓存 ===
分析页每次进入/切范围都打 index/stats,实测 5 分钟窗口冷查询 1.2 秒
(60 分钟更慢)。这类聚合值可容忍分钟级陈旧,加与指标查询一致的缓存:
  90s TTL + 时间窗按 2 分钟取整 + 上限 200 条 + 过期清理
实测:重复请求 1227ms → 8ms。

=== 3. 顺带确认已生效的优化 ===
  histogram(24h)   : 54ms(volume_range)
  daily-volume     : 10ms(只读库)
  stats 二次请求    : 8ms(新缓存)
  长范围(>1h)       : 不跑 count_over_time(预聚合快照 + 跳过内容扫描)

Pod = Hub latest = 12453ea723c1
2026-10-30 16:46:40 +00:00
opsx 5c7a3ec81d perf(daily-volume): 每日量改用 volume_range(20s → 54ms),图表改圆柱
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
=== 核心技巧:体积用 index/volume_range,不要用 index/stats ===
同为 24 小时、同一 selector,两条索引查询差 370 倍:
    index/stats      : 整天聚合,实测 20s 直接超时(之前每日量接口卡 20s 的根因)
    index/volume_range: 按 step 分桶只累加 chunk 字节,实测 54ms,97 个桶

原因:index/stats 要跨全部 chunk 去重统计 streams/entries;
volume_range 只把索引里记录的 chunk 字节数按时间桶求和,不解码内容。

=== 后端改动 ===
services/daily_volume.go:
  体积:LokiHistogram(走 volume_range,step 由跨度自动选)→ 累加 54ms
  条数:只能 index/stats,降级为「尽力而为」25s 超时;
        拿不到就保留上一次的值(不能因为一次超时把历史条数清零),
        绝不让它拖住体积入库。
  前端图只画体积,条数缺失不影响可读性。

=== 前端改动 ===
DailyVolumeChart:AreaChart(面积)→ BarChart(圆柱)
  顶部圆角 radius=[6,6,0,0]、渐变填充、maxBarSize=44
  当天未结束的柱子用半透明 + 虚线描边区分,避免把"才过半天"误读成"量掉了"

=== 实测 ===
  24h volume_range: 54ms,97 桶,合计 369.61 GB(与全集群 ~370GB/天 吻合)
  /logs/daily-volume 接口: 10ms(读库)
  Pod = Hub latest = 4cdc8b9c4841
2026-10-30 16:40:49 +00:00
opsx d0adc095dd feat(analysis): 长范围应用排行改走「小时级预聚合」,不再扫日志内容
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
=== 问题(实测)===
按应用排行必须读日志内容(Loki 3.4 的 index/volume 不支持 app 维度聚合,
aggregateBy=app 无效),成本 ∝ 时间范围:
   5 分钟  ≈ 1.3GB  →  9.8s
  15 分钟  ≈ 3.9GB  → 19.5s
   1 小时  ≈  15GB  → 预计 60~90s
   6 小时  ≈  92GB  → 5~8 分钟
  24 小时  ≈ 370GB  → 不可用
用户在时间范围里选 6h/24h 时,页面必然卡死。

=== 方案 ===
新增 models.AppVolumeSnapshot + services/app_volume.go:
  - 后台任务每小时第 5 分钟抓「上一个完整小时」的 per-app 条数
  - 用**瞬时查询**(/loki/api/v1/query + [1h])而不是 query_range:
    单点评估、扫描量相同、省掉多步开销;并绕开 LokiMetrics 的 90s 上限
    与重型查询信号量(后台任务不该和前台抢令牌)
  - 每源每小时只留 top 50 应用,保留 90 天
  - 幂等:同 (hour, source, ns, app) 更新而非重复插入
  - 超出 Loki 26h 保留期的窗口跳过(抓也抓不到,白等几分钟)
新增 GET /api/v1/logs/app-ranking?sourceId&from&to&limit → 读库毫秒级

前端:analysis 判断范围 > 60 分钟就走预聚合接口,并停掉实时重查询
(rankingEnabled 置 false);表格副标题说明「读的是预聚合快照,已留存 N 小时,
不含错误数与趋势」。

=== 顺带修掉的根因 bug ===
useInView 原来用 useRef + useEffect([inView]):分析页首屏 data===null 时
内容块不渲染,ref.current 为 null,effect 直接 return;等数据到了 DOM 才挂上,
但依赖没变 → effect 不重跑 → IntersectionObserver 从未创建 → inView 永远 false
→ 重查询永不发出 → 页面一直显示"没有应用维度的数据"。
改成 callback ref(节点挂载即作为依赖),并加 scroll/resize 兜底 + 挂载瞬间量一次。
另外分析页加 8 秒无条件兜底:可见性判定再失灵,8 秒后也一定查一次。
默认时间范围 15 分钟 → 5 分钟(19.5s → 9.8s)。

=== 实测 ===
  Pod = Hub latest = 98de87e7f895
  5 分钟窗口 by-app:9.8s(75 序列);15 分钟:19.5s
2026-10-30 16:35:35 +00:00
opsx 3b297d0429 feat(collectors): 采集项支持「表单 / YAML」双模式编辑(方案 A)
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
=== 设计边界(刻意不做"编辑完整 promtail.yaml")===
开放的是**采集项本身**的 YAML 表示(字段与 models.ScrapeJob 一一对应):
  name / paths / labels / encoding / json / system
  multiline(firstline,maxLines) / timestamp / dropIfMatch / rateLimit
clients、positions、server、scrape_configs 外层仍由平台生成。

理由(真实故障面):
  用户删掉 positions → 重启后从头重采,大量重复 + Loki 以
  entry too far behind 拒绝整批;删掉 clients → 完全不外发。
  这两类问题在页面上看不出来,只能登机器看日志,排障成本极高。
  只开放采集规则,写坏了最多某条日志没采到,不会让采集器整体失效。

=== 后端 ===
新增 services/collector_jobs_yaml.go:
  RenderJobsYAML(jobs) → 带边界说明注释的 YAML(明确写"clients/positions 不在此修改")
  ParseJobsYAML(text)  → 复用 models.ValidateScrapeJobs,与表单完全同一套校验
错误信息必须具体:
  YAML 语法错 → 「YAML 语法错误:yaml: line 1: did not find expected ',' or ']'」
  语义错     → 「采集项「app」至少需要填写一个日志路径」
新增路由(用 /collector-jobs/* 前缀,避开与 /collectors/:id 的 gin 通配冲突):
  POST /api/v1/collector-jobs/render
  POST /api/v1/collector-jobs/parse

=== 前端 ===
编辑弹框(主机模式)加「表单 / YAML」切换:
  - 切到 YAML:调 render 把当前采集项渲染出来
  - 切回表单:调 parse 回填;解析失败**留在 YAML 模式**并显示原因
    (绝不能悄悄丢掉用户改动)
  - 停留在 YAML 模式点保存:先 parse,失败则中止保存
  - 编辑区 min-h 固定、等宽字体、可拖拽高度

=== 测试 ===
TestRenderParseJobsYAMLRoundTrip:
  表单→YAML→表单 无损往返,逐字段核对
  (multiline.maxLines / rateLimit / dropIfMatch / timestamp / labels / system)
TestParseJobsYAMLErrors:空内容、语法错、空列表、缺名称、缺路径、名称非法
  → 6 种输入都给出具体错误,不是笼统"格式错误"

=== 实测(c11d2badce64)===
  render: code=0,17 行,含边界说明
  parse : code=0,1 个采集项;往返校验 name=app paths=1 multiline=True
          rateLimit={rate:2000,burst:4000} drop=1 json=True
  错误: 「采集项「app」至少需要填写一个日志路径」
        「YAML 语法错误:yaml: line 1: did not find expected ',' or ']'」
  Pod = Hub latest = c11d2badce64
2026-10-30 16:10:51 +00:00
opsx c8d026adc7 feat(collectors): DaemonSet 不再填日志路径,改为「只采集哪些命名空间」多选
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
用户指出的三个问题,逐条核实后的结论:

1) 「采集日志路径」对 DaemonSet 确实是多余的 —— 确认后端模板里写死了
   `replacement: /var/log/pods/*$1/*.log`(按 pod UID 精确匹配容器标准输出),
   表单里填的 logPath 对 DaemonSet 完全不参与渲染。
   → 选择 DaemonSet 时隐藏该字段,提交时强制置空。

2) 「可以指定采集多个命名空间」—— 后端**早就支持**:
   models.Collector.TargetNamespaces + promtailNamespaceFilter(),
   空数组 = 全集群。只是**前端新建弹框没暴露**(编辑弹框里有)。
   → 新建弹框补上多选 chips(全集群 / 各命名空间可点选),并补齐前后端透传:
     dto.CreateCollectorRequest.TargetNamespaces
     controller → services.CreateCollectorRequest.TargetNamespaces
     models.Collector.TargetNamespaces
   之前前端即使传了也会被 Go 静默丢弃(无该字段)。

3) 「命名空间」字段语义已在上一提交补了分类型说明:
   - DaemonSet:采集器自身部署在哪个命名空间(不影响采什么)
   - Sidecar:目标应用 Deployment 所在命名空间

新增 TestDaemonsetTargetNamespacesFilter:
  nil → 不生成 namespaces 白名单(全集群)
  ["astralx"] → 生成且包含 astralx
  ["astralx","default"] → 两个都在

=== 实测(d45dfa18ecc5)===
  前端文案「只采集这些命名空间」「不选 = 采集整个集群…」均已进包
  渲染测试 3 种输入全部符合预期
  注:本次 docker push latest 遇到 Docker Hub 502,v0.1 成功而 latest 仍指向旧镜像,
     导致首次滚动后 Pod 仍是旧代码;重推 latest + 强制重建 Pod 后确认
     Pod digest = d45dfa18ecc5 —— 记录这个坑:push 失败必须核对三方 digest。
2026-10-30 16:03:24 +00:00
opsx 419a81c2af perf(loki): 查询并行度改为自动跟随 CPU 核数(不再硬编码)
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
=== 问题 ===
max_query_parallelism 原来是硬编码值,改过两轮(8 → 6):
  - 8:4 核机器上 76% 调度周期被限流,label values 要 20 秒
  - 6:仍然高于核数,25% 周期限流,查询并没有更快
根因不是"该填几",而是**并行度只有一个正确值:等于 CPU 核数**。
把它做成可配置项反而是坑 —— 用户改了 CPU 忘了改并行度就出现错配。

=== 做法 ===
新增 queryParallelism(resourcesCPU):
  - 从部署记录的资源规格推导,模板渲染成 {{ .QueryParallelism }}
  - 容错:支持 "4000m"/"500m" 毫核写法(向上取整)
  - 边界:低于 2 核按 2,高于 12 核按 12(再高频只会互相抢 CPU)
  - 非法值/空值回退 4
用户在「组件部署」里改 CPU,并行度自动跟随,页面上不需要新字段。

=== 实测(4 核 + 并行 4)===
  by-app(15m)    11913ms  75 条序列   (并行 6 时 12438ms)
  label/namespace   38ms
  label/app         25ms
  label/pod         25ms
  限流 61/252 周期(仅重查询那几秒)
附带 TestQueryParallelism 覆盖 10 种输入(含毫核/越界/非法值)。
2026-10-30 15:41:38 +00:00
opsx ad0d35f72a feat(alerts,logs): A 方案—告警窗口 15m→5m 消重复扫描;数据源可编辑/删除且删后不被自动重建
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
=== 1. 告警窗口 15m → 5m(阈值同比例 1/3)===
背景:检查间隔已改 5 分钟,但规则窗口仍是 15 分钟 ——
同一个 15 分钟窗口每 5 分钟被完整重扫一遍,等于 3 倍重复扫描。
在 370GB/天、全命名空间 selector 下,这是 Loki CPU 的最大稳态来源。

    FATAL            10 → 4        窗口 5m
    Go panic         10 → 4        窗口 5m
    JVM OOM           5 → 2        窗口 5m
    StackOverflow     5 → 2        窗口 5m
    应用错误激增  100000 → 35000    窗口 5m(改名 error > 3.5万)
    ingress 5xx      2000 → 700    窗口 5m
    ingress 4xx     20000 → 7000   窗口 5m
    DB 连接失败      1000 → 350    窗口 5m

阈值按 1/3 缩放是必须的:窗口缩短 3 倍后,同样的流量在 5 分钟内只有
1/3 的条数,阈值不动就永远打不到了。
迁移 v10→v11 只改"窗口仍是 15 且名字匹配内置规则"的项,用户手调的不动。

=== 2. 数据源支持编辑/删除(含托管源)===
原来前后端都把托管源锁死:
  - 前端 `!managed &&` 直接不渲染编辑/删除按钮
  - 后端 DeleteSource 返回「平台部署的数据源随部署管理,请先删除对应部署」
用户的诉求是"数据源必须能编辑和删除",且确实有场景(不想让某个源出现在
查询下拉里、想改认证头)。

=== 3. 修「删了又回来」的真 bug ===
EnsureLogSourcesForRunningDeployments() 在 **每次打开数据源列表和启动时**
都会给 running 部署补齐数据源 —— 所以删掉托管源后,刷新页面它又回来了,
用户看到的现象就是"删不掉"。

新增 LokiDeployment.AutoSourceDisabled:
  - 删除托管源时置位(DisableAutoSourceFor)
  - EnsureLogSourceForDeployment 见到该开关直接 return
  - 部署被编辑/重新部署时重置(EnableAutoSourceFor),即"你要用它了就恢复"
前端配套文案:
  - 编辑托管源弹窗提示"重新部署会覆盖集群/Service/端口,认证与请求头保留"
  - 删除确认明确"删除后不会再自动重建,重新部署该 Loki 才恢复"

=== 实测(20300eef92d2)===
  v11 迁移后 8 条规则全部 窗口=5m,阈值 4/4/2/2/35000/700/7000/350
  go test ./... 全绿
2026-10-30 15:28:30 +00:00
opsx c9e604b35f perf(logs): 修 label 下拉 20 秒、Loki CPU 限流 76%、告警检查频率
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
1) Loki CPU 限流 76%(limits 4 核,实测 3923m,nr_throttled 920656/1209379)
   → limits 提到 8 核 / requests 2 核。限流归零,histogram 84ms,
     by-app 从 60s 超时降到 14.9s。
2) label values 未传时间范围 → Loki 默认扫 6 小时索引(370GB/天 → 10~21s)
   → 默认只查最近 1 小时。实测 namespace 21330ms→33ms,app 24781ms→30ms。
3) max_query_parallelism 8 > 实际核数 → 调到 6。
4) 告警检查 1 分钟 → 5 分钟(规则窗口本身 15 分钟,稳态负载降到 1/5)。
2026-10-30 03:42:03 +00:00
logan 7624baa564 feat(logs): 数据源连接方式自动探测
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
部署完成及启动时实测 cluster-svc(API proxy) 与 url 直连,
选择可用方式并落库,兼容平台/Loki 分别在主机或集群内的组合。
2026-10-30 01:23:45 +08:00
logan b433b5f9dd fix: 删除操作清理远端资源并拦截悬空引用
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
- 部署删除: k8s 模式清理 StatefulSet/Service/ConfigMap 与 PVC,先卸载关联采集器
- 采集器删除: 同步卸载成功后再删记录,失败保留避免孤儿
- 集群/存储/主机删除: 有部署或采集器引用时拒绝,避免悬空外键
- Token RBAC 补 services/proxy,保证 cluster-svc 数据源可查询
2026-10-30 01:17:26 +08:00
logan 56e1245b70 fix(logs): 启动时清理孤儿托管数据源
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
旧版本删除部署后残留的 managed 数据源无法通过 API 删除
(提示随部署管理),启动时按部署名/集群存在性统一清理,
并把默认源转移到剩余数据源。
2026-10-30 01:10:15 +08:00
opsx 48e68250bb feat(docs): 用户手册接入界面截图
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
- 6 张核心页面截图(仪表盘/向导/日志查询/日志分析/告警/设置)存 docs/assets/screenshots,
  随发布包分发;产品介绍页新增「界面预览」两列网格,各功能页顶部内嵌对应截图
- goldmark 开启 WithUnsafe 以渲染网格 HTML(文档自维护,无注入面);截图经 DOM 校验全部加载成功
2026-10-29 16:36:21 +08:00
opsx 437ff56992 feat(docs): 文档站重制 — 参照 opsxapi/opsxshell 体系,服务端渲染三栏布局
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
- 后端 goldmark 渲染管线:/docs/:slug 页面 + /docs/search-data.json 搜索索引,
  manifest.json 驱动分组导航,正文剥 H1 避免与页面头重复
- 三栏布局(分组侧栏/内容/本页目录)+ 顶栏(品牌/版本徽章/搜索面板/官网入口),
  docs.css 品牌绿版 + docs.js(代码复制/轻量高亮/提示块/TOC 滚动高亮/移动端抽屉)
- 替换 docsify;无上一页时空占位无边框;visual-judge 两轮评审(初版 fail → 修复后 pass)
2026-10-29 15:30:25 +08:00
opsx c1f822fb10 refactor: 移除资源用量的可选 Prometheus 接入与平台监控页
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
- 资源用量弹窗:删除 applyPrometheus/promQueryValue 与峰值/限流字段
  (LOKI_USAGE_PROMETHEUS_URL 从未内置,代码恒走降级),统一提示
  「未接入 Prometheus:暂无峰值/限流数据」;扩容建议改基于 metrics-server 实时值
- 移除「平台监控」菜单页与 /api/v1/system/metrics-summary;
  原始指标保留在标准 /api/v1/metrics(Prometheus 文本格式)可自行抓取
- 手册/落地页/CHANGELOG 同步(去除备份恢复与监控页宣传,指标端点表述中性化)
2026-10-29 15:16:18 +08:00
opsx f87fb4b2b3 refactor: 移除备份恢复功能(API/CLI/手册)
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
日志数据的保留与安全由对象存储 + Loki retention 承担,平台侧整库备份恢复价值有限:
删除 services/backup.go、/system/{backup,restore,restore/apply,backups} 路由与 handler、
CLI backup/restore 子命令、前端 api 方法与类型;自检所需的 dataDirPath/密钥指纹并入 selfcheck;
用户手册删备份章节并修正 FAQ 升级建议;CHANGELOG 记录移除。
2026-10-29 14:48:25 +08:00
opsx c33ea9d0a8 ci(lint): golangci-lint v2 入门禁并归零
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
- .golangci.yml:standard 集 - errcheck(存量 38 处裸忽略待专项),staticcheck 关纯风格/quickfix
- 清理 3 个无引用死函数;SanitizedJSON 不可达分支收敛为接口分支 + nil 指针防护(原死分支中的 nil 检查从未执行)
- CI backend job 增加 golangci-lint-action@v6
2026-10-29 13:29:25 +08:00
opsx 572efc89a4 fix(reliability): 采集器并发去重 + 备份恢复即时重载 + context 贯穿请求链
- CollectorService 安装/卸载按采集器互斥去重(重复触发不再并发 docker rm/run 互踩);删除后卸载失败留日志
- ApplyStagedBackup 替换文件后立即 Reload 内存态,消除「重启前旧内存落盘覆盖恢复文件」竞态;暂存文件名升级 lokistack.json(兼容旧名)
- GetUsage/Validate 全链路 context 贯穿:请求取消即中止 SSH/k8s 查询(异步调用点显式 Background)
2026-10-29 13:29:25 +08:00
opsx 2975452c21 fix(security,data): P0 — 命令注入/停止毁数据/采集路径绕过/DB 清库/前端竞态
后端(实机 10.10.10.161 验证 host-binary 全链路 + 注入双路径 400):
- Loki 版本号严格 ^\d+.\d+.\d+$ 校验并接入 Update 路径:version 拼进 SSH shell 命令,
  旧校验放行 3.0.0'; id; echo ' 等注入载荷(root RCE)
- Stop/Restart 清理清单移除 PVC:停止部署曾连带删除 loki-data PVC,filesystem 存储日志被永久清空(测试锁死红线)
- 采集敏感路径防护覆盖默认 LogPath 分支与 collector Create(/etc/shadow 不再可绕过入库)
- 数据文件损坏时原文件改名 .corrupt-* 留档再空库启动,不再被静默覆盖(loadFromDisk 抽出可测)

前端:
- 审计页用户名过滤 300ms 防抖 + AbortController/版本号(逐键竞态数据错乱)
- 向导自动部署改用创建返回实例(或 id 最大且 pending),不再盲取 list[0] 对错误实例发起真实部署
- 路由级 ErrorBoundary:单页崩溃/chunk 404 只影响当前页
2026-10-29 13:02:11 +08:00
opsx 5e59347c40 feat(settings,analysis): 日志保留时间可配(发送历史保留 7 天)+ 分析页手动刷新;修复我引入的 RWMutex 死锁
=== 0. 【严重】修复上一版引入的死锁(平台写路径全卡死)===
上一版把保留期读取写成 `GetRetentionSettings()`(内部 db.RLock()),
却在 `db.Lock()` 已持有的情况下调用(recordDelivery / RecordAudit 的裁剪路径)。
Go 的 sync.RWMutex **不可重入** → 第一次写投递历史/审计日志就永久死锁。
现象:登录 28 秒返回空(登录也要写审计)、所有写接口挂住。
go build / go vet / 之前的单测(不触发写路径)全都查不出来。
修法:
  - 拆出 retentionFromDBLocked(db) / alertDeliveryRetentionLocked(db) /
    auditLogRetentionLocked(db),调用方已持锁时用无锁版本
  - upsertSettingLocked 里**不能**用 db.NextID()(内部 d.mu.Lock(),同样死锁),
    改为锁内自算 max(id)+1
  - 新增 retention_test.go:持写锁调用裁剪函数,2 秒超时未返回即判死锁
实测修复后:登录 103ms(原 28000ms)、panic/deadlock 命中 0。

=== 1. 平台数据保留期可配(系统设置 → 通用配置 → 日志保留时间)===
新增 GET/PUT /api/v1/settings/retention:
  alertDeliveryDays  告警发送历史保留天数,默认 7(原写死"最近 50 条")
  auditLogDays       审计日志保留天数,默认 90(原写死常量)
保存后立即按新策略裁剪一次,不必等下次触发。
校验:发送历史 1~365 天、审计 1~3650 天,超出返回明确中文原因(不是 500)。
注意与「Loki 日志保留期」区分:那个是部署级配置(组件部署 → 保留策略)。

=== 2. 发送历史:50 条硬上限 → 保留 7 天 ===
后端 Deliveries 接口原来写死 `len(out) < 50`,现在按保留期取(响应上限 500 可调),
标题改为「发送历史(保留 N 天)」,N 从设置里读。

=== 3. 菜单「日志服务」→「组件部署」===

=== 4. 分析页:去掉「全部命名空间」+ 改手动刷新 ===
- 移除「全部命名空间」选项:{namespace=~".+"} 要扫全集群(约 370GB/天),
  一次分析几十秒,得不偿失;历史值 __all__ 也会被忽略并回退到策略/第一个 ns
- 筛选改为「选择值 / 已生效值」分离:切换命名空间或时间范围**不再自动查询**,
  只把刷新按钮点亮并提示「筛选条件已改,点刷新生效」;
  点一次才应用并取数。
  原因:一次分析要跑多条 count_over_time,切一下下拉就重查一轮,
  快速连点会把 Loki 打满(并发护栏 429),还拖慢别人。

=== 实测(9739c7c092e7)===
  登录 103ms / token 273 字符
  GET retention → {alertDeliveryDays:7, auditLogDays:90}
  PUT 0 天 → 400「告警发送历史保留天数需在 1~365 之间」
  平台日志 panic/deadlock 命中 0
  go test ./... 全绿(含 2 个新死锁回归用例)
2026-10-29 04:16:52 +00:00
opsx cb1ef1dff4 fix(alerts,analysis): 告警引擎稳定性 5 项修复;每日量接口去掉同步 Loki 查询(20s→10ms)
=== 一、告警引擎稳定性审查(引擎本身是异步的:ticker + goroutine)===
1) 【高】ticker 无重叠保护
   ticker 每 1 分钟触发;一轮规则检查 8 条 × 20~30s = 2~4 分钟,远超一分钟。
   原来每过一分钟就再开一轮 → 查询越堆越多,抢走页面查询的 Loki 资源,
   每轮越来越慢,最终表现为"告警不响了"。
   → 新增 alertCheckRunning(atomic.Bool),上一轮未完成直接跳过本轮并记日志。

2) 【高】单条规则无超时
   RunAlertCheck(context.Background()) 没有 deadline:Loki 一旦挂住,整轮永久运行,
   叠加问题 1 会让引擎再也不启动新一轮(静默死亡)。
   → 每轮总超时 5 分钟(alertRoundTimeout)+ 单条规则 90s(alertRuleTimeout)。

3) 【中】panic 只在整个 tick 层 recover
   一条规则 panic 会让本轮后续规则全部跳过。
   → per-rule recover,panic 记为一条 failed 投递记录,后续规则继续。

4) 【中】冷却记录每分钟写一条,把真实发送记录挤光
   AlertDeliveries 只保留最近 50 条,而处于冷却期时每个 tick 都写一条 cooldown,
   一小时内真实的"已发送"记录就全被挤掉了,发送历史没法看。
   → recordCooldownThrottled():同一规则 10 分钟最多写一条。附单测。

5) 规则串行本身保留(避免同时压垮 Loki),但配合 1) 才安全。

=== 二、每日日志量接口:20 秒 → 10 毫秒 ===
根因:GetDailyVolumeHistory 里**同步** await 一次"整天 index/stats"补抓当天数据,
那次查询超时 20s → 整个接口 20s → 前端超时 → 页面显示"每日日志量没数据";
更糟的是这条重查询还和页面的 histogram/stats 抢 Loki,导致那两个接口同时 502
(访问日志实证:daily-volume latency_ms 20000.997,stats/histogram 502)。
→ 接口改为**只读库**,不触发任何 Loki 查询;当天数据由后台每 30 分钟的
  定时任务补(CaptureRecentDailyVolumes)。
→ 同时把整天 index/stats 的超时从 20s 提到 90s(370GB/天,20s 不够)。
实测:10ms。

=== 三、通知通道改名「默认告警」(schemaVersion v8→v9) ===
"lark 默认告警" / "默认通知通道(待配置 Webhook)" → 统一为「默认告警」。
只改名,不动 Webhook、不动 enabled:
首次部署创建的占位通道必须保持「空 Webhook + 禁用」,由管理员自己填,
否则会出现"已启用但空地址"的通道,告警一命中就全部投递失败。

=== 四、其它文案 ===
「每日日志量」副标题精简为「每天的日志总量,平台保留历史趋势」,
并移除「平台留存历史」徽标(用户反馈多余)。

=== 实测(303134c0c718)===
  /logs/daily-volume:10ms(原 20000ms)
  通道列表:id=1 name="默认告警" enabled=true hasWebhook=true
  go test ./... 全绿(含新增 TestRecordCooldownThrottled)
2026-10-29 03:38:30 +00:00
opsx 607b67a2ed fix(alerts,analysis,dashboard): 阈值再上调 + 告警风暴全局闸门;修 subtitle/门控/饼图 Top5
=== 1. 告警阈值第二轮上调(500~5000 这一档仍然太敏感)===
实测业务高峰期这些量级就是常态,阈值贴着基线等于"每天准点报警":
    应用错误激增   50000 → 100000(冷却 120m → 240m)
    ingress 5xx      500 → 2000  (冷却 240m)
    ingress 4xx     5000 → 20000 (冷却 240m)
    数据库连接失败   200 → 1000  (冷却 240m)
迁移 v7→v8,只改"仍等于旧默认值"的规则,用户手动调过的不动。

=== 2. 告警风暴抑制:新增全局闸门 ===
单条规则的 cooldown 只能防"同一条重复",挡不住"多条规则同时炸"。
实际踩过:一次存储抖动让 8 条内置规则同时命中,10 秒内群里刷了 8 条,
之后大家开始无视这个群 —— 告警疲劳比漏报更难挽回。
新增 overStormLimit():最近 1 小时内全平台成功发送数 ≥ 12 条时,
后续命中不再发送,只写一条状态为 cooldown 的发送记录并注明
"告警风暴抑制:最近 1 小时已发送 N 条(上限 12)",可见可追溯。

=== 3. 分析页 subtitle 仍是裸 key ===
中文语言包缺顶层 subtitle(英文包有),i18next 找不到就把 key 原样输出。
补齐:「按应用与命名空间看日志量、错误与趋势,找出最吵的业务」。

=== 4. 分析页三条"没有应用维度的数据"的真正原因 ===
「活跃应用」「应用日志量 Top 5」「应用日志量流图」都依赖 by-app 重查询,
而这条查询被**底部排行卡片的可见性**门控(rankingEnabled: rankingView.inView)——
用户不滚到底就永远不发请求,于是顶部三块永远是空的。
改为进页面就发(两阶段渲染:索引类指标先出,app 维度到货后补)。
懒加载只保留在真正位于底部的「每日日志量」「Ingress 分析」。

=== 5. 饼图改 Top 5 + 文案 ===
用户要求:应用日志量 Top 5 用饼图。TOP_SLICES 8 → 5,
标题改「应用日志量 Top 5」,副标题写明"其余归入其他"。

=== 6. 卡片顺序 ===
「每日日志量」从中间移到「应用排行」之后(更靠底部,慢慢加载)。

=== 7. 仪表盘 ===
整个「平台自检」卡片(含所有指标行)按用户要求移除。

=== 实测(2e8c88817b6a)===
  v8 迁移:4 条规则阈值上调,日志齐全
  当前阈值:FATAL>10 / panic>10 / OOM>5 / StackOverflow>5 /
            应用错误>100000 / ingress 5xx>2000 / 4xx>20000 / DB 连接>1000
  冷却:低频规则 120m,高频规则 240m
2026-10-28 23:43:14 +00:00
opsx fbefcc86df feat(logs,analysis): 分页改"加载更多"+触底自动加载;修懒加载不触发;指标短缓存
=== 1. 日志列表:去掉页码分页,改「加载更多」+ 触底自动加载(节流)===
为什么不用"上一页/下一页/共 N 页":
  - Loki 是**游标分页**,不支持 offset,页码天生跳不了页
  - 规模上没意义:15 分钟 4000 万条 → 按 200/页算 40 万页
  - 带 |= / |~ 行过滤时总数根本算不出来(index/stats 只接受 label matcher)
日志排查的真实交互是"继续往下看",所以改为:
  - 列表底部:「已加载 N 条 · 约 M 条」+「加载更多」按钮
  - 滚动到底**自动加载**,但 2 秒节流 —— 每批都要后端扫 Loki,
    快速滚动连续触发会撞并发护栏(429),反而比手动点更卡
  - 到 5000 条停止并提示"缩小时间范围或加过滤条件"(虚拟滚动只省 DOM,
    不省数组内存,无上限会一直涨)
  - 修正语义 bug:分页原来是 setEntries(result.entries) **整页替换**,
    改成追加(否则"加载更多"等于翻页)
  - 删除 pageCursors 翻页栈与 prevPage/nextPage/pageOf/pageOfTotal 文案

=== 2. /logs/stats 错误映射:Loki 400 → 我们的 400 + 人话 ===
之前带行过滤时 Loki 回 400 "only label matchers are supported",
被统一包装成 502「Kubernetes API 代理返回 HTTP 400…」,
前端只能当失败静默 → 用户看到"总页数没了"却不知道原因。
现在返回 400 +「含过滤条件(|= / |~)时无法统计总页数:Loki 只支持按标签统计」,
前端据此在条数旁给出提示。

=== 3. 分析页:修"延迟加载永远不触发"(数据一直不出来)===
现象:应用排行/流图/饼图/活跃应用全是"没有应用维度的数据",每日量"已有 0 天",
但接口实测正常(by-app 返回 75 条序列、daily-volume 返回 2 天)。
根因:这三块用 useInView 门控,而 ref 挂在 <Card> 上,观察器不触发 → 永远 false
→ hook 直接 return → 页面永远空。
修法:
  - 改用普通 <div ref> 包裹,不依赖组件是否转发 ref
  - useInView 加**几何兜底**:1 秒后若元素已在视口内就直接置 true
    (有些容器里 IntersectionObserver 的 root 判定会和预期不一致)

=== 4. 分析页指标短缓存(26s → 11ms)===
`sum by (app)(count_over_time(...))` 扫 15 分钟 astralx(约 6.6GB)实测 16~28 秒,
Loki 自带结果缓存不命中(时间窗每次都在变)。点刷新/切页都要再等半分钟。
现在按 (数据源, 查询, 时间窗) 缓存 90 秒,时间窗按 2 分钟取整当 key:
  实测第 1 次 23514ms → 第 2 次 **11ms**
代价:命中缓存时数据最多旧 ~2 分钟,对聚合视图可接受。

=== 5. 仪表盘 ===
去掉「平台自检」「N 项需关注」两段文字:卡片本身就是自检结果,
标题重复一遍没信息量;状态改由图标+边框颜色表达。

=== 6. 其它文案修正 ===
饼图空态从「没有命名空间维度的数据」改为「没有应用维度的数据」
(饼图上一版已改成应用维度,文案没跟上)。

=== 实测(1da8a148d756)===
  Pod = Hub latest = v0.1 = 1da8a148d756
  stats:纯 selector → 200(7005 万条);带 |= error → 400 + 人话
  分析指标缓存:23514ms → 11ms
  每日量:2 天(10-27 32.5GB / 10-28 356.5GB)
  文案:加载更多 / 已到末尾 / 达到上限 均进包;代码中已无 prevPage/nextPage 引用
2026-10-28 23:36:32 +00:00
opsx bad61e5027 fix(deploy): 删除部署时同步清理自动注册的查询数据源(孤儿数据源)
=== 现象(用户实测)===
在 /deployments 页面删掉两个主机模式部署后,本机(172.19.16.8)容器确实清干净了:
  docker ps -a 无任何 lokistack-* 容器
  /etc/promtail、/var/lib/promtail 等目录均不存在
  无 promtail systemd 单元
但**平台里残留一个数据源** host-verify-loki(url=http://172.19.16.8:3100),
「日志查询」下拉里还能选到它,选中必然连接失败。

=== 根因 ===
DeploymentService.Delete 只做了两件事:删容器、删部署记录。
部署创建时由 EnsureLogSourceForDeployment 自动注册的托管数据源(Managed=true)
没有任何清理逻辑 —— 删除动作只完成了一半。

=== 修法 ===
1) DeploymentService.Delete 删完记录后调用 CleanupManagedLogSource(部署名)
   - 删掉同名且 Managed=true 的数据源(手填的 Managed=false 不动)
   - DeleteLogSource 内部会连带删除指向它的查询授权策略组
   - 绑定该源的告警规则置为禁用(保留配置便于审计,但不让它每次评估都失败)
   - 注意锁顺序:必须先 db.Unlock() 再调用,CleanupManagedLogSource 内部会自己加锁,
     持有锁再调用会死锁
2) schemaVersion v6→v7 迁移 removeOrphanManagedLogSources()
   清理历史遗留:Managed=true 但没有任何同名部署的数据源 → 删除,
   连带删策略组、禁用告警规则

=== 实测(d9487694ece6)===
  v7 迁移日志:移除孤儿数据源 "host-verify-loki"(id=2):对应部署已删除
  数据源:1 个(只剩 loki-pre,managed=true)
  部署:1 / 采集器:1 / 策略组:1
  本机:docker ps -a 无 lokistack-* 容器,/etc/promtail 等目录不存在,无 systemd 单元
2026-10-28 23:18:43 +00:00
opsx 37e2f0febc feat(analysis,alerts): 每日量落库看历史趋势、趋势图/饼图可读性、告警三分页
=== 1. 每日日志量必须落库(核心问题:Loki 只留 24 小时)===
实测:请求"近 7 天每日量",Loki 只返回 27 个**小时**桶,全部在最近 24h 内 ——
因为 retention_period=24h,更早的 chunk 已经被删了。
也就是说「固定近 7 天」这张卡片,过去一直是"6 天空白 + 1 天有数据",
用户看着像坏了,实际是数据物理不存在。

方案:平台自己留存历史。
  - models.DailyVolumeSnapshot{Date(本地YYYY-MM-DD), SourceID, Bytes, Entries, Partial}
  - services/daily_volume.go
      CaptureDailyVolume        按天抓 index/stats 落库(幂等覆盖)
      StartDailyVolumeCapture   启动 20s 后回补 + 每 30 分钟检查一次
      GetDailyVolumeHistory     读库返回最近 N 天(今天会实时补抓)
  - 保留 400 天(够看年度趋势),按 source+date 去重
  - 前端可选 7 / 30 / 90 / 365 天,能看多长取决于平台积累多久

为什么用"每 30 分钟检查昨天有没有落库"而不是固定 00:10 定时:
平台可能整晚没运行,固定时点的定时器会直接错过;检查式回补不依赖平台在线时刻。

Partial 标记(关键,否则会误读):
  - 当天未结束 → partial=true
  - 待抓取日期的起点已超出 24h 保留期 → partial=true(Loki 里就没那么多数据)
实测:10-27 32.5GB/2.65亿条 partial=true(保留期外),
      10-28 348GB/28.56亿条 partial=true(当天进行中)

=== 2. 修一个 Go 副本 bug(自己写的,靠实测发现)===
第一版用 `for _, snap := range db.DailyVolumeSnapshots` 后改 snap 字段,
range 拿到的是**副本**,改动不会写回切片 → 同一天永远保持首次抓取的值,
partial 也永不更新(实测发现 10-27 该是 true 却显示 false)。
改为 `for i := range ... { snap := &db.DailyVolumeSnapshots[i] }`。

=== 3. 趋势图可读性 ===
从"裸折线"换成"渐变面积图",并在图上方给出峰值 / 均值:
  - 面积图能一眼看出"这一段整体比上一段高",折线只能靠对齐格子数
  - 峰值/均值避免用户自己对刻度
  - Y 轴与 tooltip 统一 formatBytes,"每桶体积"写进 subtitle
顺带确认:趋势图只画体积(字节),不再混入 error(条数)——单位不同不能同轴。

=== 4. 饼图:命名空间分布 → 应用日志量分布 ===
本环境的命名空间只有 astralx / log 两个,饼图没有信息量;
改成按应用看谁在刷日志(前 8 名 + 其他),才是运维真正要的答案。

=== 5. 告警页拆成三个 Tab ===
「告警规则 / 通知通道 / 发送历史」独立成分页(用 ui/tabs)。
之前通道是折叠块、历史堆在规则下面,规则一多就要滚很久才看得到。
发送历史增加空状态文案,不再是"空页面不知道有没有在发"。

=== 6. 内置规则关联默认通道 + 首次部署 webhook 留空的评估 ===
结论:**webhook 可以留空,但通道必须是禁用状态**。
  - 若种子给的是"已启用 + 空地址"通道,规则一命中就必然投递失败,
    发送历史堆满红色失败,反而让人以为平台坏了
  - 所以首次部署建一个**禁用**的占位通道「默认通知通道(待配置 Webhook)」,
    内置规则全部挂到它;管理员填好 Webhook 再启用即可,不用逐条关联
  - 保存时加校验:启用则必须有非空 Webhook(返回明确原因,不是裸 500)
  - schemaVersion v5→v6:把已有的"无通道"规则补挂到默认通道
  实测:8 条规则全部已关联通道

=== 实测(v11-1028-2252 / 187062644c0f)===
  /logs/daily-volume?days=7  → 2 天数据,partial 标记正确
  规则 8 条全部有 channelIds
  平台 ready、健康 ok
2026-10-28 22:54:12 +00:00
opsx fa7ddd67c1 perf(analysis): 分析页按索引/内容分层,告警默认阈值抬高
=== 1. 分析页"一直转圈/请求超时(40000ms)"的真实原因 ===
三个问题叠加,逐个量化:

a) 首屏用空 namespace 发查询
   页面初始 namespace='',等策略返回前就发出 `{namespace=~".+"}` 全集群查询,
   策略到位后又查一次 → 一次访问跑两遍。
   → 加 namespaceReady 门控:解析完再发(effectiveSourceId)。

b) histogram 索引查询和内容查询抢同一个信号量
   /logs/histogram 首选 index/volume_range(毫秒级),但仍要先抢重型令牌,
   而令牌被 4 条 count_over_time(各 14~29s)占满 → 排队/429/超时。
   → 索引路径**不再占用重型令牌**(15s 独立超时),只有回退到
     count_over_time 时才抢令牌。实测各跨度(15m/1h/6h/24h)均 0s。

c) 重查询条数过多
   原来 4 条全局 count_over_time(total/error/warn)+ 2 条 by-app,
   串行 1~2 分钟。现在只留 2 条 by-app:
     - 全局总量已由 histogram(索引)提供
     - 全局 error 由 errByApp 序列求和得出
     - 全局 warn 放弃(优先级最低却要 20s)
   实测 by-app 单条 26s,两条串行 ~52s;主图 0s 先出。

d) 日报 7 天 count_over_time(2.6TB 扫描)必然失败
   → 改走 /logs/histogram(索引)按自然日聚合,只出"每日总量",
     应用维度交给下方 Top 应用表格(有自己的时间范围)。
   实测 7 天跨度毫秒级。

=== 2. 告警默认阈值抬高(告警疲劳治理)===
验证时用 `{namespace="astralx"}` + 阈值 0 触发了一次:
15 分钟命中 3051 万条 —— 阈值 0 等于把告警变成日志流复读机。

新基线(15 分钟窗口,冷却统一 120 分钟):
    致命日志 FATAL           0 → 10
    Go panic                 0 → 10
    JVM OutOfMemoryError     0 → 5
    StackOverflowError       0 → 5
    应用错误激增             1000 → 50000(改名,原"error > 1000"名不副实)
    ingress-nginx 5xx         100 → 500
    ingress-nginx 4xx        1000 → 5000
    数据库连接失败             50 → 200

迁移只改"仍等于旧默认值"的规则,用户手动调过的阈值不动。
设计原则写进 seed 注释:绝不用 threshold 0;宁可少报也不要让人对告警脱敏;
绝对阈值不跨环境通用,大集群必须按 app/namespace 收窄。

=== 实测(v6-1028-2218 / 0bf66199de66)===
  规则 8 条,阈值=0 的规则数 0,冷却全部 120m
  histogram astralx(15m/1h/6h/24h): 0s
  by-app metrics astralx 15m: 26s,正常返回数据
2026-10-28 22:20:19 +00:00