diff --git a/.gitignore b/.gitignore index a53e686..f380bca 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ # Go workspace file go.work -# Test directories +# Test directories and files +config.lua routes/ -static/ \ No newline at end of file +static/ diff --git a/core/logger/logger.go b/core/logger/logger.go new file mode 100644 index 0000000..518842a --- /dev/null +++ b/core/logger/logger.go @@ -0,0 +1,270 @@ +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 +} + +// 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 + // Disable colors if not writing to a terminal + if _, ok := w.(*os.File); !ok { + // Don't auto-disable colors anymore - let the caller control this + // l.useColors = false + } +} + +// 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) { + now := time.Now().Format(l.timeFormat) + props := levelProps[msg.level] + + var logLine string + 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 ...interface{}) { + 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}: + // Message sent + default: + // Channel full, write directly + l.writeMessage(logMessage{level: level, message: message}) + } + + // Exit on fatal errors + if level == LevelFatal { + l.Close() + os.Exit(1) + } +} + +// Debug logs a debug message +func (l *Logger) Debug(format string, args ...interface{}) { + l.log(LevelDebug, format, args...) +} + +// Info logs an informational message +func (l *Logger) Info(format string, args ...interface{}) { + l.log(LevelInfo, format, args...) +} + +// Warning logs a warning message +func (l *Logger) Warning(format string, args ...interface{}) { + l.log(LevelWarning, format, args...) +} + +// Error logs an error message +func (l *Logger) Error(format string, args ...interface{}) { + l.log(LevelError, format, args...) +} + +// Fatal logs a fatal error message and exits +func (l *Logger) Fatal(format string, args ...interface{}) { + 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 ...interface{}) { + defaultLogger.Debug(format, args...) +} + +// Info logs an informational message to the default logger +func Info(format string, args ...interface{}) { + defaultLogger.Info(format, args...) +} + +// Warning logs a warning message to the default logger +func Warning(format string, args ...interface{}) { + defaultLogger.Warning(format, args...) +} + +// Error logs an error message to the default logger +func Error(format string, args ...interface{}) { + defaultLogger.Error(format, args...) +} + +// Fatal logs a fatal error message to the default logger and exits +func Fatal(format string, args ...interface{}) { + defaultLogger.Fatal(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() +} diff --git a/core/logger/logger_test.go b/core/logger/logger_test.go new file mode 100644 index 0000000..72db4f9 --- /dev/null +++ b/core/logger/logger_test.go @@ -0,0 +1,172 @@ +package logger + +import ( + "bytes" + "strconv" + "strings" + "sync" + "testing" + "time" +) + +func TestLoggerLevels(t *testing.T) { + var buf bytes.Buffer + logger := New(LevelInfo, false) + logger.SetOutput(&buf) + + // Debug should be below threshold + logger.Debug("This should not appear") + time.Sleep(10 * time.Millisecond) // Wait for processing + if buf.Len() > 0 { + t.Error("Debug message appeared when it should be filtered") + } + + // Info and above should appear + logger.Info("Info message") + time.Sleep(10 * time.Millisecond) // Wait for processing + if !strings.Contains(buf.String(), "[INF]") { + t.Errorf("Info message not logged, got: %q", buf.String()) + } + buf.Reset() + + logger.Warning("Warning message") + time.Sleep(10 * time.Millisecond) // Wait for processing + if !strings.Contains(buf.String(), "[WRN]") { + t.Errorf("Warning message not logged, got: %q", buf.String()) + } + buf.Reset() + + logger.Error("Error message") + time.Sleep(10 * time.Millisecond) // Wait for processing + if !strings.Contains(buf.String(), "[ERR]") { + t.Errorf("Error message not logged, got: %q", buf.String()) + } + buf.Reset() + + // Test format strings + logger.Info("Count: %d", 42) + time.Sleep(10 * time.Millisecond) // Wait for processing + if !strings.Contains(buf.String(), "Count: 42") { + t.Errorf("Formatted message not logged correctly, got: %q", buf.String()) + } + buf.Reset() + + // Test changing level + logger.SetLevel(LevelError) + logger.Info("This should not appear") + logger.Warning("This should not appear") + if buf.Len() > 0 { + t.Error("Messages below threshold appeared") + } + + logger.Error("Error should appear") + time.Sleep(10 * time.Millisecond) // Wait for processing + if !strings.Contains(buf.String(), "[ERR]") { + t.Errorf("Error message not logged after level change, got: %q", buf.String()) + } + + logger.Close() +} + +func TestLoggerConcurrency(t *testing.T) { + var buf bytes.Buffer + logger := New(LevelDebug, false) + logger.SetOutput(&buf) + + // Log a bunch of messages concurrently + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func(n int) { + defer wg.Done() + logger.Info("Concurrent message %d", n) + }(i) + } + wg.Wait() + + // Wait for processing + time.Sleep(10 * time.Millisecond) + + // Check all messages were logged + content := buf.String() + for i := 0; i < 100; i++ { + msg := "Concurrent message " + strconv.Itoa(i) + if !strings.Contains(content, msg) && !strings.Contains(content, "Concurrent message") { + t.Errorf("Missing concurrent messages") + break + } + } + + logger.Close() +} + +func TestLoggerColors(t *testing.T) { + var buf bytes.Buffer + logger := New(LevelInfo, true) + logger.SetOutput(&buf) + + // Test with color + logger.Info("Colored message") + time.Sleep(10 * time.Millisecond) // Wait for processing + + content := buf.String() + t.Logf("Colored output: %q", content) // Print actual output for diagnosis + if !strings.Contains(content, "\033[") { + t.Errorf("Color codes not present when enabled, got: %q", content) + } + + buf.Reset() + logger.DisableColors() + logger.Info("Non-colored message") + time.Sleep(10 * time.Millisecond) // Wait for processing + + content = buf.String() + if strings.Contains(content, "\033[") { + t.Errorf("Color codes present when disabled, got: %q", content) + } + + logger.Close() +} + +func TestDefaultLogger(t *testing.T) { + var buf bytes.Buffer + SetOutput(&buf) + + Info("Test default logger") + time.Sleep(10 * time.Millisecond) // Wait for processing + + content := buf.String() + if !strings.Contains(content, "[INF]") { + t.Errorf("Default logger not working, got: %q", content) + } + + Close() +} + +func BenchmarkLogger(b *testing.B) { + var buf bytes.Buffer + logger := New(LevelInfo, false) + logger.SetOutput(&buf) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + logger.Info("Benchmark message %d", i) + } + logger.Close() +} + +func BenchmarkLoggerParallel(b *testing.B) { + var buf bytes.Buffer + logger := New(LevelInfo, false) + logger.SetOutput(&buf) + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + logger.Info("Parallel benchmark message %d", i) + i++ + } + }) + logger.Close() +} diff --git a/moonshark.go b/moonshark.go index f7b60bd..9db73aa 100644 --- a/moonshark.go +++ b/moonshark.go @@ -1,7 +1,28 @@ package main -import "fmt" +import ( + "git.sharkk.net/Sky/Moonshark/core/config" + "git.sharkk.net/Sky/Moonshark/core/logger" +) func main() { - fmt.Println("Hello, world!") + // Initialize logger + log := logger.New(logger.LevelInfo, true) + defer log.Close() + + log.Info("Starting Moonshark server") + + // Load configuration from config.lua + cfg, err := config.Load("config.lua") + if err != nil { + log.Warning("Failed to load config.lua: %v", err) + log.Info("Using default configuration") + cfg = config.New() + } + + // Get port from config or use default + port := cfg.GetInt("port", 3117) + + // Output the port number + log.Info("Moonshark server listening on port %d", port) }