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() }