v2.4.0 79b3461

logging.go RawHistoryFolder

1// Package middleware provides HTTP middleware for the adapters layer.
2package middleware
3
4import (
5	"net/http"
6	"time"
7
8	"github.com/bnema/zerowrap"
9)
10
11// ResponseWriter wraps http.ResponseWriter to capture status code and bytes written.
12type ResponseWriter struct {
13	http.ResponseWriter
14	statusCode int
15	bytes      int
16}
17
18// NewResponseWriter creates a new wrapped response writer.
19func NewResponseWriter(w http.ResponseWriter) *ResponseWriter {
20	return &ResponseWriter{
21		ResponseWriter: w,
22		statusCode:     http.StatusOK, // Default status code
23	}
24}
25
26// WriteHeader captures the status code.
27func (rw *ResponseWriter) WriteHeader(code int) {
28	rw.statusCode = code
29	rw.ResponseWriter.WriteHeader(code)
30}
31
32// Write captures bytes written.
33func (rw *ResponseWriter) Write(b []byte) (int, error) {
34	n, err := rw.ResponseWriter.Write(b)
35	rw.bytes += n
36	return n, err
37}
38
39// StatusCode returns the captured status code.
40func (rw *ResponseWriter) StatusCode() int {
41	return rw.statusCode
42}
43
44// BytesWritten returns the number of bytes written.
45func (rw *ResponseWriter) BytesWritten() int {
46	return rw.bytes
47}
48
49// RequestLogger is a middleware that logs HTTP requests using zerowrap.
50// It also attaches the logger to the request context for downstream handlers.
51func RequestLogger(log zerowrap.Logger) func(http.Handler) http.Handler {
52	return func(next http.Handler) http.Handler {
53		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
54			start := time.Now()
55
56			// Wrap the response writer to capture status and bytes
57			rw := NewResponseWriter(w)
58
59			// Get client IP (handle X-Forwarded-For and X-Real-IP headers)
60			clientIP := getClientIP(r)
61
62			// Attach the logger to the request context for downstream handlers
63			ctx := zerowrap.WithCtx(r.Context(), log)
64			r = r.WithContext(ctx)
65
66			// Call the next handler
67			next.ServeHTTP(rw, r)
68
69			// Calculate duration
70			duration := time.Since(start)
71
72			// Log the request
73			log.Info().
74				Str(zerowrap.FieldLayer, "adapter").
75				Str(zerowrap.FieldAdapter, "http").
76				Str(zerowrap.FieldMethod, r.Method).
77				Str(zerowrap.FieldPath, r.URL.Path).
78				Str("query", r.URL.RawQuery).
79				Str(zerowrap.FieldHost, r.Host).
80				Str("user_agent", r.UserAgent()).
81				Str("referer", r.Referer()).
82				Str(zerowrap.FieldClientIP, clientIP).
83				Int(zerowrap.FieldStatus, rw.StatusCode()).
84				Int("bytes", rw.BytesWritten()).
85				Dur(zerowrap.FieldDuration, duration).
86				Str("proto", r.Proto).
87				Msg("HTTP request")
88		})
89	}
90}
91
92// getClientIP extracts the client IP from various headers.
93func getClientIP(r *http.Request) string {
94	// Check X-Forwarded-For header (comma-separated list, first is original client)
95	if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
96		// Take the first IP (original client)
97		for idx := 0; idx < len(xff); idx++ {
98			if xff[idx] == ',' {
99				return xff[:idx]
100			}
101		}
102		return xff
103	}
104
105	// Check X-Real-IP header
106	if xri := r.Header.Get("X-Real-IP"); xri != "" {
107		return xri
108	}
109
110	// Check X-Forwarded header (less common)
111	if xf := r.Header.Get("X-Forwarded"); xf != "" {
112		return xf
113	}
114
115	// Fallback to RemoteAddr
116	return r.RemoteAddr
117}
118
119// PanicRecovery middleware recovers from panics and logs them.
120func PanicRecovery(log zerowrap.Logger) func(http.Handler) http.Handler {
121	return func(next http.Handler) http.Handler {
122		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
123			defer func() {
124				if err := recover(); err != nil {
125					log.Error().
126						Str(zerowrap.FieldLayer, "adapter").
127						Str(zerowrap.FieldAdapter, "http").
128						Interface("panic", err).
129						Str(zerowrap.FieldMethod, r.Method).
130						Str(zerowrap.FieldPath, r.URL.Path).
131						Str(zerowrap.FieldHost, r.Host).
132						Msg("panic recovered")
133
134					http.Error(w, "Internal Server Error", http.StatusInternalServerError)
135				}
136			}()
137
138			next.ServeHTTP(w, r)
139		})
140	}
141}
142
143// CORS middleware adds CORS headers.
144func CORS(next http.Handler) http.Handler {
145	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
146		w.Header().Set("Access-Control-Allow-Origin", "*")
147		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
148		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
149
150		// Handle preflight OPTIONS request
151		if r.Method == "OPTIONS" {
152			w.WriteHeader(http.StatusOK)
153			return
154		}
155
156		next.ServeHTTP(w, r)
157	})
158}
159
160// Chain combines multiple middleware functions.
161func Chain(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler {
162	return func(final http.Handler) http.Handler {
163		for i := len(middlewares) - 1; i >= 0; i-- {
164			final = middlewares[i](final)
165		}
166		return final
167	}
168}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close