logging.go

 1package web
 2
 3import (
 4	"bufio"
 5	"fmt"
 6	"net"
 7	"net/http"
 8	"time"
 9
10	log "github.com/charmbracelet/log/v2"
11	humanize "github.com/dustin/go-humanize"
12)
13
14// logWriter is a wrapper around http.ResponseWriter that allows us to capture
15// the HTTP status code and bytes written to the response.
16type logWriter struct {
17	http.ResponseWriter
18	code, bytes int
19}
20
21var _ http.ResponseWriter = (*logWriter)(nil)
22
23var _ http.Flusher = (*logWriter)(nil)
24
25var _ http.Hijacker = (*logWriter)(nil)
26
27var _ http.CloseNotifier = (*logWriter)(nil) // nolint: staticcheck
28
29// Write implements http.ResponseWriter.
30func (r *logWriter) Write(p []byte) (int, error) {
31	written, err := r.ResponseWriter.Write(p)
32	r.bytes += written
33	return written, err
34}
35
36// Note this is generally only called when sending an HTTP error, so it's
37// important to set the `code` value to 200 as a default.
38func (r *logWriter) WriteHeader(code int) {
39	r.code = code
40	r.ResponseWriter.WriteHeader(code)
41}
42
43// Unwrap returns the underlying http.ResponseWriter.
44func (r *logWriter) Unwrap() http.ResponseWriter {
45	return r.ResponseWriter
46}
47
48// Flush implements http.Flusher.
49func (r *logWriter) Flush() {
50	if f, ok := r.ResponseWriter.(http.Flusher); ok {
51		f.Flush()
52	}
53}
54
55// CloseNotify implements http.CloseNotifier.
56func (r *logWriter) CloseNotify() <-chan bool {
57	if cn, ok := r.ResponseWriter.(http.CloseNotifier); ok { // nolint: staticcheck
58		return cn.CloseNotify()
59	}
60	return nil
61}
62
63// Hijack implements http.Hijacker.
64func (r *logWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
65	if h, ok := r.ResponseWriter.(http.Hijacker); ok {
66		return h.Hijack()
67	}
68	return nil, nil, fmt.Errorf("http.Hijacker not implemented")
69}
70
71// NewLoggingMiddleware returns a new logging middleware.
72func NewLoggingMiddleware(next http.Handler, logger *log.Logger) http.Handler {
73	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
74		start := time.Now()
75		writer := &logWriter{code: http.StatusOK, ResponseWriter: w}
76		logger.Debug("request",
77			"method", r.Method,
78			"path", r.URL,
79			"addr", r.RemoteAddr)
80		next.ServeHTTP(writer, r)
81		elapsed := time.Since(start)
82		logger.Debug("response",
83			"status", fmt.Sprintf("%d %s", writer.code, http.StatusText(writer.code)),
84			"bytes", humanize.Bytes(uint64(writer.bytes)), //nolint:gosec
85			"time", elapsed)
86	})
87}