Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make metrics collection in throttle and middleware packages more flexible #11

Merged
merged 1 commit into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we safe here due to the combinatorics of different HTTP routes?

Copy link
Member Author

@vasayxtx vasayxtx Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's supposed to be the route pattern, not the final URL.

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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add header comments for public functions

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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add header comments for public functions

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
Loading