Update internal/log/logger.go
This commit is contained in:
1 parent
2ba38f1000
commit
d0b234b61e
1 file changed
+263
-128
+263
-128
@@ -6,11 +6,33 @@
|
|||||||
*
|
*
|
||||||
* 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
|
||||||
@@ -20,12 +42,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LogLevel представляет уровень логирования
|
// =============================================================================
|
||||||
|
// УРОВНИ ЛОГИРОВАНИЯ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// LogLevel представляет уровень логирования.
|
||||||
type LogLevel int32
|
type LogLevel int32
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -36,7 +63,7 @@ const (
|
|||||||
FatalLevel
|
FatalLevel
|
||||||
)
|
)
|
||||||
|
|
||||||
// String возвращает строковое представление уровня
|
// String возвращает строковое представление уровня.
|
||||||
func (l LogLevel) String() string {
|
func (l LogLevel) String() string {
|
||||||
switch l {
|
switch l {
|
||||||
case DebugLevel:
|
case DebugLevel:
|
||||||
@@ -54,9 +81,9 @@ func (l LogLevel) String() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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":
|
||||||
@@ -72,7 +99,11 @@ func ParseLogLevel(levelStr string) LogLevel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogEntry представляет одну запись в логе
|
// =============================================================================
|
||||||
|
// ЗАПИСЬ ЛОГА
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// LogEntry представляет одну запись в логе.
|
||||||
type LogEntry struct {
|
type LogEntry struct {
|
||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
Level string `json:"level"`
|
Level string `json:"level"`
|
||||||
@@ -83,33 +114,23 @@ type LogEntry struct {
|
|||||||
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,
|
||||||
@@ -118,9 +139,12 @@ func NewTextFormatter(includeTimestamp, includeLevel, includeSource bool) *TextF
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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"))
|
||||||
@@ -140,20 +164,20 @@ func (f *TextFormatter) Format(entry LogEntry) string {
|
|||||||
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"`
|
||||||
@@ -183,12 +207,40 @@ func (f *JSONFormatter) Format(entry LogEntry) string {
|
|||||||
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 dir != "" && dir != "." {
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
return nil, fmt.Errorf("failed to create log directory: %v", err)
|
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 {
|
||||||
@@ -209,55 +261,77 @@ func NewLogger(filename string, levelStr string) (*Logger, error) {
|
|||||||
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() {
|
||||||
|
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 {
|
for entry := range l.writeChan {
|
||||||
l.writeEntry(entry)
|
l.writeEntry(entry)
|
||||||
}
|
}
|
||||||
close(l.done)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка ротации.
|
||||||
if l.currentSize >= l.maxSize {
|
if l.currentSize >= l.maxSize {
|
||||||
l.rotate()
|
l.rotateLocked()
|
||||||
}
|
}
|
||||||
|
|
||||||
formatted := l.formatter.Format(entry)
|
formatted := l.formatter.Format(entry)
|
||||||
data := []byte(formatted + "\n")
|
data := []byte(formatted + "\n")
|
||||||
|
|
||||||
if _, err := l.file.Write(data); err != nil {
|
if _, err := l.file.Write(data); err != nil {
|
||||||
// Не можем залогировать ошибку, так как это приведёт к рекурсии
|
// Не можем залогировать ошибку — это привело бы к рекурсии.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.currentSize += int64(len(data))
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// Переименовываем существующий файл
|
// Синхронизируем и закрываем текущий файл.
|
||||||
|
_ = l.file.Sync()
|
||||||
|
_ = l.file.Close()
|
||||||
|
|
||||||
|
// Переименовываем существующие бэкапы, начиная с самого старого.
|
||||||
for i := l.rotateCount - 1; i >= 0; i-- {
|
for i := l.rotateCount - 1; i >= 0; i-- {
|
||||||
oldName := fmt.Sprintf("%s.%d", l.path, i)
|
oldName := fmt.Sprintf("%s.%d", l.path, i)
|
||||||
newName := fmt.Sprintf("%s.%d", l.path, i+1)
|
newName := fmt.Sprintf("%s.%d", l.path, i+1)
|
||||||
@@ -267,23 +341,39 @@ func (l *Logger) rotate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(oldName); err == nil {
|
if _, err := os.Stat(oldName); err == nil {
|
||||||
os.Rename(oldName, newName)
|
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)
|
file, err := os.OpenFile(l.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "log rotate: cannot create %s: %v\n", l.path, err)
|
||||||
|
l.file = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.file = file
|
l.file = file
|
||||||
l.currentSize = 0
|
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
|
||||||
|
}
|
||||||
|
if level < LogLevel(atomic.LoadInt32(&l.level)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,103 +387,120 @@ func (l *Logger) log(level LogLevel, msg string, fields map[string]interface{})
|
|||||||
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 {
|
||||||
|
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()
|
l.mu.Lock()
|
||||||
defer l.mu.Unlock()
|
defer l.mu.Unlock()
|
||||||
if l.file != nil {
|
if l.file != nil {
|
||||||
@@ -402,24 +509,44 @@ func (l *Logger) Sync() error {
|
|||||||
return nil
|
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() {
|
||||||
|
l.closeOnce.Do(func() {
|
||||||
|
atomic.StoreInt32(&l.closed, 1)
|
||||||
close(l.writeChan)
|
close(l.writeChan)
|
||||||
<-l.done
|
<-l.done
|
||||||
|
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
defer l.mu.Unlock()
|
defer l.mu.Unlock()
|
||||||
if l.file != nil {
|
if l.file != nil {
|
||||||
l.file.Sync()
|
_ = l.file.Sync()
|
||||||
l.file.Close()
|
_ = 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() {
|
||||||
@@ -428,96 +555,104 @@ func InitDefaultLogger(filename string, level string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultLogger возвращает глобальный логгер
|
// GetDefaultLogger возвращает глобальный логгер.
|
||||||
func GetDefaultLogger() *Logger {
|
func GetDefaultLogger() *Logger {
|
||||||
|
defaultLoggerMu.RLock()
|
||||||
|
defer defaultLoggerMu.RUnlock()
|
||||||
return defaultLogger
|
return defaultLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaultLogger устанавливает глобальный логгер
|
// SetDefaultLogger устанавливает глобальный логгер.
|
||||||
func SetDefaultLogger(logger *Logger) {
|
func SetDefaultLogger(logger *Logger) {
|
||||||
|
defaultLoggerMu.Lock()
|
||||||
|
defer defaultLoggerMu.Unlock()
|
||||||
defaultLogger = logger
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user