Refactored internals more

drew/lets-go
Drew Bednar 6 days ago
parent ff81b307e3
commit 012ae2783e

@ -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) - [Understand Mutexs in Go](https://www.alexedwards.net/blog/understanding-mutexes)
- [Structured Logging in Go with log/slog](https://pkg.go.dev/log/slog) - [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) - [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 ### Go http.FileServer

@ -6,33 +6,37 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"os" "os"
"strings"
"git.runcible.io/learning/ratchet" "git.runcible.io/learning/ratchet/internal/logging"
ratchethttp "git.runcible.io/learning/ratchet/internal" "git.runcible.io/learning/ratchet/internal/server"
// "git.runcible.io/learning/ratchet"
// ratchethttp "git.runcible.io/learning/ratchet/internal"
) )
var ( // var (
version string // version string
commit string // commit string
) // )
func main() { func main() {
// Commandline options // CONFIGURATION
// Parse command line options
addr := flag.String("addr", "0.0.0.0", "HTTP network address") addr := flag.String("addr", "0.0.0.0", "HTTP network address")
port := flag.String("port", "5001", "HTTP port") port := flag.String("port", "5001", "HTTP port")
logLevel := flag.String("logging", "INFO", "Logging Level. Valid values [INFO, DEBUG, WARN, ERROR].") logLevel := flag.String("logging", "INFO", "Logging Level. Valid values [INFO, DEBUG, WARN, ERROR].")
// must call parse or all values will be the defaults // must call parse or all values will be the defaults
flag.Parse() flag.Parse()
// DEPENDENCY INJECTION FOR HANDLERS
// Setup Logging // Setup Logging
ratchethttp.InitLogging(*logLevel) logger := logging.InitLogging(*logLevel)
// Propagate build information to root package to share globally // Propagate build information to root package to share globally
ratchet.Version = strings.TrimPrefix(version, "") // ratchet.Version = strings.TrimPrefix(version, "")
ratchet.Commit = commit // ratchet.Commit = commit
server := server.NewRatchetServer(logger)
server := ratchethttp.NewRatchetServer() // START SERVING REQUESTS
slog.Debug("Herp dirp!") slog.Debug("Herp dirp!")
slog.Info(fmt.Sprintf("Listening on http://%s:%s", *addr, *port)) slog.Info(fmt.Sprintf("Listening on http://%s:%s", *addr, *port))
//log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%s", *addr, *port), server)) //log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%s", *addr, *port), server))

@ -1,4 +1,4 @@
package ratchet package apperror
import ( import (
"errors" "errors"

@ -1,4 +1,4 @@
package ratchet package apperror
import ( import (
"errors" "errors"

@ -1,8 +1,10 @@
package ratchet package user
import ( import (
"context" "context"
"time" "time"
"git.runcible.io/learning/ratchet/internal/apperror"
) )
type User struct { type User struct {
@ -27,7 +29,7 @@ type User struct {
func (u *User) Validate() error { func (u *User) Validate() error {
if u.Name == "" { if u.Name == "" {
return Errorf(EINVALID, "User name required.") return apperror.Errorf(apperror.EINVALID, "User name required.")
} }
return nil return nil
} }

@ -1,11 +1,15 @@
package ratchet package user
import "testing" import (
"testing"
"git.runcible.io/learning/ratchet/internal/apperror"
)
func TestUserValidation(t *testing.T) { func TestUserValidation(t *testing.T) {
t.Run("user should return invalid", func(t *testing.T) { t.Run("user should return invalid", func(t *testing.T) {
u := &User{} u := &User{}
if ErrorCode(u.Validate()) != EINVALID { if apperror.ErrorCode(u.Validate()) != apperror.EINVALID {
t.Errorf("User validation should have failed but passed instead.") t.Errorf("User validation should have failed but passed instead.")
} }
}) })

@ -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
}

@ -1,52 +1,26 @@
package ratchet package server
import ( import (
"fmt" "fmt"
"html/template" "html/template"
"log/slog" "log/slog"
"net/http" "net/http"
"os"
"strconv" "strconv"
"strings"
"git.runcible.io/learning/ratchet/internal/domain/user"
) )
type RatchetServer struct { type RatchetServer struct {
http.Handler http.Handler
logger *slog.Logger
//Services used by HTTP routes //Services used by HTTP routes
UserService UserService UserService user.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
}
} }
func InitLogging(level string) { func NewRatchetServer(logger *slog.Logger) *RatchetServer {
// Use os.Stderr rs := new(RatchetServer)
// rs.logger = logger
// 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)
// TODO implement middleware that disables directory listings // TODO implement middleware that disables directory listings
fileServer := http.FileServer(http.Dir("./ui/static/")) fileServer := http.FileServer(http.Dir("./ui/static/"))
router := http.NewServeMux() router := http.NewServeMux()
@ -58,22 +32,22 @@ func NewRatchetServer() *RatchetServer {
// resulting in this route requiring an exact match on "/" only // resulting in this route requiring an exact match on "/" only
// You can only include one HTTP method in a route pattern if you choose // You can only include one HTTP method in a route pattern if you choose
// GET will match GET & HEAD http request methods // GET will match GET & HEAD http request methods
router.HandleFunc("GET /{$}", home) router.HandleFunc("GET /{$}", rs.home)
router.HandleFunc("GET /snippet/view/{id}", snippetView) router.HandleFunc("GET /snippet/view/{id}", rs.snippetView)
router.HandleFunc("GET /snippet/create", snippetCreate) router.HandleFunc("GET /snippet/create", rs.snippetCreate)
// FYI The http.HandlerFunc() adapter works by automatically adding a ServeHTTP() method to // FYI The http.HandlerFunc() adapter works by automatically adding a ServeHTTP() method to
// the passed function // 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. // Mux Router implements the Handler interface. AKA it has a ServeHTTP receiver.
r.Handler = router rs.Handler = router
return r 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 // 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") w.Header().Add("Server", "Go")
@ -89,23 +63,25 @@ func home(w http.ResponseWriter, r *http.Request) {
// read template file into template set. // read template file into template set.
ts, err := template.ParseFiles(files...) ts, err := template.ParseFiles(files...)
if err != nil { if err != nil {
slog.Error(err.Error()) rs.serverError(w, r, err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return return
} }
// Write template content to response body // Write template content to response body
err = ts.ExecuteTemplate(w, "base", nil) err = ts.ExecuteTemplate(w, "base", nil)
if err != nil { if err != nil {
slog.Error(err.Error()) // This is the older more verbose way of doing what RatchetServer.serverError does
http.Error(w, "Internal Server Error", http.StatusInternalServerError) // 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")) id, err := strconv.Atoi(r.PathValue("id"))
if err != nil || id < 1 { if err != nil || id < 1 {
http.NotFound(w, r) // http.NotFound(w, r)
rs.clientError(w, http.StatusNotFound)
return return
} }
@ -124,12 +100,12 @@ func snippetView(w http.ResponseWriter, r *http.Request) {
} }
// snippetCreate handles display of the form used to create snippets // 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...")) w.Write([]byte("Displaying snippetCreate form..."))
} }
// snippetCreate handles display of the form used to create snippets // 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 // example of a custom header. Must be done before calling WriteHeader
// or they will fail to take effect. // or they will fail to take effect.
w.Header().Add("Server", "Dirp") w.Header().Add("Server", "Dirp")

@ -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)
}
Loading…
Cancel
Save