From 7d81d1505afd708041432a1f71cee8d2d521b61c Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 24 Mar 2026 22:50:13 +0100 Subject: [PATCH] feat: logger, middleware --- projects/greenlight/cmd/api/errors.go | 7 +++-- projects/greenlight/cmd/api/main.go | 22 +++++++++++----- projects/greenlight/cmd/api/midlleware.go | 32 +++++++++++++++++++++++ projects/greenlight/cmd/api/routes.go | 4 +-- 4 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 projects/greenlight/cmd/api/midlleware.go diff --git a/projects/greenlight/cmd/api/errors.go b/projects/greenlight/cmd/api/errors.go index 97fe43f..72ae823 100644 --- a/projects/greenlight/cmd/api/errors.go +++ b/projects/greenlight/cmd/api/errors.go @@ -15,14 +15,17 @@ func (app *application) failedValidationResponse(w http.ResponseWriter, r *http. // book we'll upgrade this to use structured logging, and record additional information // about the request including the HTTP method and URL. func (app *application) logError(r *http.Request, err error) { - app.logger.Println(err) + app.logger.PrintError(err, map[string]string{ + "request_method": r.Method, + "request_url": r.URL.String(), + }) } // The errorResponse() method is a generic helper for sending JSON-formatted error // messages to the client with a given status code. Note that we're using an interface{} // type for the message parameter, rather than just a string type, as this gives us // more flexibility over the values that we can include in the response. -func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message interface{}) { +func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message any) { env := envelope{"error": message} // Write the response using the writeJSON() helper. If this happens to return an diff --git a/projects/greenlight/cmd/api/main.go b/projects/greenlight/cmd/api/main.go index 4e680a6..4208d51 100644 --- a/projects/greenlight/cmd/api/main.go +++ b/projects/greenlight/cmd/api/main.go @@ -13,6 +13,7 @@ import ( "github.com/joho/godotenv" _ "github.com/lib/pq" "greenlight.debuggingjon.dev/internal/data" + "greenlight.debuggingjon.dev/internal/jsonlog" ) // Declare a string containing the application version number. Later in the book we'll @@ -41,17 +42,20 @@ type config struct { // logger, but it will grow to include a lot more as our build progresses. type application struct { config config - logger *log.Logger + logger *jsonlog.Logger models data.Models } func main() { - logger := log.New(os.Stdout, "", log.Ldate|log.Ltime) var cfg config + // Initialize a new jsonlog.Logger which writes any messages *at or above* the INFO + // severity level to the standard out stream. + logger := jsonlog.New(os.Stdout, jsonlog.LevelInfo) + err := godotenv.Load() if err != nil { - logger.Fatal(err) + logger.PrintFatal(err, nil) } dsn := os.Getenv("DATABASE_DSN") @@ -73,7 +77,7 @@ func main() { // application immediately. db, err := openDB(cfg) if err != nil { - logger.Fatal(err) + logger.PrintFatal(err, nil) } // Defer a call to db.Close() so that the connection pool is closed before the @@ -85,7 +89,7 @@ func main() { }() // Also log a message to say that the connection pool has been successfully // established. - logger.Printf("database connection pool established") + logger.PrintInfo("database connection pool established", nil) app := &application{ config: cfg, @@ -98,13 +102,17 @@ func main() { Addr: fmt.Sprintf(":%d", cfg.port), Handler: app.routes(), IdleTimeout: time.Minute, + ErrorLog: log.New(logger, "", 0), ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, } - logger.Printf("starting %s server on %s", cfg.env, srv.Addr) + logger.PrintInfo("starting server", map[string]string{ + "addr": srv.Addr, + "env": cfg.env, + }) err = srv.ListenAndServe() - logger.Fatal(err) + logger.PrintFatal(err, nil) } func openDB(cfg config) (*sql.DB, error) { diff --git a/projects/greenlight/cmd/api/midlleware.go b/projects/greenlight/cmd/api/midlleware.go new file mode 100644 index 0000000..14ba557 --- /dev/null +++ b/projects/greenlight/cmd/api/midlleware.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "net/http" +) + +func (app *application) recoverPanic(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Create a deferred function (which will always be run in the event of a panic + // as Go unwinds the stack). + defer func() { + // Use the builtin recover function to check if there has been a panic or + // not. + if err := recover(); err != nil { + // If there was a panic, set a "Connection: close" header on the + // response. This acts as a trigger to make Go's HTTP server + // automatically close the current connection after a response has been + // sent. + w.Header().Set("Connection", "close") + // The value returned by recover() has the type interface{}, so we use + // fmt.Errorf() to normalize it into an error and call our + // serverErrorResponse() helper. In turn, this will log the error using + // our custom Logger type at the ERROR level and send the client a 500 + // Internal Server Error response. + app.serverErrorResponse(w, r, fmt.Errorf("%s", err)) + } + }() + + next.ServeHTTP(w, r) + }) +} diff --git a/projects/greenlight/cmd/api/routes.go b/projects/greenlight/cmd/api/routes.go index 042ed4a..715e279 100644 --- a/projects/greenlight/cmd/api/routes.go +++ b/projects/greenlight/cmd/api/routes.go @@ -6,7 +6,7 @@ import ( "github.com/julienschmidt/httprouter" ) -func (app *application) routes() *httprouter.Router { +func (app *application) routes() http.Handler { router := httprouter.New() // Convert the notFoundResponse() helper to a http.Handler using the @@ -24,5 +24,5 @@ func (app *application) routes() *httprouter.Router { router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler) router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler) - return router + return app.recoverPanic(router) }