diff --git a/cmd/main.go b/cmd/main.go index 18edb9d..bad9fe7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,8 +2,8 @@ package main import ( "context" - "fmt" "log" + "log/slog" "net/http" "os" "os/signal" @@ -17,10 +17,21 @@ import ( ) func main() { + + // @Logger + // ------------------------------------------------------------ + var handler slog.Handler + if os.Getenv("ENV") == "production" { + handler = slog.NewJSONHandler(os.Stdout, nil) + } else { + handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}) + } + logger := slog.New(handler) + ctx, ctxCancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer ctxCancel() - // DB init + // @DB // ------------------------------------------------------------ connStr := os.Getenv("DATABASE_URL") if connStr == "" { @@ -46,23 +57,23 @@ func main() { if err := pool.Ping(ctx); err != nil { log.Fatalf("Unable to reach database: %v", err) } - fmt.Println("Database connection established") + logger.Info("database connection established", "max_conns", poolConfig.MaxConns, "min_conns", poolConfig.MinConns) defer func() { pool.Close() - fmt.Println("Database pool closed") + logger.Info("database pool closed") }() // Create Store sqlc wrapper store := db.NewStore(pool) - // Server Init + // @Server // ------------------------------------------------------------ jwtSecret := os.Getenv("JWT_SECRET") if jwtSecret == "" { log.Fatal("JWT_SECRET environment variable is required") } - server := web.NewServer(store, jwtSecret) + server := web.NewServer(store, logger, jwtSecret) // We run it in a goroutine so it doesn't block main from reaching the signal listener. go func() { @@ -70,17 +81,16 @@ func main() { if connStr == "" { log.Fatal("SERVER_PORT environment variable is not set") } - fmt.Printf("Server starting on %s\n", addr) if err := server.Start(addr); err != nil && err != http.ErrServerClosed { log.Fatalf("Server failed: %v", err) } }() - // Graceful Shutdown + // @GracefulShutdown // ------------------------------------------------------------ <-ctx.Done() - fmt.Println("\nShutdown signal received. Starting graceful exit...") + logger.Warn("shutdown signal received", "signal", "SIGTERM/Interrupt") shutdownCtx, shutdownCtxCancel := context.WithTimeout(context.Background(), 10*time.Second) defer shutdownCtxCancel() @@ -89,5 +99,5 @@ func main() { log.Fatalf("Graceful shutdown failed: %v", err) } - fmt.Println("Server exited properly") + logger.Info("Server shutdown successful") } diff --git a/internal/web/middleware.go b/internal/web/middleware.go index 4df797d..515fd50 100644 --- a/internal/web/middleware.go +++ b/internal/web/middleware.go @@ -5,8 +5,11 @@ import ( "errors" "fmt" "log" + "log/slog" "net/http" + "time" + "github.com/go-chi/chi/v5/middleware" "github.com/golang-jwt/jwt/v5" ) @@ -14,6 +17,46 @@ type contextKey string const userIDKey contextKey = "userID" +func (s *Server) logRequest(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // 1. Wrap the response writer + // This intercepts the Status code when it's written later in the chain + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + + // 2. Call the next handler in the chain + next.ServeHTTP(ww, r) + + // 3. Capture the actual results + status := ww.Status() + duration := time.Since(start) + + // 4. Build the log attributes + // Using slog.Group keeps the "request" data organized + requestGroup := slog.Group("request", + slog.String("method", r.Method), + slog.String("path", r.URL.Path), + slog.String("ip", r.RemoteAddr), + ) + + // 5. Log with appropriate level based on status + if status >= 400 { + s.logger.Error("HTTP Request Error", + requestGroup, + slog.Int("status", status), + slog.Duration("took", duration), + ) + } else { + s.logger.Info("HTTP Request", + requestGroup, + slog.Int("status", status), + slog.Duration("took", duration), + ) + } + }) +} + func (s *Server) hasAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var tokenString string diff --git a/internal/web/routes.go b/internal/web/routes.go index 7caef9b..b8e0fd9 100644 --- a/internal/web/routes.go +++ b/internal/web/routes.go @@ -10,7 +10,10 @@ import ( func (s *Server) setupRoutes() *chi.Mux { router := chi.NewRouter() - router.Use(middleware.Logger) + // Global Middleware + router.Use(middleware.RequestID) + router.Use(middleware.RealIP) + router.Use(s.logRequest) router.Use(middleware.Recoverer) // TODO: CLEANUP: Not sure this is needed right now diff --git a/internal/web/server.go b/internal/web/server.go index de48943..6efb17d 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -2,6 +2,7 @@ package web import ( "context" + "log/slog" "net/http" "nfeeder/internal/db" @@ -12,15 +13,16 @@ var MAX_USER_SESSIONS = 3 type Server struct { httpServer *http.Server + logger *slog.Logger store *db.Store jwtSecret []byte } -func NewServer(store *db.Store, secret string) *Server { +func NewServer(store *db.Store, logger *slog.Logger, secret string) *Server { s := &Server{ - store: store, - // could extend this to something more generic - // will do if more fields needed + store: store, + logger: logger, + // could extend this to something more generic will do if more fields needed jwtSecret: []byte(secret), } @@ -33,9 +35,12 @@ func NewServer(store *db.Store, secret string) *Server { func (s *Server) Start(addr string) error { s.httpServer.Addr = addr + s.logger.Info("server starting", "addr", addr) + return s.httpServer.ListenAndServe() } func (s *Server) Shutdown(ctx context.Context) error { + s.logger.Info("shutting down http server") return s.httpServer.Shutdown(ctx) }