diff --git a/cmd/server/main.go b/cmd/server/main.go index ca10c3a..34f3e28 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -192,6 +192,19 @@ func main() { defer dataLogger.Close() eventLogger.Event("DATA_ЛОГЕР_ГОТОВ") + // 3.5 Retention (автоматическая очистка) + retention := logger.NewRetention("logs/data", 48, 5000, eventLogger) // 48 часов, 5 GB + retention.Start() + eventLogger.Event("RETENTION_ГОТОВ") + + // Получаем информацию о доступных данных + if oldest, newest, count := retention.GetAvailableRange(); count > 0 { + log.Printf("Доступны данные с %s по %s (%d файлов)", + oldest.Format("2006-01-02 15:04"), + newest.Format("2006-01-02 15:04"), + count) + } + // 4. RingBuffer buf := adapter.NewRingBuffer(1024 * 10) diff --git a/internal/logger/retention.go b/internal/logger/retention.go new file mode 100644 index 0000000..ce90631 --- /dev/null +++ b/internal/logger/retention.go @@ -0,0 +1,204 @@ +package logger + +import ( + "log" + "os" + "path/filepath" + "sort" + "strings" + "time" +) + +type Retention struct { + dataDir string + maxAgeHours int // максимальный возраст файлов в часах + maxSizeBytes int64 // максимальный размер всех файлов в байтах + eventLogger *EventLogger +} + +func NewRetention(dataDir string, maxAgeHours int, maxSizeMB int, eventLogger *EventLogger) *Retention { + return &Retention{ + dataDir: dataDir, + maxAgeHours: maxAgeHours, + maxSizeBytes: int64(maxSizeMB) * 1024 * 1024, + eventLogger: eventLogger, + } +} + +func (r *Retention) Start() { + // Запускаем проверку каждые 15 минут + ticker := time.NewTicker(15 * time.Minute) + go func() { + for range ticker.C { + r.Cleanup() + } + }() + + // Первая очистка через 1 минуту после запуска + time.AfterFunc(1*time.Minute, func() { + r.Cleanup() + }) +} + +func (r *Retention) Cleanup() { + if r.eventLogger != nil { + r.eventLogger.Event("ЗАПУЩЕНА_ОЧИСТКА_ЛОГОВ") + } + + // 1. Удаляем старые файлы + deletedByAge := r.cleanByAge() + + // 2. Проверяем общий размер и удаляем самые старые если превышен лимит + deletedBySize := r.cleanBySize() + + if r.eventLogger != nil && (deletedByAge > 0 || deletedBySize > 0) { + r.eventLogger.Event("УДАЛЕНО_ФАЙЛОВ") + log.Printf("🧹 Очистка логов: удалено %d файлов (по возрасту: %d, по размеру: %d)", + deletedByAge+deletedBySize, deletedByAge, deletedBySize) + } +} + +func (r *Retention) cleanByAge() int { + cutoff := time.Now().Add(-time.Duration(r.maxAgeHours) * time.Hour) + deleted := 0 + + // Получаем все .bin файлы + files, err := filepath.Glob(filepath.Join(r.dataDir, "gpio-*.bin")) + if err != nil { + return 0 + } + + for _, file := range files { + info, err := os.Stat(file) + if err != nil { + continue + } + + // Извлекаем время из имени файла + fileTime := r.parseFilenameTime(file) + if fileTime.IsZero() { + fileTime = info.ModTime() + } + + if fileTime.Before(cutoff) { + if err := os.Remove(file); err == nil { + deleted++ + if r.eventLogger != nil { + r.eventLogger.Event("УДАЛЕН_ФАЙЛ_ПО_ВОЗРАСТУ") + } + } + } + } + + return deleted +} + +func (r *Retention) cleanBySize() int { + // Получаем все файлы + files, err := filepath.Glob(filepath.Join(r.dataDir, "gpio-*.bin")) + if err != nil { + return 0 + } + + if len(files) == 0 { + return 0 + } + + // Сортируем по времени (старые первыми) + type fileInfo struct { + path string + time time.Time + size int64 + } + + var fileList []fileInfo + var totalSize int64 + + for _, file := range files { + info, err := os.Stat(file) + if err != nil { + continue + } + + fileTime := r.parseFilenameTime(file) + if fileTime.IsZero() { + fileTime = info.ModTime() + } + + fileList = append(fileList, fileInfo{ + path: file, + time: fileTime, + size: info.Size(), + }) + totalSize += info.Size() + } + + // Сортируем по времени (старые первые) + sort.Slice(fileList, func(i, j int) bool { + return fileList[i].time.Before(fileList[j].time) + }) + + // Удаляем самые старые пока не уложимся в лимит + deleted := 0 + for totalSize > r.maxSizeBytes && len(fileList) > 1 { + // Удаляем самый старый файл + oldest := fileList[0] + if err := os.Remove(oldest.path); err == nil { + totalSize -= oldest.size + deleted++ + fileList = fileList[1:] + if r.eventLogger != nil { + r.eventLogger.Event("УДАЛЕН_ФАЙЛ_ПО_РАЗМЕРУ") + } + } else { + break + } + } + + return deleted +} + +func (r *Retention) parseFilenameTime(filename string) time.Time { + base := filepath.Base(filename) + // Формат: gpio-2026-06-09-11.bin + parts := strings.Split(strings.TrimSuffix(base, ".bin"), "-") + if len(parts) != 5 { + return time.Time{} + } + + year, _ := time.Parse("2006", parts[1]) + month, _ := time.Parse("01", parts[2]) + day, _ := time.Parse("02", parts[3]) + hour, _ := time.Parse("15", parts[4]) + + return time.Date(year.Year(), month.Month(), day.Day(), hour.Hour(), 0, 0, 0, time.Local) +} + +// GetAvailableRange возвращает диапазон доступных данных +func (r *Retention) GetAvailableRange() (oldest, newest time.Time, count int) { + files, err := filepath.Glob(filepath.Join(r.dataDir, "gpio-*.bin")) + if err != nil { + return + } + + count = len(files) + if count == 0 { + return + } + + for _, file := range files { + fileTime := r.parseFilenameTime(file) + if fileTime.IsZero() { + continue + } + + if oldest.IsZero() || fileTime.Before(oldest) { + oldest = fileTime + } + if newest.IsZero() || fileTime.After(newest) { + newest = fileTime + } + } + + return +} \ No newline at end of file