This commit is contained in:
Sky Johnson 2025-03-07 07:25:01 -06:00
parent c48ab8c433
commit 75a307551c
5 changed files with 412 additions and 38 deletions

44
core/http/httplogger.go Normal file
View File

@ -0,0 +1,44 @@
package http
import (
"net/http"
"time"
"git.sharkk.net/Sky/Moonshark/core/logger"
)
// StatusColors for different status code ranges
const (
colorGreen = "\033[32m" // 2xx - Success
colorCyan = "\033[36m" // 3xx - Redirection
colorYellow = "\033[33m" // 4xx - Client Errors
colorRed = "\033[31m" // 5xx - Server Errors
colorReset = "\033[0m" // Reset color
)
// LogRequest logs an HTTP request with custom formatting
func LogRequest(log *logger.Logger, statusCode int, r *http.Request, duration time.Duration) {
statusColor := getStatusColor(statusCode)
// Use the logger's raw message writer to bypass the standard format
log.LogRaw("%s [%s%d%s] %s %s (%v)",
time.Now().Format(log.TimeFormat()),
statusColor, statusCode, colorReset,
r.Method, r.URL.Path, duration)
}
// getStatusColor returns the ANSI color code for a status code
func getStatusColor(code int) string {
switch {
case code >= 200 && code < 300:
return colorGreen
case code >= 300 && code < 400:
return colorCyan
case code >= 400 && code < 500:
return colorYellow
case code >= 500:
return colorRed
default:
return ""
}
}

184
core/http/server.go Normal file
View File

@ -0,0 +1,184 @@
package http
import (
"context"
"encoding/json"
"net/http"
"time"
"git.sharkk.net/Sky/Moonshark/core/logger"
"git.sharkk.net/Sky/Moonshark/core/routers"
"git.sharkk.net/Sky/Moonshark/core/workers"
)
// Server handles HTTP requests using Lua and static file routers
type Server struct {
luaRouter *routers.LuaRouter
staticRouter *routers.StaticRouter
workerPool *workers.Pool
logger *logger.Logger
httpServer *http.Server
}
// New creates a new HTTP server
func New(luaRouter *routers.LuaRouter, staticRouter *routers.StaticRouter, pool *workers.Pool, log *logger.Logger) *Server {
server := &Server{
luaRouter: luaRouter,
staticRouter: staticRouter,
workerPool: pool,
logger: log,
httpServer: &http.Server{},
}
server.httpServer.Handler = server
return server
}
// ListenAndServe starts the server on the given address
func (s *Server) ListenAndServe(addr string) error {
s.httpServer.Addr = addr
s.logger.Info("Server starting on %s", addr)
return s.httpServer.ListenAndServe()
}
// Shutdown gracefully shuts down the server
func (s *Server) Shutdown(ctx context.Context) error {
s.logger.Info("Server shutting down...")
return s.httpServer.Shutdown(ctx)
}
// ServeHTTP handles HTTP requests
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap the ResponseWriter to capture status code
wrappedWriter := newStatusCaptureWriter(w)
// Process the request
s.handleRequest(wrappedWriter, r)
// Calculate request duration
duration := time.Since(start)
// Get the status code
statusCode := wrappedWriter.StatusCode()
// Log the request with our custom format
LogRequest(s.logger, statusCode, r, duration)
}
// handleRequest processes the actual request
func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
s.logger.Debug("Processing request %s %s", r.Method, r.URL.Path)
// Try Lua routes first
params := &routers.Params{}
if bytecode, found := s.luaRouter.GetBytecode(r.Method, r.URL.Path, params); found {
s.logger.Debug("Found Lua route match for %s %s with %d params", r.Method, r.URL.Path, params.Count)
s.handleLuaRoute(w, r, bytecode, params)
return
}
// Then try static files
if filePath, found := s.staticRouter.Match(r.URL.Path); found {
http.ServeFile(w, r, filePath)
return
}
// No route found
http.NotFound(w, r)
}
// handleLuaRoute executes a Lua route
func (s *Server) handleLuaRoute(w http.ResponseWriter, r *http.Request, bytecode []byte, params *routers.Params) {
ctx := workers.NewContext()
// Log bytecode size
s.logger.Debug("Executing Lua route with %d bytes of bytecode", len(bytecode))
// Add request info directly to context
ctx.Set("method", r.Method)
ctx.Set("path", r.URL.Path)
ctx.Set("host", r.Host)
ctx.Set("headers", makeHeaderMap(r.Header))
// Add URL parameters
if params.Count > 0 {
paramMap := make(map[string]any, params.Count)
for i := 0; i < params.Count; i++ {
paramMap[params.Keys[i]] = params.Values[i]
}
ctx.Set("params", paramMap)
}
// Add query parameters
if queryParams := QueryToLua(r); queryParams != nil {
ctx.Set("query", queryParams)
}
// Add form data
if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodPatch {
if formData, err := ParseForm(r); err == nil && len(formData) > 0 {
ctx.Set("form", formData)
}
}
// Execute Lua script
result, err := s.workerPool.Submit(bytecode, ctx)
if err != nil {
s.logger.Error("Error executing Lua route: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
writeResponse(w, result, s.logger)
}
// makeHeaderMap converts HTTP headers to a map
func makeHeaderMap(header http.Header) map[string]any {
result := make(map[string]any, len(header))
for name, values := range header {
if len(values) == 1 {
result[name] = values[0]
} else {
result[name] = values
}
}
return result
}
// writeResponse writes the Lua result to the HTTP response
func writeResponse(w http.ResponseWriter, result any, log *logger.Logger) {
if result == nil {
w.WriteHeader(http.StatusNoContent)
return
}
switch res := result.(type) {
case string:
// String result
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(res))
case map[string]any:
// Table result - convert to JSON
w.Header().Set("Content-Type", "application/json")
data, err := json.Marshal(res)
if err != nil {
log.Error("Failed to marshal response: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Write(data)
default:
// Other result types - convert to JSON
w.Header().Set("Content-Type", "application/json")
data, err := json.Marshal(result)
if err != nil {
log.Error("Failed to marshal response: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Write(data)
}
}

33
core/http/status.go Normal file
View File

@ -0,0 +1,33 @@
package http
import (
"net/http"
)
// statusCaptureWriter is a ResponseWriter that captures the status code
type statusCaptureWriter struct {
http.ResponseWriter
statusCode int
}
// WriteHeader captures the status code and passes it to the wrapped ResponseWriter
func (w *statusCaptureWriter) WriteHeader(code int) {
w.statusCode = code
w.ResponseWriter.WriteHeader(code)
}
// StatusCode returns the captured status code
func (w *statusCaptureWriter) StatusCode() int {
if w.statusCode == 0 {
return http.StatusOK // Default to 200 if not explicitly set
}
return w.statusCode
}
// newStatusCaptureWriter creates a new statusCaptureWriter
func newStatusCaptureWriter(w http.ResponseWriter) *statusCaptureWriter {
return &statusCaptureWriter{
ResponseWriter: w,
statusCode: 0,
}
}

View File

@ -48,6 +48,7 @@ const timeFormat = "15:04:05"
type logMessage struct {
level int
message string
rawMode bool // Indicates if raw formatting should be used
}
// Logger handles logging operations
@ -84,11 +85,11 @@ func (l *Logger) SetOutput(w io.Writer) {
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
}
}
// TimeFormat returns the current time format
func (l *Logger) TimeFormat() string {
return l.timeFormat
}
// SetTimeFormat changes the time format string
@ -139,10 +140,16 @@ func (l *Logger) processLogs() {
// writeMessage writes a formatted log message
func (l *Logger) writeMessage(msg logMessage) {
var logLine string
if msg.rawMode {
// Raw mode - message is already formatted, just append newline
logLine = msg.message + "\n"
} else {
// Standard format with timestamp, level tag, and message
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)
@ -150,6 +157,7 @@ func (l *Logger) writeMessage(msg logMessage) {
logLine = fmt.Sprintf("%s [%s] %s\n",
now, props.tag, msg.message)
}
}
// Synchronize writing
l.mu.Lock()
@ -165,7 +173,7 @@ func (l *Logger) writeMessage(msg logMessage) {
}
// log sends a message to the logger goroutine
func (l *Logger) log(level int, format string, args ...interface{}) {
func (l *Logger) log(level int, format string, args ...any) {
if level < l.level {
return
}
@ -179,11 +187,11 @@ func (l *Logger) log(level int, format string, args ...interface{}) {
// Don't block if channel is full
select {
case l.messages <- logMessage{level: level, message: message}:
case l.messages <- logMessage{level: level, message: message, rawMode: false}:
// Message sent
default:
// Channel full, write directly
l.writeMessage(logMessage{level: level, message: message})
l.writeMessage(logMessage{level: level, message: message, rawMode: false})
}
// Exit on fatal errors
@ -193,28 +201,83 @@ func (l *Logger) log(level int, format string, args ...interface{}) {
}
}
// 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
// Simple approach to strip common ANSI codes
message = removeAnsiColors(message)
}
// Don't block if channel is full
select {
case l.messages <- logMessage{level: LevelInfo, message: message, rawMode: true}:
// Message sent
default:
// Channel full, write directly
l.writeMessage(logMessage{level: LevelInfo, message: message, rawMode: true})
}
}
// 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 ...interface{}) {
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 ...interface{}) {
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 ...interface{}) {
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 ...interface{}) {
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 ...interface{}) {
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()
}
@ -230,30 +293,35 @@ func (l *Logger) Close() {
var defaultLogger = New(LevelInfo, true)
// Debug logs a debug message to the default logger
func Debug(format string, args ...interface{}) {
func Debug(format string, args ...any) {
defaultLogger.Debug(format, args...)
}
// Info logs an informational message to the default logger
func Info(format string, args ...interface{}) {
func Info(format string, args ...any) {
defaultLogger.Info(format, args...)
}
// Warning logs a warning message to the default logger
func Warning(format string, args ...interface{}) {
func Warning(format string, args ...any) {
defaultLogger.Warning(format, args...)
}
// Error logs an error message to the default logger
func Error(format string, args ...interface{}) {
func Error(format string, args ...any) {
defaultLogger.Error(format, args...)
}
// Fatal logs a fatal error message to the default logger and exits
func Fatal(format string, args ...interface{}) {
func Fatal(format string, args ...any) {
defaultLogger.Fatal(format, args...)
}
// LogRaw logs a raw message to the default logger
func LogRaw(format string, args ...any) {
defaultLogger.LogRaw(format, args...)
}
// SetLevel changes the minimum log level of the default logger
func SetLevel(level int) {
defaultLogger.SetLevel(level)

View File

@ -1,15 +1,24 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"git.sharkk.net/Sky/Moonshark/core/config"
"git.sharkk.net/Sky/Moonshark/core/http"
"git.sharkk.net/Sky/Moonshark/core/logger"
"git.sharkk.net/Sky/Moonshark/core/routers"
"git.sharkk.net/Sky/Moonshark/core/utils"
"git.sharkk.net/Sky/Moonshark/core/workers"
)
func main() {
// Initialize logger
log := logger.New(logger.LevelInfo, true)
log := logger.New(logger.LevelDebug, true)
defer log.Close()
log.Info("Starting Moonshark server")
@ -29,32 +38,68 @@ func main() {
routesDir := cfg.GetString("routes_dir", "./routes")
staticDir := cfg.GetString("static_dir", "./static")
// Ensure the Lua routes directory exists
err = utils.EnsureDir(routesDir)
if err != nil {
log.Fatal("Routes directory doesn't exist, and could not create it. %v", err)
// Get worker pool size from config or use default
workerPoolSize := cfg.GetInt("worker_pool_size", 4)
// Ensure directories exist
if err = utils.EnsureDir(routesDir); err != nil {
log.Fatal("Routes directory doesn't exist, and could not create it: %v", err)
}
if err = utils.EnsureDir(staticDir); err != nil {
log.Fatal("Static directory doesn't exist, and could not create it: %v", err)
}
// Ensure the static directory exists
err = utils.EnsureDir(staticDir)
// Initialize worker pool
pool, err := workers.NewPool(workerPoolSize)
if err != nil {
log.Fatal("Static directory doesn't exist, and could not create it. %v", err)
log.Fatal("Failed to initialize worker pool: %v", err)
}
log.Info("Worker pool initialized with %d workers", workerPoolSize)
defer pool.Shutdown()
// Initialize Lua router for dynamic routes
_, err = routers.NewLuaRouter(routesDir)
luaRouter, err := routers.NewLuaRouter(routesDir)
if err != nil {
log.Fatal("Failed to initialize Lua router: %v", err)
}
log.Info("Lua router initialized with routes from %s", routesDir)
// Initialize static file router
_, err = routers.NewStaticRouter(staticDir)
staticRouter, err := routers.NewStaticRouter(staticDir)
if err != nil {
log.Fatal("Failed to initialize static router: %v", err)
}
log.Info("Static router initialized with files from %s", staticDir)
// Output the port number
log.Info("Moonshark server listening on port %d", port)
// Create HTTP server
server := http.New(luaRouter, staticRouter, pool, log)
// Handle graceful shutdown
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
// Start server in a goroutine
go func() {
addr := fmt.Sprintf(":%d", port)
log.Info("Server listening on http://localhost%s", addr)
if err := server.ListenAndServe(addr); err != nil {
if err.Error() != "http: Server closed" {
log.Error("Server error: %v", err)
}
}
}()
// Wait for interrupt signal
<-stop
log.Info("Shutdown signal received")
// Gracefully shut down the server
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Error("Server shutdown error: %v", err)
}
log.Info("Server stopped")
}