package httpapi import ( "fmt" "net/http" "os" "path/filepath" "strings" "sync" "time" ) // WithRequestLogging wraps h with a handler that logs each request to dir/access.log. // Uses dir "var/logs" by default. Returns h unchanged if dir is empty. func WithRequestLogging(dir string, h http.Handler) (http.Handler, error) { if dir == "" { return h, nil } if err := os.MkdirAll(dir, 0755); err != nil { return nil, err } f, err := os.OpenFile(filepath.Join(dir, "access.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return nil, err } return &requestLogger{handler: h, file: f}, nil } type requestLogger struct { handler http.Handler file *os.File mu sync.Mutex } func (l *requestLogger) ServeHTTP(w http.ResponseWriter, r *http.Request) { start := time.Now() ip := clientIP(r) method := r.Method path := r.URL.Path userAgent := r.Header.Get("User-Agent") if userAgent == "" { userAgent = "-" } wrapped := &responseRecorder{ResponseWriter: w, status: http.StatusOK} l.handler.ServeHTTP(wrapped, r) elapsed := time.Since(start).Milliseconds() line := formatLogLine(ip, method, path, wrapped.status, elapsed, strings.ReplaceAll(userAgent, "\"", "'")) l.mu.Lock() _, _ = l.file.WriteString(line) l.mu.Unlock() } type responseRecorder struct { http.ResponseWriter status int } func (r *responseRecorder) WriteHeader(code int) { r.status = code r.ResponseWriter.WriteHeader(code) } func formatLogLine(ip, method, path string, status int, elapsedMs int64, userAgent string) string { // Common log format: ip - - [timestamp] "method path protocol" status size "referer" "user-agent" elapsed_ms t := time.Now().UTC().Format("02/Jan/2006:15:04:05 -0700") return fmt.Sprintf("%s - - [%s] \"%s %s\" %d %d \"-\" \"%s\" %dms\n", ip, t, method, path, status, 0, userAgent, elapsedMs) } func clientIP(r *http.Request) string { if xff := r.Header.Get("X-Forwarded-For"); xff != "" { // First IP in the list is the original client for i := 0; i < len(xff); i++ { if xff[i] == ',' { return xff[:i] } } return xff } if xri := r.Header.Get("X-Real-IP"); xri != "" { return xri } host, _ := splitHostPort(r.RemoteAddr) return host } func splitHostPort(addr string) (host, port string) { if idx := strings.LastIndex(addr, ":"); idx >= 0 { return addr[:idx], addr[idx+1:] } return addr, "" }