Files
LokiStack/backend/services/daily_volume_capture_test.go
opsx d4422f1541
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
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
2026-10-30 17:13:18 +00:00

103 lines
3.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package services
import (
"fmt"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"lokistack/models"
"lokistack/repository"
)
// 条数(index/stats)只给"已结束且还没有条数"的天补,并且每天最多 3 次。
// 这条护栏的直接目的:别让 30 分钟一次的定时任务把整天 index/stats 反复重跑。
func TestCaptureDailyEntriesOnlyForPastDaysAndWrites(t *testing.T) {
setupDailyVolumeDB(t)
var calls int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&calls, 1)
if r.URL.Path != "/loki/api/v1/index/stats" {
t.Errorf("意外请求路径: %s", r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"streams":3,"chunks":9,"bytes":123456,"entries":456}`)
}))
defer srv.Close()
src := &models.LogSource{ID: 1, Name: "test", TargetType: "url", URL: srv.URL}
yesterday := dateKey(time.Now().AddDate(0, 0, -1))
today := dateKey(time.Now())
db := repository.GetDB()
db.Lock()
db.DailyVolumeSnapshots = []models.DailyVolumeSnapshot{
{ID: 1, Date: yesterday, SourceID: 1, Bytes: 100},
{ID: 2, Date: today, SourceID: 1, Bytes: 10, Partial: true},
}
db.Unlock()
captureDailyEntries(src)
got := snapshotsByDate(t)
if snap := got[yesterday]; snap.Entries != 456 {
t.Fatalf("昨天的条数应写入 456,得到 %d", snap.Entries)
}
if snap := got[yesterday]; snap.EntriesAttempts != 1 {
t.Errorf("昨天应记录 1 次尝试,得到 %d", snap.EntriesAttempts)
}
if snap := got[today]; snap.EntriesAttempts != 0 || snap.Entries != 0 {
t.Errorf("今天没结束,不应抓条数(attempts=%d entries=%d)", snap.EntriesAttempts, snap.Entries)
}
if n := atomic.LoadInt32(&calls); n != 1 {
t.Errorf("应只打 1 次 index/stats,实际 %d 次", n)
}
// 1 小时内重复触发不应再打 Loki
captureDailyEntries(src)
if n := atomic.LoadInt32(&calls); n != 1 {
t.Errorf("重试间隔内不应再查询,实际 %d 次", n)
}
}
// 失败(Loki 报错)时必须吃得下,并且最多尝试 entriesMaxAttempts 次就放弃。
func TestCaptureDailyEntriesRetryBudget(t *testing.T) {
setupDailyVolumeDB(t)
var calls int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&calls, 1)
http.Error(w, "boom", http.StatusInternalServerError)
}))
defer srv.Close()
src := &models.LogSource{ID: 1, Name: "test", TargetType: "url", URL: srv.URL}
yesterday := dateKey(time.Now().AddDate(0, 0, -1))
db := repository.GetDB()
db.Lock()
db.DailyVolumeSnapshots = []models.DailyVolumeSnapshot{{ID: 1, Date: yesterday, SourceID: 1, Bytes: 100}}
db.Unlock()
// 模拟"每轮定时任务都到点":把上次尝试时间清空,绕过 1 小时间隔
for i := 0; i < entriesMaxAttempts+3; i++ {
captureDailyEntries(src)
db.Lock()
for j := range db.DailyVolumeSnapshots {
db.DailyVolumeSnapshots[j].LastEntriesAttempt = time.Time{}
}
db.Unlock()
}
if n := atomic.LoadInt32(&calls); n != entriesMaxAttempts {
t.Errorf("应恰好尝试 %d 次后放弃,实际 %d 次", entriesMaxAttempts, n)
}
if snap := snapshotsByDate(t)[yesterday]; snap.Entries != 0 || snap.EntriesAttempts != entriesMaxAttempts {
t.Errorf("失败后应留 entries=0 / attempts=%d,得到 entries=%d attempts=%d",
entriesMaxAttempts, snap.Entries, snap.EntriesAttempts)
}
}