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)
- [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

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

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

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

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

@ -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.")
}
})

@ -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 (
"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")

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