# HG changeset patch
# User Christophe de Vienne <christophe@cdevienne.info>
# Date 1602692921 -7200
#      Wed Oct 14 18:28:41 2020 +0200
# Node ID 1ed99df25b0ed1e16c0dab292edbcde5c2e01627
# Parent  9363ce22972fae5993e1538d91fae4bf08482c40
Add panic and log middlewares

diff --git a/go.mod b/go.mod
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@
 	github.com/golang-migrate/migrate/v4 v4.12.2
 	github.com/jmoiron/sqlx v1.2.1-0.20200615141059-0794cb1f47ee
 	github.com/json-iterator/go v1.1.6
+	github.com/justinas/alice v1.2.0
 	github.com/lann/builder v0.0.0-20180802200727-47ae307949d0
 	github.com/orus-io/go-flags v1.4.0
 	github.com/rs/zerolog v1.19.0
diff --git a/go.sum b/go.sum
--- a/go.sum
+++ b/go.sum
@@ -347,6 +347,8 @@
 github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
 github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
 github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
+github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
+github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
 github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
 github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
@@ -445,6 +447,7 @@
 github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
 github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
 github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
 github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
diff --git a/log_middlewares.go b/log_middlewares.go
new file mode 100644
--- /dev/null
+++ b/log_middlewares.go
@@ -0,0 +1,41 @@
+package orusapi
+
+import (
+	"net/http"
+	"time"
+
+	"github.com/justinas/alice"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/hlog"
+)
+
+// LogStack ...
+func LogStack(log zerolog.Logger, status4XXLogLevel zerolog.Level) []alice.Constructor {
+	return []alice.Constructor{
+		hlog.NewHandler(log),
+		CatchPanics,
+		hlog.RemoteAddrHandler("ip"),
+		hlog.UserAgentHandler("user_agent"),
+		hlog.RefererHandler("referer"),
+		hlog.RequestIDHandler("req_id", "Request-Id"),
+		hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
+			log := hlog.FromRequest(r)
+			var event *zerolog.Event
+			if status >= http.StatusInternalServerError {
+				event = log.Error()
+			} else if status >= http.StatusBadRequest {
+				event = log.WithLevel(status4XXLogLevel)
+			} else {
+				event = log.Info()
+			}
+			event.
+				Dict("request", zerolog.Dict().
+					Str("method", r.Method).
+					Str("url", r.URL.String())).
+				Int("status", status).
+				Int("size", size).
+				Dur("duration", duration).
+				Msg(http.StatusText(status))
+		}),
+	}
+}
diff --git a/panic_middleware.go b/panic_middleware.go
new file mode 100644
--- /dev/null
+++ b/panic_middleware.go
@@ -0,0 +1,42 @@
+package orusapi
+
+import (
+	"fmt"
+	"net/http"
+	"runtime/debug"
+
+	"github.com/getsentry/sentry-go"
+	jsoniter "github.com/json-iterator/go"
+	"github.com/rs/zerolog"
+)
+
+// CatchPanics catches the panics and log them as errors
+func CatchPanics(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		defer func() {
+			if r := recover(); r != nil {
+				log := zerolog.Ctx(req.Context())
+
+				exc := zerolog.Dict().
+					Str("value", fmt.Sprintf("%#v", r)).
+					Str("type", fmt.Sprintf("%T", r))
+
+				stack := sentry.NewStacktrace()
+				stack.Frames = stack.Frames[:len(stack.Frames)-1]
+				b, err := jsoniter.Marshal(stack)
+				if err != nil {
+					log.Err(err).Msg("Could not marshal stacktrace, fallback to plain debug.Stack()")
+					b = debug.Stack()
+					exc = exc.Str("plainstack", string(b))
+				} else {
+					exc = exc.RawJSON("stacktrace", b)
+				}
+
+				log.Error().Dict("exception", exc).Msg("Caught a panic")
+
+				rw.WriteHeader(http.StatusInternalServerError)
+			}
+		}()
+		next.ServeHTTP(rw, req)
+	})
+}