feat: setup logs retention by max file size
This commit is contained in:
1 parent
70c8492f4f
commit
f914eae3c5
44 files changed
+2316
-161
No files matched your search
@@ -83,6 +83,7 @@ func encodeJSON(value any, empty string) (string, error) {
|
||||
func AuditSettingsToDomain(m models.AuditSettings) audit.Settings {
|
||||
return audit.Settings{
|
||||
RetentionDays: m.RetentionDays,
|
||||
RetentionMaxGB: m.RetentionMaxGB,
|
||||
LastCleanupAt: m.LastCleanupAt,
|
||||
LastCleanupRemoved: m.LastCleanupRemoved,
|
||||
}
|
||||
@@ -91,6 +92,7 @@ func AuditSettingsToDomain(m models.AuditSettings) audit.Settings {
|
||||
func AuditSettingsToModel(s audit.Settings) models.AuditSettings {
|
||||
return models.AuditSettings{
|
||||
RetentionDays: s.RetentionDays,
|
||||
RetentionMaxGB: s.RetentionMaxGB,
|
||||
LastCleanupAt: s.LastCleanupAt,
|
||||
LastCleanupRemoved: s.LastCleanupRemoved,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import "time"
|
||||
type AuditSettings struct {
|
||||
ID uint32 `gorm:"primaryKey"`
|
||||
RetentionDays int `gorm:"not null"`
|
||||
RetentionMaxGB int `gorm:"not null;default:0"`
|
||||
LastCleanupAt *time.Time
|
||||
LastCleanupRemoved int `gorm:"not null"`
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"RenewCMS/internal/domain/audit"
|
||||
"RenewCMS/internal/infrastructure/persistence/mappers"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -186,3 +187,49 @@ func (a *AuditEventRepository) DeleteOlderThan(cutoff time.Time) (int, error) {
|
||||
result := a.db.Where("created_at < ?", cutoff.UTC()).Delete(&entity.AuditEvent{})
|
||||
return int(result.RowsAffected), result.Error
|
||||
}
|
||||
|
||||
const (
|
||||
deleteChunkSize = 500
|
||||
payloadSizeSQL = `COALESCE(SUM(
|
||||
OCTET_LENGTH(action) + OCTET_LENGTH(actor_name) + OCTET_LENGTH(target_type) +
|
||||
OCTET_LENGTH(target_id) + OCTET_LENGTH(target_name) + COALESCE(OCTET_LENGTH(failure_reason), 0) +
|
||||
COALESCE(OCTET_LENGTH(ip), 0) + COALESCE(OCTET_LENGTH(user_agent), 0) +
|
||||
OCTET_LENGTH(params) + OCTET_LENGTH(changes)), 0) AS payload, COUNT(*) AS total`
|
||||
)
|
||||
|
||||
type payloadRow struct {
|
||||
Payload int64
|
||||
Total int64
|
||||
}
|
||||
|
||||
func (a *AuditEventRepository) SizeBytes() (int64, error) {
|
||||
var row payloadRow
|
||||
if err := a.db.Model(&entity.AuditEvent{}).Select(payloadSizeSQL).Scan(&row).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return row.Payload + row.Total*audit.RowOverheadBytes, nil
|
||||
}
|
||||
|
||||
func (a *AuditEventRepository) DeleteOldest(limit int, before time.Time) (int, error) {
|
||||
if limit < 1 {
|
||||
return 0, nil
|
||||
}
|
||||
var ids []uint64
|
||||
err := a.db.Model(&entity.AuditEvent{}).
|
||||
Where("created_at < ?", before.UTC()).
|
||||
Order("created_at ASC, id ASC").
|
||||
Limit(limit).
|
||||
Pluck("id", &ids).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
removed := 0
|
||||
for chunk := range slices.Chunk(ids, deleteChunkSize) {
|
||||
result := a.db.Where("id IN ?", chunk).Delete(&entity.AuditEvent{})
|
||||
if result.Error != nil {
|
||||
return removed, result.Error
|
||||
}
|
||||
removed += int(result.RowsAffected)
|
||||
}
|
||||
return removed, nil
|
||||
}
|
||||
@@ -34,9 +34,10 @@ func (a *AuditSettingsRepository) Get() (audit.Settings, error) {
|
||||
return mappers.AuditSettingsToDomain(model), nil
|
||||
}
|
||||
|
||||
func (a *AuditSettingsRepository) SaveRetention(days int) error {
|
||||
return a.update(map[string]any{"retention_days": days}, func(settings *audit.Settings) {
|
||||
func (a *AuditSettingsRepository) SaveRetention(days int, maxGB int) error {
|
||||
return a.update(map[string]any{"retention_days": days, "retention_max_gb": maxGB}, func(settings *audit.Settings) {
|
||||
settings.RetentionDays = days
|
||||
settings.RetentionMaxGB = maxGB
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,12 @@ import (
|
||||
|
||||
type GetActivityRetentionUseCase struct {
|
||||
settings audit.SettingsRepository
|
||||
events audit.Repository
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewGetActivityRetentionUseCase(settings audit.SettingsRepository, now func() time.Time) *GetActivityRetentionUseCase {
|
||||
return &GetActivityRetentionUseCase{settings, now}
|
||||
func NewGetActivityRetentionUseCase(settings audit.SettingsRepository, events audit.Repository, now func() time.Time) *GetActivityRetentionUseCase {
|
||||
return &GetActivityRetentionUseCase{settings, events, now}
|
||||
}
|
||||
|
||||
func (g *GetActivityRetentionUseCase) GetActivityRetention() (audit.Settings, error) {
|
||||
@@ -20,38 +21,80 @@ func (g *GetActivityRetentionUseCase) GetActivityRetention() (audit.Settings, er
|
||||
if err != nil {
|
||||
return audit.Settings{}, err
|
||||
}
|
||||
current.LogSizeBytes, err = g.events.SizeBytes()
|
||||
if err != nil {
|
||||
return audit.Settings{}, err
|
||||
}
|
||||
return current.WithNextCleanup(g.now()), nil
|
||||
}
|
||||
|
||||
type UpdateActivityRetentionCommand struct {
|
||||
Days *int
|
||||
MaxGB *int
|
||||
}
|
||||
|
||||
type UpdateActivityRetentionUseCase struct {
|
||||
settings audit.SettingsRepository
|
||||
events audit.Repository
|
||||
recorder audit.Recorder
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewUpdateActivityRetentionUseCase(settings audit.SettingsRepository, recorder audit.Recorder, now func() time.Time) *UpdateActivityRetentionUseCase {
|
||||
return &UpdateActivityRetentionUseCase{settings, recorder, now}
|
||||
func NewUpdateActivityRetentionUseCase(settings audit.SettingsRepository, events audit.Repository, recorder audit.Recorder, now func() time.Time) *UpdateActivityRetentionUseCase {
|
||||
return &UpdateActivityRetentionUseCase{settings, events, recorder, now}
|
||||
}
|
||||
|
||||
func (u *UpdateActivityRetentionUseCase) UpdateActivityRetention(ctx context.Context, days int) (audit.Settings, error) {
|
||||
if err := audit.ValidateRetentionDays(days); err != nil {
|
||||
return audit.Settings{}, err
|
||||
func (u *UpdateActivityRetentionUseCase) UpdateActivityRetention(ctx context.Context, command UpdateActivityRetentionCommand) (audit.Settings, error) {
|
||||
if command.Days == nil && command.MaxGB == nil {
|
||||
return audit.Settings{}, audit.ErrInvalidRetention
|
||||
}
|
||||
if command.Days != nil {
|
||||
if err := audit.ValidateRetentionDays(*command.Days); err != nil {
|
||||
return audit.Settings{}, err
|
||||
}
|
||||
}
|
||||
if command.MaxGB != nil {
|
||||
if err := audit.ValidateRetentionMaxGB(*command.MaxGB); err != nil {
|
||||
return audit.Settings{}, err
|
||||
}
|
||||
}
|
||||
current, err := u.settings.Get()
|
||||
if err != nil {
|
||||
return audit.Settings{}, err
|
||||
}
|
||||
if current.RetentionDays == days {
|
||||
size, err := u.events.SizeBytes()
|
||||
if err != nil {
|
||||
return audit.Settings{}, err
|
||||
}
|
||||
current.LogSizeBytes = size
|
||||
|
||||
updated := current
|
||||
if command.Days != nil {
|
||||
updated.RetentionDays = *command.Days
|
||||
}
|
||||
if command.MaxGB != nil {
|
||||
updated.RetentionMaxGB = *command.MaxGB
|
||||
}
|
||||
daysChanged := updated.RetentionDays != current.RetentionDays
|
||||
sizeChanged := updated.RetentionMaxGB != current.RetentionMaxGB
|
||||
if !daysChanged && !sizeChanged {
|
||||
return current.WithNextCleanup(u.now()), nil
|
||||
}
|
||||
if err := u.settings.SaveRetention(days); err != nil {
|
||||
if err := u.settings.SaveRetention(updated.RetentionDays, updated.RetentionMaxGB); err != nil {
|
||||
return audit.Settings{}, err
|
||||
}
|
||||
|
||||
event := audit.NewEvent(audit.ActionRetentionChanged, audit.CategorySystem)
|
||||
event.Params = map[string]any{audit.ParamFrom: current.RetentionDays, audit.ParamTo: days}
|
||||
event.Params = map[string]any{}
|
||||
if daysChanged {
|
||||
event.Params[audit.ParamFrom] = current.RetentionDays
|
||||
event.Params[audit.ParamTo] = updated.RetentionDays
|
||||
}
|
||||
if sizeChanged {
|
||||
event.Params[audit.ParamMaxGBFrom] = current.RetentionMaxGB
|
||||
event.Params[audit.ParamMaxGBTo] = updated.RetentionMaxGB
|
||||
}
|
||||
u.recorder.Record(ctx, event)
|
||||
|
||||
current.RetentionDays = days
|
||||
return current.WithNextCleanup(u.now()), nil
|
||||
return updated.WithNextCleanup(u.now()), nil
|
||||
}
|
||||
@@ -6,14 +6,22 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultCleanupBatchSize = 1000
|
||||
|
||||
type CleanupActivityUseCase struct {
|
||||
events audit.Repository
|
||||
settings audit.SettingsRepository
|
||||
recorder audit.Recorder
|
||||
events audit.Repository
|
||||
settings audit.SettingsRepository
|
||||
recorder audit.Recorder
|
||||
batchSize int
|
||||
}
|
||||
|
||||
func NewCleanupActivityUseCase(events audit.Repository, settings audit.SettingsRepository, recorder audit.Recorder) *CleanupActivityUseCase {
|
||||
return &CleanupActivityUseCase{events, settings, recorder}
|
||||
return &CleanupActivityUseCase{events, settings, recorder, defaultCleanupBatchSize}
|
||||
}
|
||||
|
||||
func (c *CleanupActivityUseCase) WithBatchSize(batchSize int) *CleanupActivityUseCase {
|
||||
c.batchSize = batchSize
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *CleanupActivityUseCase) Tick(now time.Time) (audit.CleanupResult, bool, error) {
|
||||
@@ -21,13 +29,23 @@ func (c *CleanupActivityUseCase) Tick(now time.Time) (audit.CleanupResult, bool,
|
||||
if err != nil {
|
||||
return audit.CleanupResult{}, false, err
|
||||
}
|
||||
if settings.RetentionDays == audit.ForeverRetentionDays || !settings.CleanupDue(now) {
|
||||
if !settings.HasRetentionLimit() || !settings.CleanupDue(now) {
|
||||
return audit.CleanupResult{}, false, nil
|
||||
}
|
||||
|
||||
removed, err := c.events.DeleteOlderThan(audit.RetentionCutoff(now, settings.RetentionDays))
|
||||
if err != nil {
|
||||
return audit.CleanupResult{}, false, err
|
||||
removed := 0
|
||||
if settings.RetentionDays != audit.ForeverRetentionDays {
|
||||
removed, err = c.events.DeleteOlderThan(audit.RetentionCutoff(now, settings.RetentionDays))
|
||||
if err != nil {
|
||||
return audit.CleanupResult{}, false, err
|
||||
}
|
||||
}
|
||||
if settings.RetentionMaxGB != audit.NoSizeLimitGB {
|
||||
trimmed, err := c.trimToSize(now, settings.MaxLogSizeBytes())
|
||||
if err != nil {
|
||||
return audit.CleanupResult{}, false, err
|
||||
}
|
||||
removed += trimmed
|
||||
}
|
||||
if err := c.settings.SaveLastCleanup(now, removed); err != nil {
|
||||
return audit.CleanupResult{}, false, err
|
||||
@@ -38,6 +56,28 @@ func (c *CleanupActivityUseCase) Tick(now time.Time) (audit.CleanupResult, bool,
|
||||
return audit.CleanupResult{Removed: removed}, true, nil
|
||||
}
|
||||
|
||||
func (c *CleanupActivityUseCase) trimToSize(now time.Time, limitBytes int64) (int, error) {
|
||||
guard := audit.GuardCutoff(now)
|
||||
removed := 0
|
||||
for {
|
||||
size, err := c.events.SizeBytes()
|
||||
if err != nil {
|
||||
return removed, err
|
||||
}
|
||||
if size <= limitBytes {
|
||||
return removed, nil
|
||||
}
|
||||
batch, err := c.events.DeleteOldest(c.batchSize, guard)
|
||||
if err != nil {
|
||||
return removed, err
|
||||
}
|
||||
if batch == 0 {
|
||||
return removed, nil
|
||||
}
|
||||
removed += batch
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CleanupActivityUseCase) recordCleanup(removed int) {
|
||||
event := audit.NewEvent(audit.ActionCleanup, audit.CategorySystem)
|
||||
event.Actor = audit.SystemActor()
|
||||
|
||||
Reference in new issue
Block a user