v2.2.7 aefe19e

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.
50func RequestLogger(log zerowrap.Logger) func(http.Handler) http.Handler {
51	return func(next http.Handler) http.Handler {
52		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
53			start := time.Now()
54
55			// Wrap the response writer to capture status and bytes
56			rw := NewResponseWriter(w)
57
58			// Get client IP (handle X-Forwarded-For and X-Real-IP headers)
59			clientIP := getClientIP(r)
60
61			// Call the next handler
62			next.ServeHTTP(rw, r)
63
64			// Calculate duration
65			duration := time.Since(start)
66
67			// Log the request
68			log.Info().
69				Str(zerowrap.FieldLayer, "adapter").
70				Str(zerowrap.FieldAdapter, "http").
71				Str(zerowrap.FieldMethod, r.Method).
72				Str(zerowrap.FieldPath, r.URL.Path).
73				Str("query", r.URL.RawQuery).
74				Str(zerowrap.FieldHost, r.Host).
75				Str("user_agent", r.UserAgent()).
76				Str("referer", r.Referer()).
77				Str(zerowrap.FieldClientIP, clientIP).
78				Int(zerowrap.FieldStatus, rw.StatusCode()).
79				Int("bytes", rw.BytesWritten()).
80				Dur(zerowrap.FieldDuration, duration).
81				Str("proto", r.Proto).
82				Msg("HTTP request")
83		})
84	}
85}
86
87// getClientIP extracts the client IP from various headers.
88func getClientIP(r *http.Request) string {
89	// Check X-Forwarded-For header (comma-separated list, first is original client)
90	if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
91		// Take the first IP (original client)
92		for idx := 0; idx < len(xff); idx++ {
93			if xff[idx] == ',' {
94				return xff[:idx]
95			}
96		}
97		return xff
98	}
99
100	// Check X-Real-IP header
101	if xri := r.Header.Get("X-Real-IP"); xri != "" {
102		return xri
103	}
104
105	// Check X-Forwarded header (less common)
106	if xf := r.Header.Get("X-Forwarded"); xf != "" {
107		return xf
108	}
109
110	// Fallback to RemoteAddr
111	return r.RemoteAddr
112}
113
114// PanicRecovery middleware recovers from panics and logs them.
115func PanicRecovery(log zerowrap.Logger) func(http.Handler) http.Handler {
116	return func(next http.Handler) http.Handler {
117		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
118			defer func() {
119				if err := recover(); err != nil {
120					log.Error().
121						Str(zerowrap.FieldLayer, "adapter").
122						Str(zerowrap.FieldAdapter, "http").
123						Interface("panic", err).
124						Str(zerowrap.FieldMethod, r.Method).
125						Str(zerowrap.FieldPath, r.URL.Path).
126						Str(zerowrap.FieldHost, r.Host).
127						Msg("panic recovered")
128
129					http.Error(w, "Internal Server Error", http.StatusInternalServerError)
130				}
131			}()
132
133			next.ServeHTTP(w, r)
134		})
135	}
136}
137
138// CORS middleware adds CORS headers.
139func CORS(next http.Handler) http.Handler {
140	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
141		w.Header().Set("Access-Control-Allow-Origin", "*")
142		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
143		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
144
145		// Handle preflight OPTIONS request
146		if r.Method == "OPTIONS" {
147			w.WriteHeader(http.StatusOK)
148			return
149		}
150
151		next.ServeHTTP(w, r)
152	})
153}
154
155// Chain combines multiple middleware functions.
156func Chain(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler {
157	return func(final http.Handler) http.Handler {
158		for i := len(middlewares) - 1; i >= 0; i-- {
159			final = middlewares[i](final)
160		}
161		return final
162	}
163}

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