server 1
This commit is contained in:
parent
c48ab8c433
commit
75a307551c
44
core/http/httplogger.go
Normal file
44
core/http/httplogger.go
Normal 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
184
core/http/server.go
Normal 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
33
core/http/status.go
Normal 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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
69
moonshark.go
69
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")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user