2025-05-01 12:24:26 -05:00

435 lines
10 KiB
Go

package logger
import (
"fmt"
"io"
"os"
"strings"
"sync"
"sync/atomic"
"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"
colorGray = "\033[90m"
)
// Log levels
const (
LevelDebug = iota
LevelInfo
LevelWarning
LevelError
LevelServer
LevelFatal
)
// Level names and colors
var levelProps = map[int]struct {
tag string
color string
}{
LevelDebug: {"D", colorCyan},
LevelInfo: {"I", colorBlue},
LevelWarning: {"W", colorYellow},
LevelError: {"E", colorRed},
LevelServer: {"S", colorGreen},
LevelFatal: {"F", colorPurple},
}
// Time format for log messages
const timeFormat = "15:04:05"
// Single global logger instance with mutex for safe initialization
var (
globalLogger *Logger
globalLoggerOnce sync.Once
)
// Logger handles logging operations
type Logger struct {
writer io.Writer
level int
useColors bool
timeFormat string
showTimestamp bool // Whether to show timestamp
mu sync.Mutex // Mutex for thread-safe writing
debugMode atomic.Bool // Force debug logging regardless of level
}
// GetLogger returns the global logger instance, creating it if needed
func GetLogger() *Logger {
globalLoggerOnce.Do(func() {
globalLogger = newLogger(LevelInfo, true, true)
})
return globalLogger
}
// InitGlobalLogger initializes the global logger with custom settings
func InitGlobalLogger(minLevel int, useColors bool, showTimestamp bool) {
// Reset the global logger instance
globalLogger = newLogger(minLevel, useColors, showTimestamp)
}
// newLogger creates a new logger instance (internal use)
func newLogger(minLevel int, useColors bool, showTimestamp bool) *Logger {
logger := &Logger{
writer: os.Stdout,
level: minLevel,
useColors: useColors,
timeFormat: timeFormat,
showTimestamp: showTimestamp,
}
return logger
}
// New creates a new logger (deprecated - use GetLogger() instead)
func New(minLevel int, useColors bool, showTimestamp bool) *Logger {
return newLogger(minLevel, useColors, showTimestamp)
}
// 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.mu.Lock()
defer l.mu.Unlock()
l.timeFormat = format
}
// EnableTimestamp enables timestamp display
func (l *Logger) EnableTimestamp() {
l.showTimestamp = true
}
// DisableTimestamp disables timestamp display
func (l *Logger) DisableTimestamp() {
l.showTimestamp = false
}
// 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
}
// EnableDebug forces debug logs to be shown regardless of level
func (l *Logger) EnableDebug() {
l.debugMode.Store(true)
}
// DisableDebug stops forcing debug logs
func (l *Logger) DisableDebug() {
l.debugMode.Store(false)
}
// IsDebugEnabled returns true if debug mode is enabled
func (l *Logger) IsDebugEnabled() bool {
return l.debugMode.Load()
}
// applyColor applies color to text if colors are enabled
func (l *Logger) applyColor(text, color string) string {
if l.useColors {
return color + text + colorReset
}
return text
}
// stripAnsiColors removes ANSI color codes from a string
func stripAnsiColors(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
}
// writeMessage writes a formatted log message directly to the writer
func (l *Logger) writeMessage(level int, message string, rawMode bool) {
var logLine string
if rawMode {
// Raw mode - message is already formatted, just append newline
logLine = message + "\n"
} else {
// Standard format with level tag and optional timestamp
props := levelProps[level]
if l.showTimestamp {
now := time.Now().Format(l.timeFormat)
if l.useColors {
timestamp := l.applyColor(now, colorGray)
tag := l.applyColor("["+props.tag+"]", props.color)
logLine = fmt.Sprintf("%s %s %s\n", timestamp, tag, message)
} else {
logLine = fmt.Sprintf("%s [%s] %s\n", now, props.tag, message)
}
} else {
// No timestamp, just level tag and message
if l.useColors {
tag := l.applyColor("["+props.tag+"]", props.color)
logLine = fmt.Sprintf("%s %s\n", tag, message)
} else {
logLine = fmt.Sprintf("[%s] %s\n", props.tag, message)
}
}
}
// Synchronously write the log message
l.mu.Lock()
_, _ = fmt.Fprint(l.writer, logLine)
// For fatal errors, ensure we sync immediately
if level == LevelFatal {
if f, ok := l.writer.(*os.File); ok {
_ = f.Sync()
}
}
l.mu.Unlock()
}
// log handles the core logging logic with level filtering
func (l *Logger) log(level int, format string, args ...any) {
// Check if we should log this message
// Either level is high enough OR (it's a debug message AND debug mode is enabled)
if level < l.level && !(level == LevelDebug && l.debugMode.Load()) {
return
}
// Format message
var message string
if len(args) > 0 {
message = fmt.Sprintf(format, args...)
} else {
message = format
}
l.writeMessage(level, message, false)
// Exit on fatal errors
if level == LevelFatal {
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
message = stripAnsiColors(message)
}
l.writeMessage(LevelInfo, message, true)
}
// 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()
}
// Server logs a server message
func (l *Logger) Server(format string, args ...any) {
l.log(LevelServer, format, args...)
}
// Global helper functions that use the global logger
// Debug logs a debug message to the global logger
func Debug(format string, args ...any) {
GetLogger().Debug(format, args...)
}
// Info logs an informational message to the global logger
func Info(format string, args ...any) {
GetLogger().Info(format, args...)
}
// Warning logs a warning message to the global logger
func Warning(format string, args ...any) {
GetLogger().Warning(format, args...)
}
// Error logs an error message to the global logger
func Error(format string, args ...any) {
GetLogger().Error(format, args...)
}
// Fatal logs a fatal error message to the global logger and exits
func Fatal(format string, args ...any) {
GetLogger().Fatal(format, args...)
}
// Server logs a server message to the global logger
func Server(format string, args ...any) {
GetLogger().Server(format, args...)
}
// LogRaw logs a raw message to the global logger
func LogRaw(format string, args ...any) {
GetLogger().LogRaw(format, args...)
}
// SetLevel changes the minimum log level of the global logger
func SetLevel(level int) {
GetLogger().SetLevel(level)
}
// SetOutput changes the output destination of the global logger
func SetOutput(w io.Writer) {
GetLogger().SetOutput(w)
}
// TimeFormat returns the current time format of the global logger
func TimeFormat() string {
return GetLogger().TimeFormat()
}
// EnableDebug enables debug messages regardless of log level
func EnableDebug() {
GetLogger().EnableDebug()
}
// DisableDebug disables forced debug messages
func DisableDebug() {
GetLogger().DisableDebug()
}
// IsDebugEnabled returns true if debug mode is enabled
func IsDebugEnabled() bool {
return GetLogger().IsDebugEnabled()
}
// EnableTimestamp enables timestamp display
func EnableTimestamp() {
GetLogger().EnableTimestamp()
}
// DisableTimestamp disables timestamp display
func DisableTimestamp() {
GetLogger().DisableTimestamp()
}
// LogSpacer adds a horizontal line separator to the log output
func (l *Logger) LogSpacer() {
l.mu.Lock()
defer l.mu.Unlock()
// Calculate spacer width
tagWidth := 7 // Standard width of tag area "[DEBUG]"
var spacer string
if l.showTimestamp {
// Format: "15:04:05 [DEBUG] ----"
timeWidth := len(time.Now().Format(l.timeFormat))
tagSpacer := strings.Repeat("-", tagWidth)
restSpacer := strings.Repeat("-", 20) // Fixed width for the rest
if l.useColors {
timeStr := l.applyColor(strings.Repeat("-", timeWidth), colorGray)
tagStr := l.applyColor(tagSpacer, colorCyan)
spacer = fmt.Sprintf("%s %s %s\n", timeStr, tagStr, restSpacer)
} else {
spacer = fmt.Sprintf("%s %s %s\n",
strings.Repeat("-", timeWidth), tagSpacer, restSpacer)
}
} else {
// No timestamp: "[DEBUG] ----"
tagSpacer := strings.Repeat("-", tagWidth)
restSpacer := strings.Repeat("-", 20) // Fixed width for the rest
if l.useColors {
tagStr := l.applyColor(tagSpacer, colorCyan)
spacer = fmt.Sprintf("%s %s\n", tagStr, restSpacer)
} else {
spacer = fmt.Sprintf("%s %s\n", tagSpacer, restSpacer)
}
}
_, _ = fmt.Fprint(l.writer, spacer)
}
// LogSpacer adds a horizontal line separator to the global logger
func LogSpacer() {
GetLogger().LogSpacer()
}