This commit is contained in:
Sky Johnson 2025-03-06 21:13:13 -06:00
parent 3d61501eb9
commit af8fd397ea
4 changed files with 468 additions and 4 deletions

5
.gitignore vendored
View File

@ -21,6 +21,7 @@
# Go workspace file # Go workspace file
go.work go.work
# Test directories # Test directories and files
config.lua
routes/ routes/
static/ static/

270
core/logger/logger.go Normal file
View File

@ -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()
}

172
core/logger/logger_test.go Normal file
View File

@ -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()
}

View File

@ -1,7 +1,28 @@
package main package main
import "fmt" import (
"git.sharkk.net/Sky/Moonshark/core/config"
"git.sharkk.net/Sky/Moonshark/core/logger"
)
func main() { 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)
} }