Skip to content

Commit 42dda5f

Browse files
committed
Better middleware
1 parent b523706 commit 42dda5f

File tree

7 files changed

+253
-78
lines changed

7 files changed

+253
-78
lines changed

parrot/errors.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ import (
66
)
77

88
var (
9-
ErrNilRoute = errors.New("route is nil")
10-
ErrNoMethod = errors.New("no method specified")
11-
ErrInvalidPath = errors.New("invalid path")
12-
ErrNoResponse = errors.New("route must have a handler or some response")
13-
ErrOnlyOneResponse = errors.New("route can only have one response type")
14-
ErrResponseMarshal = errors.New("unable to marshal response body to JSON")
15-
ErrRouteNotFound = errors.New("route not found")
9+
ErrNilRoute = errors.New("route is nil")
10+
ErrNoMethod = errors.New("no method specified")
11+
ErrInvalidPath = errors.New("invalid path")
12+
ErrNoResponse = errors.New("route must have a handler or some response")
13+
ErrOnlyOneResponse = errors.New("route can only have one response type")
14+
ErrResponseMarshal = errors.New("unable to marshal response body to JSON")
15+
ErrRouteNotFound = errors.New("route not found")
16+
17+
ErrNoRecorderURL = errors.New("no recorder URL specified")
18+
ErrNilRecorder = errors.New("recorder is nil")
1619
ErrRecorderNotFound = errors.New("recorder not found")
1720
)
1821

parrot/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.23.4
44

55
require (
66
github.com/go-resty/resty/v2 v2.16.3
7+
github.com/google/uuid v1.6.0
78
github.com/rs/zerolog v1.33.0
89
github.com/spf13/cobra v1.8.1
910
github.com/stretchr/testify v1.9.0
@@ -15,6 +16,7 @@ require (
1516
github.com/mattn/go-colorable v0.1.13 // indirect
1617
github.com/mattn/go-isatty v0.0.19 // indirect
1718
github.com/pmezard/go-difflib v1.0.0 // indirect
19+
github.com/rs/xid v1.5.0 // indirect
1820
github.com/spf13/pflag v1.0.5 // indirect
1921
golang.org/x/net v0.33.0 // indirect
2022
golang.org/x/sys v0.28.0 // indirect

parrot/go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
55
github.com/go-resty/resty/v2 v2.16.3 h1:zacNT7lt4b8M/io2Ahj6yPypL7bqx9n1iprfQuodV+E=
66
github.com/go-resty/resty/v2 v2.16.3/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
77
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
8+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
9+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
810
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
911
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
1012
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -15,6 +17,7 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
1517
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
1618
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1719
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
20+
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
1821
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
1922
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
2023
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=

parrot/parrot.go

Lines changed: 108 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ import (
1818
"time"
1919

2020
"github.com/go-resty/resty/v2"
21+
"github.com/google/uuid"
2122
"github.com/rs/zerolog"
23+
"github.com/rs/zerolog/hlog"
2224
)
2325

2426
// Route holds information about the mock route configuration
@@ -173,7 +175,7 @@ func Wake(options ...ServerOption) (*Server, error) {
173175
if p.jsonLogs {
174176
writers = append(writers, os.Stderr)
175177
} else {
176-
consoleOut := zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339Nano}
178+
consoleOut := zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "2006-01-02T15:04:05.000"}
177179
writers = append(writers, consoleOut)
178180
}
179181

@@ -208,7 +210,7 @@ func Wake(options ...ServerOption) (*Server, error) {
208210
p.server = &http.Server{
209211
ReadHeaderTimeout: 5 * time.Second,
210212
Addr: listener.Addr().String(),
211-
Handler: mux,
213+
Handler: p.loggingMiddleware(mux),
212214
}
213215

214216
if err = p.load(); err != nil {
@@ -290,69 +292,71 @@ func (p *Server) Register(route *Route) error {
290292
p.routesMu.Lock()
291293
defer p.routesMu.Unlock()
292294
p.routes[route.ID()] = route
295+
p.log.Info().
296+
Str("Route ID", route.ID()).
297+
Str("Path", route.Path).
298+
Str("Method", route.Method).
299+
Msg("Route registered")
293300

294301
return nil
295302
}
296303

297304
// registerRouteHandler handles the dynamic route registration.
298305
func (p *Server) registerRouteHandler(w http.ResponseWriter, r *http.Request) {
299-
const parrotPath = "/register"
306+
registerLogger := zerolog.Ctx(r.Context())
300307
if r.Method == http.MethodDelete {
301308
var routeRequest *RouteRequest
302309
if err := json.NewDecoder(r.Body).Decode(&routeRequest); err != nil {
303310
http.Error(w, "Invalid request body", http.StatusBadRequest)
311+
registerLogger.Debug().Err(err).Msg("Failed to decode request body")
304312
return
305313
}
306314
defer r.Body.Close()
307315

308316
if routeRequest.ID == "" {
309-
err := errors.New("ID required")
310-
http.Error(w, err.Error(), http.StatusBadRequest)
317+
http.Error(w, "Route ID required", http.StatusBadRequest)
318+
registerLogger.Debug().Msg("No Route ID provided")
311319
return
312320
}
313321

314322
err := p.Unregister(routeRequest.ID)
315323
if err != nil {
316324
http.Error(w, err.Error(), http.StatusBadRequest)
317-
p.log.Trace().Err(err).Str("Path", parrotPath).Msg("Failed to unregister route")
325+
registerLogger.Debug().Err(err).Msg("Failed to unregister route")
318326
return
319327
}
320328

321329
w.WriteHeader(http.StatusNoContent)
322-
p.log.Info().
330+
registerLogger.Info().
323331
Str("Route ID", routeRequest.ID).
324-
Str("Parrot Path", parrotPath).
325332
Msg("Route unregistered")
326333
} else if r.Method == http.MethodPost {
327334
var route *Route
328335
if err := json.NewDecoder(r.Body).Decode(&route); err != nil {
329336
http.Error(w, "Invalid request body", http.StatusBadRequest)
337+
registerLogger.Debug().Err(err).Msg("Failed to decode request body")
330338
return
331339
}
332340
defer r.Body.Close()
333341

334342
if route.Method == "" || route.Path == "" {
335343
err := errors.New("Method and path are required")
336344
http.Error(w, err.Error(), http.StatusBadRequest)
345+
registerLogger.Debug().Err(err).Msg("Method and path are required")
337346
return
338347
}
339348

340349
err := p.Register(route)
341350
if err != nil {
342351
http.Error(w, err.Error(), http.StatusBadRequest)
343-
p.log.Trace().Err(err).Msg("Failed to register route")
352+
registerLogger.Debug().Err(err).Msg("Failed to register route")
344353
return
345354
}
346355

347356
w.WriteHeader(http.StatusCreated)
348-
p.log.Info().
349-
Str("Parrot Path", parrotPath).
350-
Str("Route Path", route.Path).
351-
Str("Method", route.Method).
352-
Msg("Route registered")
353357
} else {
354358
http.Error(w, "Invalid method, only use POST or DELETE", http.StatusMethodNotAllowed)
355-
p.log.Trace().Str("Method", r.Method).Msg("Invalid method")
359+
registerLogger.Debug().Msg("Invalid method")
356360
return
357361
}
358362
}
@@ -361,34 +365,44 @@ func (p *Server) registerRouteHandler(w http.ResponseWriter, r *http.Request) {
361365
func (p *Server) Record(recorder *Recorder) error {
362366
p.recordersMu.Lock()
363367
defer p.recordersMu.Unlock()
368+
if recorder == nil {
369+
return ErrNilRecorder
370+
}
371+
if recorder.URL == "" {
372+
return ErrNoRecorderURL
373+
}
374+
_, err := url.Parse(recorder.URL)
375+
if err != nil {
376+
return fmt.Errorf("failed to parse recorder URL: %w", err)
377+
}
364378
p.recorderHooks = append(p.recorderHooks, recorder.URL)
365379
return nil
366380
}
367381

368382
func (p *Server) recordHandler(w http.ResponseWriter, r *http.Request) {
369-
const parrotPath = "/record"
383+
recordLogger := zerolog.Ctx(r.Context())
370384
if r.Method != http.MethodPost {
371385
http.Error(w, "Invalid method, only use POST or DELETE", http.StatusMethodNotAllowed)
372-
p.log.Trace().Str("Method", r.Method).Msg("Invalid method")
386+
recordLogger.Debug().Msg("Invalid method")
373387
return
374388
}
375389

376390
var recorder *Recorder
377391
if err := json.NewDecoder(r.Body).Decode(&recorder); err != nil {
378392
http.Error(w, "Invalid request body", http.StatusBadRequest)
379-
p.log.Trace().Err(err).Str("Parrot Path", parrotPath).Msg("Failed to decode request body")
393+
recordLogger.Err(err).Msg("Failed to decode request body")
380394
return
381395
}
382396

383397
err := p.Record(recorder)
384398
if err != nil {
385399
http.Error(w, err.Error(), http.StatusBadRequest)
386-
p.log.Trace().Err(err).Str("Parrot Path", parrotPath).Msg("Failed to add recorder")
400+
recordLogger.Debug().Err(err).Msg("Failed to add recorder")
387401
return
388402
}
389403

390404
w.WriteHeader(http.StatusCreated)
391-
p.log.Info().Str("Recorder URL", recorder.URL).Str("Parrot Path", parrotPath).Msg("Recorder added")
405+
recordLogger.Info().Str("Recorder URL", recorder.URL).Msg("Recorder added")
392406
}
393407

394408
// Unregister removes a route from the parrot
@@ -423,78 +437,98 @@ func (p *Server) dynamicHandler(w http.ResponseWriter, r *http.Request) {
423437
route, exists := p.routes[r.Method+":"+r.URL.Path]
424438
p.routesMu.RUnlock()
425439

440+
dynamicLogger := zerolog.Ctx(r.Context())
441+
if !exists {
442+
http.NotFound(w, r)
443+
dynamicLogger.Debug().Msg("Route not found")
444+
return
445+
}
446+
447+
requestID := uuid.New().String()[0:8]
448+
dynamicLogger.UpdateContext(func(c zerolog.Context) zerolog.Context {
449+
return c.Str("Request ID", requestID).Str("Route ID", route.ID())
450+
})
451+
452+
requestBody, err := io.ReadAll(r.Body)
453+
if err != nil {
454+
dynamicLogger.Debug().
455+
Err(err).
456+
Msg("Failed to read request body")
457+
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
458+
return
459+
}
460+
426461
routeCall := &RouteCall{
427462
RouteID: r.Method + ":" + r.URL.Path,
428-
Request: r,
463+
Request: &RouteCallRequest{
464+
Method: r.Method,
465+
URL: r.URL,
466+
Header: r.Header,
467+
Body: requestBody,
468+
},
429469
}
430470
recordingWriter := newResponseWriterRecorder(w)
431471

432472
defer func() {
433-
routeCall.Response = recordingWriter.Result()
473+
res := recordingWriter.Result()
474+
resBody, err := io.ReadAll(res.Body)
475+
if err != nil {
476+
dynamicLogger.Debug().Err(err).Msg("Failed to read response body")
477+
http.Error(w, "Failed to read response body", http.StatusInternalServerError)
478+
return
479+
}
480+
481+
routeCall.Response = &RouteCallResponse{
482+
StatusCode: res.StatusCode,
483+
Header: res.Header,
484+
Body: resBody,
485+
}
434486
p.sendToRecorders(routeCall)
435487
}()
436488

437-
if !exists { // Route not found
438-
http.NotFound(recordingWriter, r)
439-
p.log.Trace().Str("Remote Addr", r.RemoteAddr).Str("Path", r.URL.Path).Str("Method", r.Method).Msg("Route not found")
440-
return
441-
}
442-
443489
// Let the custom handler take over if it exists
444490
if route.Handler != nil {
445-
p.log.Trace().Str("Remote Addr", r.RemoteAddr).Str("Path", r.URL.Path).Str("Method", r.Method).Msg("Calling route handler")
491+
dynamicLogger.Debug().Msg("Calling route handler")
446492
route.Handler(recordingWriter, r)
447493
return
448494
}
449495

450-
recordingWriter.WriteHeader(route.ResponseStatusCode)
451-
452496
if route.RawResponseBody != "" {
453497
if _, err := w.Write([]byte(route.RawResponseBody)); err != nil {
454-
p.log.Trace().Err(err).Str("Remote Addr", r.RemoteAddr).Str("Path", r.URL.Path).Str("Method", r.Method).Msg("Failed to write response")
498+
dynamicLogger.Debug().Err(err).Msg("Failed to write response")
455499
http.Error(recordingWriter, "Failed to write response", http.StatusInternalServerError)
456500
return
457501
}
458-
p.log.Trace().
459-
Str("Remote Addr", r.RemoteAddr).
502+
dynamicLogger.Debug().
460503
Str("Response", route.RawResponseBody).
461-
Str("Path", r.URL.Path).
462-
Str("Method", r.Method).
463504
Msg("Returned raw response")
505+
recordingWriter.WriteHeader(route.ResponseStatusCode)
464506
return
465507
}
466508

467509
if route.ResponseBody != nil {
468510
rawJSON, err := json.Marshal(route.ResponseBody)
469511
if err != nil {
470-
p.log.Trace().Err(err).
471-
Str("Remote Addr", r.RemoteAddr).
472-
Str("Path", r.URL.Path).
473-
Str("Method", r.Method).
474-
Msg("Failed to marshal JSON response")
512+
dynamicLogger.Debug().Err(err).Msg("Failed to marshal JSON response")
475513
http.Error(recordingWriter, "Failed to marshal response into json", http.StatusInternalServerError)
476514
return
477515
}
478516
if _, err = w.Write(rawJSON); err != nil {
479-
p.log.Trace().Err(err).
517+
dynamicLogger.Debug().Err(err).
480518
RawJSON("Response", rawJSON).
481-
Str("Remote Addr", r.RemoteAddr).
482-
Str("Path", r.URL.Path).
483-
Str("Method", r.Method).
484519
Msg("Failed to write response")
485520
http.Error(recordingWriter, "Failed to write JSON response", http.StatusInternalServerError)
486521
return
487522
}
488-
p.log.Trace().
489-
Str("Remote Addr", r.RemoteAddr).
523+
dynamicLogger.Debug().
490524
RawJSON("Response", rawJSON).
491-
Str("Path", r.URL.Path).
492-
Str("Method", r.Method).
493525
Msg("Returned JSON response")
526+
recordingWriter.WriteHeader(route.ResponseStatusCode)
494527
return
495528
}
496529

497-
p.log.Error().Str("Remote Addr", r.RemoteAddr).Str("Path", r.URL.Path).Str("Method", r.Method).Msg("Route has no response")
530+
dynamicLogger.Error().Msg("Route has no response")
531+
http.Error(recordingWriter, "Route has no response", http.StatusInternalServerError)
498532
}
499533

500534
// load loads all registered routes from a file.
@@ -554,12 +588,15 @@ func (p *Server) save() error {
554588
func (p *Server) sendToRecorders(routeCall *RouteCall) {
555589
p.recordersMu.RLock()
556590
defer p.recordersMu.RUnlock()
591+
if len(p.recorderHooks) == 0 {
592+
return
593+
}
557594

558595
client := resty.New()
596+
p.log.Trace().Strs("Recorders", p.recorderHooks).Str("Route ID", routeCall.RouteID).Msg("Sending route call to recorders")
559597

560598
for _, hook := range p.recorderHooks {
561599
go func(hook string) {
562-
p.log.Trace().Str("Recorder Hook", hook).Msg("Sending route call to recorder")
563600
resp, err := client.R().SetBody(routeCall).Post(hook)
564601
if err != nil {
565602
p.log.Error().Err(err).Str("Recorder Hook", hook).Msg("Failed to send route call to recorder")
@@ -573,11 +610,30 @@ func (p *Server) sendToRecorders(routeCall *RouteCall) {
573610
Msg("Failed to send route call to recorder")
574611
return
575612
}
576-
p.log.Debug().Str("Recorder Hook", hook).Msg("Route call sent to recorder")
613+
p.log.Trace().Str("Route ID", routeCall.RouteID).Str("Recorder Hook", hook).Msg("Route call sent to recorder")
577614
}(hook)
578615
}
579616
}
580617

618+
func (p *Server) loggingMiddleware(next http.Handler) http.Handler {
619+
h := hlog.NewHandler(p.log)
620+
621+
accessHandler := hlog.AccessHandler(
622+
func(r *http.Request, status, size int, duration time.Duration) {
623+
hlog.FromRequest(r).Trace().
624+
Str("Method", r.Method).
625+
Stringer("URL", r.URL).
626+
Int("Status Code", status).
627+
Int("Response Size Bytes", size).
628+
Str("Duration", duration.String()).
629+
Str("Remote Addr", r.RemoteAddr).
630+
Msg("Handled request")
631+
},
632+
)
633+
634+
return h(accessHandler(next))
635+
}
636+
581637
var pathRegex = regexp.MustCompile(`^\/[a-zA-Z0-9\-._~%!$&'()*+,;=:@\/]*$`)
582638

583639
func isValidPath(path string) bool {

0 commit comments

Comments
 (0)