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

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