1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
// Package http provides HTTP middleware and utilities, including request logging using slog.
//
// The LoggingHandler middleware logs HTTP request details, response status, bytes written,
// and handles panics gracefully with stack trace logging.
package http
import (
"bufio"
"log/slog"
"net"
"net/http"
"time"
"go.sudomsg.com/kit/logging"
)
type tracingWriter struct {
http.ResponseWriter
StatusCode int
BytesWritten int
Hijacked bool
}
var (
_ http.ResponseWriter = &tracingWriter{}
_ http.Hijacker = &tracingWriter{}
_ interface{ Unwrap() http.ResponseWriter } = &tracingWriter{}
)
func (tw *tracingWriter) WriteHeader(code int) {
tw.ResponseWriter.WriteHeader(code)
tw.StatusCode = code
}
func (tw *tracingWriter) Write(b []byte) (int, error) {
if tw.StatusCode == 0 {
tw.WriteHeader(http.StatusOK)
}
n, err := tw.ResponseWriter.Write(b)
tw.BytesWritten += n
return n, err
}
func (tw *tracingWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
rc := http.NewResponseController(tw.ResponseWriter)
conn, rw, err := rc.Hijack()
if err == nil {
tw.Hijacked = true
}
return conn, rw, err
}
func (tw *tracingWriter) Unwrap() http.ResponseWriter {
return tw.ResponseWriter
}
// LoggingHandler is an HTTP middleware that logs requests and responses using slog.
//
// It recovers panics during request processing, logs them with stack traces,
// and returns a 500 Internal Server Error if the connection is not hijacked.
type LoggingHandler struct {
next http.Handler
}
var _ http.Handler = &LoggingHandler{}
// New creates a new LoggingHandler wrapping the next http.Handler, using the provided slog.Logger.
//
// If logger is nil, it attempts to obtain one from the request context.
func New(next http.Handler) *LoggingHandler {
return &LoggingHandler{
next: next,
}
}
// ServeHTTP implements the http.Handler interface.
//
// It logs the HTTP method, path, client IP, protocol, host, user-agent, referer,
// request latency, response status, and bytes written.
// It also recovers panics, logs them, and returns HTTP 500 if appropriate.
func (h *LoggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
logger := logging.FromContext(ctx)
logger = logger.With(
slog.String("method", r.Method),
slog.String("path", r.URL.RequestURI()),
slog.String("ip", r.RemoteAddr),
)
ctx = logging.WithLogger(ctx, logger)
r = r.WithContext(ctx)
tw := &tracingWriter{ResponseWriter: w}
defer func() {
if err := recover(); err != nil {
logging.RecoverAndLog(ctx, "http panic", err)
if !tw.Hijacked && tw.StatusCode == 0 {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}
}()
start := time.Now()
h.next.ServeHTTP(tw, r)
if !tw.Hijacked {
latency := time.Since(start)
logger.LogAttrs(r.Context(), slog.LevelInfo, "HTTP request Handled",
slog.String("proto", r.Proto),
slog.String("host", r.Host),
slog.String("user-agent", r.UserAgent()),
slog.String("referer", r.Referer()),
slog.Duration("latency", latency),
slog.Int("status", tw.StatusCode),
slog.Int("bytes", tw.BytesWritten),
)
}
}
|