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
This commit is contained in:
opsx committed 2026-10-30 16:35:35 +00:00
1 parent 7f6fb364de
commit d0adc095dd
11 files changed
+467 -40

No files matched your search

+23
View File
@@ -330,6 +330,29 @@ func (ctrl *LogsController) Health(c *gin.Context) {
utils.Success(c, gin.H{"ready": services.LokiReady(ctx, src)})
}
// AppRanking 按应用排行(长范围走平台预聚合,毫秒级;短范围由前端走实时查询)。
func (ctrl *LogsController) AppRanking(c *gin.Context) {
sourceID, err := strconv.ParseUint(c.Query("sourceId"), 10, 64)
if err != nil || sourceID == 0 {
utils.BadRequest(c, "sourceId 无效")
return
}
from, err1 := strconv.ParseInt(c.DefaultQuery("from", "0"), 10, 64)
to, err2 := strconv.ParseInt(c.DefaultQuery("to", "0"), 10, 64)
if err1 != nil || err2 != nil || from <= 0 || to <= 0 || to <= from {
utils.BadRequest(c, "时间范围无效(from/to 需为纳秒时间戳且 to > from)")
return
}
limit := 10
if raw := c.Query("limit"); raw != "" {
if v, convErr := strconv.Atoi(raw); convErr == nil && v > 0 && v <= 100 {
limit = v
}
}
rows := services.GetAppRanking(uint(sourceID), time.Unix(0, from), time.Unix(0, to), limit)
utils.Success(c, gin.H{"list": rows, "hours": services.AppVolumeCoverage(uint(sourceID))})
}
// DailyVolume 每日日志量历史(平台留存,不受 Loki retention 限制)。
//
// query: sourceId, days(默认 7,最长 400)