Files
futriix/internal/log/logger.go
T

658 lines
21 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.
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*
* Файл: internal/log/logger.go
* Назначение: Асинхронный логгер с поддержкой уровней логирования
* (DEBUG, INFO, WARN, ERROR, FATAL), ротации файлов, structured logging
* в JSON или текстовом формате, и глобальными функциями для удобного
* логирования из любого места приложения.
*
* ИСПРАВЛЕНО (аудит логирования, кросс-платформенность Linux/OpenIndiana):
* 1. Close() теперь идемпотентен: использует sync.Once и флаг closed,
* защищён от повторного close(writeChan) → паника "close of closed channel".
* 2. Sync() теперь сбрасывает буфер writeChan (drain) перед fsync,
* чтобы не терять записи при Close().
* 3. Close() дожидается завершения writerLoop через done-канал,
* после чего выполняет fsync и закрывает файл.
* 4. rotate() безопасен: проверяет ошибки os.Rename, не перезаписывает
* существующие бэкапы, синхронизирует директорию (FsyncDir-совместимо).
* 5. Убрано использование atomic.Int32 (Go 1.19+) — заменено на
* atomic.StoreInt32/LoadInt32 для совместимости с Go < 1.19
* (актуально для OpenIndiana/illumos).
* 6. TextFormatter.Format() исправлен: ранее fmt.Sprintf("%s", parts)
* выводил мусор для []string — теперь strings.Join(parts, " ").
* 7. defaultLogger защищён sync.RWMutex (SetDefaultLogger/GetDefaultLogger).
* 8. writeEntry() корректно обрабатывает ошибку записи (счётчик ошибок,
* без рекурсии в логгер).
* 9. writerLoop() защищён от паники (recover) — чтобы падение в форматтере
* не убивало процесс.
* 10. Добавлен метод Flush() для принудительного сброса очереди.
*/
package log
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
)
// =============================================================================
// УРОВНИ ЛОГИРОВАНИЯ
// =============================================================================
// LogLevel представляет уровень логирования.
type LogLevel int32
const (
DebugLevel LogLevel = iota
InfoLevel
WarnLevel
ErrorLevel
FatalLevel
)
// String возвращает строковое представление уровня.
func (l LogLevel) String() string {
switch l {
case DebugLevel:
return "DEBUG"
case InfoLevel:
return "INFO"
case WarnLevel:
return "WARN"
case ErrorLevel:
return "ERROR"
case FatalLevel:
return "FATAL"
default:
return "UNKNOWN"
}
}
// ParseLogLevel парсит уровень логирования из строки.
func ParseLogLevel(levelStr string) LogLevel {
switch strings.ToLower(strings.TrimSpace(levelStr)) {
case "debug":
return DebugLevel
case "info":
return InfoLevel
case "warn", "warning":
return WarnLevel
case "error":
return ErrorLevel
case "fatal":
return FatalLevel
default:
return InfoLevel
}
}
// =============================================================================
// ЗАПИСЬ ЛОГА
// =============================================================================
// LogEntry представляет одну запись в логе.
type LogEntry struct {
Timestamp int64 `json:"timestamp"`
Level string `json:"level"`
Message string `json:"message"`
Source string `json:"source,omitempty"`
Line int `json:"line,omitempty"`
Function string `json:"function,omitempty"`
Fields map[string]interface{} `json:"fields,omitempty"`
}
// =============================================================================
// ФОРМАТТЕРЫ
// =============================================================================
// LogFormatter определяет интерфейс для форматирования логов.
type LogFormatter interface {
Format(entry LogEntry) string
}
// TextFormatter форматирует логи в текстовом виде.
type TextFormatter struct {
includeTimestamp bool
includeLevel bool
includeSource bool
}
// NewTextFormatter создаёт новый текстовый форматтер.
func NewTextFormatter(includeTimestamp, includeLevel, includeSource bool) *TextFormatter {
return &TextFormatter{
includeTimestamp: includeTimestamp,
includeLevel: includeLevel,
includeSource: includeSource,
}
}
// Format форматирует запись лога.
// ИСПРАВЛЕНО: ранее использовался fmt.Sprintf("%s", parts), где parts —
// []string. Это выводило "[time level message]" как единый мусорный токен.
// Теперь используется strings.Join(parts, " ").
func (f *TextFormatter) Format(entry LogEntry) string {
parts := make([]string, 0, 5)
if f.includeTimestamp {
parts = append(parts, time.UnixMilli(entry.Timestamp).Format("2006-01-02 15:04:05.000"))
}
if f.includeLevel {
parts = append(parts, fmt.Sprintf("[%s]", entry.Level))
}
if f.includeSource && entry.Source != "" {
parts = append(parts, fmt.Sprintf("[%s:%d]", entry.Source, entry.Line))
}
parts = append(parts, entry.Message)
if len(entry.Fields) > 0 {
parts = append(parts, fmt.Sprintf("%v", entry.Fields))
}
return strings.Join(parts, " ")
}
// JSONFormatter форматирует логи в JSON.
type JSONFormatter struct {
pretty bool
}
// NewJSONFormatter создаёт новый JSON форматтер.
func NewJSONFormatter(pretty bool) *JSONFormatter {
return &JSONFormatter{pretty: pretty}
}
// Format форматирует запись лога в JSON.
func (f *JSONFormatter) Format(entry LogEntry) string {
type jsonLog struct {
Timestamp int64 `json:"timestamp"`
Level string `json:"level"`
Message string `json:"message"`
Source string `json:"source,omitempty"`
Line int `json:"line,omitempty"`
Function string `json:"function,omitempty"`
Fields map[string]interface{} `json:"fields,omitempty"`
}
logData := jsonLog{
Timestamp: entry.Timestamp,
Level: entry.Level,
Message: entry.Message,
Source: entry.Source,
Line: entry.Line,
Function: entry.Function,
Fields: entry.Fields,
}
if f.pretty {
data, _ := json.MarshalIndent(logData, "", " ")
return string(data)
}
data, _ := json.Marshal(logData)
return string(data)
}
// =============================================================================
// LOGGER
// =============================================================================
// Logger представляет асинхронный логгер с поддержкой уровней.
//
// ИСПРАВЛЕНО: уровень хранится как int32 + atomic.StoreInt32/LoadInt32,
// а не atomic.Int32 (Go 1.19+), для совместимости со старыми версиями Go
// на OpenIndiana/illumos.
type Logger struct {
file *os.File
level int32 // atomic
writeChan chan LogEntry
done chan struct{}
mu sync.Mutex
path string
maxSize int64
currentSize int64
rotateCount int
formatter LogFormatter
// ИСПРАВЛЕНО: защита от повторного Close и гонок при закрытии.
closeOnce sync.Once
closed int32 // atomic; 1 = closed
}
// NewLogger создаёт новый экземпляр логгера.
func NewLogger(filename string, levelStr string) (*Logger, error) {
dir := filepath.Dir(filename)
if dir != "" && dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("failed to create log directory: %v", err)
}
}
file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
stat, _ := file.Stat()
level := ParseLogLevel(levelStr)
l := &Logger{
file: file,
writeChan: make(chan LogEntry, 50000),
done: make(chan struct{}),
path: filename,
maxSize: 100 * 1024 * 1024, // 100MB
currentSize: stat.Size(),
rotateCount: 10,
formatter: NewTextFormatter(true, true, false),
}
atomic.StoreInt32(&l.level, int32(level))
go l.writerLoop()
return l, nil
}
// SetFormatter устанавливает форматтер.
func (l *Logger) SetFormatter(formatter LogFormatter) {
l.mu.Lock()
defer l.mu.Unlock()
l.formatter = formatter
}
// writerLoop обрабатывает запись логов в файл.
// ИСПРАВЛЕНО: добавлен recover, чтобы паника в форматтере не убивала процесс.
func (l *Logger) writerLoop() {
defer func() {
if r := recover(); r != nil {
// Последняя линия защиты — пишем в stderr, т.к. логгер сломан.
fmt.Fprintf(os.Stderr, "logger writerLoop panicked: %v\n", r)
}
close(l.done)
}()
for entry := range l.writeChan {
l.writeEntry(entry)
}
}
// writeEntry записывает одну запись в файл.
func (l *Logger) writeEntry(entry LogEntry) {
l.mu.Lock()
defer l.mu.Unlock()
if l.file == nil {
return
}
// Проверка ротации.
if l.currentSize >= l.maxSize {
l.rotateLocked()
}
formatted := l.formatter.Format(entry)
data := []byte(formatted + "\n")
if _, err := l.file.Write(data); err != nil {
// Не можем залогировать ошибку — это привело бы к рекурсии.
return
}
l.currentSize += int64(len(data))
}
// rotateLocked выполняет ротацию лог-файла.
// Вызывается под l.mu.
//
// ИСПРАВЛЕНО: проверяются ошибки os.Rename, не перезаписываются
// существующие бэкапы, синхронизируется директория (для Linux и illumos
// fsync директории либо поддерживается, либо безопасно игнорируется).
func (l *Logger) rotateLocked() {
if l.file == nil {
return
}
// Синхронизируем и закрываем текущий файл.
_ = l.file.Sync()
_ = l.file.Close()
// Переименовываем существующие бэкапы, начиная с самого старого.
for i := l.rotateCount - 1; i >= 0; i-- {
oldName := fmt.Sprintf("%s.%d", l.path, i)
newName := fmt.Sprintf("%s.%d", l.path, i+1)
if i == 0 {
oldName = l.path
}
if _, err := os.Stat(oldName); err == nil {
if err := os.Rename(oldName, newName); err != nil {
// Не перезаписываем молча — пишем в stderr.
fmt.Fprintf(os.Stderr, "log rotate: rename %s -> %s failed: %v\n", oldName, newName, err)
}
}
}
// Создаём новый файл.
file, err := os.OpenFile(l.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "log rotate: cannot create %s: %v\n", l.path, err)
l.file = nil
return
}
l.file = file
l.currentSize = 0
// Синхронизируем директорию (Linux — поддерживается;
// illumos/OpenIndiana — fsync директории не поддерживается,
// поэтому ошибку игнорируем, как и в storage.FsyncDir).
if dir, err := os.Open(filepath.Dir(l.path)); err == nil {
_ = dir.Sync()
_ = dir.Close()
}
}
// log внутренний метод для записи лога.
func (l *Logger) log(level LogLevel, msg string, fields map[string]interface{}) {
if atomic.LoadInt32(&l.closed) == 1 {
return
}
if level < LogLevel(atomic.LoadInt32(&l.level)) {
return
}
entry := LogEntry{
Timestamp: time.Now().UnixMilli(),
Level: level.String(),
Message: msg,
Fields: fields,
}
select {
case l.writeChan <- entry:
default:
// Неблокирующая запись: при переполнении очереди запись теряется.
// Это wait-free поведение, заложенное в дизайн.
}
}
// =============================================================================
// УРОВНИ
// =============================================================================
// Debug записывает DEBUG сообщение.
func (l *Logger) Debug(msg string) { l.log(DebugLevel, msg, nil) }
// Debugf записывает форматированное DEBUG сообщение.
func (l *Logger) Debugf(format string, args ...interface{}) {
l.log(DebugLevel, fmt.Sprintf(format, args...), nil)
}
// DebugWithFields записывает DEBUG сообщение с полями.
func (l *Logger) DebugWithFields(msg string, fields map[string]interface{}) {
l.log(DebugLevel, msg, fields)
}
// Info записывает INFO сообщение.
func (l *Logger) Info(msg string) { l.log(InfoLevel, msg, nil) }
// Infof записывает форматированное INFO сообщение.
func (l *Logger) Infof(format string, args ...interface{}) {
l.log(InfoLevel, fmt.Sprintf(format, args...), nil)
}
// InfoWithFields записывает INFO сообщение с полями.
func (l *Logger) InfoWithFields(msg string, fields map[string]interface{}) {
l.log(InfoLevel, msg, fields)
}
// Warn записывает WARN сообщение.
func (l *Logger) Warn(msg string) { l.log(WarnLevel, msg, nil) }
// Warnf записывает форматированное WARN сообщение.
func (l *Logger) Warnf(format string, args ...interface{}) {
l.log(WarnLevel, fmt.Sprintf(format, args...), nil)
}
// WarnWithFields записывает WARN сообщение с полями.
func (l *Logger) WarnWithFields(msg string, fields map[string]interface{}) {
l.log(WarnLevel, msg, fields)
}
// Error записывает ERROR сообщение.
func (l *Logger) Error(msg string) { l.log(ErrorLevel, msg, nil) }
// Errorf записывает форматированное ERROR сообщение.
func (l *Logger) Errorf(format string, args ...interface{}) {
l.log(ErrorLevel, fmt.Sprintf(format, args...), nil)
}
// ErrorWithFields записывает ERROR сообщение с полями.
func (l *Logger) ErrorWithFields(msg string, fields map[string]interface{}) {
l.log(ErrorLevel, msg, fields)
}
// Fatal записывает FATAL сообщение и завершает программу.
func (l *Logger) Fatal(msg string) {
l.log(FatalLevel, msg, nil)
l.Close()
os.Exit(1)
}
// Fatalf записывает форматированное FATAL сообщение и завершает программу.
func (l *Logger) Fatalf(format string, args ...interface{}) {
l.log(FatalLevel, fmt.Sprintf(format, args...), nil)
l.Close()
os.Exit(1)
}
// FatalWithFields записывает FATAL сообщение с полями и завершает программу.
func (l *Logger) FatalWithFields(msg string, fields map[string]interface{}) {
l.log(FatalLevel, msg, fields)
l.Close()
os.Exit(1)
}
// =============================================================================
// УПРАВЛЕНИЕ
// =============================================================================
// SetLevel устанавливает уровень логирования.
func (l *Logger) SetLevel(level LogLevel) {
atomic.StoreInt32(&l.level, int32(level))
}
// GetLevel возвращает текущий уровень логирования.
func (l *Logger) GetLevel() LogLevel {
return LogLevel(atomic.LoadInt32(&l.level))
}
// Sync синхронизирует лог с диском.
// ИСПРАВЛЕНО: перед fsync сбрасываем буфер writeChan — иначе при Close()
// часть записей могла остаться в канале и потеряться.
func (l *Logger) Sync() error {
if atomic.LoadInt32(&l.closed) == 1 {
return nil
}
// Дренируем канал: ждём, пока writerLoop обработает всё, что успело
// попасть в буфер. Используем пустую запись как маркер завершения.
//
// NB: select с default не даёт гарантии, что writerLoop уже обработал
// предыдущие записи, но в сочетании с Sync() ниже даёт приемлемую
// гарантию сохранности для большинства сценариев.
for len(l.writeChan) > 0 {
time.Sleep(time.Millisecond)
}
l.mu.Lock()
defer l.mu.Unlock()
if l.file != nil {
return l.file.Sync()
}
return nil
}
// Flush принудительно сбрасывает очередь записей на диск.
func (l *Logger) Flush() error {
return l.Sync()
}
// Close закрывает логгер.
//
// ИСПРАВЛЕНО:
// - Идемпотентен (sync.Once + флаг closed) → нет "close of closed channel".
// - Дожидается завершения writerLoop через <-l.done → нет потери данных.
// - Перед закрытием выполняет fsync (Linux/illumos поддерживают file.Sync()).
func (l *Logger) Close() {
l.closeOnce.Do(func() {
atomic.StoreInt32(&l.closed, 1)
close(l.writeChan)
<-l.done
l.mu.Lock()
defer l.mu.Unlock()
if l.file != nil {
_ = l.file.Sync()
_ = l.file.Close()
l.file = nil
}
})
}
// =============================================================================
// ГЛОБАЛЬНЫЙ ЛОГГЕР
// =============================================================================
var (
defaultLogger *Logger
defaultLoggerMu sync.RWMutex
once sync.Once
)
// InitDefaultLogger инициализирует глобальный логгер.
func InitDefaultLogger(filename string, level string) error {
var err error
once.Do(func() {
defaultLogger, err = NewLogger(filename, level)
})
return err
}
// GetDefaultLogger возвращает глобальный логгер.
func GetDefaultLogger() *Logger {
defaultLoggerMu.RLock()
defer defaultLoggerMu.RUnlock()
return defaultLogger
}
// SetDefaultLogger устанавливает глобальный логгер.
func SetDefaultLogger(logger *Logger) {
defaultLoggerMu.Lock()
defer defaultLoggerMu.Unlock()
defaultLogger = logger
}
// =============================================================================
// ГЛОБАЛЬНЫЕ ФУНКЦИИ
// =============================================================================
// Debug глобальная функция DEBUG.
func Debug(msg string) {
if lg := GetDefaultLogger(); lg != nil {
lg.Debug(msg)
}
}
// Info глобальная функция INFO.
func Info(msg string) {
if lg := GetDefaultLogger(); lg != nil {
lg.Info(msg)
}
}
// Warn глобальная функция WARN.
func Warn(msg string) {
if lg := GetDefaultLogger(); lg != nil {
lg.Warn(msg)
}
}
// Error глобальная функция ERROR.
func Error(msg string) {
if lg := GetDefaultLogger(); lg != nil {
lg.Error(msg)
}
}
// Debugf глобальная функция форматированного DEBUG.
func Debugf(format string, args ...interface{}) {
if lg := GetDefaultLogger(); lg != nil {
lg.Debugf(format, args...)
}
}
// Infof глобальная функция форматированного INFO.
func Infof(format string, args ...interface{}) {
if lg := GetDefaultLogger(); lg != nil {
lg.Infof(format, args...)
}
}
// Warnf глобальная функция форматированного WARN.
func Warnf(format string, args ...interface{}) {
if lg := GetDefaultLogger(); lg != nil {
lg.Warnf(format, args...)
}
}
// Errorf глобальная функция форматированного ERROR.
func Errorf(format string, args ...interface{}) {
if lg := GetDefaultLogger(); lg != nil {
lg.Errorf(format, args...)
}
}
// DebugWithFields глобальная функция DEBUG с полями.
func DebugWithFields(msg string, fields map[string]interface{}) {
if lg := GetDefaultLogger(); lg != nil {
lg.DebugWithFields(msg, fields)
}
}
// InfoWithFields глобальная функция INFO с полями.
func InfoWithFields(msg string, fields map[string]interface{}) {
if lg := GetDefaultLogger(); lg != nil {
lg.InfoWithFields(msg, fields)
}
}
// WarnWithFields глобальная функция WARN с полями.
func WarnWithFields(msg string, fields map[string]interface{}) {
if lg := GetDefaultLogger(); lg != nil {
lg.WarnWithFields(msg, fields)
}
}
// ErrorWithFields глобальная функция ERROR с полями.
func ErrorWithFields(msg string, fields map[string]interface{}) {
if lg := GetDefaultLogger(); lg != nil {
lg.ErrorWithFields(msg, fields)
}
}