diff --git a/internal/api/handler.go b/internal/api/handler.go index cab7a1f..71ed616 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -37,6 +37,7 @@ func NewHandler(cfg config.ConfigInstance) http.Handler { mux.HandleFunc("GET", "/v1/schema/{filename...}", v1.GetSchemaFile) mux.HandleFunc("GET", "/v1/stats", useCfg(cfg, v1.Stats)) mux.HandleFunc("GET", "/v1/stats/ws", useCfg(cfg, v1.StatsWS)) + mux.HandleFunc("GET", "/v1/health/ws", useCfg(cfg, v1.HealthWS)) mux.HandleFunc("GET", "/v1/favicon/{alias}", auth.RequireAuth(favicon.GetFavIcon)) return mux } diff --git a/internal/api/v1/health.go b/internal/api/v1/health.go new file mode 100644 index 0000000..82fe9a5 --- /dev/null +++ b/internal/api/v1/health.go @@ -0,0 +1,18 @@ +package v1 + +import ( + "net/http" + "time" + + "github.com/coder/websocket" + "github.com/coder/websocket/wsjson" + U "github.com/yusing/go-proxy/internal/api/v1/utils" + config "github.com/yusing/go-proxy/internal/config/types" + "github.com/yusing/go-proxy/internal/route/routes" +) + +func HealthWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) { + U.PeriodicWS(cfg, w, r, 1*time.Second, func(conn *websocket.Conn) error { + return wsjson.Write(r.Context(), conn, routes.HealthMap()) + }) +} diff --git a/internal/config/config.go b/internal/config/config.go index 6a18c23..66fc0b2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,7 @@ package config import ( + "context" "os" "strconv" "strings" @@ -146,6 +147,10 @@ func (cfg *Config) Task() *task.Task { return cfg.task } +func (cfg *Config) Context() context.Context { + return cfg.task.Context() +} + func (cfg *Config) Start() { cfg.StartAutoCert() cfg.StartProxyProviders() diff --git a/internal/config/types/config.go b/internal/config/types/config.go index 9590337..ca4a0ac 100644 --- a/internal/config/types/config.go +++ b/internal/config/types/config.go @@ -1,6 +1,8 @@ package types import ( + "context" + "github.com/yusing/go-proxy/internal/net/http/accesslog" "github.com/yusing/go-proxy/internal/utils" @@ -31,6 +33,7 @@ type ( Value() *Config Reload() E.Error Statistics() map[string]any + Context() context.Context } ) diff --git a/internal/docker/idlewatcher/waker.go b/internal/docker/idlewatcher/waker.go index c2c34c8..6a657d9 100644 --- a/internal/docker/idlewatcher/waker.go +++ b/internal/docker/idlewatcher/waker.go @@ -117,6 +117,11 @@ func (w *Watcher) Uptime() time.Duration { return 0 } +// Latency implements health.HealthMonitor. +func (w *Watcher) Latency() time.Duration { + return 0 +} + // Status implements health.HealthMonitor. func (w *Watcher) Status() health.Status { status := w.getStatusUpdateReady() diff --git a/internal/net/http/loadbalancer/types/server.go b/internal/net/http/loadbalancer/types/server.go index b3df394..db10dcf 100644 --- a/internal/net/http/loadbalancer/types/server.go +++ b/internal/net/http/loadbalancer/types/server.go @@ -28,7 +28,7 @@ type ( Name() string URL() types.URL Weight() Weight - SetWeight(Weight) + SetWeight(weight Weight) TryWake() error } diff --git a/internal/route/http.go b/internal/route/http.go index d47a207..b2b87a3 100755 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -30,7 +30,7 @@ type ( HealthMon health.HealthMonitor `json:"health,omitempty"` loadBalancer *loadbalancer.LoadBalancer - server *loadbalancer.Server + server loadbalancer.Server handler http.Handler rp *reverseproxy.ReverseProxy @@ -180,11 +180,8 @@ func (r *HTTPRoute) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.handler.ServeHTTP(w, req) } -func (r *HTTPRoute) Health() health.Status { - if r.HealthMon != nil { - return r.HealthMon.Status() - } - return health.StatusUnknown +func (r *HTTPRoute) HealthMonitor() health.HealthMonitor { + return r.HealthMon } func (r *HTTPRoute) addToLoadBalancer(parent task.Parent) { diff --git a/internal/route/provider/stats.go b/internal/route/provider/stats.go index 276a140..f62b84f 100644 --- a/internal/route/provider/stats.go +++ b/internal/route/provider/stats.go @@ -26,7 +26,12 @@ type ( func (stats *RouteStats) Add(r *R.Route) { stats.Total++ - switch r.Health() { + mon := r.HealthMonitor() + if mon == nil { + stats.NumUnknown++ + return + } + switch mon.Status() { case health.StatusHealthy: stats.NumHealthy++ case health.StatusUnhealthy: diff --git a/internal/route/route.go b/internal/route/route.go index 569d727..220f5db 100755 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -11,7 +11,6 @@ import ( "github.com/yusing/go-proxy/internal/task" U "github.com/yusing/go-proxy/internal/utils" F "github.com/yusing/go-proxy/internal/utils/functional" - "github.com/yusing/go-proxy/internal/watcher/health" ) type ( @@ -24,12 +23,11 @@ type ( Routes = F.Map[string, *Route] impl interface { - entry.Entry + types.Route task.TaskStarter task.TaskFinisher String() string TargetURL() url.URL - Health() health.Status } RawEntry = types.RawEntry RawEntries = types.RawEntries diff --git a/internal/route/routes/query.go b/internal/route/routes/query.go index 6554d65..c06a22d 100644 --- a/internal/route/routes/query.go +++ b/internal/route/routes/query.go @@ -2,6 +2,7 @@ package routes import ( "strings" + "time" "github.com/yusing/go-proxy/internal/homepage" "github.com/yusing/go-proxy/internal/route/entry" @@ -10,6 +11,33 @@ import ( "github.com/yusing/go-proxy/internal/utils/strutils" ) +func getHealthInfo(r route.Route) map[string]string { + mon := r.HealthMonitor() + if mon == nil { + return map[string]string{ + "status": "unknown", + "uptime": "n/a", + "latency": "n/a", + } + } + return map[string]string{ + "status": mon.Status().String(), + "uptime": mon.Uptime().Round(time.Second).String(), + "latency": mon.Latency().Round(time.Microsecond).String(), + } +} + +func HealthMap() map[string]map[string]string { + healthMap := make(map[string]map[string]string) + httpRoutes.RangeAll(func(alias string, r route.HTTPRoute) { + healthMap[alias] = getHealthInfo(r) + }) + streamRoutes.RangeAll(func(alias string, r route.StreamRoute) { + healthMap[alias] = getHealthInfo(r) + }) + return healthMap +} + func HomepageConfig(useDefaultCategories bool) homepage.Config { hpCfg := homepage.NewHomePageConfig() GetHTTPRoutes().RangeAll(func(alias string, r route.HTTPRoute) { @@ -77,8 +105,8 @@ func HomepageConfig(useDefaultCategories bool) homepage.Config { return hpCfg } -func RoutesByAlias(typeFilter ...route.RouteType) map[string]any { - rts := make(map[string]any) +func RoutesByAlias(typeFilter ...route.RouteType) map[string]route.Route { + rts := make(map[string]route.Route) if len(typeFilter) == 0 || typeFilter[0] == "" { typeFilter = []route.RouteType{route.RouteTypeReverseProxy, route.RouteTypeStream} } diff --git a/internal/route/stream.go b/internal/route/stream.go index 3fc2bdf..1660fa6 100755 --- a/internal/route/stream.go +++ b/internal/route/stream.go @@ -116,12 +116,8 @@ func (r *StreamRoute) Finish(reason any) { r.task.Finish(reason) } - -func (r *StreamRoute) Health() health.Status { - if r.HealthMon != nil { - return r.HealthMon.Status() - } - return health.StatusUnknown +func (r *StreamRoute) HealthMonitor() health.HealthMonitor { + return r.HealthMon } func (r *StreamRoute) acceptConnections() { diff --git a/internal/route/types/route.go b/internal/route/types/route.go index 4b56b30..b607e31 100644 --- a/internal/route/types/route.go +++ b/internal/route/types/route.go @@ -8,14 +8,16 @@ import ( ) type ( - HTTPRoute interface { + Route interface { Entry + HealthMonitor() health.HealthMonitor + } + HTTPRoute interface { + Route http.Handler - Health() health.Status } StreamRoute interface { - Entry + Route net.Stream - Health() health.Status } ) diff --git a/internal/watcher/health/monitor/monitor.go b/internal/watcher/health/monitor/monitor.go index 25d710e..bb28497 100644 --- a/internal/watcher/health/monitor/monitor.go +++ b/internal/watcher/health/monitor/monitor.go @@ -142,6 +142,14 @@ func (mon *monitor) Uptime() time.Duration { return time.Since(mon.startTime) } +// Latency implements HealthMonitor. +func (mon *monitor) Latency() time.Duration { + if mon.lastResult == nil { + return 0 + } + return mon.lastResult.Latency +} + // Name implements HealthMonitor. func (mon *monitor) Name() string { parts := strutils.SplitRune(mon.service, '/')