96 lines
2.8 KiB
Go
96 lines
2.8 KiB
Go
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()
|
||
}
|