=== 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
103 lines
3.3 KiB
Go
103 lines
3.3 KiB
Go
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)
|
||
}
|
||
}
|