Browse Source

Add HTTP panic handling.

Matt Bolt 6 years ago
parent
commit
0bebf8bce7
2 changed files with 56 additions and 4 deletions
  1. 2 1
      cmd/costmodel/main.go
  2. 54 3
      pkg/errors/panic.go

+ 2 - 1
cmd/costmodel/main.go

@@ -5,6 +5,7 @@ import (
 
 	"github.com/julienschmidt/httprouter"
 	"github.com/kubecost/cost-model/pkg/costmodel"
+	"github.com/kubecost/cost-model/pkg/errors"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 	"k8s.io/klog"
 )
@@ -22,5 +23,5 @@ func main() {
 	costmodel.Router.GET("/healthz", Healthz)
 	rootMux.Handle("/", costmodel.Router)
 	rootMux.Handle("/metrics", promhttp.Handler())
-	klog.Fatal(http.ListenAndServe(":9003", rootMux))
+	klog.Fatal(http.ListenAndServe(":9003", errors.PanicHandlerMiddleware(rootMux)))
 }

+ 54 - 3
pkg/errors/panic.go

@@ -2,13 +2,36 @@ package errors
 
 import (
 	"fmt"
+	"net/http"
 	"runtime"
 )
 
+//--------------------------------------------------------------------------
+//  PanicType
+//--------------------------------------------------------------------------
+
+// PanicType defines the context in which the panic occurred
+type PanicType int
+
+const (
+	PanicTypeDefault PanicType = iota
+	PanicTypeHTTP
+)
+
+// The string representation of PanicContext
+func (pt PanicType) String() string {
+	return []string{"PanicTypeDefault", "PanicTypeHTTP"}[pt]
+}
+
+//--------------------------------------------------------------------------
+//  Panic
+//--------------------------------------------------------------------------
+
 // Panic represents a panic that occurred, captured by a recovery.
 type Panic struct {
 	Error interface{}
 	Stack string
+	Type  PanicType
 }
 
 // PanicHandler is a func that receives a Panic and returns a bool representing whether or not
@@ -44,27 +67,55 @@ func SetPanicHandler(handler PanicHandler) error {
 	return nil
 }
 
+// PanicHandlerMiddleware should wrap any of the http handlers to capture panics.
+func PanicHandlerMiddleware(handler http.Handler) http.Handler {
+	return http.HandlerFunc(func(rw http.ResponseWriter, rq *http.Request) {
+		defer HandleHTTPPanic(rw, rq)
+
+		handler.ServeHTTP(rw, rq)
+	})
+}
+
 // HandlePanic should be executed in a deferred method (or deferred directly). It will
 // capture any panics that occur in the goroutine it exists, and report to the registered
 // global panic handler.
 func HandlePanic() {
-	// Do not handle/recover if disabled
+	// NOTE: For each "special" type of panic that is added, you must repeat this pattern. The recover()
+	// NOTE: call cannot exist in a func outside of the deferred func.
 	if !enabled {
 		return
 	}
 
 	if err := recover(); err != nil {
-		dispatch(err)
+		dispatch(err, PanicTypeDefault)
+	}
+}
+
+// HandleHTTPPanic should be executed in a deferred method (or deferred directly) in http middleware.
+// It will capture any panics that occur in the goroutine it exists, and report to the registered
+// global panic handler. HTTP handler panics will have the errors.PanicTypeHTTP Type.
+func HandleHTTPPanic(rw http.ResponseWriter, rq *http.Request) {
+	// NOTE: For each "special" type of panic that is added, you must repeat this pattern. The recover()
+	// NOTE: call cannot exist in a func outside of the deferred func.
+	if !enabled {
+		return
+	}
+
+	if err := recover(); err != nil {
+		rw.WriteHeader(http.StatusInternalServerError)
+
+		dispatch(err, PanicTypeHTTP)
 	}
 }
 
 // generate stacktrace, dispatch the panic via channel
-func dispatch(err interface{}) {
+func dispatch(err interface{}, panicType PanicType) {
 	stack := make([]byte, 1024*8)
 	stack = stack[:runtime.Stack(stack, false)]
 
 	dispatcher <- Panic{
 		Error: err,
 		Stack: string(stack),
+		Type:  panicType,
 	}
 }