Moonshark/core/logger/logger.go
2025-03-07 07:25:01 -06:00

339 lines
7.0 KiB
Go

package logger
import (
"fmt"
"io"
"os"
"sync"
"time"
)
// ANSI color codes
const (
colorReset = "\033[0m"
colorRed = "\033[31m"
colorGreen = "\033[32m"
colorYellow = "\033[33m"
colorBlue = "\033[34m"
colorPurple = "\033[35m"
colorCyan = "\033[36m"
colorWhite = "\033[37m"
)
// Log levels
const (
LevelDebug = iota
LevelInfo
LevelWarning
LevelError
LevelFatal
)
// Level names and colors
var levelProps = map[int]struct {
tag string
color string
}{
LevelDebug: {"DBG", colorCyan},
LevelInfo: {"INF", colorBlue},
LevelWarning: {"WRN", colorYellow},
LevelError: {"ERR", colorRed},
LevelFatal: {"FTL", colorPurple},
}
// Time format for log messages
const timeFormat = "15:04:05"
// logMessage represents a message to be logged
type logMessage struct {
level int
message string
rawMode bool // Indicates if raw formatting should be used
}
// Logger handles logging operations
type Logger struct {
writer io.Writer
messages chan logMessage
wg sync.WaitGroup
level int
useColors bool
done chan struct{}
timeFormat string
mu sync.Mutex // Mutex for thread-safe writing
}
// New creates a new logger
func New(minLevel int, useColors bool) *Logger {
l := &Logger{
writer: os.Stdout,
messages: make(chan logMessage, 100), // Buffer 100 messages
level: minLevel,
useColors: useColors,
done: make(chan struct{}),
timeFormat: timeFormat,
}
l.wg.Add(1)
go l.processLogs()
return l
}
// SetOutput changes the output destination
func (l *Logger) SetOutput(w io.Writer) {
l.mu.Lock()
defer l.mu.Unlock()
l.writer = w
}
// TimeFormat returns the current time format
func (l *Logger) TimeFormat() string {
return l.timeFormat
}
// SetTimeFormat changes the time format string
func (l *Logger) SetTimeFormat(format string) {
l.timeFormat = format
}
// SetLevel changes the minimum log level
func (l *Logger) SetLevel(level int) {
l.level = level
}
// EnableColors enables ANSI color codes in the output
func (l *Logger) EnableColors() {
l.useColors = true
}
// DisableColors disables ANSI color codes in the output
func (l *Logger) DisableColors() {
l.useColors = false
}
// processLogs processes incoming log messages
func (l *Logger) processLogs() {
defer l.wg.Done()
for {
select {
case msg := <-l.messages:
if msg.level >= l.level {
l.writeMessage(msg)
}
case <-l.done:
// Process remaining messages
for {
select {
case msg := <-l.messages:
if msg.level >= l.level {
l.writeMessage(msg)
}
default:
return
}
}
}
}
}
// writeMessage writes a formatted log message
func (l *Logger) writeMessage(msg logMessage) {
var logLine string
if msg.rawMode {
// Raw mode - message is already formatted, just append newline
logLine = msg.message + "\n"
} else {
// Standard format with timestamp, level tag, and message
now := time.Now().Format(l.timeFormat)
props := levelProps[msg.level]
if l.useColors {
logLine = fmt.Sprintf("%s %s[%s]%s %s\n",
now, props.color, props.tag, colorReset, msg.message)
} else {
logLine = fmt.Sprintf("%s [%s] %s\n",
now, props.tag, msg.message)
}
}
// Synchronize writing
l.mu.Lock()
_, _ = fmt.Fprint(l.writer, logLine)
l.mu.Unlock()
// Auto-flush for fatal errors
if msg.level == LevelFatal {
if f, ok := l.writer.(*os.File); ok {
_ = f.Sync()
}
}
}
// log sends a message to the logger goroutine
func (l *Logger) log(level int, format string, args ...any) {
if level < l.level {
return
}
var message string
if len(args) > 0 {
message = fmt.Sprintf(format, args...)
} else {
message = format
}
// Don't block if channel is full
select {
case l.messages <- logMessage{level: level, message: message, rawMode: false}:
// Message sent
default:
// Channel full, write directly
l.writeMessage(logMessage{level: level, message: message, rawMode: false})
}
// Exit on fatal errors
if level == LevelFatal {
l.Close()
os.Exit(1)
}
}
// LogRaw logs a message with raw formatting, bypassing the standard format
func (l *Logger) LogRaw(format string, args ...any) {
// Use info level for filtering
if LevelInfo < l.level {
return
}
var message string
if len(args) > 0 {
message = fmt.Sprintf(format, args...)
} else {
message = format
}
// Don't apply colors if disabled
if !l.useColors {
// Strip ANSI color codes if colors are disabled
// Simple approach to strip common ANSI codes
message = removeAnsiColors(message)
}
// Don't block if channel is full
select {
case l.messages <- logMessage{level: LevelInfo, message: message, rawMode: true}:
// Message sent
default:
// Channel full, write directly
l.writeMessage(logMessage{level: LevelInfo, message: message, rawMode: true})
}
}
// Simple helper to remove ANSI color codes
func removeAnsiColors(s string) string {
result := ""
inEscape := false
for _, c := range s {
if inEscape {
if c == 'm' {
inEscape = false
}
continue
}
if c == '\033' {
inEscape = true
continue
}
result += string(c)
}
return result
}
// Debug logs a debug message
func (l *Logger) Debug(format string, args ...any) {
l.log(LevelDebug, format, args...)
}
// Info logs an informational message
func (l *Logger) Info(format string, args ...any) {
l.log(LevelInfo, format, args...)
}
// Warning logs a warning message
func (l *Logger) Warning(format string, args ...any) {
l.log(LevelWarning, format, args...)
}
// Error logs an error message
func (l *Logger) Error(format string, args ...any) {
l.log(LevelError, format, args...)
}
// Fatal logs a fatal error message and exits
func (l *Logger) Fatal(format string, args ...any) {
l.log(LevelFatal, format, args...)
// No need for os.Exit here as it's handled in log()
}
// Close shuts down the logger goroutine
func (l *Logger) Close() {
close(l.done)
l.wg.Wait()
close(l.messages)
}
// Default global logger
var defaultLogger = New(LevelInfo, true)
// Debug logs a debug message to the default logger
func Debug(format string, args ...any) {
defaultLogger.Debug(format, args...)
}
// Info logs an informational message to the default logger
func Info(format string, args ...any) {
defaultLogger.Info(format, args...)
}
// Warning logs a warning message to the default logger
func Warning(format string, args ...any) {
defaultLogger.Warning(format, args...)
}
// Error logs an error message to the default logger
func Error(format string, args ...any) {
defaultLogger.Error(format, args...)
}
// Fatal logs a fatal error message to the default logger and exits
func Fatal(format string, args ...any) {
defaultLogger.Fatal(format, args...)
}
// LogRaw logs a raw message to the default logger
func LogRaw(format string, args ...any) {
defaultLogger.LogRaw(format, args...)
}
// SetLevel changes the minimum log level of the default logger
func SetLevel(level int) {
defaultLogger.SetLevel(level)
}
// SetOutput changes the output destination of the default logger
func SetOutput(w io.Writer) {
defaultLogger.SetOutput(w)
}
// Close shuts down the default logger
func Close() {
defaultLogger.Close()
}