diff --git a/core/http/httplogger.go b/core/http/httplogger.go new file mode 100644 index 0000000..0dee1e3 --- /dev/null +++ b/core/http/httplogger.go @@ -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 "" + } +} diff --git a/core/http/server.go b/core/http/server.go new file mode 100644 index 0000000..daab312 --- /dev/null +++ b/core/http/server.go @@ -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) + } +} diff --git a/core/http/status.go b/core/http/status.go new file mode 100644 index 0000000..9b96ba5 --- /dev/null +++ b/core/http/status.go @@ -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, + } +} diff --git a/core/logger/logger.go b/core/logger/logger.go index 518842a..ee0f9f2 100644 --- a/core/logger/logger.go +++ b/core/logger/logger.go @@ -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,16 +140,23 @@ func (l *Logger) processLogs() { // 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) + + if msg.rawMode { + // Raw mode - message is already formatted, just append newline + logLine = msg.message + "\n" } else { - logLine = fmt.Sprintf("%s [%s] %s\n", - now, props.tag, msg.message) + // Standard format with timestamp, level tag, and message + now := time.Now().Format(l.timeFormat) + props := levelProps[msg.level] + + 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 @@ -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) diff --git a/moonshark.go b/moonshark.go index fac0ef7..ce69bd1 100644 --- a/moonshark.go +++ b/moonshark.go @@ -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") }