96 lines
2.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"bufio"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"time"
)
// requestLoggingMiddleware логирует каждый HTTP-запрос с Request ID, методом, путём, статусом и длительностью.
func requestLoggingMiddleware(logFormat string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Не логируем health-check запросы и OPTIONS
if r.URL.Path == "/api/health/live" || r.URL.Path == "/api/health/ready" || r.Method == "OPTIONS" {
next.ServeHTTP(w, r)
return
}
reqID := generateRequestID()
r.Header.Set("X-Request-ID", reqID)
w.Header().Set("X-Request-ID", reqID)
start := time.Now()
lrw := &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(lrw, r)
duration := time.Since(start)
// Извлекаем пользователя из контекста (может быть nil)
userStr := "-"
if user := GetUserFromContext(r.Context()); user != nil {
userStr = user.Username
}
switch logFormat {
case "json":
entry := map[string]interface{}{
"ts": start.UTC().Format(time.RFC3339),
"id": reqID,
"method": r.Method,
"path": r.URL.Path,
"status": lrw.statusCode,
"duration_ms": float64(duration.Microseconds()) / 1000.0,
"remote_addr": r.RemoteAddr,
"user": userStr,
}
data, _ := json.Marshal(entry)
log.Printf("%s", string(data))
default:
log.Printf("[%s] %s %s %d %.2fms %s user=%s",
reqID, r.Method, r.URL.Path, lrw.statusCode,
float64(duration.Microseconds())/1000.0, r.RemoteAddr, userStr)
}
})
}
}
// generateRequestID генерирует случайный Request ID: req-XXXXXXXX.
func generateRequestID() string {
b := make([]byte, 6)
if _, err := rand.Read(b); err != nil {
return "req-" + hex.EncodeToString([]byte{0, 0, 0, 0, 0, 0})
}
return "req-" + hex.EncodeToString(b)
}
// loggingResponseWriter оборачивает http.ResponseWriter для перехвата статус-кода.
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
func (w *loggingResponseWriter) WriteHeader(code int) {
w.statusCode = code
w.ResponseWriter.WriteHeader(code)
}
func (w *loggingResponseWriter) Write(b []byte) (int, error) {
return w.ResponseWriter.Write(b)
}
func (w *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hijacker, ok := w.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, fmt.Errorf("loggingResponseWriter: underlying ResponseWriter does not implement http.Hijacker")
}
return hijacker.Hijack()
}