refactor(analysis): 去掉排行 10/30/50 选择器,改为一次取 50 + 前端分页;零点结算昨天
=== 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
This commit is contained in:
1 parent
63dd491c5b
commit
d4422f1541
22 files changed
+715
-117
No files matched your search
@@ -409,6 +409,109 @@ func LokiLabelValues(ctx context.Context, src *models.LogSource, name, selector
|
||||
return resp.Data, nil
|
||||
}
|
||||
|
||||
// LokiLabelValuesBatch 一次取多个 label 的候选值。
|
||||
//
|
||||
// 前端快捷查询要对 app / service_name / container 做逐级回退(哪个有值用哪个),
|
||||
// 原来最多并发 3 个请求、每个请求各走一次 Loki 索引查询 + 一次 kube-apiserver
|
||||
// service proxy;合并成本接口后前端只发一次,后端内部并发(上限 4)。
|
||||
//
|
||||
// 语义:单个 label 失败不影响其它 label(返回空数组),**全部失败**才报错 ——
|
||||
// 否则某个 label 在特定 selector 下索引异常,整个下拉框都会变成"加载失败"。
|
||||
func LokiLabelValuesBatch(ctx context.Context, src *models.LogSource, names []string, selector string, start, end int64) (map[string][]string, error) {
|
||||
out := make(map[string][]string, len(names))
|
||||
var (
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
firstErr error
|
||||
okCount int
|
||||
seen = make(map[string]bool, len(names))
|
||||
)
|
||||
sem := make(chan struct{}, 4)
|
||||
for _, raw := range names {
|
||||
name := strings.TrimSpace(raw)
|
||||
if name == "" || seen[name] {
|
||||
continue
|
||||
}
|
||||
seen[name] = true
|
||||
out[name] = []string{}
|
||||
wg.Add(1)
|
||||
go func(labelName string) {
|
||||
defer wg.Done()
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
values, err := LokiLabelValues(ctx, src, labelName, selector, start, end)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
return
|
||||
}
|
||||
okCount++
|
||||
out[labelName] = values
|
||||
}(name)
|
||||
}
|
||||
wg.Wait()
|
||||
if okCount == 0 && firstErr != nil {
|
||||
return out, firstErr
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// LokiVolumeBytesFor 用 index/volume 读某个 selector 的日志体积(字节)。
|
||||
//
|
||||
// 这是 Loki 的**索引接口**:只读 TSDB 里记录的 chunk 字节数,不解码日志内容。
|
||||
// 实测单个 app、一小时窗口约 80ms(对比 bytes_over_time 同窗口 13s/15 分钟、
|
||||
// 约 50s/小时 —— 因为它要把整小时的日志解压出来数行字节)。
|
||||
//
|
||||
// 代价:index/volume 返回的 metric 只包含 selector 里出现过的 label,
|
||||
// 所以"按 app 取体积"没法一条查询拿全,必须一个 app 调一次(见 app_volume.go)。
|
||||
// aggregateBy=series 让返回值带上 selector 里的全部 label,便于调用方核对。
|
||||
func LokiVolumeBytesFor(ctx context.Context, src *models.LogSource, selector string, start, end int64) (int64, error) {
|
||||
if end <= 0 {
|
||||
end = time.Now().UnixNano()
|
||||
}
|
||||
if start <= 0 {
|
||||
start = end - int64(time.Hour)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
q := url.Values{}
|
||||
q.Set("query", selector)
|
||||
q.Set("start", strconv.FormatInt(start, 10))
|
||||
q.Set("end", strconv.FormatInt(end, 10))
|
||||
q.Set("aggregateBy", "series")
|
||||
body, _, err := lokiProxyGET(ctx, src, "/loki/api/v1/index/volume", q)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var resp struct {
|
||||
Data struct {
|
||||
Result []struct {
|
||||
Metric map[string]string `json:"metric"`
|
||||
Value [2]interface{} `json:"value"`
|
||||
} `json:"result"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
return 0, fmt.Errorf("解析 volume 响应失败: %w", err)
|
||||
}
|
||||
var total int64
|
||||
for _, item := range resp.Data.Result {
|
||||
switch v := item.Value[1].(type) {
|
||||
case float64:
|
||||
total += int64(v)
|
||||
case string:
|
||||
if n, convErr := strconv.ParseFloat(v, 64); convErr == nil {
|
||||
total += int64(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// extractStreamSelector 从 LogQL 中提取 {...} 部分(labels API 仅接受 stream selector)
|
||||
func extractStreamSelector(query string) string {
|
||||
i := strings.Index(query, "{")
|
||||
@@ -826,9 +929,12 @@ func pickHistogramStep(spanNano int64) string {
|
||||
return "1m"
|
||||
case span <= 6*time.Hour:
|
||||
return "5m"
|
||||
case span <= 24*time.Hour:
|
||||
case span <= 12*time.Hour:
|
||||
return "15m"
|
||||
case span <= 7*24*time.Hour:
|
||||
// 12 小时以上统一 1 小时一桶。
|
||||
// 24 小时原来是 15m(96 根柱子):横轴挤成一片、且每根柱子只有十来分钟的数据,
|
||||
// 看不出趋势;1h → 24 根,和"按小时看日志量"的心智一致。
|
||||
return "1h"
|
||||
default:
|
||||
return "6h"
|
||||
|
||||
Reference in new issue
Block a user