519 lines
15 KiB
Go
519 lines
15 KiB
Go
|
|
/*
|
|||
|
|
* 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
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
package log
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"encoding/json"
|
|||
|
|
"fmt"
|
|||
|
|
"os"
|
|||
|
|
"path/filepath"
|
|||
|
|
"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 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"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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 определяет интерфейс для форматирования логов
|
|||
|
|
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 форматирует запись лога
|
|||
|
|
func (f *TextFormatter) Format(entry LogEntry) string {
|
|||
|
|
var parts []string
|
|||
|
|
|
|||
|
|
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 fmt.Sprintf("%s", 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)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewLogger создаёт новый экземпляр логгера
|
|||
|
|
func NewLogger(filename string, levelStr string) (*Logger, error) {
|
|||
|
|
dir := filepath.Dir(filename)
|
|||
|
|
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),
|
|||
|
|
}
|
|||
|
|
l.level.Store(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 обрабатывает запись логов в файл
|
|||
|
|
func (l *Logger) writerLoop() {
|
|||
|
|
for entry := range l.writeChan {
|
|||
|
|
l.writeEntry(entry)
|
|||
|
|
}
|
|||
|
|
close(l.done)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// writeEntry записывает одну запись в файл
|
|||
|
|
func (l *Logger) writeEntry(entry LogEntry) {
|
|||
|
|
l.mu.Lock()
|
|||
|
|
defer l.mu.Unlock()
|
|||
|
|
|
|||
|
|
// Проверка ротации
|
|||
|
|
if l.currentSize >= l.maxSize {
|
|||
|
|
l.rotate()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
formatted := l.formatter.Format(entry)
|
|||
|
|
data := []byte(formatted + "\n")
|
|||
|
|
|
|||
|
|
if _, err := l.file.Write(data); err != nil {
|
|||
|
|
// Не можем залогировать ошибку, так как это приведёт к рекурсии
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
l.currentSize += int64(len(data))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// rotate выполняет ротацию лог-файла
|
|||
|
|
func (l *Logger) rotate() {
|
|||
|
|
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 {
|
|||
|
|
os.Rename(oldName, newName)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Создаём новый файл
|
|||
|
|
file, err := os.OpenFile(l.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
|||
|
|
if err != nil {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
l.file = file
|
|||
|
|
l.currentSize = 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// log внутренний метод для записи лога
|
|||
|
|
func (l *Logger) log(level LogLevel, msg string, fields map[string]interface{}) {
|
|||
|
|
if level < LogLevel(l.level.Load()) {
|
|||
|
|
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) {
|
|||
|
|
l.level.Store(int32(level))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetLevel возвращает текущий уровень логирования
|
|||
|
|
func (l *Logger) GetLevel() LogLevel {
|
|||
|
|
return LogLevel(l.level.Load())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Sync синхронизирует лог с диском
|
|||
|
|
func (l *Logger) Sync() error {
|
|||
|
|
l.mu.Lock()
|
|||
|
|
defer l.mu.Unlock()
|
|||
|
|
if l.file != nil {
|
|||
|
|
return l.file.Sync()
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Close закрывает логгер
|
|||
|
|
func (l *Logger) Close() {
|
|||
|
|
close(l.writeChan)
|
|||
|
|
<-l.done
|
|||
|
|
l.mu.Lock()
|
|||
|
|
defer l.mu.Unlock()
|
|||
|
|
if l.file != nil {
|
|||
|
|
l.file.Sync()
|
|||
|
|
l.file.Close()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== Глобальные функции для удобства ==========
|
|||
|
|
|
|||
|
|
var defaultLogger *Logger
|
|||
|
|
var 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 {
|
|||
|
|
return defaultLogger
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SetDefaultLogger устанавливает глобальный логгер
|
|||
|
|
func SetDefaultLogger(logger *Logger) {
|
|||
|
|
defaultLogger = logger
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Debug глобальная функция DEBUG
|
|||
|
|
func Debug(msg string) {
|
|||
|
|
if defaultLogger != nil {
|
|||
|
|
defaultLogger.Debug(msg)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Info глобальная функция INFO
|
|||
|
|
func Info(msg string) {
|
|||
|
|
if defaultLogger != nil {
|
|||
|
|
defaultLogger.Info(msg)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Warn глобальная функция WARN
|
|||
|
|
func Warn(msg string) {
|
|||
|
|
if defaultLogger != nil {
|
|||
|
|
defaultLogger.Warn(msg)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Error глобальная функция ERROR
|
|||
|
|
func Error(msg string) {
|
|||
|
|
if defaultLogger != nil {
|
|||
|
|
defaultLogger.Error(msg)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Debugf глобальная функция форматированного DEBUG
|
|||
|
|
func Debugf(format string, args ...interface{}) {
|
|||
|
|
if defaultLogger != nil {
|
|||
|
|
defaultLogger.Debugf(format, args...)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Infof глобальная функция форматированного INFO
|
|||
|
|
func Infof(format string, args ...interface{}) {
|
|||
|
|
if defaultLogger != nil {
|
|||
|
|
defaultLogger.Infof(format, args...)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Warnf глобальная функция форматированного WARN
|
|||
|
|
func Warnf(format string, args ...interface{}) {
|
|||
|
|
if defaultLogger != nil {
|
|||
|
|
defaultLogger.Warnf(format, args...)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Errorf глобальная функция форматированного ERROR
|
|||
|
|
func Errorf(format string, args ...interface{}) {
|
|||
|
|
if defaultLogger != nil {
|
|||
|
|
defaultLogger.Errorf(format, args...)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DebugWithFields глобальная функция DEBUG с полями
|
|||
|
|
func DebugWithFields(msg string, fields map[string]interface{}) {
|
|||
|
|
if defaultLogger != nil {
|
|||
|
|
defaultLogger.DebugWithFields(msg, fields)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// InfoWithFields глобальная функция INFO с полями
|
|||
|
|
func InfoWithFields(msg string, fields map[string]interface{}) {
|
|||
|
|
if defaultLogger != nil {
|
|||
|
|
defaultLogger.InfoWithFields(msg, fields)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// WarnWithFields глобальная функция WARN с полями
|
|||
|
|
func WarnWithFields(msg string, fields map[string]interface{}) {
|
|||
|
|
if defaultLogger != nil {
|
|||
|
|
defaultLogger.WarnWithFields(msg, fields)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ErrorWithFields глобальная функция ERROR с полями
|
|||
|
|
func ErrorWithFields(msg string, fields map[string]interface{}) {
|
|||
|
|
if defaultLogger != nil {
|
|||
|
|
defaultLogger.ErrorWithFields(msg, fields)
|
|||
|
|
}
|
|||
|
|
}
|