254 lines
6.4 KiB
Go
254 lines
6.4 KiB
Go
package router
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
_ "net/http/pprof" // debug
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/pkg/errors"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
)
|
|
|
|
// swagger:model ErrorResponßse
|
|
type ErrorResponse struct {
|
|
Err string `json:"error"`
|
|
}
|
|
|
|
type BuildInfo struct {
|
|
Start time.Time `json:"-"`
|
|
Uptime string `json:"uptime,omitempty"`
|
|
Version string `json:"version,omitempty"`
|
|
BuildDate string `json:"build_date,omitempty"`
|
|
BuildHost string `json:"build_host,omitempty"`
|
|
GitURL string `json:"git_url,omitempty"`
|
|
Branch string `json:"branch,omitempty"`
|
|
Debug bool `json:"debug"`
|
|
}
|
|
|
|
type metrics struct {
|
|
InFlight prometheus.Gauge
|
|
Counter *prometheus.CounterVec
|
|
Duration *prometheus.HistogramVec
|
|
}
|
|
|
|
var m *metrics
|
|
|
|
func init() {
|
|
m = &metrics{
|
|
InFlight: prometheus.NewGauge(
|
|
prometheus.GaugeOpts{
|
|
Name: "http_requests_in_flight",
|
|
Help: "In Flight HTTP requests.",
|
|
},
|
|
),
|
|
Counter: prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "http_requests_total",
|
|
Help: "Counter of HTTP requests.",
|
|
},
|
|
[]string{"handler", "code", "method"},
|
|
),
|
|
Duration: prometheus.NewHistogramVec(
|
|
prometheus.HistogramOpts{
|
|
Name: "http_request_duration_seconds",
|
|
Help: "Histogram of latencies for HTTP requests.",
|
|
Buckets: []float64{.01, .05, .1, .2, .4, 1, 3, 8, 20, 60, 120},
|
|
},
|
|
[]string{"handler", "code", "method"},
|
|
),
|
|
}
|
|
m.register()
|
|
}
|
|
|
|
func (m *metrics) handler(path string, handler http.Handler) (string, http.Handler) {
|
|
return path,
|
|
promhttp.InstrumentHandlerCounter(m.Counter.MustCurryWith(prometheus.Labels{"handler": path}),
|
|
promhttp.InstrumentHandlerInFlight(m.InFlight,
|
|
promhttp.InstrumentHandlerDuration(m.Duration.MustCurryWith(prometheus.Labels{"handler": path}),
|
|
handler,
|
|
),
|
|
),
|
|
)
|
|
}
|
|
|
|
func (m *metrics) handlerFunc(path string, f http.HandlerFunc) (string, http.HandlerFunc) {
|
|
return path,
|
|
promhttp.InstrumentHandlerCounter(m.Counter.MustCurryWith(prometheus.Labels{"handler": path}),
|
|
promhttp.InstrumentHandlerInFlight(m.InFlight,
|
|
promhttp.InstrumentHandlerDuration(m.Duration.MustCurryWith(prometheus.Labels{"handler": path}),
|
|
f,
|
|
),
|
|
),
|
|
)
|
|
}
|
|
|
|
func (m *metrics) register() {
|
|
prometheus.MustRegister(m.InFlight, m.Counter, m.Duration)
|
|
}
|
|
|
|
func recovery(h http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
defer func() {
|
|
defer r.Body.Close()
|
|
if r := recover(); r != nil {
|
|
var err error
|
|
switch t := r.(type) {
|
|
case string:
|
|
err = errors.New(t)
|
|
case error:
|
|
err = errors.WithStack(t)
|
|
default:
|
|
err = errors.New("unknown error")
|
|
}
|
|
log.Error().Stack().Caller().Err(err).Msg("an unexpected error occurred")
|
|
|
|
notify(err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}()
|
|
h.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func notify(err error) {
|
|
// stack := pkgerrors.MarshalStack(err)
|
|
// TODO: send error notification with stacktrace
|
|
}
|
|
|
|
type Router struct {
|
|
Info *BuildInfo
|
|
mux *mux.Router
|
|
metrics *metrics
|
|
}
|
|
|
|
// GetRoute ...
|
|
func (r *Router) GetRoute(name string) *mux.Route {
|
|
return r.mux.GetRoute(name)
|
|
}
|
|
|
|
// Handle implements http.Handler.
|
|
func (r *Router) Handle(path string, handler http.Handler) *mux.Route {
|
|
handler = recovery(handler)
|
|
return r.mux.Handle(path, handler)
|
|
}
|
|
|
|
// HandleFunc implements http.Handler.
|
|
func (r *Router) HandleFunc(path string, f http.HandlerFunc) *mux.Route {
|
|
return r.Handle(path, f)
|
|
}
|
|
|
|
// Handle implements http.Handler wrapping handler with m.
|
|
func (r *Router) HandleWithMetrics(path string, handler http.Handler) *mux.Route {
|
|
handler = recovery(handler)
|
|
return r.mux.Handle(r.metrics.handler(path, handler))
|
|
}
|
|
|
|
// HandleFunc implements http.Handler wrapping handler func with m.
|
|
func (r *Router) HandleFuncWithMetrics(path string, f http.HandlerFunc) *mux.Route {
|
|
return r.HandleWithMetrics(path, f)
|
|
}
|
|
|
|
// ServeHTTP implements http.Handler.
|
|
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
r.mux.ServeHTTP(w, req)
|
|
}
|
|
|
|
func (r *Router) PathPrefix(prefix string) *mux.Route {
|
|
return r.mux.PathPrefix(prefix)
|
|
}
|
|
|
|
// New returns a new Router.
|
|
func NewRouter(buildinfo *BuildInfo) *Router {
|
|
router := &Router{
|
|
Info: buildinfo,
|
|
mux: mux.NewRouter(),
|
|
metrics: m,
|
|
}
|
|
|
|
router.Handle("/info", info(router.Info)).Methods(http.MethodGet, http.MethodHead).Name("INFO")
|
|
router.Handle("/health", health()).Methods(http.MethodGet, http.MethodHead).Name("HEALTH")
|
|
router.Handle("/metrics", promhttp.Handler()).Name("METRICS")
|
|
|
|
if buildinfo.Debug {
|
|
log.Warn().Msg("pprof enabled")
|
|
router.mux.PathPrefix("/debug/pprof").Handler(http.DefaultServeMux).Name("PPROF")
|
|
go func() {
|
|
log.Error().Err(http.ListenAndServe("localhost:6060", nil)).Send()
|
|
}()
|
|
}
|
|
|
|
return router
|
|
}
|
|
|
|
func health() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
Respond(w, http.StatusOK, []byte(http.StatusText(http.StatusOK)))
|
|
}
|
|
}
|
|
|
|
func info(buildinfo *BuildInfo) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
buildinfo.Uptime = time.Now().Sub(buildinfo.Start).String()
|
|
RespondWithJSON(w, http.StatusOK, buildinfo)
|
|
}
|
|
}
|
|
|
|
func RespondWithError(w http.ResponseWriter, code int, err error) {
|
|
RespondWithJSON(w, code, ErrorResponse{Err: err.Error()})
|
|
}
|
|
|
|
func RespondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
|
|
body, _ := json.Marshal(payload)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
Respond(w, code, body)
|
|
}
|
|
|
|
func Respond(w http.ResponseWriter, code int, body []byte) {
|
|
w.WriteHeader(code)
|
|
w.Write(body)
|
|
}
|
|
|
|
type ReadyChecker interface {
|
|
Ready() bool
|
|
}
|
|
|
|
func Ready(dependencies ...ReadyChecker) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ready := []byte("OK")
|
|
numDeps := len(dependencies)
|
|
if numDeps == 0 {
|
|
Respond(w, http.StatusOK, ready)
|
|
return
|
|
}
|
|
wg := sync.WaitGroup{}
|
|
checks := make(chan bool, numDeps)
|
|
for _, dep := range dependencies {
|
|
wg.Add(1)
|
|
if dep != nil {
|
|
go func(d ReadyChecker) {
|
|
checks <- d.Ready()
|
|
wg.Done()
|
|
}(dep)
|
|
}
|
|
}
|
|
wg.Wait()
|
|
close(checks)
|
|
for ok := range checks {
|
|
if !ok {
|
|
Respond(w, http.StatusServiceUnavailable, nil)
|
|
return
|
|
}
|
|
}
|
|
Respond(w, http.StatusOK, ready)
|
|
return
|
|
}
|
|
}
|