Files
go-service/internal/logger/retention.go

243 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package logger
import (
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
// minKeepFiles — сколько самых свежих файлов всегда сохраняем, даже если
// возраст или размер говорят удалить. Защита от полного стирания директории.
const minKeepFiles = 2
type Retention struct {
maxAgeHours int
maxSizeBytes int64
eventLogger *EventLogger
stopCh chan struct{}
}
func NewRetention(maxAgeHours int, maxSizeMB int, eventLogger *EventLogger) *Retention {
return &Retention{
maxAgeHours: maxAgeHours,
maxSizeBytes: int64(maxSizeMB) * 1024 * 1024,
eventLogger: eventLogger,
stopCh: make(chan struct{}),
}
}
func (r *Retention) Start() {
// Запускаем проверку каждые 15 минут
r.StartWithInterval(15 * time.Minute)
}
func (r *Retention) StartWithInterval(interval time.Duration) {
// Запускаем проверку с указанным интервалом
ticker := time.NewTicker(interval)
go func() {
for {
select {
case <-ticker.C:
r.Cleanup()
case <-r.stopCh:
ticker.Stop()
return
}
}
}()
// Первая очистка через 1 минуту после запуска
time.AfterFunc(1*time.Minute, func() {
r.Cleanup()
})
}
func (r *Retention) Stop() {
close(r.stopCh)
}
func (r *Retention) Cleanup() {
// Если оба лимита отключены, ничего не делаем
if r.maxAgeHours <= 0 && r.maxSizeBytes <= 0 {
return
}
// Получаем директорию с данными
dataDir, err := GetDataLogsDir()
if err != nil {
if r.eventLogger != nil {
r.eventLogger.Event("ОШИБКАОЛУЧЕНИЯ_ДИРЕКТОРИИ")
}
return
}
// 1. Удаляем старые файлы (если включено)
deletedByAge := 0
var deletedByAgeBytes int64
if r.maxAgeHours > 0 {
deletedByAge, deletedByAgeBytes = r.cleanByAge(dataDir)
}
// 2. Проверяем общий размер и удаляем самые старые если превышен лимит (если включено)
deletedBySize := 0
var deletedBySizeBytes int64
if r.maxSizeBytes > 0 {
deletedBySize, deletedBySizeBytes = r.cleanBySize(dataDir)
}
totalDeleted := deletedByAge + deletedBySize
totalDeletedBytes := deletedByAgeBytes + deletedBySizeBytes
// Логируем ТОЛЬКО если были удаления
if totalDeleted > 0 {
// Логируем суммарное событие
if r.eventLogger != nil {
eventMsg := fmt.Sprintf("ОЧИСТКАОГОВ_УДАЛЕНО_%d_ФАЙЛОВ_%d_MB",
totalDeleted, totalDeletedBytes/(1024*1024))
r.eventLogger.Event(eventMsg)
}
// Пишем в консоль для отладки
log.Printf("Очистка логов: удалено %d файлов (%.2f MB) [по возрасту: %d, по размеру: %d]",
totalDeleted, float64(totalDeletedBytes)/(1024*1024), deletedByAge, deletedBySize)
}
}
// fileInfo описывает один лог-файл: путь, время из имени (или ModTime как
// запасной вариант), реальное время последней записи и размер.
type fileInfo struct {
path string
time time.Time
modTime time.Time
size int64
}
// collectFiles возвращает все gpio-*.bin, отсортированные от старых к новым
// (FIFO-порядок), и их суммарный размер в байтах.
func (r *Retention) collectFiles(dataDir string) ([]fileInfo, int64) {
files, err := filepath.Glob(filepath.Join(dataDir, "gpio-*.bin"))
if err != nil {
return nil, 0
}
var fileList []fileInfo
var totalSize int64
for _, file := range files {
info, err := os.Stat(file)
if err != nil {
continue
}
// Время из имени файла; если распарсить не удалось — берём ModTime.
fileTime := r.parseFilenameTime(file)
if fileTime.IsZero() {
fileTime = info.ModTime()
}
fileList = append(fileList, fileInfo{
path: file,
time: fileTime,
modTime: info.ModTime(),
size: info.Size(),
})
totalSize += info.Size()
}
// Сортируем по времени (старые первые) — FIFO.
sort.Slice(fileList, func(i, j int) bool {
return fileList[i].time.Before(fileList[j].time)
})
return fileList, totalSize
}
// cleanByAge удаляет старые файлы по FIFO: с самого старого края, пока файл
// старше cutoff И по имени, И по ModTime, и пока не упрёмся в minKeepFiles.
// Возвращает количество удалённых файлов и их общий размер в байтах.
func (r *Retention) cleanByAge(dataDir string) (int, int64) {
cutoff := time.Now().Add(-time.Duration(r.maxAgeHours) * time.Hour)
files, _ := r.collectFiles(dataDir)
deleted := 0
var deletedBytes int64
for len(files) > minKeepFiles {
f := files[0]
// Возраст — это цель, а не гильотина. Файл удаляем только если он старше
// cutoff и по имени, и по реальному времени записи. Это защищает от
// скачка часов (после NTP на устройстве без RTC): свежий по ModTime
// файл со «старым» именем не будет снесён.
if !f.time.Before(cutoff) || !f.modTime.Before(cutoff) {
// Список отсортирован от старых к новым — дальше только моложе.
break
}
if err := os.Remove(f.path); err != nil {
break
}
deleted++
deletedBytes += f.size
files = files[1:]
}
return deleted, deletedBytes
}
// cleanBySize удаляет самые старые файлы, пока суммарный размер превышает лимит,
// сохраняя как минимум minKeepFiles файлов.
// Возвращает количество удалённых файлов и их общий размер в байтах.
func (r *Retention) cleanBySize(dataDir string) (int, int64) {
fileList, totalSize := r.collectFiles(dataDir)
deleted := 0
var deletedBytes int64
for totalSize > r.maxSizeBytes && len(fileList) > minKeepFiles {
// Удаляем самый старый файл.
oldest := fileList[0]
if err := os.Remove(oldest.path); err != nil {
break
}
totalSize -= oldest.size
deletedBytes += oldest.size
deleted++
fileList = fileList[1:]
}
return deleted, deletedBytes
}
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, err := time.Parse("2006", parts[1])
if err != nil {
return time.Time{}
}
month, err := time.Parse("01", parts[2])
if err != nil {
return time.Time{}
}
day, err := time.Parse("02", parts[3])
if err != nil {
return time.Time{}
}
hour, err := time.Parse("15", parts[4])
if err != nil {
return time.Time{}
}
return time.Date(year.Year(), month.Month(), day.Day(), hour.Hour(), 0, 0, 0, time.Local)
}