=== 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
157 lines
5.2 KiB
Go
157 lines
5.2 KiB
Go
package services
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
|
||
"lokistack/models"
|
||
"lokistack/repository"
|
||
)
|
||
|
||
func setupDailyVolumeDB(t *testing.T) {
|
||
t.Helper()
|
||
t.Setenv("DATA_DIR", t.TempDir())
|
||
t.Setenv("JWT_SECRET", "test-jwt-secret-with-at-least-32-characters")
|
||
t.Setenv("ENCRYPTION_KEY", "test-encryption-key-with-at-least-32-characters")
|
||
if err := repository.InitDB(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Cleanup(func() { _ = repository.Shutdown() })
|
||
|
||
// 每天的快照都挂着数据源,prune 会清掉"数据源已删除"的孤儿快照,
|
||
// 所以测试里先登记好 id=1 的数据源。
|
||
db := repository.GetDB()
|
||
db.Lock()
|
||
if len(db.LogSources) == 0 {
|
||
db.LogSources = append(db.LogSources, models.LogSource{ID: 1, Name: "test-loki", TargetType: "url"})
|
||
}
|
||
db.Unlock()
|
||
}
|
||
|
||
func snapshotsByDate(t *testing.T) map[string]models.DailyVolumeSnapshot {
|
||
t.Helper()
|
||
got := map[string]models.DailyVolumeSnapshot{}
|
||
db := repository.GetDB()
|
||
db.RLock()
|
||
defer db.RUnlock()
|
||
for _, snap := range db.DailyVolumeSnapshots {
|
||
got[snap.Date] = snap
|
||
}
|
||
return got
|
||
}
|
||
|
||
// 过去的天不可能"还在进行中":只有在那天结束之后才抓的快照才算完整一天,
|
||
// 当天中途抓到的必须保持 partial —— 否则会把"只抓到半天"伪装成完整数据。
|
||
func TestPruneDailyVolumeNormalizesPartialAndDropsExpired(t *testing.T) {
|
||
setupDailyVolumeDB(t)
|
||
now := time.Now()
|
||
midday := func(d time.Time) time.Time {
|
||
return time.Date(d.Year(), d.Month(), d.Day(), 12, 0, 0, 0, time.Local)
|
||
}
|
||
yesterday := now.AddDate(0, 0, -1)
|
||
dayBefore := now.AddDate(0, 0, -2)
|
||
expired := now.AddDate(0, 0, -(dailyVolumeRetentionDays + 5))
|
||
|
||
db := repository.GetDB()
|
||
db.Lock()
|
||
db.DailyVolumeSnapshots = []models.DailyVolumeSnapshot{
|
||
// 昨天结束之后(次日 12:00)抓的 → 完整一天
|
||
{ID: 1, Date: dateKey(yesterday), SourceID: 1, Bytes: 100, Entries: 7,
|
||
Partial: true, CapturedAt: midday(now)},
|
||
// 前天中午抓的 → 只有半天,保持 partial
|
||
{ID: 2, Date: dateKey(dayBefore), SourceID: 1, Bytes: 50,
|
||
Partial: true, CapturedAt: midday(dayBefore)},
|
||
// 今天 → 永远 partial
|
||
{ID: 3, Date: dateKey(now), SourceID: 1, Bytes: 10, Partial: true},
|
||
// 超过留存期 → 必须被清理
|
||
{ID: 4, Date: dateKey(expired), SourceID: 1, Bytes: 999},
|
||
// 数据源已删除的孤儿快照 → 必须被清理
|
||
{ID: 5, Date: dateKey(yesterday), SourceID: 99, Bytes: 888},
|
||
}
|
||
db.Unlock()
|
||
|
||
pruneDailyVolumeSnapshots()
|
||
|
||
got := snapshotsByDate(t)
|
||
if _, ok := got[dateKey(expired)]; ok {
|
||
t.Error("超过留存期的快照没有被清理")
|
||
}
|
||
// 孤儿快照(SourceID=99 的数据源已不存在)必须被清掉
|
||
db = repository.GetDB()
|
||
db.RLock()
|
||
for _, snap := range db.DailyVolumeSnapshots {
|
||
if snap.SourceID == 99 {
|
||
t.Error("数据源已删除的孤儿快照没有被清理")
|
||
}
|
||
}
|
||
db.RUnlock()
|
||
if snap := got[dateKey(yesterday)]; snap.Partial {
|
||
t.Error("昨天结束之后抓的快照应标记为完整(partial=false)")
|
||
}
|
||
if snap := got[dateKey(dayBefore)]; !snap.Partial {
|
||
t.Error("当天中途抓的快照必须保持 partial=true")
|
||
}
|
||
if snap := got[dateKey(now)]; !snap.Partial {
|
||
t.Error("今天必须保持 partial=true")
|
||
}
|
||
if snap := got[dateKey(yesterday)]; snap.Entries != 7 {
|
||
t.Errorf("条数不应被清理流程清空,得到 %d", snap.Entries)
|
||
}
|
||
}
|
||
|
||
// 刷新体积不能把已经抓到的条数(贵、每天最多 3 次)冲掉。
|
||
func TestUpsertVolumeSnapshotKeepsEntries(t *testing.T) {
|
||
setupDailyVolumeDB(t)
|
||
|
||
db := repository.GetDB()
|
||
db.Lock()
|
||
db.DailyVolumeSnapshots = []models.DailyVolumeSnapshot{
|
||
{ID: 1, Date: "2026-10-01", SourceID: 1, Bytes: 111, Entries: 42, Partial: true},
|
||
}
|
||
db.Unlock()
|
||
|
||
if err := upsertVolumeSnapshot(1, "2026-10-01", 1000, false); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got := snapshotsByDate(t)["2026-10-01"]
|
||
if got.Bytes != 1000 || got.Entries != 42 || got.Partial {
|
||
t.Fatalf("更新后 bytes=%d entries=%d partial=%v;期望 1000/42/false", got.Bytes, got.Entries, got.Partial)
|
||
}
|
||
|
||
if err := upsertVolumeSnapshot(1, "2026-10-02", 2000, true); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := snapshotsByDate(t); len(got) != 2 {
|
||
t.Fatalf("新增一天后应有 2 条快照,得到 %d", len(got))
|
||
}
|
||
}
|
||
|
||
// 接口必须始终返回 days 个点,缺的标 Missing("不知道"),绝不伪造 0("确实没日志")。
|
||
func TestGetDailyVolumeHistoryReturnsAllDaysWithMissing(t *testing.T) {
|
||
setupDailyVolumeDB(t)
|
||
now := time.Now()
|
||
|
||
db := repository.GetDB()
|
||
db.Lock()
|
||
db.DailyVolumeSnapshots = []models.DailyVolumeSnapshot{
|
||
{ID: 1, Date: dateKey(now), SourceID: 1, Bytes: 123, Partial: true},
|
||
{ID: 2, Date: dateKey(now), SourceID: 2, Bytes: 999}, // 别的数据源,不应混进来
|
||
}
|
||
db.Unlock()
|
||
|
||
points := GetDailyVolumeHistory(1, 7)
|
||
if len(points) != 7 {
|
||
t.Fatalf("期望 7 个点,得到 %d", len(points))
|
||
}
|
||
today := points[len(points)-1]
|
||
if today.Missing || today.Bytes != 123 || !today.Partial {
|
||
t.Errorf("今天应命中快照:missing=%v bytes=%d partial=%v", today.Missing, today.Bytes, today.Partial)
|
||
}
|
||
for i := 0; i < len(points)-1; i++ {
|
||
if !points[i].Missing || points[i].Bytes != 0 {
|
||
t.Errorf("%s 应标记 Missing 且 bytes=0,得到 missing=%v bytes=%d",
|
||
points[i].Date, points[i].Missing, points[i].Bytes)
|
||
}
|
||
}
|
||
}
|