Files
webui4frpc/internal/httpapi/handlers_logs.go
JianFeeeee 4a41608d94 refactor: 审查修复 — netload 消除重复 /sys 读 + 全项目 gofmt
- 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
2026-08-24 22:24:35 +08:00

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)
}