Доработка. Защита от переполнения диска
This commit is contained in:
@@ -192,6 +192,19 @@ func main() {
|
|||||||
defer dataLogger.Close()
|
defer dataLogger.Close()
|
||||||
eventLogger.Event("DATA_ЛОГЕР_ГОТОВ")
|
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
|
// 4. RingBuffer
|
||||||
buf := adapter.NewRingBuffer(1024 * 10)
|
buf := adapter.NewRingBuffer(1024 * 10)
|
||||||
|
|
||||||
|
|||||||
204
internal/logger/retention.go
Normal file
204
internal/logger/retention.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user