Skip to content

Commit edfa256

Browse files
committed
chore(allsrv): add http server observability
The HTTP server now has quantification for different metrics important to an HTTP server. The basis of our observability is now in place. We can now create dashboards/insights to understand the deployed service. One thing to note here is we have not touched on logging just yet. Good logging is inherently coupled to good error handling. We'll wait until we have a better handle of our error handling before proceeding.
1 parent 4f89d8c commit edfa256

File tree

3 files changed

+128
-10
lines changed

3 files changed

+128
-10
lines changed

allsrv/observer_http_handler.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package allsrv
2+
3+
import (
4+
"io"
5+
"net/http"
6+
"strconv"
7+
"time"
8+
9+
"github.com/hashicorp/go-metrics"
10+
"github.com/opentracing/opentracing-go"
11+
"github.com/opentracing/opentracing-go/log"
12+
)
13+
14+
// ObserveHandler provides observability to an http handler.
15+
func ObserveHandler(name string, met *metrics.Metrics) func(http.Handler) http.Handler {
16+
return func(next http.Handler) http.Handler {
17+
return &handlerMW{
18+
name: name,
19+
next: next,
20+
met: met,
21+
}
22+
}
23+
}
24+
25+
type handlerMW struct {
26+
name string
27+
next http.Handler
28+
met *metrics.Metrics
29+
}
30+
31+
func (h *handlerMW) ServeHTTP(w http.ResponseWriter, r *http.Request) {
32+
span, ctx := opentracing.StartSpanFromContext(r.Context(), "http_request_"+h.name)
33+
defer span.Finish()
34+
span.LogFields(log.String("url_path", r.URL.Path))
35+
36+
start := time.Now()
37+
name := []string{metricsPrefix, h.name, r.URL.Path}
38+
39+
labels := []metrics.Label{
40+
{
41+
Name: "method",
42+
Value: r.Method,
43+
},
44+
{
45+
Name: "url_path",
46+
Value: r.URL.Path,
47+
},
48+
}
49+
50+
h.met.IncrCounterWithLabels(append(name, "reqs"), 1, labels)
51+
52+
reqBody := &readRec{ReadCloser: r.Body}
53+
r.Body = reqBody
54+
55+
rec := &responseWriterRec{ResponseWriter: w}
56+
57+
h.next.ServeHTTP(rec, r.WithContext(ctx))
58+
59+
if rec.code == 0 {
60+
rec.code = http.StatusOK
61+
}
62+
63+
labels = append(labels,
64+
metrics.Label{
65+
Name: "status",
66+
Value: strconv.Itoa(rec.code),
67+
},
68+
metrics.Label{
69+
Name: "request_body_size",
70+
Value: strconv.Itoa(reqBody.size),
71+
},
72+
metrics.Label{
73+
Name: "response_body_size",
74+
Value: strconv.Itoa(rec.size),
75+
},
76+
)
77+
if rec.code > 299 {
78+
h.met.IncrCounterWithLabels(append(name, "errs"), 1, labels)
79+
}
80+
81+
h.met.MeasureSinceWithLabels(append(name, "dur"), start, labels)
82+
}
83+
84+
type readRec struct {
85+
size int
86+
io.ReadCloser
87+
}
88+
89+
func (r *readRec) Read(p []byte) (int, error) {
90+
n, err := r.ReadCloser.Read(p)
91+
r.size += n
92+
return n, err
93+
}
94+
95+
type responseWriterRec struct {
96+
size int
97+
code int
98+
http.ResponseWriter
99+
}
100+
101+
func (r *responseWriterRec) Write(b []byte) (int, error) {
102+
n, err := r.ResponseWriter.Write(b)
103+
r.size += n
104+
return n, err
105+
}
106+
107+
func (r *responseWriterRec) WriteHeader(statusCode int) {
108+
r.code = statusCode
109+
r.ResponseWriter.WriteHeader(statusCode)
110+
}

allsrv/server.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ import (
3434
a) there is nothing actionable, so how does the consumer know to handle the error?
3535
b) if the APIs evolve, how does the consumer distinguish between old and new?
3636
10) Observability....
37-
a) metrics
37+
a) metrics
3838
b) logging
39-
c) tracing
39+
c) tracing
4040
✅11) hard coding UUID generation into db
4141
12) possible race conditions in inmem store
4242
✅13) there is a bug in the delete foo inmem db implementation

allsrv/server_test.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ import (
1919
func TestServer(t *testing.T) {
2020
t.Run("foo create", func(t *testing.T) {
2121
t.Run("when provided a valid foo should pass", func(t *testing.T) {
22-
db := allsrv.ObserveDB("inmem", newTestMetrics(t))(new(allsrv.InmemDB))
23-
svr := allsrv.NewServer(db,
22+
met := newTestMetrics(t)
23+
db := allsrv.ObserveDB("inmem", met)(new(allsrv.InmemDB))
24+
var svr http.Handler = allsrv.NewServer(db,
2425
allsrv.WithBasicAuth("dodgers@stink.com", "PaSsWoRd"),
2526
allsrv.WithIDFn(func() string {
2627
return "id1"
2728
}),
2829
)
30+
svr = allsrv.ObserveHandler("allsrv", met)(svr)
2931

3032
req := httptest.NewRequest("POST", "/foo", newJSONBody(t, allsrv.Foo{
3133
Name: "first-foo",
@@ -65,15 +67,17 @@ func TestServer(t *testing.T) {
6567

6668
t.Run("foo read", func(t *testing.T) {
6769
t.Run("when querying for existing foo id should pass", func(t *testing.T) {
68-
db := allsrv.ObserveDB("inmem", newTestMetrics(t))(new(allsrv.InmemDB))
70+
met := newTestMetrics(t)
71+
db := allsrv.ObserveDB("inmem", met)(new(allsrv.InmemDB))
6972
err := db.CreateFoo(context.TODO(), allsrv.Foo{
7073
ID: "reader1",
7174
Name: "read",
7275
Note: "another note",
7376
})
7477
require.NoError(t, err)
7578

76-
svr := allsrv.NewServer(db, allsrv.WithBasicAuth("dodgers@stink.com", "PaSsWoRd"))
79+
var svr http.Handler = allsrv.NewServer(db, allsrv.WithBasicAuth("dodgers@stink.com", "PaSsWoRd"))
80+
svr = allsrv.ObserveHandler("allsrv", met)(svr)
7781

7882
req := httptest.NewRequest("GET", "/foo?id=reader1", nil)
7983
req.SetBasicAuth("dodgers@stink.com", "PaSsWoRd")
@@ -107,15 +111,17 @@ func TestServer(t *testing.T) {
107111

108112
t.Run("foo update", func(t *testing.T) {
109113
t.Run("when updating an existing foo with valid changes should pass", func(t *testing.T) {
110-
db := allsrv.ObserveDB("inmem", newTestMetrics(t))(new(allsrv.InmemDB))
114+
met := newTestMetrics(t)
115+
db := allsrv.ObserveDB("inmem", met)(new(allsrv.InmemDB))
111116
err := db.CreateFoo(context.TODO(), allsrv.Foo{
112117
ID: "id1",
113118
Name: "first_name",
114119
Note: "first note",
115120
})
116121
require.NoError(t, err)
117122

118-
svr := allsrv.NewServer(db, allsrv.WithBasicAuth("dodgers@stink.com", "PaSsWoRd"))
123+
var svr http.Handler = allsrv.NewServer(db, allsrv.WithBasicAuth("dodgers@stink.com", "PaSsWoRd"))
124+
svr = allsrv.ObserveHandler("allsrv", met)(svr)
119125

120126
req := httptest.NewRequest("PUT", "/foo", newJSONBody(t, allsrv.Foo{
121127
ID: "id1",
@@ -158,15 +164,17 @@ func TestServer(t *testing.T) {
158164

159165
t.Run("foo delete", func(t *testing.T) {
160166
t.Run("when deleting an existing foo should pass", func(t *testing.T) {
161-
db := allsrv.ObserveDB("inmem", newTestMetrics(t))(new(allsrv.InmemDB))
167+
met := newTestMetrics(t)
168+
db := allsrv.ObserveDB("inmem", met)(new(allsrv.InmemDB))
162169
err := db.CreateFoo(context.TODO(), allsrv.Foo{
163170
ID: "id1",
164171
Name: "first_name",
165172
Note: "first note",
166173
})
167174
require.NoError(t, err)
168175

169-
svr := allsrv.NewServer(db, allsrv.WithBasicAuth("dodgers@stink.com", "PaSsWoRd"))
176+
var svr http.Handler = allsrv.NewServer(db, allsrv.WithBasicAuth("dodgers@stink.com", "PaSsWoRd"))
177+
svr = allsrv.ObserveHandler("allsrv", met)(svr)
170178

171179
req := httptest.NewRequest("DELETE", "/foo?id=id1", nil)
172180
req.SetBasicAuth("dodgers@stink.com", "PaSsWoRd")

0 commit comments

Comments
 (0)