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:
JianFeeeee
2026-08-24 23:09:00 +08:00
parent ef98d9dca1
commit 2eaa26ba95
8 changed files with 102 additions and 40 deletions

View File

@ -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,