agent-orchestrator/backend/internal/httpd/recover.go

69 lines
2.1 KiB
Go

package httpd
import (
"fmt"
"log/slog"
"net/http"
"runtime/debug"
"strings"
"time"
"github.com/go-chi/chi/v5/middleware"
"github.com/aoagents/agent-orchestrator/backend/internal/httpd/envelope"
"github.com/aoagents/agent-orchestrator/backend/internal/ports"
"github.com/aoagents/agent-orchestrator/backend/internal/telemetrymeta"
)
func recoverTelemetry(log *slog.Logger, sink ports.EventSink) func(http.Handler) http.Handler {
log = loggerOrDefault(log)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
stack := string(debug.Stack())
log.Error("http handler panic",
"id", middleware.GetReqID(r.Context()),
"method", r.Method,
"path", r.URL.Path,
"panic", fmt.Sprint(rec),
"stack", stack,
)
if sink != nil {
path := telemetrymeta.RoutePattern(r)
panicKind := telemetrymeta.PanicKind(rec)
sink.Emit(r.Context(), ports.TelemetryEvent{
Name: "ao.daemon.panic",
Source: "http",
OccurredAt: time.Now().UTC(),
Level: ports.TelemetryLevelError,
RequestID: middleware.GetReqID(r.Context()),
Payload: map[string]any{
"component": "httpd",
"operation": "http_request_panic",
"method": r.Method,
"path": path,
"panic_kind": panicKind,
"stack_fingerprint": telemetrymeta.Fingerprint("httpd", "http_request_panic", path, panicKind, stack),
"fingerprint": telemetrymeta.Fingerprint("httpd", "http_request_panic", r.Method, path, panicKind),
},
})
}
writeRecoveredError(w, r)
}
}()
next.ServeHTTP(w, r)
})
}
}
func writeRecoveredError(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/api/") {
envelope.WriteAPIError(w, r, http.StatusInternalServerError, "internal_error", "INTERNAL_ERROR", "Internal server error", nil)
return
}
envelope.WriteJSON(w, http.StatusInternalServerError, map[string]any{
"status": "error",
})
}