- 自动备份:每日一份(按最新备份是否在 24h 内判定,重启不重复)、保留份数可配、列表/下载/恢复/删除,恢复前先校验归档;顺手修 writeArchive 逐个检查 tar/gzip/文件 Close()(审查报告 §二) - 健康持续化:只存状态变更(up/down/unknown)并推导可用率与不可用区间;公开状态页默认关闭、访问码存 SHA-256、按 IP 限流、只暴露域名与可用率 - 隧道判定修复:RouteHealthCandidate 新增 tunnel_known,frps 管理接口不可用时不再把「判不出来」当成「不通」(此前会误报「隧道连续不通」);汇总与徽标同样区分「无法判定」 - 指标历史:metrics_samples 纵向点表(本机 CPU/内存/磁盘 + 每规则上下行,30 天保留、查询按桶平均、不做降采样表);仪表盘趋势卡支持 1h/24h/7d - 仪表盘布局:运行状态并入顶部统计区(运行时长补进「服务」卡);性能并入系统资源那一行,整行 4 列 - 测试:backup/metrics/frp/api/app 新增用例;gofmt、go build/vet/test、vue-tsc、vite build 全绿 - agent 无改动,AgentVersion 保持 0.12.2
109 lines
3.7 KiB
Go
109 lines
3.7 KiB
Go
package frp
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func findUptimeStat(t *testing.T, stats []RouteUptimeStat, domain string) RouteUptimeStat {
|
||
t.Helper()
|
||
for _, stat := range stats {
|
||
if stat.Domain == domain {
|
||
return stat
|
||
}
|
||
}
|
||
t.Fatalf("结果里没有 %s:%+v", domain, stats)
|
||
return RouteUptimeStat{}
|
||
}
|
||
|
||
func TestComputeRouteUptimeIncidentAndUnknown(t *testing.T) {
|
||
from := time.Date(2026, 9, 25, 0, 0, 0, 0, time.UTC)
|
||
to := from.Add(24 * time.Hour)
|
||
at := func(offset time.Duration) string { return from.Add(offset).Format(time.RFC3339) }
|
||
|
||
events := []RouteHealthEvent{
|
||
// 窗口开始前就是 up:状态延续到窗口开始
|
||
{ServerID: 1, Domain: "a.com", State: RouteHealthUp, At: at(-2 * time.Hour)},
|
||
{ServerID: 1, Domain: "a.com", State: RouteHealthDown, At: at(time.Hour), Reason: "tunnel"},
|
||
{ServerID: 1, Domain: "a.com", State: RouteHealthUp, At: at(2 * time.Hour)},
|
||
// 窗口内才第一次出现
|
||
{ServerID: 1, Domain: "b.com", State: RouteHealthUp, At: at(6 * time.Hour)},
|
||
// 判不出来
|
||
{ServerID: 2, Domain: "c.com", State: RouteHealthUnknown, At: at(-time.Hour)},
|
||
}
|
||
|
||
stats := ComputeRouteUptime(events, from, to)
|
||
if len(stats) != 3 {
|
||
t.Fatalf("应有 3 条规则的统计,实际 %d:%+v", len(stats), stats)
|
||
}
|
||
|
||
a := findUptimeStat(t, stats, "a.com")
|
||
if a.CoveredMinutes != 24*60 {
|
||
t.Fatalf("窗口开始前的状态应延续,覆盖 24h,实际 %d 分钟", a.CoveredMinutes)
|
||
}
|
||
if a.DownMinutes != 60 {
|
||
t.Fatalf("不可用应为 60 分钟,实际 %d", a.DownMinutes)
|
||
}
|
||
if a.Uptime < 95 || a.Uptime > 96 {
|
||
t.Fatalf("可用率应约 95.8%%,实际 %.2f", a.Uptime)
|
||
}
|
||
if len(a.Incidents) != 1 || a.Incidents[0].Minutes != 60 || a.Incidents[0].Reason != "tunnel" {
|
||
t.Fatalf("不可用区间不对:%+v", a.Incidents)
|
||
}
|
||
if a.Incidents[0].To == "" {
|
||
t.Fatal("已恢复的区间应有结束时间")
|
||
}
|
||
if a.State != RouteHealthUp {
|
||
t.Fatalf("窗口末尾状态应为 up,实际 %s", a.State)
|
||
}
|
||
|
||
b := findUptimeStat(t, stats, "b.com")
|
||
if b.CoveredMinutes != 18*60 {
|
||
t.Fatalf("窗口内才出现的规则应从第一条记录起算 18h,实际 %d", b.CoveredMinutes)
|
||
}
|
||
if b.Uptime != 100 || b.DownMinutes != 0 {
|
||
t.Fatalf("没有任何 down 时可用率应为 100:%+v", b)
|
||
}
|
||
|
||
c := findUptimeStat(t, stats, "c.com")
|
||
if c.UnknownMinutes != 24*60 || c.DownMinutes != 0 {
|
||
t.Fatalf("全程 unknown 不该算作不可用:%+v", c)
|
||
}
|
||
if c.Uptime != 0 {
|
||
t.Fatalf("全程判不出来时不能编造可用率(不给 100),实际 %.2f", c.Uptime)
|
||
}
|
||
}
|
||
|
||
func TestComputeRouteUptimeOngoingIncident(t *testing.T) {
|
||
from := time.Date(2026, 9, 25, 0, 0, 0, 0, time.UTC)
|
||
to := from.Add(24 * time.Hour)
|
||
|
||
events := []RouteHealthEvent{
|
||
{ServerID: 1, Domain: "a.com", State: RouteHealthUp, At: from.Add(0).Format(time.RFC3339)},
|
||
{ServerID: 1, Domain: "a.com", State: RouteHealthDown, At: from.Add(20 * time.Hour).Format(time.RFC3339), Reason: "service"},
|
||
}
|
||
|
||
stats := ComputeRouteUptime(events, from, to)
|
||
stat := findUptimeStat(t, stats, "a.com")
|
||
if stat.DownMinutes != 4*60 {
|
||
t.Fatalf("到窗口结束仍在持续应计 240 分钟,实际 %d", stat.DownMinutes)
|
||
}
|
||
if len(stat.Incidents) != 1 {
|
||
t.Fatalf("应有 1 段区间:%+v", stat.Incidents)
|
||
}
|
||
if stat.Incidents[0].To != "" {
|
||
t.Fatalf("仍在持续的区间不应有结束时间,实际 %q", stat.Incidents[0].To)
|
||
}
|
||
if stat.State != RouteHealthDown {
|
||
t.Fatalf("窗口末尾状态应为 down,实际 %s", stat.State)
|
||
}
|
||
}
|
||
|
||
func TestComputeRouteUptimeSkipsRulesWithoutEvents(t *testing.T) {
|
||
from := time.Date(2026, 9, 25, 0, 0, 0, 0, time.UTC)
|
||
// 窗口内没有任何记录 → 不出现在结果里,而不是给一个 100%
|
||
if stats := ComputeRouteUptime(nil, from, from.Add(time.Hour)); len(stats) != 0 {
|
||
t.Fatalf("没有记录时不该有统计:%+v", stats)
|
||
}
|
||
}
|