diff --git a/README.md b/README.md index 8a44ab0..3741584 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,31 @@ # Playground GO -A simple web server implementation in Go that demonstrates basic HTTP server concepts. +A modern web server implementation in Go that demonstrates production-ready HTTP server concepts. ## Overview -This project implements a basic HTTP server that: -- Listens on a specified address and port -- Handles HTTP requests +This project implements a robust HTTP server that: +- Listens on a configurable port +- Handles HTTP requests with a custom router - Processes requests concurrently using goroutines +- Implements middleware for logging and request processing +- Provides graceful shutdown capabilities +- Uses structured logging +- Follows clean architecture principles ## Project Structure ``` . -├── main.go # Application entry point -├── server/ -│ ├── server.go # Server implementation -│ └── handler.go # HTTP request handlers -├── go.mod # Go module definition -└── README.md # This file +├── main.go # Application entry point +├── handlers/ # HTTP request handlers +├── middleware/ # HTTP middleware components +├── logger/ # Structured logging implementation +├── internal/ # Internal packages and configuration +├── static/ # Static file serving +├── go.mod # Go module definition +├── .gitignore # Git ignore rules +└── README.md # This file ``` ## Prerequisites @@ -27,38 +34,49 @@ This project implements a basic HTTP server that: ## Usage -1. Import the server package in your Go code: - -```go -import "your-module-name/server" +1. Clone the repository: +```bash +git clone https://github.com/yourusername/playground-go.git +cd playground-go ``` -2. Create a new server instance: - -```go -server := server.NewServer(":8080") // Listen on port 8080 +2. Run the server: +```bash +go run main.go ``` -3. Start the server: - -```go -server.ListenAndServe() -``` +The server will start on the configured port (default: 8080). ## Features - Concurrent request handling using goroutines -- HTTP request processing -- Graceful error handling -- Simple and clean API design +- Graceful server shutdown +- Structured logging +- Middleware support for request processing +- Configuration management +- Clean architecture with separated concerns +- Static file serving +- Error handling and logging + +## Configuration + +The server can be configured through environment variables: +- `PORT`: The port number to listen on (default: 8080) +- Additional configuration options can be added in the `internal/config` package ## Error Handling -The server includes basic error handling for: -- Address binding errors -- Request processing errors -- HTTP response errors +The server includes comprehensive error handling for: +- Server startup and shutdown +- Request processing +- Configuration loading +- Graceful shutdown with timeout +- Structured error logging ## Contributing -Feel free to submit issues and enhancement requests! +Feel free to submit issues and enhancement requests! When contributing, please: + +1. Fork the repository +2. Create a new branch for your feature +3. Submit a pull request with a clear description of your changes diff --git a/handlers/routes.go b/handlers/routes.go new file mode 100644 index 0000000..5e99ba0 --- /dev/null +++ b/handlers/routes.go @@ -0,0 +1,30 @@ +package handlers + +import ( + "fmt" + "net/http" +) + +func RegisterRoutes() http.Handler { + mux := http.NewServeMux() + + mux.HandleFunc("/", serveIndex) + mux.HandleFunc("/hello", helloHandler) + mux.HandleFunc("/goodbye", goodbyeHandler) + + mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + + return mux +} + +func serveIndex(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "static/index.html") +} + +func helloHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "Hello, world!") +} + +func goodbyeHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "Goodbye, world!") +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..cbbe06d --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,28 @@ +package config + +import ( + "os" +) + +type Config struct { + Port string + StaticDir string + Environment string +} + +var AppConfig Config + +func Load() { + AppConfig = Config{ + Port: getEnv("PORT", "7878"), + StaticDir: getEnv("STATIC_DIR", "static"), + Environment: getEnv("ENVIRONMENT", "development"), + } +} + +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..0e41792 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,41 @@ +package logger + +import ( + "fmt" + "os" + "time" +) + +// ANSI colors +const ( + colorReset = "\033[0m" + colorRed = "\033[31m" + colorYellow = "\033[33m" + colorCyan = "\033[36m" + colorWhite = "\033[97m" +) + +// Log levels +const ( + LevelInfo = "INFO" + LevelWarn = "WARN" + LevelError = "ERROR" + LevelFatal = "FATAL" +) + +func logMessage(level, color, msg string, args ...any) { + timestamp := time.Now().Format("2006-01-02 15:04:05") + formatted := fmt.Sprintf(msg, args...) + output := fmt.Sprintf("[%s] %s[%s]%s %s\n", timestamp, color, level, colorReset, formatted) + fmt.Print(output) + + if level == LevelFatal { + os.Exit(1) + } +} + +// Public logging functions +func Info(msg string, args ...any) { logMessage(LevelInfo, colorCyan, msg, args...) } +func Warn(msg string, args ...any) { logMessage(LevelWarn, colorYellow, msg, args...) } +func Error(msg string, args ...any) { logMessage(LevelError, colorRed, msg, args...) } +func Fatal(msg string, args ...any) { logMessage(LevelFatal, colorRed, msg, args...) } diff --git a/main.go b/main.go index 7a8b17f..cf0bf4e 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,54 @@ package main -import "git.okseby.com/okseby/playground-go/server" +import ( + "context" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "git.okseby.com/okseby/playground-go/handlers" + "git.okseby.com/okseby/playground-go/internal/config" + "git.okseby.com/okseby/playground-go/logger" + "git.okseby.com/okseby/playground-go/middleware" +) func main() { - server := server.NewServer("0.0.0.0:7878") - server.ListenAndServe() + // Load config + config.Load() + + // Register routes + mux := handlers.RegisterRoutes() + handler := middleware.Logger(mux) + + server := &http.Server{ + Addr: ":" + config.AppConfig.Port, + Handler: handler, + } + + // Channel to listen for shutdown signals + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + + // Run the server in a goroutine + go func() { + logger.Info("Starting server on port %s...", config.AppConfig.Port) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logger.Fatal("Error starting server: %v", err) + } + }() + + // Wait for shutdown signal + <-quit + logger.Info("Shutting down...") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + logger.Fatal("Server forced to shutdown: %v", err) + } + + logger.Info("Server exited cleanly.") } diff --git a/middleware/logging.go b/middleware/logging.go new file mode 100644 index 0000000..c52aa85 --- /dev/null +++ b/middleware/logging.go @@ -0,0 +1,56 @@ +package middleware + +import ( + "net/http" + "time" + + "git.okseby.com/okseby/playground-go/logger" +) + +// ANSI color codes +const ( + ColorReset = "\033[0m" + ColorRed = "\033[31m" + ColorGreen = "\033[32m" + ColorYellow = "\033[33m" + ColorCyan = "\033[36m" + ColorGrey = "\033[90m" +) + +type statusRecorder struct { + http.ResponseWriter + status int +} + +func (r *statusRecorder) WriteHeader(code int) { + r.status = code + r.ResponseWriter.WriteHeader(code) +} + +func Logger(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + recorder := &statusRecorder{ResponseWriter: w, status: 200} + next.ServeHTTP(recorder, r) + + duration := time.Since(start) + + statusColor := ColorGreen + switch { + case recorder.status >= 500: + statusColor = ColorRed + case recorder.status >= 400: + statusColor = ColorRed + case recorder.status >= 300: + statusColor = ColorYellow + } + + logger.Info("%s%-7s%s %s%-30s%s %s%d%s [%s]", + ColorCyan, r.Method, ColorReset, + ColorGrey, r.URL.Path, ColorReset, + statusColor, recorder.status, ColorReset, + duration.Round(time.Millisecond), + ) + }) +} diff --git a/server/handler.go b/server/handler.go deleted file mode 100644 index f9f66be..0000000 --- a/server/handler.go +++ /dev/null @@ -1,30 +0,0 @@ -package server - -import ( - "bufio" - "fmt" - "io" - "net" -) - -func handleConnection(conn net.Conn) { - defer conn.Close() - - reader := bufio.NewReader(conn) - request, err := reader.ReadString('\n') - if err != nil { - if err != io.EOF { - fmt.Println("read request error:", err) - } - return - } - - fmt.Println("request:", request) - - response := "HTTP/1.1 200 OK\r\n" + - "Content-Length: 13\r\n" + - "Content-Type: text/plain\r\n\r\n" + - "Hello, world!" - - conn.Write([]byte(response)) -} diff --git a/server/server.go b/server/server.go deleted file mode 100644 index d14f16a..0000000 --- a/server/server.go +++ /dev/null @@ -1,33 +0,0 @@ -package server - -import ( - "fmt" - "net" -) - -type Server struct { - addr string -} - -func NewServer(addr string) *Server { - return &Server{addr: addr} -} - -func (s *Server) ListenAndServe() { - listener, err := net.Listen("tcp", s.addr) - if err != nil { - fmt.Println("Error binding to address:", err) - } - defer listener.Close() - - fmt.Printf("Server listening on %s\n", s.addr) - - for { - conn, err := listener.Accept() - if err != nil { - fmt.Println("Error accepting connection:", err) - continue - } - go handleConnection(conn) - } -} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..b89c5c4 --- /dev/null +++ b/static/index.html @@ -0,0 +1,56 @@ + + + + + + Cool Web Server + + + +
+
+

Welcome to My Web Server

+

A modern and efficient web server implementation

+
+
+ +
+
+
+

Fast & Efficient

+

Built with performance in mind, ensuring quick response times and optimal resource usage.

+
+
+

Modern Stack

+

Utilizing the latest web technologies and best practices for a robust server implementation.

+
+
+

Easy to Use

+

Simple configuration and straightforward setup process for hassle-free deployment.

+
+
+ +
+ Get Started +
+
+ + + + + + diff --git a/static/styles/index.css b/static/styles/index.css new file mode 100644 index 0000000..97d6daf --- /dev/null +++ b/static/styles/index.css @@ -0,0 +1,110 @@ +:root { + --primary-color: #2563eb; + --secondary-color: #1e40af; + --background-color: #f8fafc; + --text-color: #1e293b; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; + line-height: 1.6; + color: var(--text-color); + background-color: var(--background-color); +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +header { + text-align: center; + padding: 4rem 0; + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + color: white; + margin-bottom: 3rem; +} + +h1 { + font-size: 3rem; + margin-bottom: 1rem; + animation: fadeIn 1s ease-in; +} + +.subtitle { + font-size: 1.2rem; + opacity: 0.9; +} + +.features { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + margin-bottom: 3rem; +} + +.feature-card { + background: white; + padding: 2rem; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; +} + +.feature-card:hover { + transform: translateY(-5px); +} + +.feature-card h3 { + color: var(--primary-color); + margin-bottom: 1rem; +} + +.cta-button { + display: inline-block; + background-color: var(--primary-color); + color: white; + padding: 1rem 2rem; + border-radius: 5px; + text-decoration: none; + transition: background-color 0.3s ease; +} + +.cta-button:hover { + background-color: var(--secondary-color); +} + +footer { + text-align: center; + padding: 2rem; + margin-top: 3rem; + border-top: 1px solid #e2e8f0; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + h1 { + font-size: 2rem; + } +} \ No newline at end of file