From 012ae2783e930ff90061b558f2e1065067357c21 Mon Sep 17 00:00:00 2001 From: Drew Bednar Date: Sun, 12 Jan 2025 14:23:51 -0500 Subject: [PATCH] Refactored internals more --- README.md | 1 + cmd/ratchetd/main.go | 28 ++++--- internal/{error.go => apperror/apperror.go} | 2 +- .../apperror_test.go} | 2 +- internal/{ => domain/user}/user.go | 6 +- internal/{ => domain/user}/user_test.go | 10 ++- internal/logging/logging.go | 36 +++++++++ internal/{server.go => server/base_server.go} | 76 +++++++------------ internal/server/helpers.go | 29 +++++++ 9 files changed, 121 insertions(+), 69 deletions(-) rename internal/{error.go => apperror/apperror.go} (99%) rename internal/{error_test.go => apperror/apperror_test.go} (98%) rename internal/{ => domain/user}/user.go (94%) rename internal/{ => domain/user}/user_test.go (70%) create mode 100644 internal/logging/logging.go rename internal/{server.go => server/base_server.go} (59%) create mode 100644 internal/server/helpers.go diff --git a/README.md b/README.md index 7a08fd2..f995bd6 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ The `ratchetd` cmd binary uses Oauth so you will need to create a new Oauth App. - [Understand Mutexs in Go](https://www.alexedwards.net/blog/understanding-mutexes) - [Structured Logging in Go with log/slog](https://pkg.go.dev/log/slog) - [Exit Codes with special meaning](https://tldp.org/LDP/abs/html/exitcodes.html) +- [Organizing Database Access in Go](https://www.alexedwards.net/blog/organising-database-access) ### Go http.FileServer diff --git a/cmd/ratchetd/main.go b/cmd/ratchetd/main.go index 114b52f..898765a 100644 --- a/cmd/ratchetd/main.go +++ b/cmd/ratchetd/main.go @@ -6,33 +6,37 @@ import ( "log/slog" "net/http" "os" - "strings" - "git.runcible.io/learning/ratchet" - ratchethttp "git.runcible.io/learning/ratchet/internal" + "git.runcible.io/learning/ratchet/internal/logging" + "git.runcible.io/learning/ratchet/internal/server" + // "git.runcible.io/learning/ratchet" + // ratchethttp "git.runcible.io/learning/ratchet/internal" ) -var ( - version string - commit string -) +// var ( +// version string +// commit string +// ) func main() { - // Commandline options + // CONFIGURATION + // Parse command line options addr := flag.String("addr", "0.0.0.0", "HTTP network address") port := flag.String("port", "5001", "HTTP port") logLevel := flag.String("logging", "INFO", "Logging Level. Valid values [INFO, DEBUG, WARN, ERROR].") // must call parse or all values will be the defaults flag.Parse() + // DEPENDENCY INJECTION FOR HANDLERS // Setup Logging - ratchethttp.InitLogging(*logLevel) + logger := logging.InitLogging(*logLevel) // Propagate build information to root package to share globally - ratchet.Version = strings.TrimPrefix(version, "") - ratchet.Commit = commit + // ratchet.Version = strings.TrimPrefix(version, "") + // ratchet.Commit = commit + server := server.NewRatchetServer(logger) - server := ratchethttp.NewRatchetServer() + // START SERVING REQUESTS slog.Debug("Herp dirp!") slog.Info(fmt.Sprintf("Listening on http://%s:%s", *addr, *port)) //log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%s", *addr, *port), server)) diff --git a/internal/error.go b/internal/apperror/apperror.go similarity index 99% rename from internal/error.go rename to internal/apperror/apperror.go index 42daca0..6560556 100644 --- a/internal/error.go +++ b/internal/apperror/apperror.go @@ -1,4 +1,4 @@ -package ratchet +package apperror import ( "errors" diff --git a/internal/error_test.go b/internal/apperror/apperror_test.go similarity index 98% rename from internal/error_test.go rename to internal/apperror/apperror_test.go index 20917c0..ea4b633 100644 --- a/internal/error_test.go +++ b/internal/apperror/apperror_test.go @@ -1,4 +1,4 @@ -package ratchet +package apperror import ( "errors" diff --git a/internal/user.go b/internal/domain/user/user.go similarity index 94% rename from internal/user.go rename to internal/domain/user/user.go index aa726f1..90b991b 100644 --- a/internal/user.go +++ b/internal/domain/user/user.go @@ -1,8 +1,10 @@ -package ratchet +package user import ( "context" "time" + + "git.runcible.io/learning/ratchet/internal/apperror" ) type User struct { @@ -27,7 +29,7 @@ type User struct { func (u *User) Validate() error { if u.Name == "" { - return Errorf(EINVALID, "User name required.") + return apperror.Errorf(apperror.EINVALID, "User name required.") } return nil } diff --git a/internal/user_test.go b/internal/domain/user/user_test.go similarity index 70% rename from internal/user_test.go rename to internal/domain/user/user_test.go index f124f67..c3c70e8 100644 --- a/internal/user_test.go +++ b/internal/domain/user/user_test.go @@ -1,11 +1,15 @@ -package ratchet +package user -import "testing" +import ( + "testing" + + "git.runcible.io/learning/ratchet/internal/apperror" +) func TestUserValidation(t *testing.T) { t.Run("user should return invalid", func(t *testing.T) { u := &User{} - if ErrorCode(u.Validate()) != EINVALID { + if apperror.ErrorCode(u.Validate()) != apperror.EINVALID { t.Errorf("User validation should have failed but passed instead.") } }) diff --git a/internal/logging/logging.go b/internal/logging/logging.go new file mode 100644 index 0000000..18c38c6 --- /dev/null +++ b/internal/logging/logging.go @@ -0,0 +1,36 @@ +package logging + +import ( + "log/slog" + "os" + "strings" +) + +func parseLogLevel(levelStr string) slog.Level { + switch strings.ToUpper(levelStr) { + case "DEBUG": + return slog.LevelDebug + case "INFO": + return slog.LevelInfo + case "WARN": + return slog.LevelWarn + case "ERROR": + return slog.LevelError + default: + return slog.LevelInfo // Default level + } +} + +// InitLogggin initializes global structured logging for the entire application +func InitLogging(level string) *slog.Logger { + // Use os.Stderr + // + // Stderr is used for diagnostics and logging. Stdout is used for program + // output. Stderr also have greater likely hood of being seen if a programs + // output is being redirected. + parsedLogLevel := parseLogLevel(level) + loggerHandler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: parsedLogLevel, AddSource: true}) + logger := slog.New(loggerHandler) + slog.SetDefault(logger) + return logger +} diff --git a/internal/server.go b/internal/server/base_server.go similarity index 59% rename from internal/server.go rename to internal/server/base_server.go index d0775a2..c9219b8 100644 --- a/internal/server.go +++ b/internal/server/base_server.go @@ -1,52 +1,26 @@ -package ratchet +package server import ( "fmt" "html/template" "log/slog" "net/http" - "os" "strconv" - "strings" + + "git.runcible.io/learning/ratchet/internal/domain/user" ) type RatchetServer struct { http.Handler + logger *slog.Logger //Services used by HTTP routes - UserService UserService -} - -func parseLogLevel(levelStr string) slog.Level { - switch strings.ToUpper(levelStr) { - case "DEBUG": - return slog.LevelDebug - case "INFO": - return slog.LevelInfo - case "WARN": - return slog.LevelWarn - case "ERROR": - return slog.LevelError - default: - return slog.LevelInfo // Default level - } + UserService user.UserService } -func InitLogging(level string) { - // Use os.Stderr - // - // Stderr is used for diagnostics and logging. Stdout is used for program - // output. Stderr also have greater likely hood of being seen if a programs - // output is being redirected. - parsedLogLevel := parseLogLevel(level) - loggerHandler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: parsedLogLevel, AddSource: true}) - logger := slog.New(loggerHandler) - slog.SetDefault(logger) -} - -func NewRatchetServer() *RatchetServer { - r := new(RatchetServer) - +func NewRatchetServer(logger *slog.Logger) *RatchetServer { + rs := new(RatchetServer) + rs.logger = logger // TODO implement middleware that disables directory listings fileServer := http.FileServer(http.Dir("./ui/static/")) router := http.NewServeMux() @@ -58,22 +32,22 @@ func NewRatchetServer() *RatchetServer { // resulting in this route requiring an exact match on "/" only // You can only include one HTTP method in a route pattern if you choose // GET will match GET & HEAD http request methods - router.HandleFunc("GET /{$}", home) - router.HandleFunc("GET /snippet/view/{id}", snippetView) - router.HandleFunc("GET /snippet/create", snippetCreate) + router.HandleFunc("GET /{$}", rs.home) + router.HandleFunc("GET /snippet/view/{id}", rs.snippetView) + router.HandleFunc("GET /snippet/create", rs.snippetCreate) // FYI The http.HandlerFunc() adapter works by automatically adding a ServeHTTP() method to // the passed function - router.HandleFunc("POST /snippet/create", snippetCreatePost) + router.HandleFunc("POST /snippet/create", rs.snippetCreatePost) // Mux Router implements the Handler interface. AKA it has a ServeHTTP receiver. - r.Handler = router - return r + rs.Handler = router + return rs } -func home(w http.ResponseWriter, r *http.Request) { +func (rs *RatchetServer) home(w http.ResponseWriter, r *http.Request) { // TODO middleware should be able to print out these lines for all routes - slog.Info("request received", "method", "GET", "path", "/") + rs.logger.Info("request received", "method", "GET", "path", "/") w.Header().Add("Server", "Go") @@ -89,23 +63,25 @@ func home(w http.ResponseWriter, r *http.Request) { // read template file into template set. ts, err := template.ParseFiles(files...) if err != nil { - slog.Error(err.Error()) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) + rs.serverError(w, r, err) return } // Write template content to response body err = ts.ExecuteTemplate(w, "base", nil) if err != nil { - slog.Error(err.Error()) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) + // This is the older more verbose way of doing what RatchetServer.serverError does + // rs.logger.Error(err.Error()) + // http.Error(w, "Internal Server Error", http.StatusInternalServerError) + rs.serverError(w, r, err) } } -func snippetView(w http.ResponseWriter, r *http.Request) { +func (rs *RatchetServer) snippetView(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(r.PathValue("id")) if err != nil || id < 1 { - http.NotFound(w, r) + // http.NotFound(w, r) + rs.clientError(w, http.StatusNotFound) return } @@ -124,12 +100,12 @@ func snippetView(w http.ResponseWriter, r *http.Request) { } // snippetCreate handles display of the form used to create snippets -func snippetCreate(w http.ResponseWriter, r *http.Request) { +func (rs *RatchetServer) snippetCreate(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Displaying snippetCreate form...")) } // snippetCreate handles display of the form used to create snippets -func snippetCreatePost(w http.ResponseWriter, r *http.Request) { +func (rs *RatchetServer) snippetCreatePost(w http.ResponseWriter, r *http.Request) { // example of a custom header. Must be done before calling WriteHeader // or they will fail to take effect. w.Header().Add("Server", "Dirp") diff --git a/internal/server/helpers.go b/internal/server/helpers.go new file mode 100644 index 0000000..2c235e4 --- /dev/null +++ b/internal/server/helpers.go @@ -0,0 +1,29 @@ +package server + +import ( + "net/http" + "runtime/debug" +) + +// serverError helper writes a log entry at Error level (including the request +// method and URI as attributes), then sends a generic 500 Internal Server Error +// response to the user. +func (rs *RatchetServer) serverError(w http.ResponseWriter, r *http.Request, err error) { + var ( + method = r.Method + uri = r.URL.RequestURI() + // Use debug.Stack() to get the stack trace. This returns a byte slice, which + // we need to convert to a string so that it's readable in the log entry. + trace = string(debug.Stack()) + ) + + rs.logger.Error(err.Error(), "method", method, "uri", uri, "trace", trace) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) +} + +// clientError helper sends a specific status code and corresponding description +// to the user. We'll use this later in the book to send responses like 400 "Bad +// Request" when there's a problem with the request that the user sent +func (rs *RatchetServer) clientError(w http.ResponseWriter, status int) { + http.Error(w, http.StatusText(status), status) +}