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 引用
This commit is contained in:
opsx committed 2026-10-28 23:36:32 +00:00
1 parent bad61e5027
commit fbefcc86df
10 files changed
+231 -69

No files matched your search

+65
View File
@@ -16,6 +16,7 @@ import (
"sort"
"strconv"
"strings"
"sync"
"time"
"k8s.io/client-go/rest"
@@ -624,7 +625,71 @@ type MetricSeries struct {
// LokiMetrics 通用 LogQL 指标查询(日志分析页用):query 为完整指标表达式,
// 其中所有 [$step] 占位符替换为按时间跨度自适应的步长后走 query_range,
// 返回矩阵多序列(直方图只取单序列,这里按 by (...) 分组整表返回)
// ── 分析页指标查询的短缓存 ────────────────────────────────────────────
//
// 为什么需要:`sum by (app)(count_over_time(...))` 在这台集群上扫 15 分钟
// astralx(约 6.6GB)实测 16~28 秒,Loki 的结果缓存也不命中(请求的时间窗
// 每次都在变)。用户点刷新、切页、换 tab 都要再等半分钟。
//
// 做法:按 (数据源, 查询, 时间窗) 缓存 90 秒,时间窗按 **2 分钟取整**再当 key。
// 代价是命中缓存时数据最多旧 2 分钟 × 90 秒 TTL —— 对"日志量趋势/排行"这种
// 聚合视图完全可接受,换来的是重复访问从 ~26s 降到毫秒级。
const (
metricCacheTTL = 90 * time.Second
metricCacheMax = 200
// 时间窗取整粒度:key 用取整后的值,避免"每次点刷新差几秒就穿透"
metricCacheBucket = 2 * time.Minute
)
type metricCacheEntry struct {
series []MetricSeries
expiresAt time.Time
}
var (
metricCacheMu sync.Mutex
metricCache = map[string]metricCacheEntry{}
)
// LokiMetrics 带短缓存的分析指标查询(语义见 metricCache 注释)。
func LokiMetrics(ctx context.Context, src *models.LogSource, query string, start, end int64) ([]MetricSeries, error) {
key := ""
if src != nil {
bucket := int64(metricCacheBucket)
key = fmt.Sprintf("%d|%s|%d|%d", src.ID, query, start/bucket, end/bucket)
metricCacheMu.Lock()
if entry, ok := metricCache[key]; ok && time.Now().Before(entry.expiresAt) {
metricCacheMu.Unlock()
return entry.series, nil
}
metricCacheMu.Unlock()
}
series, err := lokiMetricsUncached(ctx, src, query, start, end)
if err != nil || key == "" {
return series, err
}
metricCacheMu.Lock()
if len(metricCache) >= metricCacheMax {
now := time.Now()
for k, v := range metricCache {
if now.After(v.expiresAt) {
delete(metricCache, k)
}
}
// 清完还满:整体重置,避免无界增长把内存吃掉
if len(metricCache) >= metricCacheMax {
metricCache = map[string]metricCacheEntry{}
}
}
metricCache[key] = metricCacheEntry{series: series, expiresAt: time.Now().Add(metricCacheTTL)}
metricCacheMu.Unlock()
return series, nil
}
// lokiMetricsUncached 真正打 Loki 的实现(不带缓存)。
func lokiMetricsUncached(ctx context.Context, src *models.LogSource, query string, start, end int64) ([]MetricSeries, error) {
if end <= 0 {
end = time.Now().UnixNano()
}