mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-10-03 15:43:59 +00:00
feat: cluster reliability (leader failover, crash rejoin, key exchange) + auth/users + canvas/forwards enhancements + comprehensive README + API docs
- Cluster: forwardToNext offline detection (leader+non-leader), WatchLeader 1s heartbeat fallback, 409 for standalone nodes, Node.NodeKey key exchange via token ring, ClusterPeers persistence + auto-rejoin, Forward delegates to forwardToNext (bugfix) - Auth: Basic Auth (flag-creds fast path) + bcrypt users (admin/viewer) + Bearer API keys (read/write/admin scope) - Frontend: UsersView (accounts+API keys), ClusterView (ring/nodeKey/tasks/topology/log), StatusView (group management, per-proxy status), CanvasView (edge toggle/group), PortEdge (disabled/group labels) - API: handlers split (canvas/forwards/users/logs), canvas export/import, forwards group start/stop/assign/delete, cluster endpoints - Docs: comprehensive README rewrite (all flags/APIs/auth/cluster), docs/cluster-api.md (cluster management API reference) - Deploy: run-cluster.sh now 4-node ring + 1 isolated standalone, test-forward.sh updated for 4 nodes - Removed plan.md (design notes consolidated into README + API docs)
This commit is contained in:
146
internal/httpapi/handlers_logs.go
Normal file
146
internal/httpapi/handlers_logs.go
Normal file
@ -0,0 +1,146 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// workerLog is one frpc worker's log tail on a node.
|
||||
type workerLog struct {
|
||||
Name string `json:"name"`
|
||||
Remote string `json:"remote"` // worker key = remote name
|
||||
State string `json:"state"`
|
||||
Lines string `json:"lines"`
|
||||
}
|
||||
|
||||
// nodeLogsResp is the body of GET /node/logs: all frpc workers handling
|
||||
// forwards on THIS node (cluster-owned workers that run here + local-only ones).
|
||||
type nodeLogsResp struct {
|
||||
Node string `json:"node"`
|
||||
Workers []workerLog `json:"workers"`
|
||||
}
|
||||
|
||||
// localWorkerLogs gathers this node's frpc worker logs. Shared by /node/logs
|
||||
// and the /cluster/logs/export self entry so the requesting node doesn't HTTP
|
||||
// back to itself.
|
||||
func (h *Handler) localWorkerLogs() nodeLogsResp {
|
||||
out := nodeLogsResp{Node: h.SelfAddr, Workers: []workerLog{}}
|
||||
if h.Process == nil {
|
||||
return out
|
||||
}
|
||||
for _, name := range h.Process.ListWorkers() {
|
||||
st, _ := h.Process.Status(name)
|
||||
lines, _ := tailFile(h.Process.LogPath(name), 64*1024)
|
||||
out.Workers = append(out.Workers, workerLog{
|
||||
Name: name, Remote: name, State: st.State, Lines: lines,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// handleNodeLogs returns the frpc worker logs of THIS node only. Read-level:
|
||||
// auditors can pull it directly on any node, and the cluster export fans it out.
|
||||
func (h *Handler) handleNodeLogs(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
methodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, h.localWorkerLogs())
|
||||
}
|
||||
|
||||
// clusterWorkerLogNode is one node's entry in the export bundle.
|
||||
type clusterWorkerLogNode struct {
|
||||
ID string `json:"id"`
|
||||
Addr string `json:"addr"`
|
||||
OK bool `json:"ok"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Logs *nodeLogsResp `json:"logs,omitempty"`
|
||||
}
|
||||
|
||||
// handleClusterLogsExport fans out GET /node/logs to every alive ring node
|
||||
// (using the flag Basic creds, which resolve to admin on each peer via the
|
||||
// flag fast path) and aggregates the results into a downloadable bundle.
|
||||
// Unreachable nodes are marked error but don't abort the export. The
|
||||
// requester's own logs are gathered locally without an HTTP round-trip.
|
||||
func (h *Handler) handleClusterLogsExport(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
methodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
if h.Ring == nil {
|
||||
http.Error(w, "ring engine not enabled", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
snap := h.Ring.Snapshot()
|
||||
selfID := snap.SelfID
|
||||
type target struct{ id, addr string }
|
||||
var targets []target
|
||||
for _, n := range snap.Nodes {
|
||||
if !n.Alive || n.Addr == "" {
|
||||
continue
|
||||
}
|
||||
targets = append(targets, target{n.ID, n.Addr})
|
||||
}
|
||||
results := make([]clusterWorkerLogNode, len(targets))
|
||||
var wg sync.WaitGroup
|
||||
cli := &http.Client{Timeout: 8 * time.Second}
|
||||
for i, t := range targets {
|
||||
wg.Add(1)
|
||||
go func(i int, t target) {
|
||||
defer wg.Done()
|
||||
entry := clusterWorkerLogNode{ID: t.id, Addr: t.addr}
|
||||
// Self: gather locally, no HTTP round-trip.
|
||||
if t.id == selfID {
|
||||
logs := h.localWorkerLogs()
|
||||
entry.OK = true
|
||||
entry.Logs = &logs
|
||||
results[i] = entry
|
||||
return
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, "http://"+t.addr+apiPrefix+"/node/logs", nil)
|
||||
if err != nil {
|
||||
entry.Error = err.Error()
|
||||
results[i] = entry
|
||||
return
|
||||
}
|
||||
req.SetBasicAuth(h.User, h.Password)
|
||||
resp, err := cli.Do(req)
|
||||
if err != nil {
|
||||
entry.Error = "unreachable: " + err.Error()
|
||||
results[i] = entry
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
entry.Error = "HTTP " + resp.Status
|
||||
results[i] = entry
|
||||
return
|
||||
}
|
||||
var logs nodeLogsResp
|
||||
if err := json.Unmarshal(body, &logs); err != nil {
|
||||
entry.Error = "parse: " + err.Error()
|
||||
results[i] = entry
|
||||
return
|
||||
}
|
||||
entry.OK = true
|
||||
entry.Logs = &logs
|
||||
results[i] = entry
|
||||
}(i, t)
|
||||
}
|
||||
wg.Wait()
|
||||
bundle := map[string]any{
|
||||
"_type": "webui4frpc-worker-logs",
|
||||
"version": 1,
|
||||
"exportedAt": time.Now().Unix(),
|
||||
"requester": selfID,
|
||||
"nodes": results,
|
||||
}
|
||||
body, _ := json.MarshalIndent(bundle, "", " ")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="worker-logs.json"`)
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
Reference in New Issue
Block a user