Skip to content

Commit

Permalink
custom response writer and logger
Browse files Browse the repository at this point in the history
  • Loading branch information
adoublef committed Feb 20, 2024
1 parent a3bc6f5 commit 2304e28
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 0 deletions.
19 changes: 19 additions & 0 deletions net/http/httputil/chain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package httputil

import "net/http"

// Chain applies middlewares to a http.Handler
func Chain(h http.Handler, ff ...func(http.Handler) http.Handler) http.Handler {
for _, f := range ff {
h = f(h)
}
return h
}

// ChainFunc applies middlewares to a http.HandlerFunc
func ChainFunc(hf http.HandlerFunc, ff ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc {
for _, f := range ff {
hf = f(hf)
}
return hf
}
50 changes: 50 additions & 0 deletions net/http/httputil/hlog/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package hlog

import (
"log/slog"
"net/http"
"time"

"go.adoublef.dev/sdk/net/http/httputil"
)

// Log wraps a http.Handler with a request logger.
func Log(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rw := httputil.Wrap(w, r)

start := time.Now()
defer func() {
duration := time.Since(start)

slog.LogAttrs(
r.Context(),
statusLevel(rw.Status()),
"http request",
slog.String("method", r.Method),
slog.Int64("time_ms", int64(duration/time.Millisecond)),
slog.String("path", r.URL.Path),
slog.Int("status", rw.Status()),
slog.String("duration", duration.String()),
)
}()

h.ServeHTTP(rw, r)
})
}

func statusLevel(status int) slog.Level {
switch {
case status <= 0:
return slog.LevelWarn
case status < 400: // for codes in 100s, 200s, 300s
return slog.LevelInfo
case status >= 400 && status < 500:
// switching to info level to be less noisy
return slog.LevelInfo
case status >= 500:
return slog.LevelError
default:
return slog.LevelInfo
}
}
107 changes: 107 additions & 0 deletions net/http/httputil/response_writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package httputil

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

type ResponseWriter interface {
http.ResponseWriter
http.Flusher
// Status returns the status code of the response or 0 if the response has not been written.
Status() int
// Written returns whether or not the ResponseWriter has been written.
Written() bool
// Size returns the size of the response body.
Size() int
Unwrap() http.ResponseWriter
}

type response struct {
http.ResponseWriter
method string
status int
size int
}

// Size implements ResponseWriter.
func (rw *response) Size() int {
return rw.size
}

// Unwrap implements ResponseWriter.
func (rw *response) Unwrap() http.ResponseWriter {
return rw.ResponseWriter
}

// Written implements ResponseWriter.
func (rw *response) Written() bool {
return rw.status != 0
}

// Status implements ResponseWriter.
func (rw *response) Status() int {
return rw.status
}

// Write implements ResponseWriter.
// Subtle: this method shadows the method (ResponseWriter).Write of response.ResponseWriter.
func (rw *response) Write(b []byte) (size int, err error) {
if !rw.Written() {
// The status will be StatusOK if WriteHeader has not been called yet
rw.WriteHeader(http.StatusOK)
}
if rw.method != http.MethodHead {
size, err = rw.ResponseWriter.Write(b)
rw.size += size
}
return size, err
}

// WriteHeader implements ResponseWriter.
// Subtle: this method shadows the method (ResponseWriter).WriteHeader of response.ResponseWriter.
func (rw *response) WriteHeader(s int) {
// Avoid panic if status code is not a valid HTTP status code
if s < 100 || s > 999 {
rw.ResponseWriter.WriteHeader(500)
rw.status = 500
return
}

rw.ResponseWriter.WriteHeader(s)
rw.status = s
}

func (rw *response) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hijacker, ok := rw.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, ErrHijackUnsupported
}

conn, brw, err := hijacker.Hijack()
if err == nil {
rw.status = -1
}

return conn, brw, err
}

func (rw *response) Flush() {
if flusher, ok := rw.ResponseWriter.(http.Flusher); ok {
flusher.Flush()
}
}

// Wrap
func Wrap(w http.ResponseWriter, r *http.Request) ResponseWriter {
if rw, ok := w.(ResponseWriter); ok {
return rw
}
return &response{w, r.Method, 0, 0}
}

var (
ErrHijackUnsupported = errors.New("the ResponseWriter doesn't support the Hijacker interface")
)

0 comments on commit 2304e28

Please sign in to comment.