Update internal/log/logger.go

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