logging_api.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. package server
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "net/http"
  7. "strings"
  8. "time"
  9. "vocat/internal/store"
  10. )
  11. const loggingSettingKey = "logs.retention"
  12. // loggingConfig is the persisted log retention policy.
  13. type loggingConfig struct {
  14. Mode string `json:"mode"` // "unlimited" (default) | "count" | "days"
  15. Count int `json:"count"` // keep newest N entries when mode is "count"
  16. Days int `json:"days"` // keep entries from the last N days when mode is "days"
  17. }
  18. func defaultLoggingConfig() loggingConfig {
  19. return loggingConfig{Mode: "unlimited", Count: 10000, Days: 30}
  20. }
  21. func parseLoggingConfig(config loggingConfig) (loggingConfig, error) {
  22. mode := strings.ToLower(strings.TrimSpace(config.Mode))
  23. if mode == "" {
  24. mode = "unlimited"
  25. }
  26. if mode != "unlimited" && mode != "count" && mode != "days" {
  27. return loggingConfig{}, errors.New("mode must be \"unlimited\", \"count\", or \"days\"")
  28. }
  29. config.Mode = mode
  30. if config.Count < 1 {
  31. config.Count = 10000
  32. }
  33. if config.Days < 1 {
  34. config.Days = 30
  35. }
  36. return config, nil
  37. }
  38. // loadLoggingConfig reads the persisted retention policy, defaulting to unlimited.
  39. func (s *Server) loadLoggingConfig(ctx context.Context) loggingConfig {
  40. config := defaultLoggingConfig()
  41. setting, err := s.store.AppSetting(ctx, loggingSettingKey)
  42. if err == nil {
  43. var stored loggingConfig
  44. if json.Unmarshal(setting.Value, &stored) == nil {
  45. if parsed, parseErr := parseLoggingConfig(stored); parseErr == nil {
  46. config = parsed
  47. }
  48. }
  49. }
  50. return config
  51. }
  52. // applyLogRetention enforces the current retention policy against the persisted
  53. // log events. The "unlimited" mode prunes nothing.
  54. func (s *Server) applyLogRetention(ctx context.Context) error {
  55. config := s.loadLoggingConfig(ctx)
  56. switch config.Mode {
  57. case "days":
  58. cutoff := time.Now().UTC().Add(-time.Duration(config.Days) * 24 * time.Hour)
  59. _, err := s.store.PruneLogEvents(ctx, cutoff)
  60. return err
  61. case "count":
  62. _, err := s.store.PruneLogEventsToCount(ctx, config.Count)
  63. return err
  64. default:
  65. return nil
  66. }
  67. }
  68. // StartLogRetentionLoop enforces the retention policy once at startup and then
  69. // on the given interval until the context is cancelled.
  70. func (s *Server) StartLogRetentionLoop(ctx context.Context, interval time.Duration) {
  71. if interval <= 0 {
  72. interval = time.Minute
  73. }
  74. if err := s.applyLogRetention(ctx); err != nil {
  75. s.logger.Warn("apply log retention failed", "error", err)
  76. }
  77. ticker := time.NewTicker(interval)
  78. defer ticker.Stop()
  79. for {
  80. select {
  81. case <-ctx.Done():
  82. return
  83. case <-ticker.C:
  84. if err := s.applyLogRetention(ctx); err != nil {
  85. s.logger.Warn("apply log retention failed", "error", err)
  86. }
  87. }
  88. }
  89. }
  90. // handleLoggingSettings reads and writes the log retention policy.
  91. //
  92. // GET /api/settings/logging
  93. // PUT /api/settings/logging
  94. func (s *Server) handleLoggingSettings(w http.ResponseWriter, r *http.Request) {
  95. switch r.Method {
  96. case http.MethodGet:
  97. config := s.loadLoggingConfig(r.Context())
  98. stored, err := s.store.CountLogEvents(r.Context())
  99. if err != nil {
  100. s.writeStoreError(w, err)
  101. return
  102. }
  103. writeJSON(w, http.StatusOK, map[string]any{
  104. "data": map[string]any{
  105. "mode": config.Mode,
  106. "count": config.Count,
  107. "days": config.Days,
  108. "stored_logs": stored,
  109. },
  110. })
  111. case http.MethodPut:
  112. var request loggingConfig
  113. if err := s.decodeJSON(w, r, &request); err != nil {
  114. writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
  115. return
  116. }
  117. config, err := parseLoggingConfig(request)
  118. if err != nil {
  119. writeError(w, http.StatusBadRequest, "invalid_logging_policy", err.Error())
  120. return
  121. }
  122. payload, err := json.Marshal(config)
  123. if err != nil {
  124. writeError(w, http.StatusInternalServerError, "internal_error", "an internal error occurred")
  125. return
  126. }
  127. if err := s.store.UpsertAppSetting(r.Context(), store.AppSetting{
  128. Key: loggingSettingKey,
  129. Value: payload,
  130. }); err != nil {
  131. s.writeStoreError(w, err)
  132. return
  133. }
  134. s.audit(r, "settings.logging.update", "settings", "logging", "success")
  135. if err := s.applyLogRetention(r.Context()); err != nil {
  136. s.logger.Warn("apply log retention failed", "error", err)
  137. }
  138. stored, _ := s.store.CountLogEvents(r.Context())
  139. writeJSON(w, http.StatusOK, map[string]any{
  140. "data": map[string]any{
  141. "mode": config.Mode,
  142. "count": config.Count,
  143. "days": config.Days,
  144. "stored_logs": stored,
  145. },
  146. })
  147. default:
  148. w.Header().Set("Allow", "GET, PUT")
  149. writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
  150. }
  151. }