Skip to content

Commit

Permalink
Make httpmetris work better with OTEL (#198)
Browse files Browse the repository at this point in the history
## Rationale

Here we add a option to improve HTTP metrics reporting with OTEL.

## Changes
* Add `NewV2` function to `httpmetrics` which uses attributes over metric names

## Meta
[W-13781478](https://gus.lightning.force.com/a07EE00001WSFXiYAP)
  • Loading branch information
ypaq committed Jul 19, 2023
1 parent 07e8317 commit 8431ac4
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 1 deletion.
2 changes: 1 addition & 1 deletion hmiddleware/httpmetrics/httpmetrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ var dashRe = regexp.MustCompile(`[_]+`)
// request context. For example, a chi.Router that mounts a sub-router on / to
// handle a set of paths might have a set of patterns like:
//
// []string{"/*", "/kpi/v1/apps/:id"}
// []string{"/*", "/kpi/v1/apps/:id"}
//
// which would be transformed into a metric called "kpi.v1.apps.id".
func nameRoutePatterns(patterns []string) string {
Expand Down
72 changes: 72 additions & 0 deletions hmiddleware/httpmetrics/httpmetrics_v2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package httpmetrics

import (
"net/http"
"strconv"
"time"

"github.com/heroku/x/go-kit/metrics"
"github.com/heroku/x/go-kit/metricsregistry"

"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
)

const (
// metric names
requestDuration = "http.server.duration" // duration in milliseconds

// metric attribute keys
routeKey = "http.route"
methodKey = "http.request.method"
statusKey = "http.response.status_code"
serverAddressKey = "server.address"
urlSchemeKey = "url.scheme"
)

// NewOTEL returns an HTTP middleware which captures HTTP request counts and latency
// annotated with attributes for method, route, status.
//
// See https://opentelemetry.io/docs/specs/otel/metrics/semantic_conventions/http-metrics/
func NewOTEL(p metrics.Provider) func(http.Handler) http.Handler {
reg := metricsregistry.New(p)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)

start := time.Now()
next.ServeHTTP(ww, r)
dur := time.Since(start)

labels := []string{
methodKey, r.Method,
}

if status := ww.Status(); status != 0 {
kv := []string{statusKey, strconv.Itoa(status)}
labels = append(labels, kv...)
}

ctx := r.Context()
if ctx.Value(chi.RouteCtxKey) != nil {
rtCtx := chi.RouteContext(ctx)
if len(rtCtx.RoutePatterns) > 0 {
// pick last route pattern as it is the one chi used
route := rtCtx.RoutePatterns[len(rtCtx.RoutePatterns)-1]
kv := []string{routeKey, route}
labels = append(labels, kv...)
}
}

if r.URL != nil {
kv := []string{
urlSchemeKey, r.URL.Scheme,
serverAddressKey, r.URL.Host,
}
labels = append(labels, kv...)
}

reg.GetOrRegisterExplicitHistogram(requestDuration, metrics.ThirtySecondDistribution).With(labels...).Observe(ms(dur))
})
}
}
89 changes: 89 additions & 0 deletions hmiddleware/httpmetrics/httpmetrics_v2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package httpmetrics

import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/go-chi/chi"

"github.com/heroku/x/go-kit/metrics/testmetrics"
)

func TestOTELMiddleware(t *testing.T) {
p := testmetrics.NewProvider(t)

next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
})

r := httptest.NewRequest("GET", "http://example.org/foo/bar", nil)
w := httptest.NewRecorder()

hand := NewOTEL(p)(next)
hand.ServeHTTP(w, r)

p.CheckObservationCount("http.server.duration.http.request.method:GET:url.scheme:http:server.address:example.org", 1)
}

func TestOTELResponseStatus(t *testing.T) {
p := testmetrics.NewProvider(t)

next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(502)
})

r := httptest.NewRequest("GET", "http://example.org/foo/bar", nil)
w := httptest.NewRecorder()

hand := NewOTEL(p)(next)
hand.ServeHTTP(w, r)

p.CheckObservationCount("http.server.duration.http.request.method:GET:http.response.status_code:502:url.scheme:http:server.address:example.org", 1)
}

func TestOTELChi(t *testing.T) {
p := testmetrics.NewProvider(t)

next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
})

r := httptest.NewRequest("GET", "http://example.org/foo/bar", nil)

rctx := chi.NewRouteContext()
rctx.RoutePatterns = []string{"/*", "/apps/{foo_id}/bars/{bar_id}"}
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))

w := httptest.NewRecorder()

hand := NewOTEL(p)(next)
hand.ServeHTTP(w, r)

p.CheckObservationCount("http.server.duration.http.request.method:GET:http.route:/apps/{foo_id}/bars/{bar_id}:url.scheme:http:server.address:example.org", 1)

}

func TestOTELNestedChiRouters(t *testing.T) {
p := testmetrics.NewProvider(t)

inner := chi.NewRouter()
inner.Get("/hello/{name}", func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "id")
if _, err := io.WriteString(w, fmt.Sprintf("Hello %s!", name)); err != nil {
t.Fatal("unexpected error", err)
}
})

outer := chi.NewRouter()
outer.Use(NewOTEL(p))
outer.Mount("/", inner)

r := httptest.NewRequest("GET", "http://example.org/hello/world", nil)
w := httptest.NewRecorder()
outer.ServeHTTP(w, r)

p.CheckObservationCount("http.server.duration.http.request.method:GET:http.response.status_code:200:http.route:/hello/{name}:url.scheme:http:server.address:example.org", 1)

}

0 comments on commit 8431ac4

Please sign in to comment.