243 lines
7.2 KiB
Go
243 lines
7.2 KiB
Go
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)
|
||
} |