mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-09-20 08:57:55 +00:00
- netload_linux.go: SampleNetLoad 聚合循环不再对每接口重复 readIfaceSpeed (snapshot 已汇总 cur.capMbps), 每次采样省 N 次 /sys 读 - gofmt -w: ring.go/ring_engine.go/auth.go/handlers.go/handlers_logs.go/ handlers_users.go/store.go 结构体字段对齐与注释缩进 - README.md: markdownlint 自动修复 (MD028/MD040) 审查结论: 令牌环本身即互斥协议 — OnToken(收令牌)与 StartRing(发令牌) 在同一节点上由令牌串行化, 不存在需要加锁的竞争; WatchLeader 的读为 良性读, 无需 mutex
154 lines
4.4 KiB
Go
154 lines
4.4 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"webui4frpc/internal/process"
|
|
)
|
|
|
|
// 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)
|
|
// Parse the worker key to get the real remote name for display.
|
|
remoteName := name
|
|
if _, r, _, ok := process.ParseWorkerKey(name); ok {
|
|
remoteName = r
|
|
}
|
|
out.Workers = append(out.Workers, workerLog{
|
|
Name: name, Remote: remoteName, 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)
|
|
}
|