This commit is contained in:
77
handlers/healthcheck.go
Normal file
77
handlers/healthcheck.go
Normal file
@ -0,0 +1,77 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/hlog"
|
||||
)
|
||||
|
||||
const (
|
||||
jsonContentType = "application/json;charset=utf-8"
|
||||
)
|
||||
|
||||
type Timing struct {
|
||||
TimeMillis time.Duration `json:"timeMillis"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type HealthCheckResult struct {
|
||||
Success bool `json:"success"`
|
||||
Messages []string `json:"messages"`
|
||||
Time time.Time `json:"time"`
|
||||
Timing []Timing `json:"timing"`
|
||||
Response HealthCheckResponse `json:"response"`
|
||||
}
|
||||
|
||||
type HealthCheckResponse struct {
|
||||
IpAddress string `json:"ipAddress"`
|
||||
MemUsage string `json:"memUsage"`
|
||||
}
|
||||
|
||||
func (app *Application) HealthCheck(w http.ResponseWriter, r *http.Request) {
|
||||
l := hlog.FromRequest(r)
|
||||
l.Info().Msg("HealthCheck")
|
||||
start := time.Now()
|
||||
|
||||
t, _ := json.Marshal(HealthCheckResult{
|
||||
Success: true,
|
||||
Messages: []string{},
|
||||
Time: time.Now().UTC(),
|
||||
Timing: []Timing{
|
||||
{
|
||||
Source: "HealthCheck",
|
||||
TimeMillis: time.Since(start),
|
||||
},
|
||||
},
|
||||
Response: HealthCheckResponse{
|
||||
IpAddress: getIP(r),
|
||||
MemUsage: memUsage(),
|
||||
},
|
||||
})
|
||||
|
||||
w.Header().Set("content-type", jsonContentType)
|
||||
_, _ = fmt.Fprint(w, string(t))
|
||||
}
|
||||
|
||||
func getIP(r *http.Request) string {
|
||||
// forwarded := r.Header.Get("X-FORWARDED-FOR")
|
||||
// if forwarded != "" {
|
||||
// return forwarded
|
||||
// }
|
||||
return r.RemoteAddr
|
||||
}
|
||||
|
||||
func memUsage() string {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
result := fmt.Sprintf("memoryusage::Alloc = %v MB::TotalAlloc = %v MB::Sys = %v MB::tNumGC = %v", bToMb(m.Alloc), bToMb(m.TotalAlloc), bToMb(m.Sys), m.NumGC)
|
||||
return result
|
||||
}
|
||||
|
||||
func bToMb(b uint64) uint64 {
|
||||
return b / 1024 / 1024
|
||||
}
|
22
handlers/index.go
Normal file
22
handlers/index.go
Normal file
@ -0,0 +1,22 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/hlog"
|
||||
)
|
||||
|
||||
func (app *Application) Index(w http.ResponseWriter, r *http.Request) {
|
||||
l := hlog.FromRequest(r)
|
||||
l.Info().Msg("Index")
|
||||
|
||||
reqId := RequestID(r)
|
||||
|
||||
err := indexTemplate.Execute(w, templateData{
|
||||
RequestID: reqId,
|
||||
})
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("error executing template")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
48
handlers/index_test.go
Normal file
48
handlers/index_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.vbrandl.net/vbrandl/go-web-template/assets"
|
||||
"git.vbrandl.net/vbrandl/go-web-template/common"
|
||||
"git.vbrandl.net/vbrandl/go-web-template/handlers"
|
||||
"git.vbrandl.net/vbrandl/go-web-template/service"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func PrepareApplication() handlers.Application {
|
||||
config := common.NewConfig()
|
||||
ser := service.New(config)
|
||||
zerolog.SetGlobalLevel(config.LogLevel())
|
||||
|
||||
app, err := handlers.NewApplication(ser, assets.Static)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed creating application")
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func TestIndex(t *testing.T) {
|
||||
app := PrepareApplication()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
app.Handler().ServeHTTP(w, req)
|
||||
// app.Index(w, req)
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
t.Errorf("expected status to be 200, got %d", res.StatusCode)
|
||||
}
|
||||
|
||||
reqId := res.Header.Get("x-request-id")
|
||||
if reqId == "" {
|
||||
t.Errorf("expected `x-request-id` header to be present, got %s", reqId)
|
||||
}
|
||||
}
|
23
handlers/not_found.go
Normal file
23
handlers/not_found.go
Normal file
@ -0,0 +1,23 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/hlog"
|
||||
)
|
||||
|
||||
func (app *Application) NotFound(w http.ResponseWriter, r *http.Request) {
|
||||
l := hlog.FromRequest(r)
|
||||
l.Info().Msg("Not found")
|
||||
|
||||
reqId := RequestID(r)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
||||
err := notFoundTemplate.Execute(w, templateData{
|
||||
RequestID: reqId,
|
||||
})
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("error executing template")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
37
handlers/not_found_test.go
Normal file
37
handlers/not_found_test.go
Normal file
@ -0,0 +1,37 @@
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNotFound(t *testing.T) {
|
||||
app := PrepareApplication()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/some/invalid/path", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
app.Handler().ServeHTTP(w, req)
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != 404 {
|
||||
t.Errorf("expected status to be 404, got %d", res.StatusCode)
|
||||
}
|
||||
|
||||
reqId := res.Header.Get("x-request-id")
|
||||
if reqId == "" {
|
||||
t.Errorf("expected `x-request-id` header to be present, got %s", reqId)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Errorf("expected error to be nil, got %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), reqId) {
|
||||
t.Errorf("expected body to contain request ID but it does not")
|
||||
}
|
||||
}
|
79
handlers/route.go
Normal file
79
handlers/route.go
Normal file
@ -0,0 +1,79 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.vbrandl.net/vbrandl/go-web-template/service"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/justinas/alice"
|
||||
"github.com/rs/zerolog/hlog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
Service *service.Service
|
||||
StaticFiles fs.FS
|
||||
}
|
||||
|
||||
func NewApplication(service *service.Service, staticFiles fs.FS) (Application, error) {
|
||||
application := Application{
|
||||
Service: service,
|
||||
StaticFiles: staticFiles,
|
||||
}
|
||||
|
||||
err := application.ParseTemplates()
|
||||
|
||||
return application, err
|
||||
}
|
||||
|
||||
func (app *Application) Routes() *mux.Router {
|
||||
router := mux.NewRouter().StrictSlash(true)
|
||||
|
||||
router.HandleFunc("/", app.Index).Methods("GET")
|
||||
router.HandleFunc("/health", app.HealthCheck).Methods("GET")
|
||||
|
||||
// strip off the location such that we route correctly
|
||||
staticContent, err := fs.Sub(app.StaticFiles, "static")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error with static content")
|
||||
}
|
||||
f := http.FileServerFS(staticContent)
|
||||
router.PathPrefix("/static/").Handler(http.StripPrefix("/static", f))
|
||||
|
||||
router.NotFoundHandler = http.HandlerFunc(app.NotFound)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func (app *Application) Handler() http.Handler {
|
||||
logger := log.Logger
|
||||
middlewares := alice.New().
|
||||
Append(hlog.NewHandler(logger)).
|
||||
Append(hlog.AccessHandler(func(r *http.Request, status int, size int, duration time.Duration) {
|
||||
hlog.FromRequest(r).Info().
|
||||
Str("method", r.Method).
|
||||
Stringer("url", r.URL).
|
||||
Int("status", status).
|
||||
Int("size", size).
|
||||
Str("remote-addr", r.RemoteAddr).
|
||||
Dur("duration", duration).
|
||||
Str("user-agent", r.UserAgent()).
|
||||
Msg("")
|
||||
})).
|
||||
Append(hlog.RequestIDHandler("request-id", "x-request-id"))
|
||||
|
||||
routes := app.Routes()
|
||||
return middlewares.Then(routes)
|
||||
}
|
||||
|
||||
func RequestID(r *http.Request) string {
|
||||
if id, ok := hlog.IDFromRequest(r); ok {
|
||||
return id.String()
|
||||
} else {
|
||||
l := hlog.FromRequest(r)
|
||||
l.Warn().Msg("No associated request ID")
|
||||
return "unknown"
|
||||
}
|
||||
}
|
30
handlers/template.go
Normal file
30
handlers/template.go
Normal file
@ -0,0 +1,30 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
|
||||
"git.vbrandl.net/vbrandl/go-web-template/assets"
|
||||
)
|
||||
|
||||
var indexTemplate *template.Template
|
||||
var notFoundTemplate *template.Template
|
||||
|
||||
type templateData struct {
|
||||
RequestID string
|
||||
}
|
||||
|
||||
func (app *Application) ParseTemplates() error {
|
||||
t, err := template.ParseFS(assets.Templates, "public/html/index.html", "public/html/navbar.tmpl", "public/html/footer.tmpl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
indexTemplate = t
|
||||
|
||||
t, err = template.ParseFS(assets.Templates, "public/html/notFound.html", "public/html/navbar.tmpl", "public/html/footer.tmpl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
notFoundTemplate = t
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user