- 自动备份:每日一份(按最新备份是否在 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
This commit is contained in:
+12
-1
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/fonu/fonu/internal/discovery"
|
||||
"github.com/fonu/fonu/internal/frp"
|
||||
"github.com/fonu/fonu/internal/logstore"
|
||||
"github.com/fonu/fonu/internal/metrics"
|
||||
"github.com/fonu/fonu/internal/monitor"
|
||||
"github.com/fonu/fonu/internal/nginx"
|
||||
"github.com/fonu/fonu/internal/notify"
|
||||
@@ -93,6 +94,8 @@ func New(cfg config.Config, staticFS fs.FS, migrationsDir string) (*App, error)
|
||||
ddnsSvc := ddns.NewService(ddnsStore, settingsStore, secretBox, logger, notifySvc)
|
||||
acmeSvc := acme.NewService(cfg, certStore, ddnsSvc, settingsStore, proxySvc, logger, notifySvc)
|
||||
backupSvc := backup.New(cfg.DataDir)
|
||||
// 指标历史:与巡检同生命周期,采样跟随巡检轮次
|
||||
metricsStore := metrics.NewStore(conn)
|
||||
discoverySvc := discovery.New()
|
||||
// 新增:多服务端 FRP 模块(接管原网关模式)
|
||||
frpMultiSvc := frp.NewService(cfg, frp.NewStore(conn), secretBox, logger, certStore)
|
||||
@@ -114,6 +117,7 @@ func New(cfg config.Config, staticFS fs.FS, migrationsDir string) (*App, error)
|
||||
Notify: notifySvc,
|
||||
ChinaCIDR: chinaCIDRSvc,
|
||||
Backup: backupSvc,
|
||||
Metrics: metricsStore,
|
||||
Discovery: discoverySvc,
|
||||
FRPMulti: frpMultiSvc,
|
||||
Traffic: trafficCollector,
|
||||
@@ -140,7 +144,9 @@ func New(cfg config.Config, staticFS fs.FS, migrationsDir string) (*App, error)
|
||||
chinaHours = 24
|
||||
}
|
||||
// 巡检间隔比其它任务短:「连续 N 次不通」需要足够采样次数才能判定
|
||||
monitorChecker := monitor.New(settingsStore, notifySvc, logger, monitorSource{frp: frpMultiSvc, logger: logger})
|
||||
monitorChecker := monitor.New(settingsStore, notifySvc, logger, monitorSource{frp: frpMultiSvc, logger: logger, traffic: trafficCollector}, healthRecorder{frp: frpMultiSvc}, metricsStore)
|
||||
// 自动备份:启动时会先跑一轮,靠「最新备份是否在 24h 内」避免频繁重启重复备份
|
||||
backupRunner := backup.NewAutoRunner(backupSvc, settingsStore, notifySvc, logger)
|
||||
sched := scheduler.New(
|
||||
scheduler.Job{
|
||||
Name: "ddns",
|
||||
@@ -162,6 +168,11 @@ func New(cfg config.Config, staticFS fs.FS, migrationsDir string) (*App, error)
|
||||
Interval: 2 * time.Minute,
|
||||
Run: monitorChecker.Run,
|
||||
},
|
||||
scheduler.Job{
|
||||
Name: "backup",
|
||||
Interval: backup.AutoInterval,
|
||||
Run: backupRunner.Run,
|
||||
},
|
||||
)
|
||||
sched.Start(ctx)
|
||||
trafficCollector.Start(ctx)
|
||||
|
||||
@@ -7,13 +7,15 @@ import (
|
||||
|
||||
"github.com/fonu/fonu/internal/frp"
|
||||
"github.com/fonu/fonu/internal/monitor"
|
||||
"github.com/fonu/fonu/internal/traffic"
|
||||
)
|
||||
|
||||
// monitorSource 把 frp 服务适配成 monitor.Source:monitor 只关心「状态快照」,
|
||||
// 怎么从 frp / agent 取是这里的事,两边因此互不依赖。
|
||||
type monitorSource struct {
|
||||
frp *frp.Service
|
||||
logger *slog.Logger
|
||||
frp *frp.Service
|
||||
logger *slog.Logger
|
||||
traffic *traffic.Collector
|
||||
}
|
||||
|
||||
func (s monitorSource) Routes(ctx context.Context) ([]monitor.RouteStatus, error) {
|
||||
@@ -30,7 +32,15 @@ func (s monitorSource) Routes(ctx context.Context) ([]monitor.RouteStatus, error
|
||||
continue
|
||||
}
|
||||
for _, item := range health {
|
||||
routes = append(routes, monitor.RouteStatus{Domain: item.Domain, TunnelOK: item.TunnelOK, Detail: item.TunnelDetail})
|
||||
routes = append(routes, monitor.RouteStatus{
|
||||
ServerID: server.ID,
|
||||
Domain: item.Domain,
|
||||
TunnelOK: item.TunnelOK,
|
||||
DNSOK: item.DNSOK,
|
||||
ServiceOK: item.ServiceOK,
|
||||
Known: item.TunnelKnown,
|
||||
Detail: item.TunnelDetail,
|
||||
})
|
||||
}
|
||||
}
|
||||
return routes, nil
|
||||
@@ -98,6 +108,19 @@ func (s monitorSource) Drift(ctx context.Context) ([]monitor.DriftStatus, error)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Traffic 返回若干域名的当前上下行速率:流量采集器内部按 host:port 累计,这里按域名合并。
|
||||
func (s monitorSource) Traffic(ctx context.Context, domains []string) (map[string]monitor.TrafficRate, error) {
|
||||
if s.traffic == nil {
|
||||
return nil, nil
|
||||
}
|
||||
snapshot := s.traffic.SnapshotByHost(domains)
|
||||
rates := make(map[string]monitor.TrafficRate, len(snapshot))
|
||||
for host, item := range snapshot {
|
||||
rates[host] = monitor.TrafficRate{Upload: item.UploadRate, Download: item.DownloadRate}
|
||||
}
|
||||
return rates, nil
|
||||
}
|
||||
|
||||
func (s monitorSource) enabledServers(ctx context.Context) ([]frp.Server, error) {
|
||||
servers, err := s.frp.ListServers(ctx)
|
||||
if err != nil {
|
||||
@@ -128,3 +151,40 @@ func driftKindText(kind string) string {
|
||||
return kind
|
||||
}
|
||||
}
|
||||
|
||||
// healthRecorder 把 monitor 的健康结论翻成 frp 侧样本并落库(
|
||||
// 两侧的状态字面量由 healthStateValue 显式翻译,改动一侧会在这里编译期报错)。
|
||||
type healthRecorder struct {
|
||||
frp *frp.Service
|
||||
}
|
||||
|
||||
func (r healthRecorder) RecordRouteHealth(ctx context.Context, samples []monitor.HealthSample) error {
|
||||
converted := make([]frp.RouteHealthSample, 0, len(samples))
|
||||
for _, sample := range samples {
|
||||
converted = append(converted, frp.RouteHealthSample{
|
||||
ServerID: sample.ServerID,
|
||||
Domain: sample.Domain,
|
||||
State: healthStateValue(sample.State),
|
||||
Reason: sample.Reason,
|
||||
Detail: sample.Detail,
|
||||
})
|
||||
}
|
||||
_, err := r.frp.RecordRouteHealth(ctx, converted)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r healthRecorder) PruneRouteHealth(ctx context.Context) error {
|
||||
_, err := r.frp.PruneRouteHealth(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func healthStateValue(state string) string {
|
||||
switch state {
|
||||
case monitor.HealthUp:
|
||||
return frp.RouteHealthUp
|
||||
case monitor.HealthDown:
|
||||
return frp.RouteHealthDown
|
||||
default:
|
||||
return frp.RouteHealthUnknown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/fonu/fonu/internal/frp"
|
||||
"github.com/fonu/fonu/internal/monitor"
|
||||
)
|
||||
|
||||
// monitor 与 frp 两侧的状态字面量必须一致,落库前由 healthStateValue 翻译一次;
|
||||
// 这个测试把翻译钉住,避免以后改一侧的常量而另一侧悄悄写错值。
|
||||
func TestHealthStateValue(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
monitor.HealthUp: frp.RouteHealthUp,
|
||||
monitor.HealthDown: frp.RouteHealthDown,
|
||||
monitor.HealthUnknown: frp.RouteHealthUnknown,
|
||||
"其他": frp.RouteHealthUnknown,
|
||||
}
|
||||
for input, want := range cases {
|
||||
if got := healthStateValue(input); got != want {
|
||||
t.Fatalf("%q 应翻译为 %q,实际 %q", input, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user