package observability import ( "context" "log/slog" "net" "net/http" "runtime/debug" "time" "github.com/google/uuid" ) type contextKey string const requestIDContextKey contextKey = "request_id" func Chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler { for i := len(middlewares) - 1; i >= 0; i-- { h = middlewares[i](h) } return h } func RequestID() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requestID := r.Header.Get("X-Request-Id") if requestID == "" { requestID = uuid.NewString() } w.Header().Set("X-Request-Id", requestID) ctx := context.WithValue(r.Context(), requestIDContextKey, requestID) next.ServeHTTP(w, r.WithContext(ctx)) }) } } func Recover(log *slog.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 recovered := recover(); recovered != nil { log.Error("panic recovered", slog.Any("panic", recovered), slog.String("request_id", RequestIDFromContext(r.Context())), slog.String("stack", string(debug.Stack())), ) http.Error(w, "internal server error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } } func AccessLog(log *slog.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { recorder := &statusRecorder{ResponseWriter: w, status: http.StatusOK} started := time.Now() next.ServeHTTP(recorder, r) log.Info("http request", slog.String("request_id", RequestIDFromContext(r.Context())), slog.String("method", r.Method), slog.String("path", r.URL.Path), slog.Int("status", recorder.status), slog.Duration("duration", time.Since(started)), slog.String("remote_addr", stripPort(r.RemoteAddr)), ) }) } } func Timeout(timeout time.Duration) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { if timeout <= 0 { return next } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), timeout) defer cancel() next.ServeHTTP(w, r.WithContext(ctx)) }) } } func RequestIDFromContext(ctx context.Context) string { value, _ := ctx.Value(requestIDContextKey).(string) return value } type statusRecorder struct { http.ResponseWriter status int } func (s *statusRecorder) WriteHeader(statusCode int) { s.status = statusCode s.ResponseWriter.WriteHeader(statusCode) } func stripPort(remote string) string { host, _, err := net.SplitHostPort(remote) if err != nil { return remote } return host }