149 lines
3.9 KiB
Go
149 lines
3.9 KiB
Go
package logger
|
||
|
||
import (
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
type LogLevel string
|
||
|
||
const (
|
||
LevelInfo LogLevel = "INFO"
|
||
LevelWarn LogLevel = "WARN"
|
||
LevelError LogLevel = "ERROR"
|
||
LevelFatal LogLevel = "FATAL"
|
||
LevelDebug LogLevel = "DEBUG"
|
||
)
|
||
|
||
type LogEntry struct {
|
||
Time time.Time `json:"time"`
|
||
Level LogLevel `json:"level"`
|
||
Message string `json:"message"`
|
||
}
|
||
|
||
var (
|
||
fileLog *log.Logger
|
||
mu sync.Mutex
|
||
// Кольцевой буфер последних записей (максимум 1000)
|
||
buffer []LogEntry
|
||
bufferIdx int
|
||
bufferCap = 1000
|
||
// Канал для подписчиков (один на всех, но можно расширить)
|
||
subscribers []chan LogEntry
|
||
subMu sync.RWMutex
|
||
)
|
||
|
||
func Init(filename string, maxSize int64) error {
|
||
f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
fileLog = log.New(f, "", log.LstdFlags)
|
||
buffer = make([]LogEntry, bufferCap)
|
||
bufferIdx = 0
|
||
return nil
|
||
}
|
||
|
||
// Добавляет запись в буфер и рассылает подписчикам
|
||
func addEntry(level LogLevel, format string, v ...interface{}) {
|
||
msg := fmt.Sprintf(format, v...)
|
||
entry := LogEntry{
|
||
Time: time.Now(),
|
||
Level: level,
|
||
Message: msg,
|
||
}
|
||
// Сохраняем в буфер
|
||
mu.Lock()
|
||
buffer[bufferIdx] = entry
|
||
bufferIdx = (bufferIdx + 1) % bufferCap
|
||
mu.Unlock()
|
||
|
||
// Отправляем подписчикам (асинхронно, чтобы не блокировать)
|
||
subMu.RLock()
|
||
for _, ch := range subscribers {
|
||
select {
|
||
case ch <- entry:
|
||
default:
|
||
// Если канал заполнен, пропускаем (чтобы не тормозить)
|
||
}
|
||
}
|
||
subMu.RUnlock()
|
||
|
||
// Вывод в консоль и файл
|
||
log.Printf("[%s] %s", level, msg)
|
||
if fileLog != nil {
|
||
fileLog.Printf("[%s] %s", level, msg)
|
||
}
|
||
|
||
if level == LevelFatal {
|
||
os.Exit(1)
|
||
}
|
||
}
|
||
|
||
func Info(format string, v ...interface{}) {
|
||
addEntry(LevelInfo, format, v...)
|
||
}
|
||
|
||
func Warn(format string, v ...interface{}) {
|
||
addEntry(LevelWarn, format, v...)
|
||
}
|
||
|
||
func Error(format string, v ...interface{}) {
|
||
addEntry(LevelError, format, v...)
|
||
}
|
||
|
||
func Fatal(format string, v ...interface{}) {
|
||
addEntry(LevelFatal, format, v...)
|
||
}
|
||
|
||
// GetRecent возвращает последние N записей (в хронологическом порядке)
|
||
func GetRecent(limit int) []LogEntry {
|
||
if limit > bufferCap {
|
||
limit = bufferCap
|
||
}
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
result := make([]LogEntry, 0, limit)
|
||
// Буфер заполнен циклически, начинаем с bufferIdx-1 и идём назад
|
||
start := bufferIdx - 1
|
||
if start < 0 {
|
||
start = bufferCap - 1
|
||
}
|
||
for i := 0; i < limit; i++ {
|
||
idx := (start - i + bufferCap) % bufferCap
|
||
if buffer[idx].Time.IsZero() {
|
||
break
|
||
}
|
||
result = append([]LogEntry{buffer[idx]}, result...)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// Subscribe возвращает канал для получения новых записей (буферизированный)
|
||
func Subscribe() <-chan LogEntry {
|
||
ch := make(chan LogEntry, 100)
|
||
subMu.Lock()
|
||
subscribers = append(subscribers, ch)
|
||
subMu.Unlock()
|
||
return ch
|
||
}
|
||
|
||
// Unsubscribe удаляет канал из списка подписчиков
|
||
func Unsubscribe(ch <-chan LogEntry) {
|
||
subMu.Lock()
|
||
defer subMu.Unlock()
|
||
for i, c := range subscribers {
|
||
if c == ch {
|
||
subscribers = append(subscribers[:i], subscribers[i+1:]...)
|
||
close(c)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
func Debug(format string, v ...interface{}) {
|
||
addEntry(LevelDebug, format, v...)
|
||
} |