Files
-Fonu/internal/app/monitor_source.go
T
mrlee c8264bf280
CI / verify (push) Canceled after 0s
feat(notify): 告警补服务端掉线/Agent 不可达/配置漂移,巡检改 Source 接口(v1.2.10)
- 三个新事件(默认关闭):公网服务端掉线、公网 agent 不可达、配置漂移;连续失败阈值与漂移提醒间隔均可配,达标与恢复各报一次

- 避免误报:只在「用户要服务端运行」时判定掉线;ConnectionState=unknown 按健康处理;没配 agent 的服务端不参与可达性判定

- internal/monitor 改 Source 接口(Routes/Servers/Agents/Drift),frp 适配在 internal/app/monitor_source.go;「连续 N 次」抽成共用 failureTransition,通知一律在锁外调用

- 通知图标分类补齐;通知方式标签改「Webhook / IM 机器人」;新增 6 个设置项;补 serverDown/serverLabel/stateOr 测试

- agent 无改动,AgentVersion 保持 0.12.2
2026-09-25 09:54:48 +08:00

131 lines
3.8 KiB
Go
Raw 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 app
import (
"context"
"fmt"
"log/slog"
"github.com/fonu/fonu/internal/frp"
"github.com/fonu/fonu/internal/monitor"
)
// monitorSource 把 frp 服务适配成 monitor.Source:monitor 只关心「状态快照」,
// 怎么从 frp / agent 取是这里的事,两边因此互不依赖。
type monitorSource struct {
frp *frp.Service
logger *slog.Logger
}
func (s monitorSource) Routes(ctx context.Context) ([]monitor.RouteStatus, error) {
servers, err := s.enabledServers(ctx)
if err != nil {
return nil, err
}
routes := make([]monitor.RouteStatus, 0, len(servers))
for _, server := range servers {
// 单台服务端探测失败不影响其它服务端:它本轮跳过,等下一轮
health, err := s.frp.RouteHealth(ctx, server.ID)
if err != nil {
s.logger.Warn("巡检跳过服务端隧道", "module", "MONITOR", "server_id", server.ID, "error", err.Error())
continue
}
for _, item := range health {
routes = append(routes, monitor.RouteStatus{Domain: item.Domain, TunnelOK: item.TunnelOK, Detail: item.TunnelDetail})
}
}
return routes, nil
}
func (s monitorSource) Servers(ctx context.Context) ([]monitor.ServerStatus, error) {
servers, err := s.enabledServers(ctx)
if err != nil {
return nil, err
}
out := make([]monitor.ServerStatus, 0, len(servers))
for _, server := range servers {
out = append(out, monitor.ServerStatus{
ID: server.ID,
Name: server.Name,
DesiredState: server.DesiredState,
ProcessState: server.ProcessState,
ConnectionState: server.ConnectionState,
Detail: server.LastError,
})
}
return out, nil
}
func (s monitorSource) Agents(ctx context.Context) ([]monitor.AgentStatus, error) {
servers, err := s.enabledServers(ctx)
if err != nil {
return nil, err
}
out := make([]monitor.AgentStatus, 0, len(servers))
for _, server := range servers {
// AgentClient 只在「地址与令牌都可用」时才返回 ok:没配 agent 的服务端不参与可达性判定
client, ok := s.frp.AgentClient(ctx, server.ID)
if !ok {
continue
}
status := monitor.AgentStatus{ID: server.ID, Name: server.Name, Reachable: true}
if err := client.Ping(ctx); err != nil {
status.Reachable = false
status.Detail = err.Error()
}
out = append(out, status)
}
return out, nil
}
func (s monitorSource) Drift(ctx context.Context) ([]monitor.DriftStatus, error) {
servers, err := s.enabledServers(ctx)
if err != nil {
return nil, err
}
out := make([]monitor.DriftStatus, 0, len(servers))
for _, server := range servers {
report, err := s.frp.RouteDrift(ctx, server.ID)
if err != nil {
s.logger.Warn("巡检跳过漂移检测", "module", "MONITOR", "server_id", server.ID, "error", err.Error())
continue
}
status := monitor.DriftStatus{ID: server.ID, Name: server.Name, Count: len(report.Items)}
for _, item := range report.Items {
status.Items = append(status.Items, fmt.Sprintf("%s %s", item.Domain, driftKindText(item.Kind)))
}
out = append(out, status)
}
return out, nil
}
func (s monitorSource) enabledServers(ctx context.Context) ([]frp.Server, error) {
servers, err := s.frp.ListServers(ctx)
if err != nil {
return nil, err
}
enabled := make([]frp.Server, 0, len(servers))
for _, server := range servers {
// 停用中的服务端是用户主动关掉的,不该参与任何告警判定
if server.Enabled {
enabled = append(enabled, server)
}
}
return enabled, nil
}
// driftKindText 与公网反代页漂移清单的文案保持一致,通知里才不用回页面去对照。
func driftKindText(kind string) string {
switch kind {
case frp.RouteDriftMissing:
return "未部署"
case frp.RouteDriftOrphan:
return "孤儿配置"
case frp.RouteDriftNotLoaded:
return "未生效"
case frp.RouteDriftContentMismatch:
return "内容不一致"
default:
return kind
}
}