mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-09-20 08:57:55 +00:00
feat: worker 日志 CSV 导出 + 集群日志导出菜单完善
- handleAuditWorkerLogsCsv: 全环 fan-out 收集每节点 frpc 日志, 逐行展开为 CSV (node/worker_key/remote/worker_state/log_line), Excel 可直接过滤/pivot 审计 - gatherClusterWorkerLogs: 提取 JSON 与 CSV 共用的日志聚合逻辑 - 路由: GET /audit/worker-logs.csv (read 级) - ClusterView: 导出 worker 日志改下拉菜单 (JSON/CSV 两格式) - api.ts: downloadAuditCsv 支持 worker-logs 类型 - gofmt: handlers_audit.go 格式对齐
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
internal/httpapi/dist/index.html
vendored
4
internal/httpapi/dist/index.html
vendored
@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<title>webui4frpc</title>
|
||||
<script type="module" crossorigin src="/assets/index-CepJKoxg.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B4L3dftx.css">
|
||||
<script type="module" crossorigin src="/assets/index-CaXjZqKm.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-jvouiCgh.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@ -172,6 +172,48 @@ func (h *Handler) handleAuditClusterLogCsv(w http.ResponseWriter, r *http.Reques
|
||||
rows)
|
||||
}
|
||||
|
||||
// handleAuditWorkerLogsCsv serves GET /audit/worker-logs.csv: every alive
|
||||
// node's frpc worker logs, fanned out over the ring and flattened to one row
|
||||
// per LOG LINE (node/worker/state columns repeated) so auditors can filter
|
||||
// and pivot in a spreadsheet. Unreachable nodes contribute an error row.
|
||||
func (h *Handler) handleAuditWorkerLogsCsv(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
|
||||
}
|
||||
_, results := h.gatherClusterWorkerLogs(r)
|
||||
rows := make([][]string, 0, 64)
|
||||
for _, n := range results {
|
||||
if !n.OK || n.Logs == nil {
|
||||
rows = append(rows, []string{n.ID, "", "ERROR", n.Error, ""})
|
||||
continue
|
||||
}
|
||||
for _, wk := range n.Logs.Workers {
|
||||
// One row per line keeps the CSV rectangular; the tail's embedded
|
||||
// newlines would otherwise need multi-line cells (RFC4180 allows
|
||||
// them but Excel's filtering/pivot works better on flat rows).
|
||||
for _, line := range strings.Split(strings.TrimRight(wk.Lines, "\n"), "\n") {
|
||||
if ts, rest, found := strings.Cut(line, "."); found && len(ts) > 10 {
|
||||
line = rest // strip the ANSI-prefixed timestamp prefix noise
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, []string{n.ID, wk.Name, wk.Remote, wk.State, line})
|
||||
}
|
||||
}
|
||||
}
|
||||
writeCSV(w,
|
||||
fmt.Sprintf("audit-worker-logs-%s.csv", time.Now().UTC().Format("20060102-150405")),
|
||||
[]string{"node", "worker_key", "remote", "worker_state", "log_line"},
|
||||
rows)
|
||||
}
|
||||
|
||||
// rawJSON renders the log entry's payload as compact JSON (empty when absent)
|
||||
// for the lossless audit column.
|
||||
func rawJSON(data json.RawMessage) string {
|
||||
|
||||
@ -67,20 +67,13 @@ type clusterWorkerLogNode struct {
|
||||
Logs *nodeLogsResp `json:"logs,omitempty"`
|
||||
}
|
||||
|
||||
// handleClusterLogsExport fans out GET /node/logs to every alive ring node
|
||||
// gatherClusterWorkerLogs 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
|
||||
}
|
||||
// flag fast path) and aggregates the results. Unreachable nodes are marked
|
||||
// error but don't abort the export. The requester's own logs are gathered
|
||||
// locally without an HTTP round-trip. Shared by the JSON bundle and the CSV
|
||||
// audit export.
|
||||
func (h *Handler) gatherClusterWorkerLogs(r *http.Request) (string, []clusterWorkerLogNode) {
|
||||
snap := h.Ring.Snapshot()
|
||||
selfID := snap.SelfID
|
||||
type target struct{ id, addr string }
|
||||
@ -139,6 +132,20 @@ func (h *Handler) handleClusterLogsExport(w http.ResponseWriter, r *http.Request
|
||||
}(i, t)
|
||||
}
|
||||
wg.Wait()
|
||||
return selfID, results
|
||||
}
|
||||
|
||||
// handleClusterLogsExport serves the aggregated worker-log bundle as JSON.
|
||||
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
|
||||
}
|
||||
selfID, results := h.gatherClusterWorkerLogs(r)
|
||||
bundle := map[string]any{
|
||||
"_type": "webui4frpc-worker-logs",
|
||||
"version": 1,
|
||||
|
||||
@ -117,6 +117,7 @@ func NewServeMux(h *Handler) (http.Handler, error) {
|
||||
mux.HandleFunc(apiPrefix+"/audit/users.csv", h.auth("read")(h.handleAuditUsersCsv))
|
||||
mux.HandleFunc(apiPrefix+"/audit/apikeys.csv", h.auth("read")(h.handleAuditApiKeysCsv))
|
||||
mux.HandleFunc(apiPrefix+"/audit/cluster-log.csv", h.auth("read")(h.handleAuditClusterLogCsv))
|
||||
mux.HandleFunc(apiPrefix+"/audit/worker-logs.csv", h.auth("read")(h.handleAuditWorkerLogsCsv))
|
||||
mux.HandleFunc(apiPrefix+"/cluster/token", h.auth("write")(h.handleClusterToken))
|
||||
mux.HandleFunc(apiPrefix+"/cluster/join", h.auth("write")(h.handleClusterJoin))
|
||||
mux.HandleFunc(apiPrefix+"/cluster/task", h.auth("write")(h.handleClusterTask))
|
||||
|
||||
Reference in New Issue
Block a user