Skip to content
47 changes: 47 additions & 0 deletions backend/internal/web/captured/rw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package captured

import (
"cmp"
"net/http"
"strconv"
)

type ResponseWriter struct {
wrapped http.ResponseWriter
statusCode int
written bool
}

func New(w http.ResponseWriter) *ResponseWriter {
return &ResponseWriter{wrapped: w}
}

func (rw *ResponseWriter) Header() http.Header {
return rw.wrapped.Header()
}

func (rw *ResponseWriter) Write(n []byte) (int, error) {
rw.written = true
return rw.wrapped.Write(n)
}

func (rw *ResponseWriter) WriteHeader(statusCode int) {
rw.statusCode = statusCode
rw.wrapped.WriteHeader(statusCode)
}

// Supports [http.ResponseController]
func (rw *ResponseWriter) Unwrap() http.ResponseWriter {
return rw.wrapped
}

func (rw ResponseWriter) Status() (int, bool) {
return cmp.Or(rw.statusCode, int(http.StatusOK)), rw.statusCode > 0 || rw.written
}

func (rw ResponseWriter) StatusRepresentation() string {
if !rw.written {
return strconv.Itoa(rw.statusCode) + "*"
}
return strconv.Itoa(rw.statusCode)
}
32 changes: 32 additions & 0 deletions backend/internal/web/captured/rw_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package captured

import (
"bufio"
"fmt"
"net"
"net/http"
)

// fake
type hijacker struct {
http.ResponseWriter
}

func (h hijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
panic("not to call")
}

var _ http.Hijacker = (*hijacker)(nil)

func ExampleResponseWriter_additionalMethods() {
var rw http.ResponseWriter = New(hijacker{})
if _, ok := rw.(http.Hijacker); !ok {
fmt.Println("the non-ResponseWriter methods are not available directly")
}
if _, ok := rw.(interface{ Unwrap() http.ResponseWriter }).Unwrap().(http.Hijacker); ok {
fmt.Println("become so, after unwrapping")
}
// Output:
// the non-ResponseWriter methods are not available directly
// become so, after unwrapping
}
68 changes: 44 additions & 24 deletions backend/internal/web/reception/receptionist.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,57 +10,78 @@ package reception
import (
"context"
"fmt"
"logbook/config/deployment"
"logbook/internal/logger"
"logbook/models/columns"
"net/http"
"reflect"
"runtime"
"runtime/debug"
"time"

"logbook/config/deployment"
"logbook/internal/logger"
"logbook/internal/web/captured"
"logbook/internal/web/reception/summarizer"
"logbook/models/columns"
)

type RequestId string

func (id RequestId) lastsix() string {
return string(id)[max(0, len(string(id))-6):]
}

const ZeroRequestId = RequestId("00000000-0000-0000-0000-000000000000")

func funcname(i any) string {
v := reflect.ValueOf(i)
if v.Kind() != reflect.Func {
return "(Not a function)"
}
f := runtime.FuncForPC(reflect.ValueOf(i).Pointer())
if f == nil {
return "(Unknown function)"
}
return f.Name()
}

type receptionist struct {
l *logger.Logger
handler http.Handler
deplcfg *deployment.Config
c *deployment.Config
s *summarizer.Summarizer
l *logger.Logger
h http.Handler
}

func newReceptionist(deplcfg *deployment.Config, l *logger.Logger, handler http.Handler) *receptionist {
func newReceptionist(c *deployment.Config, l *logger.Logger, h http.Handler) *receptionist {
return &receptionist{
l: l.Sub("receptionist"),
handler: handler,
deplcfg: deplcfg,
c: c,
s: summarizer.New(c.Environment == "local"),
l: l.Sub("receptionist"),
h: h,
}
}

// DONE: logging
// DONE: recover
// DONE: timeout
func (recp receptionist) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ww := &response{ResponseWriter: w}
func (rc receptionist) ServeHTTP(w http.ResponseWriter, r *http.Request) {
crw := captured.New(w)

id, err := columns.NewUuidV4[RequestId]()
if err != nil {
recp.l.Println(fmt.Errorf("generating new request id: %w", err))
http.Error(ww, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
rc.l.Println(fmt.Errorf("generating new request id: %w", err))
http.Error(crw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}

t := time.Now()

recp.l.Printf("accepted %s: %s\n", lastsix(id), summarize(recp.deplcfg, r))
defer func() {
recp.l.Printf("served %s: %s\n", lastsix(id), summarizeW(recp.deplcfg, ww, t))
}()
rc.l.Printf("accepted %s: %s\n", id.lastsix(), rc.s.Pre(r))
defer func() { rc.l.Printf("served %s: %s\n", id.lastsix(), rc.s.Post(crw, t)) }()

ctx, cancel := context.WithTimeout(r.Context(), recp.deplcfg.Reception.RequestTimeout)
ctx, cancel := context.WithTimeout(r.Context(), rc.c.Reception.RequestTimeout)
defer func() {
cancel()
if ctx.Err() == context.DeadlineExceeded {
http.Error(ww, http.StatusText(http.StatusGatewayTimeout), http.StatusGatewayTimeout)
http.Error(crw, http.StatusText(http.StatusGatewayTimeout), http.StatusGatewayTimeout)
}
}()
r = r.WithContext(ctx)
Expand All @@ -71,9 +92,9 @@ func (recp receptionist) ServeHTTP(w http.ResponseWriter, r *http.Request) {
panic(rec)
}
debug.PrintStack()
recp.l.Println(fmt.Errorf("recovered: %s: %v", funcname(recp.handler), rec))
rc.l.Println(fmt.Errorf("recovered: %s: %v", funcname(rc.h), rec))
if r.Header.Get("Connection") != "Upgrade" { // except websocket (?)
http.Error(ww, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
http.Error(crw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
Expand All @@ -84,7 +105,6 @@ func (recp receptionist) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return

default:
recp.handler.ServeHTTP(ww, r)
rc.h.ServeHTTP(crw, r)
}

}
63 changes: 0 additions & 63 deletions backend/internal/web/reception/receptionistutils.go

This file was deleted.

13 changes: 0 additions & 13 deletions backend/internal/web/reception/response.go

This file was deleted.

71 changes: 71 additions & 0 deletions backend/internal/web/reception/summarizer/summarizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package summarizer

import (
"fmt"
"net/http"
"time"

"logbook/internal/web/captured"
)

type colorizer interface {
Blue(s any) any
Cyan(s any) any
Green(s any) any
Magenta(s any) any
Red(s any) any
Yellow(s any) any
}

type albino struct{}

func (albino) Blue(s any) any { return s }
func (albino) Cyan(s any) any { return s }
func (albino) Green(s any) any { return s }
func (albino) Magenta(s any) any { return s }
func (albino) Red(s any) any { return s }
func (albino) Yellow(s any) any { return s }

type color struct{}

func (color) Blue(s any) any { return fmt.Sprintf("\033[34m%v\033[0m", s) }
func (color) Cyan(s any) any { return fmt.Sprintf("\033[36m%v\033[0m", s) }
func (color) Green(s any) any { return fmt.Sprintf("\033[32m%v\033[0m", s) }
func (color) Magenta(s any) any { return fmt.Sprintf("\033[35m%v\033[0m", s) }
func (color) Red(s any) any { return fmt.Sprintf("\033[31m%v\033[0m", s) }
func (color) Yellow(s any) any { return fmt.Sprintf("\033[33m%v\033[0m", s) }

func newColorizer(colorize bool) colorizer {
if colorize {
return color{}
}
return albino{}
}

type Summarizer struct {
c colorizer
}

func New(colorize bool) *Summarizer {
return &Summarizer{
c: newColorizer(colorize),
}
}

func (s Summarizer) Pre(r *http.Request) string {
return fmt.Sprintf("%s %s %s (%s, %s)",
s.c.Green(r.Method),
s.c.Yellow(r.URL.Path),
s.c.Red(r.Proto),
s.c.Blue(r.Host),
s.c.Magenta(r.RemoteAddr),
)
}

func (s Summarizer) Post(crw *captured.ResponseWriter, start time.Time) string {
return fmt.Sprintf("%s %s %s bytes",
s.c.Magenta(crw.StatusRepresentation()),
s.c.Green(time.Since(start)),
s.c.Cyan(crw.Header().Get("Content-Length")),
)
}
Loading