| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- 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")
- }
- }
|