diff --git a/README.md b/README.md index 3052fc3..4bc8daa 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,44 @@ run_every.week(check_python_packages) --- +## Логирование + +ALR-Updater поддерживает логирование как в консоль (stderr), так и в файл с автоматической ротацией: + +### Настройка логирования в файл + +В файле `/etc/alr-updater/config.toml`: + +```toml +[logging] + # Включить логирование в файл + enable_file = true + # Путь к файлу логов + log_file = "/var/log/alr-updater.log" + # Максимальный размер файла в байтах (100MB) + max_size = 104857600 +``` + +### Особенности: +- При достижении максимального размера файл автоматически ротируется +- Старый файл сохраняется с временной меткой (например, `alr-updater.log.20250125-143022`) +- Хранится до 5 резервных копий логов +- Логи пишутся одновременно в stderr и файл + +### Просмотр логов: +```bash +# Через systemd +journalctl -u alr-updater -f + +# Из файла +tail -f /var/log/alr-updater.log + +# Поиск ошибок +grep ERROR /var/log/alr-updater.log +``` + +--- + ## Запуск ### Ручной запуск diff --git a/alr-updater.example.toml b/alr-updater.example.toml index 8c2a8d3..d9c1401 100644 --- a/alr-updater.example.toml +++ b/alr-updater.example.toml @@ -34,4 +34,13 @@ reposBaseDir = "/var/cache/alr-updater" [webhook] # Хэш пароля для webhook. Сгенерируйте его, используя `alr-updater -g`. - pwd_hash = "CHANGE ME" \ No newline at end of file + pwd_hash = "CHANGE ME" + +[logging] + # Включить логирование в файл + enable_file = false + # Путь к файлу логов (по умолчанию /var/log/alr-updater.log) + log_file = "/var/log/alr-updater.log" + # Максимальный размер файла логов в байтах (по умолчанию 100MB) + # При достижении этого размера файл будет ротирован + max_size = 104857600 \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index 0b52cab..6465df8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -22,6 +22,7 @@ type Config struct { ReposBaseDir string `toml:"reposBaseDir" env:"REPOS_BASE_DIR"` Repositories map[string]GitRepo `toml:"repositories"` Webhook Webhook `toml:"webhook" envPrefix:"WEBHOOK_"` + Logging Logging `toml:"logging" envPrefix:"LOGGING_"` } type GitRepo struct { @@ -43,3 +44,9 @@ type Commit struct { type Webhook struct { PasswordHash string `toml:"pwd_hash" env:"PASSWORD_HASH"` } + +type Logging struct { + LogFile string `toml:"log_file" env:"LOG_FILE"` + MaxSize int64 `toml:"max_size" env:"MAX_SIZE"` + EnableFile bool `toml:"enable_file" env:"ENABLE_FILE"` +} diff --git a/internal/logger/file_logger.go b/internal/logger/file_logger.go new file mode 100644 index 0000000..4accee5 --- /dev/null +++ b/internal/logger/file_logger.go @@ -0,0 +1,149 @@ +/* + * ALR Updater - Automated updater bot for ALR packages + * Copyright (C) 2025 The ALR Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package logger + +import ( + "fmt" + "io" + "os" + "path/filepath" + "sync" + "time" +) + +type RotatingFileWriter struct { + mu sync.Mutex + file *os.File + filename string + maxSize int64 + currentSize int64 +} + +func NewRotatingFileWriter(filename string, maxSize int64) (*RotatingFileWriter, error) { + dir := filepath.Dir(filename) + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, fmt.Errorf("failed to create log directory: %w", err) + } + + rfw := &RotatingFileWriter{ + filename: filename, + maxSize: maxSize, + } + + if err := rfw.openFile(); err != nil { + return nil, err + } + + return rfw, nil +} + +func (rfw *RotatingFileWriter) openFile() error { + file, err := os.OpenFile(rfw.filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return fmt.Errorf("failed to open log file: %w", err) + } + + info, err := file.Stat() + if err != nil { + file.Close() + return fmt.Errorf("failed to stat log file: %w", err) + } + + rfw.file = file + rfw.currentSize = info.Size() + return nil +} + +func (rfw *RotatingFileWriter) rotate() error { + if rfw.file != nil { + rfw.file.Close() + } + + // Переименовываем текущий файл с временной меткой + backupName := fmt.Sprintf("%s.%s", rfw.filename, time.Now().Format("20060102-150405")) + if err := os.Rename(rfw.filename, backupName); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to rotate log file: %w", err) + } + + // Создаем новый файл + if err := rfw.openFile(); err != nil { + return err + } + + // Удаляем старые резервные копии (оставляем только последние 5) + pattern := rfw.filename + ".*" + matches, _ := filepath.Glob(pattern) + if len(matches) > 5 { + // Сортировка не требуется, так как имена файлов содержат временную метку + for i := 0; i < len(matches)-5; i++ { + os.Remove(matches[i]) + } + } + + return nil +} + +func (rfw *RotatingFileWriter) Write(p []byte) (n int, err error) { + rfw.mu.Lock() + defer rfw.mu.Unlock() + + // Проверяем, нужна ли ротация + if rfw.currentSize+int64(len(p)) > rfw.maxSize { + if err := rfw.rotate(); err != nil { + return 0, err + } + } + + n, err = rfw.file.Write(p) + if err != nil { + return n, err + } + + rfw.currentSize += int64(n) + return n, nil +} + +func (rfw *RotatingFileWriter) Close() error { + rfw.mu.Lock() + defer rfw.mu.Unlock() + + if rfw.file != nil { + return rfw.file.Close() + } + return nil +} + +// MultiWriter объединяет вывод в несколько writers +type MultiWriter struct { + writers []io.Writer +} + +func NewMultiWriter(writers ...io.Writer) *MultiWriter { + return &MultiWriter{writers: writers} +} + +func (mw *MultiWriter) Write(p []byte) (n int, err error) { + for _, w := range mw.writers { + n, err = w.Write(p) + if err != nil { + return + } + } + return len(p), nil +} \ No newline at end of file diff --git a/main.go b/main.go index adcb3cd..450290a 100644 --- a/main.go +++ b/main.go @@ -21,6 +21,7 @@ package main import ( "bufio" "fmt" + "io" "net/http" "os" "path/filepath" @@ -37,12 +38,15 @@ import ( "gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/builtins" "gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/config" "gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/generator" + filelogger "gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/logger" "go.etcd.io/bbolt" "go.starlark.net/starlark" "golang.org/x/crypto/bcrypt" "golang.org/x/term" ) +var fileWriter *filelogger.RotatingFileWriter + func init() { log.Logger = logger.NewPretty(os.Stderr) } @@ -143,6 +147,37 @@ func main() { fl.Close() } + // Настройка логирования в файл + if cfg.Logging.EnableFile { + logFile := cfg.Logging.LogFile + if logFile == "" { + logFile = "/var/log/alr-updater.log" + } + + maxSize := cfg.Logging.MaxSize + if maxSize == 0 { + maxSize = 100 * 1024 * 1024 // 100 MB по умолчанию + } + + var err error + fileWriter, err = filelogger.NewRotatingFileWriter(logFile, maxSize) + if err != nil { + log.Error("Failed to create file logger, continuing with stderr only").Err(err).Send() + } else { + // Создаем MultiWriter для вывода в stderr и файл одновременно + multiWriter := filelogger.NewMultiWriter(os.Stderr, fileWriter) + log.Logger = logger.NewPretty(multiWriter) + log.Info("File logging enabled").Str("file", logFile).Int64("maxSize", maxSize).Send() + + // Закрываем файл при завершении + defer func() { + if fileWriter != nil { + fileWriter.Close() + } + }() + } + } + // Обработка генерации плагинов if *generatePlugins { log.Info("Starting automatic plugin generation...")