// Package log реализует ASCII-логгер метрик с ротацией по дням. // Формат: "2026-06-25T10:00:00Z pbx-01 calls=42 load=0.85 idle_cpu=35 fail=0.5 score=73" package log import ( "fmt" "os" "path/filepath" "strings" "sync" "time" "github.com/pulse-lets-go/internal/models" ) // Logger — потокобезопасный логгер метрик в ASCII-файл с дневной ротацией. type Logger struct { mu sync.Mutex dir string file *os.File today string // YYYY-MM-DD последнего открытого файла } // NewLogger создаёт логгер, записывающий в dataDir/. // Файлы именуются как metrics.YYYY-MM-DD.log. func NewLogger(dataDir string) (*Logger, error) { if err := os.MkdirAll(dataDir, 0750); err != nil { return nil, fmt.Errorf("создание data/: %w", err) } l := &Logger{dir: dataDir} if err := l.ensureFile(); err != nil { return nil, err } return l, nil } // Log записывает снапшот метрик ноды в лог-файл. func (l *Logger) Log(ns *models.NodeState) { l.mu.Lock() defer l.mu.Unlock() if err := l.ensureFile(); err != nil { return // тихая ошибка — не блокируем метрики из-за логгера } line := l.formatLine(ns) l.file.WriteString(line + "\n") } // formatLine форматирует строку лога. func (l *Logger) formatLine(ns *models.NodeState) string { t := time.Unix(ns.TS, 0).UTC().Format(time.RFC3339) return fmt.Sprintf("%s %s calls=%d load=%.2f idle_cpu=%.1f fail=%.1f score=%.1f disabled=%t", t, ns.NodeID, ns.ActiveCalls, ns.LoadAvg, ns.IdleCPU, ns.CallFailureRate, ns.Score, ns.Disabled) } // Close закрывает файл лога. func (l *Logger) Close() error { l.mu.Lock() defer l.mu.Unlock() if l.file != nil { return l.file.Close() } return nil } // ensureFile открывает файл на текущий день, создавая новый, если день сменился. // Вызывающий держит mu. func (l *Logger) ensureFile() error { today := time.Now().UTC().Format("2006-01-02") if l.file != nil && l.today == today { return nil } if l.file != nil { l.file.Close() } path := filepath.Join(l.dir, "metrics."+today+".log") f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0640) if err != nil { return fmt.Errorf("открытие лог-файла %s: %w", path, err) } l.file = f l.today = today return nil } // TrimPrefix обрезает "metrics." и ".log" из имени файла (для API истории). func TrimPrefix(filename string) string { return strings.TrimPrefix(strings.TrimSuffix(filename, ".log"), "metrics.") }