650 lines
16 KiB
Go
650 lines
16 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: {"DEBUG", colorCyan},
|
|
LevelInfo: {" INFO", colorBlue},
|
|
LevelWarning: {" WARN", colorYellow},
|
|
LevelError: {"ERROR", colorRed},
|
|
LevelServer: {" SYS", colorGreen},
|
|
LevelFatal: {"FATAL", colorPurple},
|
|
}
|
|
|
|
// Time format for log messages
|
|
const timeFormat = "15:04:05"
|
|
|
|
// Default rate limiting settings
|
|
const (
|
|
defaultMaxLogs = 1000 // Max logs per second before rate limiting
|
|
defaultRateLimitTime = 10 * time.Second // How long to pause during rate limiting
|
|
)
|
|
|
|
// 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
|
|
indentCache string // Cached indent string for continuations
|
|
indentSize int // Size of the indent for continuations
|
|
lastLevel int // Last log level used, for continuations
|
|
|
|
// Simple rate limiting
|
|
logCount atomic.Int64 // Number of logs in current window
|
|
logCountStart atomic.Int64 // Start time of current counting window
|
|
rateLimited atomic.Bool // Whether we're currently rate limited
|
|
rateLimitUntil atomic.Int64 // Timestamp when rate limiting ends
|
|
maxLogsPerSec int64 // Maximum logs per second before limiting
|
|
limitDuration time.Duration // How long to pause logging when rate limited
|
|
}
|
|
|
|
// 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,
|
|
maxLogsPerSec: defaultMaxLogs,
|
|
limitDuration: defaultRateLimitTime,
|
|
lastLevel: -1, // Initialize to invalid level
|
|
}
|
|
|
|
// Initialize counters
|
|
logger.resetCounters()
|
|
|
|
// Calculate the base indent size
|
|
logger.updateIndentCache()
|
|
|
|
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)
|
|
}
|
|
|
|
// resetCounters resets the rate limiting counters
|
|
func (l *Logger) resetCounters() {
|
|
l.logCount.Store(0)
|
|
l.logCountStart.Store(time.Now().Unix())
|
|
l.rateLimited.Store(false)
|
|
l.rateLimitUntil.Store(0)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// updateIndentCache recalculates and updates the indent cache
|
|
func (l *Logger) updateIndentCache() {
|
|
tagWidth := 7
|
|
|
|
if l.showTimestamp {
|
|
// Format: "15:04:05 DEBUG "
|
|
timeWidth := len(time.Now().Format(l.timeFormat))
|
|
l.indentSize = timeWidth + 1 + tagWidth + 1
|
|
} else {
|
|
// Format: "DEBUG "
|
|
l.indentSize = tagWidth + 1
|
|
}
|
|
|
|
l.indentCache = strings.Repeat(" ", l.indentSize)
|
|
}
|
|
|
|
// SetTimeFormat changes the time format string and updates the indent cache
|
|
func (l *Logger) SetTimeFormat(format string) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
l.timeFormat = format
|
|
l.updateIndentCache()
|
|
}
|
|
|
|
// EnableTimestamp enables timestamp display
|
|
func (l *Logger) EnableTimestamp() {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
l.showTimestamp = true
|
|
l.updateIndentCache()
|
|
}
|
|
|
|
// DisableTimestamp disables timestamp display
|
|
func (l *Logger) DisableTimestamp() {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
l.showTimestamp = false
|
|
l.updateIndentCache()
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
|
|
// writeMessage writes a formatted log message directly to the writer
|
|
func (l *Logger) writeMessage(level int, message string, rawMode bool, continuation bool) {
|
|
var logLine string
|
|
|
|
if rawMode {
|
|
// Raw mode - message is already formatted, just append newline
|
|
logLine = message + "\n"
|
|
} else if continuation {
|
|
// Continuation format - just indent and message
|
|
if l.useColors {
|
|
// For colored output, use the color of the last level
|
|
props := levelProps[l.lastLevel]
|
|
logLine = fmt.Sprintf("%s%s%s\n",
|
|
l.indentCache, props.color, message+colorReset)
|
|
} else {
|
|
logLine = fmt.Sprintf("%s%s\n", l.indentCache, message)
|
|
}
|
|
} else {
|
|
// Standard format with level tag and optional timestamp
|
|
props := levelProps[level]
|
|
|
|
if l.showTimestamp {
|
|
now := time.Now().Format(l.timeFormat)
|
|
|
|
if l.useColors {
|
|
logLine = fmt.Sprintf("%s%s%s %s[%s]%s %s\n",
|
|
colorGray, now, colorReset, props.color, props.tag, colorReset, message)
|
|
} else {
|
|
logLine = fmt.Sprintf("%s [%s] %s\n",
|
|
now, props.tag, message)
|
|
}
|
|
} else {
|
|
// No timestamp, just level tag and message
|
|
if l.useColors {
|
|
logLine = fmt.Sprintf("%s[%s]%s %s\n",
|
|
props.color, props.tag, colorReset, message)
|
|
} else {
|
|
logLine = fmt.Sprintf("[%s] %s\n",
|
|
props.tag, message)
|
|
}
|
|
}
|
|
|
|
// Store the level for continuations
|
|
l.lastLevel = level
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
|
|
// checkRateLimit checks if we should rate limit logging
|
|
// Returns true if the message should be logged, false if it should be dropped
|
|
func (l *Logger) checkRateLimit(level int) bool {
|
|
// High priority messages are never rate limited
|
|
if level >= LevelWarning {
|
|
return true
|
|
}
|
|
|
|
// Check if we're currently in a rate-limited period
|
|
if l.rateLimited.Load() {
|
|
now := time.Now().Unix()
|
|
limitUntil := l.rateLimitUntil.Load()
|
|
|
|
if now >= limitUntil {
|
|
// Rate limiting period is over
|
|
l.rateLimited.Store(false)
|
|
l.resetCounters()
|
|
} else {
|
|
// Still in rate limiting period, drop the message
|
|
return false
|
|
}
|
|
}
|
|
|
|
// If not rate limited, check if we should start rate limiting
|
|
count := l.logCount.Add(1)
|
|
|
|
// Check if we need to reset the counter for a new second
|
|
now := time.Now().Unix()
|
|
start := l.logCountStart.Load()
|
|
if now > start {
|
|
// New second, reset counter
|
|
l.logCount.Store(1) // Count this message
|
|
l.logCountStart.Store(now)
|
|
return true
|
|
}
|
|
|
|
// Check if we've exceeded our threshold
|
|
if count > l.maxLogsPerSec {
|
|
// Start rate limiting
|
|
l.rateLimited.Store(true)
|
|
l.rateLimitUntil.Store(now + int64(l.limitDuration.Seconds()))
|
|
|
|
// Log a warning about rate limiting
|
|
l.writeMessage(LevelServer,
|
|
fmt.Sprintf("Rate limiting logger temporarily due to high demand (%d logs/sec exceeded)", count),
|
|
false, false)
|
|
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Check rate limiting - always log high priority messages
|
|
if !l.checkRateLimit(level) {
|
|
return
|
|
}
|
|
|
|
// Format message
|
|
var message string
|
|
if len(args) > 0 {
|
|
message = fmt.Sprintf(format, args...)
|
|
} else {
|
|
message = format
|
|
}
|
|
|
|
l.writeMessage(level, message, false, false)
|
|
|
|
// Exit on fatal errors
|
|
if level == LevelFatal {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// continuation handles continuation log messages (messages that continue from a previous log)
|
|
func (l *Logger) continuation(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
|
|
}
|
|
|
|
// Check rate limiting
|
|
if !l.checkRateLimit(level) {
|
|
return
|
|
}
|
|
|
|
// Format message
|
|
var message string
|
|
if len(args) > 0 {
|
|
message = fmt.Sprintf(format, args...)
|
|
} else {
|
|
message = format
|
|
}
|
|
|
|
// Use the continuation format
|
|
l.writeMessage(level, message, false, true)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Check rate limiting
|
|
if !l.checkRateLimit(LevelInfo) {
|
|
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 = removeAnsiColors(message)
|
|
}
|
|
|
|
l.writeMessage(LevelInfo, message, true, false)
|
|
}
|
|
|
|
// 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...)
|
|
}
|
|
|
|
// DebugCont logs a debug message as a continuation of the previous log
|
|
func (l *Logger) DebugCont(format string, args ...any) {
|
|
l.continuation(LevelDebug, format, args...)
|
|
}
|
|
|
|
// Info logs an informational message
|
|
func (l *Logger) Info(format string, args ...any) {
|
|
l.log(LevelInfo, format, args...)
|
|
}
|
|
|
|
// InfoCont logs an informational message as a continuation of the previous log
|
|
func (l *Logger) InfoCont(format string, args ...any) {
|
|
l.continuation(LevelInfo, format, args...)
|
|
}
|
|
|
|
// Warning logs a warning message
|
|
func (l *Logger) Warning(format string, args ...any) {
|
|
l.log(LevelWarning, format, args...)
|
|
}
|
|
|
|
// WarningCont logs a warning message as a continuation of the previous log
|
|
func (l *Logger) WarningCont(format string, args ...any) {
|
|
l.continuation(LevelWarning, format, args...)
|
|
}
|
|
|
|
// Error logs an error message
|
|
func (l *Logger) Error(format string, args ...any) {
|
|
l.log(LevelError, format, args...)
|
|
}
|
|
|
|
// ErrorCont logs an error message as a continuation of the previous log
|
|
func (l *Logger) ErrorCont(format string, args ...any) {
|
|
l.continuation(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()
|
|
}
|
|
|
|
// FatalCont logs a fatal error message as a continuation of the previous log and exits
|
|
func (l *Logger) FatalCont(format string, args ...any) {
|
|
l.continuation(LevelFatal, format, args...)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Server logs a server message
|
|
func (l *Logger) Server(format string, args ...any) {
|
|
l.log(LevelServer, format, args...)
|
|
}
|
|
|
|
// ServerCont logs a server message as a continuation of the previous log
|
|
func (l *Logger) ServerCont(format string, args ...any) {
|
|
l.continuation(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...)
|
|
}
|
|
|
|
// DebugCont logs a debug message as a continuation of the previous log to the global logger
|
|
func DebugCont(format string, args ...any) {
|
|
GetLogger().DebugCont(format, args...)
|
|
}
|
|
|
|
// Info logs an informational message to the global logger
|
|
func Info(format string, args ...any) {
|
|
GetLogger().Info(format, args...)
|
|
}
|
|
|
|
// InfoCont logs an informational message as a continuation of the previous log to the global logger
|
|
func InfoCont(format string, args ...any) {
|
|
GetLogger().InfoCont(format, args...)
|
|
}
|
|
|
|
// Warning logs a warning message to the global logger
|
|
func Warning(format string, args ...any) {
|
|
GetLogger().Warning(format, args...)
|
|
}
|
|
|
|
// WarningCont logs a warning message as a continuation of the previous log to the global logger
|
|
func WarningCont(format string, args ...any) {
|
|
GetLogger().WarningCont(format, args...)
|
|
}
|
|
|
|
// Error logs an error message to the global logger
|
|
func Error(format string, args ...any) {
|
|
GetLogger().Error(format, args...)
|
|
}
|
|
|
|
// ErrorCont logs an error message as a continuation of the previous log to the global logger
|
|
func ErrorCont(format string, args ...any) {
|
|
GetLogger().ErrorCont(format, args...)
|
|
}
|
|
|
|
// Fatal logs a fatal error message to the global logger and exits
|
|
func Fatal(format string, args ...any) {
|
|
GetLogger().Fatal(format, args...)
|
|
}
|
|
|
|
// FatalCont logs a fatal error message as a continuation of the previous log to the global logger and exits
|
|
func FatalCont(format string, args ...any) {
|
|
GetLogger().FatalCont(format, args...)
|
|
}
|
|
|
|
// Server logs a server message to the global logger
|
|
func Server(format string, args ...any) {
|
|
GetLogger().Server(format, args...)
|
|
}
|
|
|
|
// ServerCont logs a server message as a continuation of the previous log to the global logger
|
|
func ServerCont(format string, args ...any) {
|
|
GetLogger().ServerCont(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 {
|
|
spacer = fmt.Sprintf("%s%s%s %s%s%s %s\n",
|
|
colorGray, strings.Repeat("-", timeWidth), colorReset,
|
|
colorCyan, tagSpacer, colorReset, 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 {
|
|
spacer = fmt.Sprintf("%s%s%s %s\n",
|
|
colorCyan, tagSpacer, colorReset, 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()
|
|
}
|