data layer
This commit is contained in:
111
data-layer/api/internal/middleware/middleware.go
Normal file
111
data-layer/api/internal/middleware/middleware.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// Package middleware provides chi-compatible HTTP middleware:
|
||||
// request-id, panic recovery, structured logging, CORS.
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ctxKey string
|
||||
|
||||
const ctxKeyRequestID ctxKey = "request_id"
|
||||
|
||||
// RequestID assigns a uuid v4 to each request and stores it in context.
|
||||
func RequestID(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.Header.Get("X-Request-Id")
|
||||
if id == "" {
|
||||
id = uuid.NewString()
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), ctxKeyRequestID, id)
|
||||
w.Header().Set("X-Request-Id", id)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func RequestIDFromCtx(ctx context.Context) string {
|
||||
v, _ := ctx.Value(ctxKeyRequestID).(string)
|
||||
return v
|
||||
}
|
||||
|
||||
// Recover handles panics so a buggy handler can't take down the server.
|
||||
func Recover(log *zap.Logger) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
log.Error("panic in handler",
|
||||
zap.Any("panic", rec),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.ByteString("stack", debug.Stack()))
|
||||
http.Error(w, `{"error":"internal server error"}`, http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Logger logs one structured line per request.
|
||||
func Logger(log *zap.Logger) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
rw := &statusRecorder{ResponseWriter: w, status: 200}
|
||||
next.ServeHTTP(rw, r)
|
||||
log.Info("http",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.Int("status", rw.status),
|
||||
zap.Int64("duration_ms", time.Since(start).Milliseconds()),
|
||||
zap.String("request_id", RequestIDFromCtx(r.Context())),
|
||||
zap.String("ip", clientIP(r)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// CORS returns a permissive CORS handler. The Analytics console calls the API
|
||||
// directly from the browser during development.
|
||||
func CORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Request-Id")
|
||||
w.Header().Set("Access-Control-Max-Age", "86400")
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func clientIP(r *http.Request) string {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
if i := strings.Index(xff, ","); i >= 0 {
|
||||
return strings.TrimSpace(xff[:i])
|
||||
}
|
||||
return strings.TrimSpace(xff)
|
||||
}
|
||||
if rip := r.Header.Get("X-Real-Ip"); rip != "" {
|
||||
return rip
|
||||
}
|
||||
return r.RemoteAddr
|
||||
}
|
||||
|
||||
type statusRecorder struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (s *statusRecorder) WriteHeader(code int) {
|
||||
s.status = code
|
||||
s.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
Reference in New Issue
Block a user