mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-09-20 00:47:57 +00:00
feat: M3 load balancing + health check (model/render/admin API status/UI)
This commit is contained in:
@ -4,9 +4,12 @@ package httpapi
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"webui4frpc/internal/process"
|
||||
"webui4frpc/internal/store"
|
||||
@ -131,6 +134,18 @@ func contentTypeFor(name string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// proxyState is a per-proxy status row reported by the local frpc admin
|
||||
// API (GET /api/status). Status is one of frpc's proxy phases:
|
||||
// new | wait start | start error | running | check failed | closed.
|
||||
type proxyState struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Err string `json:"err,omitempty"`
|
||||
LocalAddr string `json:"local_addr"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
}
|
||||
|
||||
func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
methodNotAllowed(w)
|
||||
@ -149,6 +164,13 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
// ConnState is the derived frps connection state:
|
||||
// connected | connecting | failed | not_started | disabled.
|
||||
ConnState string `json:"connState"`
|
||||
// AdminEnabled is true when the remote configures a local frpc admin
|
||||
// API (adminPort > 0); then ProxyStates reflects real proxy health.
|
||||
AdminEnabled bool `json:"adminEnabled"`
|
||||
ProxyStates []proxyState `json:"proxyStates,omitempty"`
|
||||
// GroupCount is the number of distinct LB groups across this remote's
|
||||
// forwards (M3 display).
|
||||
GroupCount int `json:"groupCount,omitempty"`
|
||||
}
|
||||
|
||||
profiles := make([]profileStatus, 0, len(remotes))
|
||||
@ -159,10 +181,24 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
for _, rv := range remotes {
|
||||
st, has := h.Process.Status(rv.Name)
|
||||
fwd, _ := h.Store.LinksForRemote(rv.Name)
|
||||
profiles = append(profiles, profileStatus{
|
||||
p := profileStatus{
|
||||
Name: rv.Name, Enabled: rv.Enabled, Status: st, HasProc: has, Forwards: fwd,
|
||||
ConnState: connStateOf(h.Process.LogPath(rv.Name), rv.Enabled, st),
|
||||
})
|
||||
AdminEnabled: rv.AdminPort > 0,
|
||||
GroupCount: groupCountOf(locals, fwd),
|
||||
}
|
||||
if rv.AdminPort > 0 {
|
||||
// Query the local frpc admin API for true per-proxy state.
|
||||
if states, err := fetchProxyStates(rv.AdminAddr, rv.AdminPort, rv.AdminUser, rv.AdminPassword); err == nil {
|
||||
p.ProxyStates = states
|
||||
}
|
||||
}
|
||||
// Prefer admin-derived conn state; fall back to log inference.
|
||||
if p.AdminEnabled && len(p.ProxyStates) > 0 {
|
||||
p.ConnState = connStateAdmin(p.ProxyStates)
|
||||
} else {
|
||||
p.ConnState = connStateOf(h.Process.LogPath(rv.Name), rv.Enabled, st)
|
||||
}
|
||||
profiles = append(profiles, p)
|
||||
}
|
||||
|
||||
// Local status: each local plus its forwarding targets and whether each
|
||||
@ -251,6 +287,86 @@ func connStateOf(workerLog string, enabled bool, st process.Status) string {
|
||||
}
|
||||
}
|
||||
|
||||
// fetchProxyStates queries a worker's local frpc admin API (webServer) for
|
||||
// true per-proxy status: GET /api/status returns a map keyed by proxy type,
|
||||
// each value a list of { name, type, status, err, local_addr, remote_addr }.
|
||||
// status is one of frpc's proxy phases:
|
||||
//
|
||||
// new | wait start | start error | running | check failed | closed
|
||||
func fetchProxyStates(addr string, port int, user, pass string) ([]proxyState, error) {
|
||||
if addr == "" {
|
||||
addr = "127.0.0.1"
|
||||
}
|
||||
url := fmt.Sprintf("http://%s:%d/api/status", addr, port)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetBasicAuth(user, pass)
|
||||
cli := &http.Client{Timeout: 3 * time.Second}
|
||||
resp, err := cli.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
return nil, fmt.Errorf("admin status HTTP %d", resp.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m map[string][]proxyState
|
||||
if err := json.Unmarshal(body, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []proxyState
|
||||
for _, arr := range m {
|
||||
out = append(out, arr...)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// connStateAdmin derives the overall worker connection state from the per-proxy
|
||||
// states reported by the local frpc admin API.
|
||||
func connStateAdmin(states []proxyState) string {
|
||||
if len(states) == 0 {
|
||||
return "connecting"
|
||||
}
|
||||
anyRunning := false
|
||||
for _, s := range states {
|
||||
switch s.Status {
|
||||
case "running":
|
||||
anyRunning = true
|
||||
case "wait start":
|
||||
return "connecting"
|
||||
}
|
||||
}
|
||||
if anyRunning {
|
||||
return "connected"
|
||||
}
|
||||
return "failed"
|
||||
}
|
||||
|
||||
// groupCountOf returns the number of distinct non-empty LB groups a remote
|
||||
// participates in, by mapping each forward's service back to its local.
|
||||
func groupCountOf(locals []store.Local, fwd []store.Forward) int {
|
||||
groupByLocal := map[string]string{}
|
||||
for _, l := range locals {
|
||||
if l.LBGroup != "" {
|
||||
groupByLocal[l.Name] = l.LBGroup
|
||||
}
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, f := range fwd {
|
||||
if g := groupByLocal[f.Service]; g != "" {
|
||||
seen[g] = true
|
||||
}
|
||||
}
|
||||
return len(seen)
|
||||
}
|
||||
|
||||
func methodNotAllowed(w http.ResponseWriter) {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user