Use pattern to determine endpoint metrics. (#2689)

This ensures we don't create infinite metrics based on users hitting
non-existent endpoints.
This commit is contained in:
Jacob Hoffman-Andrews 2017-04-20 10:14:47 -07:00 committed by Daniel McCarney
parent eccca3ccd4
commit d59188c676
3 changed files with 18 additions and 61 deletions

View File

@ -235,14 +235,16 @@ as generated by Boulder's single-ocsp command.
} }
func mux(scope metrics.Scope, responderPath string, source cfocsp.Source) http.Handler { func mux(scope metrics.Scope, responderPath string, source cfocsp.Source) http.Handler {
m := http.StripPrefix(responderPath, cfocsp.NewResponder(source)) stripPrefix := http.StripPrefix(responderPath, cfocsp.NewResponder(source))
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" && r.URL.Path == "/" { if r.Method == "GET" && r.URL.Path == "/" {
w.Header().Set("Cache-Control", "max-age=43200") // Cache for 12 hours w.Header().Set("Cache-Control", "max-age=43200") // Cache for 12 hours
w.WriteHeader(200) w.WriteHeader(200)
return return
} }
m.ServeHTTP(w, r) stripPrefix.ServeHTTP(w, r)
}) })
return measured_http.New(h, clock.Default()) m := http.NewServeMux()
m.Handle("/", h)
return measured_http.New(m, clock.Default())
} }

View File

@ -3,8 +3,6 @@ package measured_http
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"regexp"
"strings"
"github.com/jmhodges/clock" "github.com/jmhodges/clock"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
@ -38,56 +36,32 @@ func (r *responseWriterWithStatus) WriteHeader(code int) {
// MeasuredHandler wraps an http.Handler and records prometheus stats // MeasuredHandler wraps an http.Handler and records prometheus stats
type MeasuredHandler struct { type MeasuredHandler struct {
http.Handler *http.ServeMux
clk clock.Clock clk clock.Clock
// Normally this is always responseTime, but we override it for testing. // Normally this is always responseTime, but we override it for testing.
stat *prometheus.HistogramVec stat *prometheus.HistogramVec
} }
func New(h http.Handler, clk clock.Clock) *MeasuredHandler { func New(m *http.ServeMux, clk clock.Clock) *MeasuredHandler {
return &MeasuredHandler{ return &MeasuredHandler{
Handler: h, ServeMux: m,
clk: clk, clk: clk,
stat: responseTime, stat: responseTime,
} }
} }
var endpointShortNameRegexp = regexp.MustCompile("^[a-z-]*$")
// endpointFromPath turns a request path into a value suitable for a Prometheus
// label value, by eliminating path components that vary widely (like user ids
// and authorization ids). It uses a simple heuristic: Remove everything after
// the first path component that doesn't match lowercase a-z, plus hyphen. This
// happens to work very well with Boulder's WFE.
func endpointFromPath(path string) string {
// Remove any query
path = strings.Split(path, "?")[0]
components := strings.Split(path, "/")
var i int
var v string
for i, v = range components {
matched := endpointShortNameRegexp.MatchString(v)
if !matched {
return strings.Join(components[:i], "/")
}
}
return path
}
func (h *MeasuredHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *MeasuredHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
begin := h.clk.Now() begin := h.clk.Now()
rwws := &responseWriterWithStatus{w, 0} rwws := &responseWriterWithStatus{w, 0}
// Copy in case handlers down the chain use StripPrefix, which modifies
// URL path.
endpoint := endpointFromPath(r.URL.Path)
subHandler, pattern := h.Handler(r)
defer func() { defer func() {
h.stat.With(prometheus.Labels{ h.stat.With(prometheus.Labels{
"endpoint": endpoint, "endpoint": pattern,
"method": r.Method, "method": r.Method,
"code": fmt.Sprintf("%d", rwws.code), "code": fmt.Sprintf("%d", rwws.code),
}).Observe(h.clk.Since(begin).Seconds()) }).Observe(h.clk.Since(begin).Seconds())
}() }()
h.Handler.ServeHTTP(rwws, r) subHandler.ServeHTTP(rwws, r)
} }

View File

@ -12,27 +12,6 @@ import (
io_prometheus_client "github.com/prometheus/client_model/go" io_prometheus_client "github.com/prometheus/client_model/go"
) )
func TestEndpointFromPath(t *testing.T) {
tc := []struct {
input, expected string
}{
{"/", "/"},
{"/acme", "/acme"},
{"/acme/new-authz", "/acme/new-authz"},
{"/acme/new-authz/", "/acme/new-authz/"},
{"/acme/authz/1234", "/acme/authz"},
{"/acme/authz/aGVsbG8K/1234", "/acme/authz"},
{"/directory?foo=bar", "/directory"},
}
for _, c := range tc {
output := endpointFromPath(c.input)
if output != c.expected {
t.Errorf("endpointFromPath(%q) = %q (expected %q)",
c.input, output, c.expected)
}
}
}
type sleepyHandler struct { type sleepyHandler struct {
clk clock.FakeClock clk clock.FakeClock
} }
@ -54,8 +33,10 @@ func TestMeasuring(t *testing.T) {
}, },
[]string{"endpoint", "method", "code"}) []string{"endpoint", "method", "code"})
mux := http.NewServeMux()
mux.Handle("/foo", sleepyHandler{clk})
mh := MeasuredHandler{ mh := MeasuredHandler{
Handler: sleepyHandler{clk}, ServeMux: mux,
clk: clk, clk: clk,
stat: stat, stat: stat,
} }