summaryrefslogtreecommitdiffstats
path: root/logging/http/http.go
blob: 04b2e0c2cb1b8e22efa13db81fb8abc790349e0f (plain) (blame)
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),
		)
	}
}