request_log.go

 1package middleware
 2
 3import (
 4	"context"
 5	"log/slog"
 6	"net/http"
 7	"strings"
 8	"time"
 9
10	"git.kilimanjaro.io/rtw/pkg/log"
11)
12
13// RequestLogger sends log lines at the start and completion of a request with details
14// of the request and response (method, path, UA, duration, bytes)
15func RequestLogger(next http.Handler) http.Handler {
16	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17		log := log.FromContext(r.Context())
18		attrs := []slog.Attr{path(r), ua(r)}
19		if len(r.URL.Query()) > 0 {
20			attrs = append(attrs, query(r))
21		}
22		log.LogAttrs(context.Background(), slog.LevelInfo, "incoming request", request(attrs...))
23		start := time.Now()
24
25		ww, ok := w.(WrapResponseWriter)
26		if !ok {
27			ww = NewWrapResponseWriter(w, r.ProtoMajor)
28		}
29
30		next.ServeHTTP(ww, r)
31
32		log.LogAttrs(context.Background(), slog.LevelInfo, "response complete", response(ww, start))
33	})
34}
35
36func duration(d time.Duration) slog.Attr {
37	return slog.Int("duration_ms", int(d)/1_000_000)
38}
39
40func request(attrs ...slog.Attr) slog.Attr {
41	return slog.Any("req", slog.GroupValue(attrs...))
42}
43
44func path(r *http.Request) slog.Attr {
45	return slog.String("path", r.Method+" "+r.URL.Path)
46}
47
48func query(r *http.Request) slog.Attr {
49	q := r.URL.Query()
50	out := make([]string, len(q))
51	for k, v := range q {
52		out = append(out, k+"="+strings.Join(v, ","))
53	}
54	return slog.String("query", strings.Join(out, "; "))
55}
56
57func ua(r *http.Request) slog.Attr {
58	return slog.String("user_agent", r.UserAgent())
59}
60
61func headers(r *http.Request) slog.Attr {
62	out := make([]string, len(r.Header))
63	for k, v := range r.Header {
64		out = append(out, k+"="+strings.Join(v, ","))
65	}
66	return slog.String("headers", strings.Join(out, "; "))
67}
68
69func response(ww WrapResponseWriter, startTime time.Time) slog.Attr {
70	attrs := []slog.Attr{slog.Int("code", ww.Status()), slog.Int("bytes_kb", ww.BytesWritten()/1024), duration(time.Since(startTime))}
71	return slog.Any("resp", slog.GroupValue(attrs...))
72}