From d4e3679e76283b97d336679af0194e8348a47668 Mon Sep 17 00:00:00 2001 From: "Stephen McQuay (smcquay)" Date: Thu, 8 Mar 2018 13:50:12 -0800 Subject: [PATCH] Adds implementation --- metrics.go | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++ resp.go | 29 +++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 metrics.go create mode 100644 resp.go diff --git a/metrics.go b/metrics.go new file mode 100644 index 0000000..b1d667c --- /dev/null +++ b/metrics.go @@ -0,0 +1,101 @@ +package metrics + +import ( + "fmt" + "net/http" + "time" + + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" +) + +// Metrics provides a simple way to track latency and http status +type Metrics struct { + latency *prometheus.SummaryVec + status *prometheus.CounterVec +} + +func New(prefix string) (*Metrics, error) { + m := &Metrics{ + latency: prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Namespace: prefix, + Subsystem: "http", + Name: "request_latency_ms", + Help: "Latency in ms of http requests grouped by req path", + }, []string{"path"}), + + status: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: prefix, + Subsystem: "http", + Name: "status_count", + Help: "The count of http responses issued classified by status and api endpoint", + }, []string{"path", "code"}), + } + if err := m.registerPromMetrics(); err != nil { + return nil, errors.Wrap(err, "registration") + } + return m, nil +} + +// registerPromMetrics registers all the metrics that eventmanger uses. +func (m *Metrics) registerPromMetrics() error { + if err := prometheus.Register(m.latency); err != nil { + return errors.Wrap(err, "http request latency") + } + + if err := prometheus.Register(m.status); err != nil { + return errors.Wrap(err, "http response counter") + } + + return nil +} + +// Wrap calls a http.Handler and tracks status code and latency. +func (m *Metrics) Wrap(prefix string, h http.Handler) http.HandlerFunc { + return m.WrapFunc(prefix, h.ServeHTTP) +} + +// WrapFunc calls a http.Handler and tracks status code and latency. +func (m *Metrics) WrapFunc(prefix string, h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + start := time.Now() + defer func() { + m.HTTPLatency(prefix, start) + }() + + lw := NewStatusRecorder(w) + h(lw, req) + + m.HTTPStatus(prefix, lw.Status()) + } +} + +// HTTPLatency records a request latency for a given url path. +func (m *Metrics) HTTPLatency(path string, start time.Time) { + m.latency.WithLabelValues(path).Observe(msSince(start)) +} + +// HTTPStatus records counts of http statuses, bucketed according to status types. +func (m *Metrics) HTTPStatus(path string, status int) { + m.status.WithLabelValues(path, fmt.Sprintf("%d", bucketHTTPStatus(status))).Inc() +} + +// bucketHTTPStatus rounds down to the nearest hundred to facilitate categorizing http statuses. +func bucketHTTPStatus(i int) int { + return i - i%100 +} + +// msSince returns milliseconds since start. +func msSince(start time.Time) float64 { + return float64(time.Since(start)) / float64(time.Millisecond) +} + +// buckets returns the default prometheus buckets scaled to milliseconds. +func buckets() []float64 { + r := []float64{} + + for _, v := range prometheus.DefBuckets { + r = append(r, v*float64(time.Second/time.Millisecond)) + } + return r +} diff --git a/resp.go b/resp.go new file mode 100644 index 0000000..371a242 --- /dev/null +++ b/resp.go @@ -0,0 +1,29 @@ +package metrics + +import ( + "net/http" +) + +// StatusRecorder is a simple http status recorder +type StatusRecorder struct { + http.ResponseWriter + + status int +} + +// NewStatusRecorder returns an initialized StatusRecorder, with 200 as the +// default status. +func NewStatusRecorder(w http.ResponseWriter) *StatusRecorder { + return &StatusRecorder{ResponseWriter: w, status: http.StatusOK} +} + +// Status returns the cached http status value. +func (sr *StatusRecorder) Status() int { + return sr.status +} + +// WriteHeader caches the status, then calls the underlying ResponseWriter. +func (sr *StatusRecorder) WriteHeader(status int) { + sr.status = status + sr.ResponseWriter.WriteHeader(status) +}