Skip to content

Commit 8fbdd90

Browse files
feat: extended metrics (#66)
* feat: count the API hits for each good route and store in expvar metrics * corrected package comment * moved declaration to pkg level to be compatible with existing tests. * addressing PR comments * addressing PR comments * using github.com/go-kit/kit for metrics * small change * some linter fixes * adjusting tests and linters * simplifying after review * change Response time to Counter * adjusted expvar keys * making sure the chi router is defined only one time. * small change after PR
1 parent 4afa376 commit 8fbdd90

File tree

6 files changed

+106
-58
lines changed

6 files changed

+106
-58
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.12
55
require (
66
github.com/go-chi/chi v4.0.2+incompatible
77
github.com/go-chi/render v1.0.1
8+
github.com/go-kit/kit v0.9.0 // indirect
89
github.com/google/uuid v1.1.1
910
github.com/nsqio/nsq v1.2.0
1011
github.com/optimizely/go-sdk v1.0.0-beta5.0.20191031194604-0f774263df60

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxm
3636
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
3737
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
3838
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
39+
github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
40+
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
3941
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
4042
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
4143
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=

pkg/api/middleware/metrics.go

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,74 @@
1818
package middleware
1919

2020
import (
21-
"expvar"
21+
"context"
2222
"net/http"
23-
"strings"
23+
"time"
2424

25-
"github.com/go-chi/chi"
25+
"github.com/go-kit/kit/metrics"
26+
"github.com/go-kit/kit/metrics/expvar"
2627
)
2728

28-
// HitCount update counts for each URL hit, key being a combination of a method and route pattern
29-
func HitCount(counts *expvar.Map) func(http.Handler) http.Handler {
29+
const metricPrefix = "timers."
30+
31+
type contextString string
32+
33+
const responseTime = contextString("responseTime")
34+
35+
// Metrics struct contains url hit counts, response time and its histogram
36+
type Metrics struct {
37+
HitCounts metrics.Counter
38+
ResponseTime metrics.Counter
39+
ResponseTimeHistogram metrics.Histogram
40+
}
41+
42+
// NewMetrics initialized metrics
43+
func NewMetrics(key string) *Metrics {
44+
45+
uniqueName := metricPrefix + key
46+
47+
return &Metrics{
48+
HitCounts: expvar.NewCounter(uniqueName + ".counts"),
49+
ResponseTime: expvar.NewCounter(uniqueName + ".responseTime"),
50+
ResponseTimeHistogram: expvar.NewHistogram(uniqueName+".responseTimeHist", 50),
51+
}
52+
}
53+
54+
// Metricize updates counts, total response time, and response time histogram
55+
// for each URL hit, key being a combination of a method and route pattern
56+
func Metricize(key string) func(http.Handler) http.Handler {
57+
singleMetric := NewMetrics(key)
58+
3059
f := func(h http.Handler) http.Handler {
60+
3161
fn := func(w http.ResponseWriter, r *http.Request) {
32-
key := r.Method + "_" + strings.ReplaceAll(chi.RouteContext(r.Context()).RoutePattern(), "/", "_")
33-
counts.Add(key, 1)
62+
63+
singleMetric.HitCounts.Add(1)
64+
ctx := r.Context()
65+
startTime, ok := ctx.Value(responseTime).(time.Time)
66+
if ok {
67+
defer func() {
68+
endTime := time.Now()
69+
timeDiff := endTime.Sub(startTime).Seconds()
70+
singleMetric.ResponseTime.Add(timeDiff)
71+
singleMetric.ResponseTimeHistogram.Observe(timeDiff)
72+
}()
73+
}
74+
3475
h.ServeHTTP(w, r)
3576
}
3677
return http.HandlerFunc(fn)
3778
}
3879
return f
3980
}
81+
82+
// SetTime middleware sets the start time in request context
83+
func SetTime(next http.Handler) http.Handler {
84+
85+
fn := func(w http.ResponseWriter, r *http.Request) {
86+
87+
ctx := context.WithValue(r.Context(), responseTime, time.Now())
88+
next.ServeHTTP(w, r.WithContext(ctx))
89+
}
90+
return http.HandlerFunc(fn)
91+
}

pkg/api/middleware/metrics_test.go

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import (
2424
"net/http"
2525
"net/http/httptest"
2626
"testing"
27+
"time"
2728

28-
"github.com/go-chi/chi"
2929
"github.com/stretchr/testify/suite"
3030
)
3131

@@ -43,24 +43,24 @@ type RequestMetrics struct {
4343
handler http.Handler
4444
}
4545

46-
func (rm *RequestMetrics) setRoute(metricsKey string) {
46+
func (rm *RequestMetrics) SetupRoute(key string) {
4747

48-
metricsMap := expvar.NewMap(metricsKey)
4948
rm.rw = httptest.NewRecorder()
5049
r := httptest.NewRequest("GET", "/", nil)
5150

52-
rctx := chi.NewRouteContext()
53-
rctx.RoutePatterns = []string{"/item/{set_item}"}
54-
55-
rm.req = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
56-
rm.handler = http.Handler(HitCount(metricsMap)(getTestMetrics()))
51+
rm.req = r.WithContext(context.WithValue(r.Context(), responseTime, time.Now()))
52+
rm.handler = http.Handler(Metricize(key)(getTestMetrics()))
5753

5854
}
5955

6056
func (rm RequestMetrics) serveRoute() {
6157
rm.handler.ServeHTTP(rm.rw, rm.req)
6258
}
6359

60+
func (rm RequestMetrics) serveSetTimehHandler() {
61+
http.Handler(SetTime(getTestMetrics())).ServeHTTP(rm.rw, rm.req)
62+
}
63+
6464
func (rm RequestMetrics) serveExpvarRoute() {
6565
expvar.Handler().ServeHTTP(rm.rw, rm.req)
6666
}
@@ -77,36 +77,32 @@ func (rm RequestMetrics) getCode() int {
7777
return rm.rw.(*httptest.ResponseRecorder).Code
7878
}
7979

80+
var sufixList = []string{".counts", ".responseTime", ".responseTimeHist.p50", ".responseTimeHist.p90", ".responseTimeHist.p95", ".responseTimeHist.p99"}
81+
8082
func (suite *RequestMetrics) TestUpdateMetricsHitOnce() {
8183

82-
var metricsKey = "counter"
84+
suite.SetupRoute("some_key")
8385

84-
suite.setRoute(metricsKey)
8586
suite.serveRoute()
8687

8788
suite.Equal(http.StatusOK, suite.getCode(), "Status code differs")
88-
8989
suite.serveExpvarRoute()
9090

9191
expVarMap := suite.getMetricsMap()
92+
for _, item := range sufixList {
93+
expectedKey := metricPrefix + "some_key" + item
94+
value, ok := expVarMap[expectedKey]
95+
suite.True(ok)
9296

93-
counterMap, ok := expVarMap[metricsKey]
94-
suite.True(ok)
95-
96-
suite.Contains(counterMap, "GET__item_{set_item}")
97-
98-
m := counterMap.(map[string]interface{})
99-
100-
suite.Equal(1.0, m["GET__item_{set_item}"])
101-
97+
suite.NotEqual(0.0, value)
98+
}
10299
}
103100

104101
func (suite *RequestMetrics) TestUpdateMetricsHitMultiple() {
105102

106-
var metricsKey = "counter1"
107103
const hitNumber = 10.0
108104

109-
suite.setRoute(metricsKey)
105+
suite.SetupRoute("different_key")
110106

111107
for i := 0; i < hitNumber; i++ {
112108
suite.serveRoute()
@@ -118,15 +114,11 @@ func (suite *RequestMetrics) TestUpdateMetricsHitMultiple() {
118114

119115
expVarMap := suite.getMetricsMap()
120116

121-
counterMap, ok := expVarMap[metricsKey]
117+
expectedKey := metricPrefix + "different_key.counts"
118+
value, ok := expVarMap[expectedKey]
122119
suite.True(ok)
123120

124-
suite.Contains(counterMap, "GET__item_{set_item}")
125-
126-
m := counterMap.(map[string]interface{})
127-
128-
suite.Equal(hitNumber, m["GET__item_{set_item}"])
129-
121+
suite.NotEqual(0.0, value)
130122
}
131123

132124
func TestRequestMetrics(t *testing.T) {

pkg/api/router.go

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
package api
1919

2020
import (
21-
"expvar"
22-
2321
"github.com/optimizely/sidedoor/pkg/api/handlers"
2422
"github.com/optimizely/sidedoor/pkg/api/middleware"
2523
"github.com/optimizely/sidedoor/pkg/optimizely"
@@ -49,31 +47,28 @@ func NewDefaultRouter(optlyCache optimizely.Cache) *chi.Mux {
4947
return NewRouter(spec)
5048
}
5149

52-
const metricsPrefix = "route_counters"
53-
54-
var routeCounts = expvar.NewMap(metricsPrefix)
55-
5650
// NewRouter returns HTTP API router backed by an optimizely.Cache implementation
5751
func NewRouter(opt *RouterOptions) *chi.Mux {
5852
r := chi.NewRouter()
5953

54+
r.Use(middleware.SetTime)
6055
r.Use(render.SetContentType(render.ContentTypeJSON), middleware.SetRequestID)
6156

62-
r.With(chimw.AllowContentType("application/json"), middleware.HitCount(routeCounts)).Post("/user-event", opt.userEventAPI.AddUserEvent)
57+
r.With(chimw.AllowContentType("application/json"), middleware.Metricize("user-event")).Post("/user-event", opt.userEventAPI.AddUserEvent)
6358

6459
r.Route("/features", func(r chi.Router) {
6560
r.Use(opt.middleware.ClientCtx)
66-
r.With(middleware.HitCount(routeCounts)).Get("/", opt.featureAPI.ListFeatures)
67-
r.With(middleware.HitCount(routeCounts)).Get("/{featureKey}", opt.featureAPI.GetFeature)
61+
r.With(middleware.Metricize("list-features")).Get("/", opt.featureAPI.ListFeatures)
62+
r.With(middleware.Metricize("get-feature")).Get("/{featureKey}", opt.featureAPI.GetFeature)
6863
})
6964

7065
r.Route("/users/{userID}", func(r chi.Router) {
7166
r.Use(opt.middleware.ClientCtx, opt.middleware.UserCtx)
7267

73-
r.With(middleware.HitCount(routeCounts)).Post("/events/{eventKey}", opt.userAPI.TrackEvent)
68+
r.With(middleware.Metricize("track-event")).Post("/events/{eventKey}", opt.userAPI.TrackEvent)
7469

75-
r.With(middleware.HitCount(routeCounts)).Get("/features/{featureKey}", opt.userAPI.GetFeature)
76-
r.With(middleware.HitCount(routeCounts)).Post("/features/{featureKey}", opt.userAPI.TrackFeature)
70+
r.With(middleware.Metricize("get-user-feature")).Get("/features/{featureKey}", opt.userAPI.GetFeature)
71+
r.With(middleware.Metricize("track-user-feature")).Post("/features/{featureKey}", opt.userAPI.TrackFeature)
7772
})
7873

7974
return r

pkg/api/router_test.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"encoding/json"
2222
"net/http"
2323
"net/http/httptest"
24+
"sync"
2425
"testing"
2526

2627
"github.com/go-chi/chi"
@@ -98,18 +99,23 @@ type RouterTestSuite struct {
9899
mux *chi.Mux
99100
}
100101

102+
var once sync.Once
103+
101104
func (suite *RouterTestSuite) SetupTest() {
102-
testClient := optimizelytest.NewClient()
103-
suite.tc = testClient
104-
105-
opts := &RouterOptions{
106-
featureAPI: new(MockFeatureAPI),
107-
userEventAPI: new(MockUserEventAPI),
108-
userAPI: new(MockUserAPI),
109-
middleware: new(MockOptlyMiddleware),
110-
}
111105

112-
suite.mux = NewRouter(opts)
106+
once.Do(func() {
107+
testClient := optimizelytest.NewClient()
108+
suite.tc = testClient
109+
110+
opts := &RouterOptions{
111+
featureAPI: new(MockFeatureAPI),
112+
userEventAPI: new(MockUserEventAPI),
113+
userAPI: new(MockUserAPI),
114+
middleware: new(MockOptlyMiddleware),
115+
}
116+
117+
suite.mux = NewRouter(opts)
118+
})
113119
}
114120

115121
func (suite *RouterTestSuite) TestListFeatures() {

0 commit comments

Comments
 (0)