Skip to content

Commit

Permalink
Make metrics collection in throttle and middleware packages more flex…
Browse files Browse the repository at this point in the history
…ible

Now middlewares receive interfaces instead of the concrete Prometheus implementations.
  • Loading branch information
vasayxtx committed Sep 5, 2024
1 parent 92089b2 commit 0d21b9c
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 136 deletions.
22 changes: 11 additions & 11 deletions httpserver/http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ type HTTPServer struct {
Logger log.FieldLogger
ShutdownTimeout time.Duration

port int32
httpServerDone chan struct{}
httpReqMetricsCollector *middleware.HTTPRequestMetricsCollector
port int32
httpServerDone chan struct{}
httpReqPrometheusMetrics *middleware.HTTPRequestPrometheusMetrics
}

var _ service.Unit = (*HTTPServer)(nil)
Expand All @@ -102,20 +102,20 @@ var _ service.MetricsRegisterer = (*HTTPServer)(nil)
// New creates a new HTTPServer with predefined logging, metrics collecting,
// recovering after panics and health-checking functionality.
func New(cfg *Config, logger log.FieldLogger, opts Opts) (*HTTPServer, error) { //nolint // hugeParam: opts is heavy, it's ok in this case.
httpReqMetricsCollector := middleware.NewHTTPRequestMetricsCollectorWithOpts(
middleware.HTTPRequestMetricsCollectorOpts{
httpReqPromMetrics := middleware.NewHTTPRequestPrometheusMetricsWithOpts(
middleware.HTTPRequestPrometheusMetricsOpts{
Namespace: opts.HTTPRequestMetrics.Namespace,
DurationBuckets: opts.HTTPRequestMetrics.DurationBuckets,
ConstLabels: opts.HTTPRequestMetrics.ConstLabels,
})
router := chi.NewRouter()
if err := applyDefaultMiddlewaresToRouter(router, cfg, logger, opts, httpReqMetricsCollector); err != nil {
if err := applyDefaultMiddlewaresToRouter(router, cfg, logger, opts, httpReqPromMetrics); err != nil {
return nil, err
}
configureRouter(router, logger, opts.routerOpts())

appSrv := NewWithHandler(cfg, logger, router)
appSrv.httpReqMetricsCollector = httpReqMetricsCollector
appSrv.httpReqPrometheusMetrics = httpReqPromMetrics
return appSrv, nil
}

Expand Down Expand Up @@ -259,15 +259,15 @@ func (s *HTTPServer) Stop(gracefully bool) error {

// MustRegisterMetrics registers metrics in Prometheus client and panics if any error occurs.
func (s *HTTPServer) MustRegisterMetrics() {
if s.httpReqMetricsCollector != nil {
s.httpReqMetricsCollector.MustRegister()
if s.httpReqPrometheusMetrics != nil {
s.httpReqPrometheusMetrics.MustRegister()
}
}

// UnregisterMetrics unregisters metrics in Prometheus client.
func (s *HTTPServer) UnregisterMetrics() {
if s.httpReqMetricsCollector != nil {
s.httpReqMetricsCollector.Unregister()
if s.httpReqPrometheusMetrics != nil {
s.httpReqPrometheusMetrics.Unregister()
}
}

Expand Down
2 changes: 1 addition & 1 deletion httpserver/middleware/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func Example() {
RequestBodyLimit(1024*1024, errDomain),
)

metricsCollector := NewHTTPRequestMetricsCollector()
metricsCollector := NewHTTPRequestPrometheusMetrics()
router.Use(HTTPRequestMetricsWithOpts(metricsCollector, getChiRoutePattern, HTTPRequestMetricsOpts{
ExcludedEndpoints: []string{"/metrics", "/healthz"}, // Metrics will not be collected for "/metrics" and "/healthz" endpoints.
}))
Expand Down
128 changes: 78 additions & 50 deletions httpserver/middleware/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,30 @@ const (
userAgentTypeHTTPClient = "http-client"
)

// HTTPRequestInfoMetrics represents a request info for collecting metrics.
type HTTPRequestInfoMetrics struct {
Method string
RoutePattern string
UserAgentType string
}

// HTTPRequestMetricsCollector is an interface for collecting metrics for incoming HTTP requests.
type HTTPRequestMetricsCollector interface {
// IncInFlightRequests increments the counter of in-flight requests.
IncInFlightRequests(requestInfo HTTPRequestInfoMetrics)

// DecInFlightRequests decrements the counter of in-flight requests.
DecInFlightRequests(requestInfo HTTPRequestInfoMetrics)

// ObserveRequestFinish observes the duration of the request and the status code.
ObserveRequestFinish(requestInfo HTTPRequestInfoMetrics, status int, startTime time.Time)
}

// DefaultHTTPRequestDurationBuckets is default buckets into which observations of serving HTTP requests are counted.
var DefaultHTTPRequestDurationBuckets = []float64{0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 150, 300, 600}

// HTTPRequestMetricsCollectorOpts represents an options for HTTPRequestMetricsCollector.
type HTTPRequestMetricsCollectorOpts struct {
// HTTPRequestPrometheusMetricsOpts represents an options for HTTPRequestPrometheusMetrics.
type HTTPRequestPrometheusMetricsOpts struct {
// Namespace is a namespace for metrics. It will be prepended to all metric names.
Namespace string

Expand All @@ -42,26 +61,26 @@ type HTTPRequestMetricsCollectorOpts struct {
ConstLabels prometheus.Labels

// CurriedLabelNames is a list of label names that will be curried with the provided labels.
// See HTTPRequestMetricsCollector.MustCurryWith method for more details.
// See HTTPRequestPrometheusMetrics.MustCurryWith method for more details.
// Keep in mind that if this list is not empty,
// HTTPRequestMetricsCollector.MustCurryWith method must be called further with the same labels.
// HTTPRequestPrometheusMetrics.MustCurryWith method must be called further with the same labels.
// Otherwise, the collector will panic.
CurriedLabelNames []string
}

// HTTPRequestMetricsCollector represents collector of metrics for incoming HTTP requests.
type HTTPRequestMetricsCollector struct {
// HTTPRequestPrometheusMetrics represents collector of metrics for incoming HTTP requests.
type HTTPRequestPrometheusMetrics struct {
Durations *prometheus.HistogramVec
InFlight *prometheus.GaugeVec
}

// NewHTTPRequestMetricsCollector creates a new metrics collector.
func NewHTTPRequestMetricsCollector() *HTTPRequestMetricsCollector {
return NewHTTPRequestMetricsCollectorWithOpts(HTTPRequestMetricsCollectorOpts{})
// NewHTTPRequestPrometheusMetrics creates a new instance of HTTPRequestPrometheusMetrics with default options.
func NewHTTPRequestPrometheusMetrics() *HTTPRequestPrometheusMetrics {
return NewHTTPRequestPrometheusMetricsWithOpts(HTTPRequestPrometheusMetricsOpts{})
}

// NewHTTPRequestMetricsCollectorWithOpts is a more configurable version of creating HTTPRequestMetricsCollector.
func NewHTTPRequestMetricsCollectorWithOpts(opts HTTPRequestMetricsCollectorOpts) *HTTPRequestMetricsCollector {
// NewHTTPRequestPrometheusMetricsWithOpts creates a new instance of HTTPRequestPrometheusMetrics with the provided options.
func NewHTTPRequestPrometheusMetricsWithOpts(opts HTTPRequestPrometheusMetricsOpts) *HTTPRequestPrometheusMetrics {
makeLabelNames := func(names ...string) []string {
l := append(make([]string, 0, len(opts.CurriedLabelNames)+len(names)), opts.CurriedLabelNames...)
return append(l, names...)
Expand Down Expand Up @@ -101,52 +120,62 @@ func NewHTTPRequestMetricsCollectorWithOpts(opts HTTPRequestMetricsCollectorOpts
),
)

return &HTTPRequestMetricsCollector{
return &HTTPRequestPrometheusMetrics{
Durations: durations,
InFlight: inFlight,
}
}

// MustCurryWith curries the metrics collector with the provided labels.
func (c *HTTPRequestMetricsCollector) MustCurryWith(labels prometheus.Labels) *HTTPRequestMetricsCollector {
return &HTTPRequestMetricsCollector{
Durations: c.Durations.MustCurryWith(labels).(*prometheus.HistogramVec),
InFlight: c.InFlight.MustCurryWith(labels),
func (pm *HTTPRequestPrometheusMetrics) MustCurryWith(labels prometheus.Labels) *HTTPRequestPrometheusMetrics {
return &HTTPRequestPrometheusMetrics{
Durations: pm.Durations.MustCurryWith(labels).(*prometheus.HistogramVec),
InFlight: pm.InFlight.MustCurryWith(labels),
}
}

// MustRegister does registration of metrics collector in Prometheus and panics if any error occurs.
func (c *HTTPRequestMetricsCollector) MustRegister() {
func (pm *HTTPRequestPrometheusMetrics) MustRegister() {
prometheus.MustRegister(
c.Durations,
c.InFlight,
pm.Durations,
pm.InFlight,
)
}

// Unregister cancels registration of metrics collector in Prometheus.
func (c *HTTPRequestMetricsCollector) Unregister() {
prometheus.Unregister(c.InFlight)
prometheus.Unregister(c.Durations)
func (pm *HTTPRequestPrometheusMetrics) Unregister() {
prometheus.Unregister(pm.InFlight)
prometheus.Unregister(pm.Durations)
}

func (c *HTTPRequestMetricsCollector) trackRequestEnd(reqInfo *httpRequestInfo, status int, startTime time.Time) {
labels := reqInfo.makeLabels()
labels[httpRequestMetricsLabelStatusCode] = strconv.Itoa(status)
c.Durations.With(labels).Observe(time.Since(startTime).Seconds())
// IncInFlightRequests increments the counter of in-flight requests.
func (pm *HTTPRequestPrometheusMetrics) IncInFlightRequests(requestInfo HTTPRequestInfoMetrics) {
pm.InFlight.With(prometheus.Labels{
httpRequestMetricsLabelMethod: requestInfo.Method,
httpRequestMetricsLabelRoutePattern: requestInfo.RoutePattern,
httpRequestMetricsLabelUserAgentType: requestInfo.UserAgentType,
}).Inc()
}

type httpRequestInfo struct {
method string
routePattern string
userAgentType string
// DecInFlightRequests decrements the counter of in-flight requests.
func (pm *HTTPRequestPrometheusMetrics) DecInFlightRequests(requestInfo HTTPRequestInfoMetrics) {
pm.InFlight.With(prometheus.Labels{
httpRequestMetricsLabelMethod: requestInfo.Method,
httpRequestMetricsLabelRoutePattern: requestInfo.RoutePattern,
httpRequestMetricsLabelUserAgentType: requestInfo.UserAgentType,
}).Dec()
}

func (hri *httpRequestInfo) makeLabels() prometheus.Labels {
return prometheus.Labels{
httpRequestMetricsLabelMethod: hri.method,
httpRequestMetricsLabelRoutePattern: hri.routePattern,
httpRequestMetricsLabelUserAgentType: hri.userAgentType,
}
// ObserveRequestFinish observes the duration of the request and the status code.
func (pm *HTTPRequestPrometheusMetrics) ObserveRequestFinish(
requestInfo HTTPRequestInfoMetrics, status int, startTime time.Time,
) {
pm.Durations.With(prometheus.Labels{
httpRequestMetricsLabelMethod: requestInfo.Method,
httpRequestMetricsLabelRoutePattern: requestInfo.RoutePattern,
httpRequestMetricsLabelUserAgentType: requestInfo.UserAgentType,
httpRequestMetricsLabelStatusCode: strconv.Itoa(status),
}).Observe(time.Since(startTime).Seconds())
}

// UserAgentTypeGetterFunc is a function for getting user agent type from the request.
Expand All @@ -161,21 +190,21 @@ type HTTPRequestMetricsOpts struct {

type httpRequestMetricsHandler struct {
next http.Handler
collector *HTTPRequestMetricsCollector
collector HTTPRequestMetricsCollector
getRoutePattern RoutePatternGetterFunc
opts HTTPRequestMetricsOpts
}

// HTTPRequestMetrics is a middleware that collects metrics for incoming HTTP requests using Prometheus data types.
func HTTPRequestMetrics(
collector *HTTPRequestMetricsCollector, getRoutePattern RoutePatternGetterFunc,
collector HTTPRequestMetricsCollector, getRoutePattern RoutePatternGetterFunc,
) func(next http.Handler) http.Handler {
return HTTPRequestMetricsWithOpts(collector, getRoutePattern, HTTPRequestMetricsOpts{})
}

// HTTPRequestMetricsWithOpts is a more configurable version of HTTPRequestMetrics middleware.
func HTTPRequestMetricsWithOpts(
collector *HTTPRequestMetricsCollector,
collector HTTPRequestMetricsCollector,
getRoutePattern RoutePatternGetterFunc,
opts HTTPRequestMetricsOpts,
) func(next http.Handler) http.Handler {
Expand Down Expand Up @@ -204,15 +233,14 @@ func (h *httpRequestMetricsHandler) ServeHTTP(rw http.ResponseWriter, r *http.Re
r = r.WithContext(NewContextWithRequestStartTime(r.Context(), startTime))
}

reqInfo := &httpRequestInfo{
method: r.Method,
routePattern: h.getRoutePattern(r),
userAgentType: h.opts.GetUserAgentType(r),
reqInfo := HTTPRequestInfoMetrics{
Method: r.Method,
RoutePattern: h.getRoutePattern(r),
UserAgentType: h.opts.GetUserAgentType(r),
}

inFlightGauge := h.collector.InFlight.With(reqInfo.makeLabels())
inFlightGauge.Inc()
defer inFlightGauge.Dec()
h.collector.IncInFlightRequests(reqInfo)
defer h.collector.DecInFlightRequests(reqInfo)

r = r.WithContext(NewContextWithHTTPMetricsEnabled(r.Context()))

Expand All @@ -222,16 +250,16 @@ func (h *httpRequestMetricsHandler) ServeHTTP(rw http.ResponseWriter, r *http.Re
return
}

if reqInfo.routePattern == "" {
reqInfo.routePattern = h.getRoutePattern(r)
if reqInfo.RoutePattern == "" {
reqInfo.RoutePattern = h.getRoutePattern(r)
}
if p := recover(); p != nil {
if p != http.ErrAbortHandler {
h.collector.trackRequestEnd(reqInfo, http.StatusInternalServerError, startTime)
h.collector.ObserveRequestFinish(reqInfo, http.StatusInternalServerError, startTime)
}
panic(p)
}
h.collector.trackRequestEnd(reqInfo, wrw.Status(), startTime)
h.collector.ObserveRequestFinish(reqInfo, wrw.Status(), startTime)
}()

h.next.ServeHTTP(wrw, r)
Expand Down
14 changes: 7 additions & 7 deletions httpserver/middleware/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func TestHttpRequestMetricsHandler_ServeHTTP(t *testing.T) {
for k := range tt.curriedLabels {
curriedLabelNames = append(curriedLabelNames, k)
}
collector := NewHTTPRequestMetricsCollectorWithOpts(HTTPRequestMetricsCollectorOpts{
collector := NewHTTPRequestPrometheusMetricsWithOpts(HTTPRequestPrometheusMetricsOpts{
CurriedLabelNames: curriedLabelNames,
})
collector = collector.MustCurryWith(tt.curriedLabels)
Expand Down Expand Up @@ -168,30 +168,30 @@ func TestHttpRequestMetricsHandler_ServeHTTP(t *testing.T) {
})

t.Run("collect 500 on panic", func(t *testing.T) {
collector := NewHTTPRequestMetricsCollector()
promMetrics := NewHTTPRequestPrometheusMetrics()
next := &mockRecoveryNextHandler{}
req := httptest.NewRequest(http.MethodGet, "/internal-error", nil)
resp := httptest.NewRecorder()
h := HTTPRequestMetrics(collector, getRoutePattern)(next)
h := HTTPRequestMetrics(promMetrics, getRoutePattern)(next)
if assert.Panics(t, func() { h.ServeHTTP(resp, req) }) {
assert.Equal(t, 1, next.called)
labels := makeLabels(http.MethodGet, "/internal-error", "http-client", "500")
hist := collector.Durations.With(labels).(prometheus.Histogram)
hist := promMetrics.Durations.With(labels).(prometheus.Histogram)
testutil.AssertSamplesCountInHistogram(t, hist, 1)
}
})

t.Run("not collect if disabled", func(t *testing.T) {
collector := NewHTTPRequestMetricsCollector()
promMetrics := NewHTTPRequestPrometheusMetrics()
next := &mockHTTPRequestMetricsDisabledHandler{}
req := httptest.NewRequest(http.MethodGet, "/hello", nil)
req.Header.Set("User-Agent", "http-client")
resp := httptest.NewRecorder()
h := HTTPRequestMetrics(collector, getRoutePattern)(next)
h := HTTPRequestMetrics(promMetrics, getRoutePattern)(next)
h.ServeHTTP(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
labels := makeLabels(http.MethodGet, "/hello", "http-client", "200")
hist := collector.Durations.With(labels).(prometheus.Histogram)
hist := promMetrics.Durations.With(labels).(prometheus.Histogram)
testutil.AssertSamplesCountInHistogram(t, hist, 0)
})
}
10 changes: 5 additions & 5 deletions httpserver/middleware/throttle/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,16 +157,16 @@ rules:
}

func makeExampleTestServer(cfg *throttle.Config, longWorkDelay time.Duration) *httptest.Server {
throttleMetrics := throttle.NewMetricsCollector("")
throttleMetrics.MustRegister()
defer throttleMetrics.Unregister()
promMetrics := throttle.NewPrometheusMetrics()
promMetrics.MustRegister()
defer promMetrics.Unregister()

// Configure middleware that should do global throttling ("all_reqs" tag says about that).
allReqsThrottleMiddleware := throttle.MiddlewareWithOpts(cfg, apiErrDomain, throttleMetrics, throttle.MiddlewareOpts{
allReqsThrottleMiddleware := throttle.MiddlewareWithOpts(cfg, apiErrDomain, promMetrics, throttle.MiddlewareOpts{
Tags: []string{"all_reqs"}})

// Configure middleware that should do per-client throttling based on the username from basic auth ("authenticated_reqs" tag says about that).
authenticatedReqsThrottleMiddleware := throttle.MiddlewareWithOpts(cfg, apiErrDomain, throttleMetrics, throttle.MiddlewareOpts{
authenticatedReqsThrottleMiddleware := throttle.MiddlewareWithOpts(cfg, apiErrDomain, promMetrics, throttle.MiddlewareOpts{
Tags: []string{"authenticated_reqs"},
GetKeyIdentity: func(r *http.Request) (key string, bypass bool, err error) {
username, _, ok := r.BasicAuth()
Expand Down
Loading

0 comments on commit 0d21b9c

Please sign in to comment.