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