logger
This commit is contained in:
parent
3d61501eb9
commit
af8fd397ea
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -21,6 +21,7 @@
|
|||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# Test directories
|
||||
# Test directories and files
|
||||
config.lua
|
||||
routes/
|
||||
static/
|
||||
static/
|
||||
|
|
270
core/logger/logger.go
Normal file
270
core/logger/logger.go
Normal 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
172
core/logger/logger_test.go
Normal 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()
|
||||
}
|
25
moonshark.go
25
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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user