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}