@@ -0,0 +1,95 @@
|
||||
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, ""
|
||||
}
|
||||
Reference in New Issue
Block a user