mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 11:30:15 +00:00
218 lines
5.7 KiB
Go
218 lines
5.7 KiB
Go
package api
|
|
|
|
import (
|
|
"archive/zip"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/logging"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
type LogHandlers struct {
|
|
config *config.Config
|
|
persistence *config.ConfigPersistence
|
|
}
|
|
|
|
func NewLogHandlers(cfg *config.Config, persistence *config.ConfigPersistence) *LogHandlers {
|
|
return &LogHandlers{
|
|
config: cfg,
|
|
persistence: persistence,
|
|
}
|
|
}
|
|
|
|
// HandleStreamLogs streams logs using SSE (Server-Sent Events)
|
|
func (h *LogHandlers) HandleStreamLogs(w http.ResponseWriter, r *http.Request) {
|
|
flusher, ok := w.(http.Flusher)
|
|
if !ok {
|
|
http.Error(w, "Streaming not supported", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
w.Header().Set("Connection", "keep-alive")
|
|
|
|
broadcaster := logging.GetBroadcaster()
|
|
id, ch, history := broadcaster.Subscribe()
|
|
defer broadcaster.Unsubscribe(id)
|
|
|
|
// Send history first
|
|
for _, line := range history {
|
|
if _, err := fmt.Fprintf(w, "data: %s\n\n", line); err != nil {
|
|
return
|
|
}
|
|
}
|
|
flusher.Flush()
|
|
|
|
notify := r.Context().Done()
|
|
|
|
for {
|
|
select {
|
|
case msg, ok := <-ch:
|
|
if !ok {
|
|
return
|
|
}
|
|
if _, err := fmt.Fprintf(w, "data: %s\n\n", msg); err != nil {
|
|
return
|
|
}
|
|
flusher.Flush()
|
|
case <-notify:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// HandleDownloadBundle creates a zip file with system logs and sanitized config
|
|
func (h *LogHandlers) HandleDownloadBundle(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/zip")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"pulse-support-bundle-%s.zip\"", time.Now().Format("20060102-150405")))
|
|
|
|
zipWriter := zip.NewWriter(w)
|
|
defer zipWriter.Close()
|
|
|
|
// 1. Add Logs
|
|
// If a log file is configured and exists, add it.
|
|
// Otherwise (or in addition), dump the in-memory buffer.
|
|
addedLogFile := false
|
|
if h.config.LogFile != "" {
|
|
if f, err := os.Open(h.config.LogFile); err == nil {
|
|
defer f.Close()
|
|
if wr, err := zipWriter.Create("pulse.log"); err == nil {
|
|
io.Copy(wr, f)
|
|
addedLogFile = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if !addedLogFile {
|
|
// Dump memory buffer
|
|
if wr, err := zipWriter.Create("pulse-tail.log"); err == nil {
|
|
for _, line := range logging.GetBroadcaster().GetHistory() {
|
|
fmt.Fprintln(wr, line)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Add Sanitized System Info
|
|
if wr, err := zipWriter.Create("system-info.json"); err == nil {
|
|
// Create a sanitized copy of config
|
|
sanitizedConfig := h.config.DeepCopy()
|
|
|
|
// Scrub top-level secrets
|
|
if sanitizedConfig.AuthPass != "" {
|
|
sanitizedConfig.AuthPass = "[REDACTED]"
|
|
}
|
|
if sanitizedConfig.APIToken != "" {
|
|
sanitizedConfig.APIToken = "[REDACTED]"
|
|
}
|
|
if sanitizedConfig.ProxyAuthSecret != "" {
|
|
sanitizedConfig.ProxyAuthSecret = "[REDACTED]"
|
|
}
|
|
|
|
// Scrub array secrets
|
|
for i := range sanitizedConfig.PVEInstances {
|
|
sanitizedConfig.PVEInstances[i].Password = "[REDACTED]"
|
|
sanitizedConfig.PVEInstances[i].TokenValue = "[REDACTED]"
|
|
}
|
|
for i := range sanitizedConfig.PBSInstances {
|
|
sanitizedConfig.PBSInstances[i].Password = "[REDACTED]"
|
|
sanitizedConfig.PBSInstances[i].TokenValue = "[REDACTED]"
|
|
}
|
|
for i := range sanitizedConfig.PMGInstances {
|
|
sanitizedConfig.PMGInstances[i].Password = "[REDACTED]"
|
|
sanitizedConfig.PMGInstances[i].TokenValue = "[REDACTED]"
|
|
}
|
|
|
|
// Sanitize environment variables
|
|
rawEnv := os.Environ()
|
|
sanitizedEnv := make([]string, 0, len(rawEnv))
|
|
for _, e := range rawEnv {
|
|
parts := strings.SplitN(e, "=", 2)
|
|
if len(parts) == 2 {
|
|
key := parts[0]
|
|
upperKey := strings.ToUpper(key)
|
|
// Redact known sensitive keys
|
|
if strings.Contains(upperKey, "TOKEN") ||
|
|
strings.Contains(upperKey, "PASS") ||
|
|
strings.Contains(upperKey, "SECRET") ||
|
|
strings.Contains(upperKey, "KEY") {
|
|
sanitizedEnv = append(sanitizedEnv, fmt.Sprintf("%s=[REDACTED]", key))
|
|
} else {
|
|
sanitizedEnv = append(sanitizedEnv, e)
|
|
}
|
|
}
|
|
}
|
|
|
|
safeConfig := struct {
|
|
Version string
|
|
GoVersion string
|
|
OS string
|
|
Config *config.Config
|
|
Env []string
|
|
}{
|
|
Version: "5.x",
|
|
GoVersion: "go1.21+",
|
|
OS: "linux",
|
|
Config: sanitizedConfig,
|
|
Env: sanitizedEnv,
|
|
}
|
|
|
|
enc := json.NewEncoder(wr)
|
|
enc.SetIndent("", " ")
|
|
enc.Encode(safeConfig)
|
|
}
|
|
}
|
|
|
|
type SetLogLevelRequest struct {
|
|
Level string `json:"level"`
|
|
}
|
|
|
|
// HandleSetLevel changes the log level at runtime and persists it
|
|
func (h *LogHandlers) HandleSetLevel(w http.ResponseWriter, r *http.Request) {
|
|
var req SetLogLevelRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
level := strings.ToLower(req.Level)
|
|
validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
|
|
if !validLevels[level] {
|
|
http.Error(w, "Invalid log level", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// 1. Update Runtime
|
|
logging.SetGlobalLevel(level)
|
|
h.config.LogLevel = level
|
|
log.Info().Str("level", level).Msg("Log level updated via API")
|
|
|
|
// 2. Persist to system.json
|
|
if h.persistence != nil {
|
|
settings, err := h.persistence.LoadSystemSettings()
|
|
if err == nil {
|
|
settings.LogLevel = level
|
|
if err := h.persistence.SaveSystemSettings(*settings); err != nil {
|
|
log.Error().Err(err).Msg("Failed to persist log level change")
|
|
// Don't fail the request, runtime update succeeded
|
|
}
|
|
}
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// HandleGetLevel returns the current log level
|
|
func (h *LogHandlers) HandleGetLevel(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"level": logging.GetGlobalLevel(),
|
|
})
|
|
}
|