Files
2026-08-09 05:33:21 +08:00

162 lines
4.5 KiB
Go

package server
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"vocat/internal/store"
)
const loggingSettingKey = "logs.retention"
// loggingConfig is the persisted log retention policy.
type loggingConfig struct {
Mode string `json:"mode"` // "unlimited" (default) | "count" | "days"
Count int `json:"count"` // keep newest N entries when mode is "count"
Days int `json:"days"` // keep entries from the last N days when mode is "days"
}
func defaultLoggingConfig() loggingConfig {
return loggingConfig{Mode: "unlimited", Count: 10000, Days: 30}
}
func parseLoggingConfig(config loggingConfig) (loggingConfig, error) {
mode := strings.ToLower(strings.TrimSpace(config.Mode))
if mode == "" {
mode = "unlimited"
}
if mode != "unlimited" && mode != "count" && mode != "days" {
return loggingConfig{}, errors.New("mode must be \"unlimited\", \"count\", or \"days\"")
}
config.Mode = mode
if config.Count < 1 {
config.Count = 10000
}
if config.Days < 1 {
config.Days = 30
}
return config, nil
}
// loadLoggingConfig reads the persisted retention policy, defaulting to unlimited.
func (s *Server) loadLoggingConfig(ctx context.Context) loggingConfig {
config := defaultLoggingConfig()
setting, err := s.store.AppSetting(ctx, loggingSettingKey)
if err == nil {
var stored loggingConfig
if json.Unmarshal(setting.Value, &stored) == nil {
if parsed, parseErr := parseLoggingConfig(stored); parseErr == nil {
config = parsed
}
}
}
return config
}
// applyLogRetention enforces the current retention policy against the persisted
// log events. The "unlimited" mode prunes nothing.
func (s *Server) applyLogRetention(ctx context.Context) error {
config := s.loadLoggingConfig(ctx)
switch config.Mode {
case "days":
cutoff := time.Now().UTC().Add(-time.Duration(config.Days) * 24 * time.Hour)
_, err := s.store.PruneLogEvents(ctx, cutoff)
return err
case "count":
_, err := s.store.PruneLogEventsToCount(ctx, config.Count)
return err
default:
return nil
}
}
// StartLogRetentionLoop enforces the retention policy once at startup and then
// on the given interval until the context is cancelled.
func (s *Server) StartLogRetentionLoop(ctx context.Context, interval time.Duration) {
if interval <= 0 {
interval = time.Minute
}
if err := s.applyLogRetention(ctx); err != nil {
s.logger.Warn("apply log retention failed", "error", err)
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := s.applyLogRetention(ctx); err != nil {
s.logger.Warn("apply log retention failed", "error", err)
}
}
}
}
// handleLoggingSettings reads and writes the log retention policy.
//
// GET /api/settings/logging
// PUT /api/settings/logging
func (s *Server) handleLoggingSettings(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
config := s.loadLoggingConfig(r.Context())
stored, err := s.store.CountLogEvents(r.Context())
if err != nil {
s.writeStoreError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{
"data": map[string]any{
"mode": config.Mode,
"count": config.Count,
"days": config.Days,
"stored_logs": stored,
},
})
case http.MethodPut:
var request loggingConfig
if err := s.decodeJSON(w, r, &request); err != nil {
writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
return
}
config, err := parseLoggingConfig(request)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_logging_policy", err.Error())
return
}
payload, err := json.Marshal(config)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal_error", "an internal error occurred")
return
}
if err := s.store.UpsertAppSetting(r.Context(), store.AppSetting{
Key: loggingSettingKey,
Value: payload,
}); err != nil {
s.writeStoreError(w, err)
return
}
s.audit(r, "settings.logging.update", "settings", "logging", "success")
if err := s.applyLogRetention(r.Context()); err != nil {
s.logger.Warn("apply log retention failed", "error", err)
}
stored, _ := s.store.CountLogEvents(r.Context())
writeJSON(w, http.StatusOK, map[string]any{
"data": map[string]any{
"mode": config.Mode,
"count": config.Count,
"days": config.Days,
"stored_logs": stored,
},
})
default:
w.Header().Set("Allow", "GET, PUT")
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
}
}