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 {
m := http.StripPrefix(responderPath, cfocsp.NewResponder(source))
stripPrefix := http.StripPrefix(responderPath, cfocsp.NewResponder(source))
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" && r.URL.Path == "/" {
w.Header().Set("Cache-Control", "max-age=43200") // Cache for 12 hours
w.WriteHeader(200)
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 (
"fmt"
"net/http"
"regexp"
"strings"
"github.com/jmhodges/clock"
"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
type MeasuredHandler struct {
http.Handler
*http.ServeMux
clk clock.Clock
// Normally this is always responseTime, but we override it for testing.
stat *prometheus.HistogramVec
}
func New(h http.Handler, clk clock.Clock) *MeasuredHandler {
func New(m *http.ServeMux, clk clock.Clock) *MeasuredHandler {
return &MeasuredHandler{
Handler: h,
clk: clk,
stat: responseTime,
ServeMux: m,
clk: clk,
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) {
begin := h.clk.Now()
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() {
h.stat.With(prometheus.Labels{
"endpoint": endpoint,
"endpoint": pattern,
"method": r.Method,
"code": fmt.Sprintf("%d", rwws.code),
}).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"
)
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 {
clk clock.FakeClock
}
@ -54,10 +33,12 @@ func TestMeasuring(t *testing.T) {
},
[]string{"endpoint", "method", "code"})
mux := http.NewServeMux()
mux.Handle("/foo", sleepyHandler{clk})
mh := MeasuredHandler{
Handler: sleepyHandler{clk},
clk: clk,
stat: stat,
ServeMux: mux,
clk: clk,
stat: stat,
}
mh.ServeHTTP(httptest.NewRecorder(), &http.Request{
URL: &url.URL{Path: "/foo"},