package server import ( "net/http" "runtime/debug" "time" "github.com/tiennm99/miti99bot/internal/log" ) // statusRecorder wraps http.ResponseWriter to capture the final status // code. http.ResponseWriter doesn't expose what was written; the middleware // needs the status to log a per-request `req` line. type statusRecorder struct { http.ResponseWriter status int } func (r *statusRecorder) WriteHeader(code int) { r.status = code r.ResponseWriter.WriteHeader(code) } // status returns the recorded status code, defaulting to 200 when no // explicit WriteHeader was called (Go's net/http implicitly writes 200 on // the first body write). func (r *statusRecorder) effectiveStatus() int { if r.status == 0 { return http.StatusOK } return r.status } // LogRequests wraps an http.Handler with a request log line: // // {"msg":"req","method":"GET","path":"/","status":200,"ms":0} // // Keep the field names stable: log-based dashboards and alerts may filter on // msg=req and status>=500. // // The req line is emitted from a deferred closure so a panic in a downstream // handler still produces an observable log entry — http.Server's own recover // only logs to stderr and never runs middleware again on the way out. func LogRequests(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() rec := &statusRecorder{ResponseWriter: w} defer func() { rec.status = recoverPanicStatus(recover(), rec.status) log.Info("req", "method", r.Method, "path", r.URL.Path, "status", rec.effectiveStatus(), "ms", time.Since(start).Milliseconds(), ) }() next.ServeHTTP(rec, r) }) } // recoverPanicStatus folds a recovered panic into the status to log: it // returns 500 if a panic was recovered, otherwise the original status // untouched. The panic is logged with its stack and absorbed, not re-raised, // so the deferred req line always runs. // // Re-panicking would lose the deferred log line in some recover-order edge // cases; absorbing the panic here matches the webhook handler's posture of // "log the failure, keep the goroutine clean". func recoverPanicStatus(rec any, currentStatus int) int { if rec == nil { return currentStatus } log.Error("middleware recovered panic", "panic", rec, "stack", string(debug.Stack())) return http.StatusInternalServerError }