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

[Metrics] Fix issue with collecting REST Metrics #4452

Merged
Merged
Show file tree
Hide file tree
Changes from 12 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
9 changes: 7 additions & 2 deletions engine/access/rest/middleware/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@ import (
"github.com/onflow/flow-go/module"
)

func MetricsMiddleware(restCollector module.RestMetrics) mux.MiddlewareFunc {
func MetricsMiddleware(restCollector module.RestMetrics, urlToRoute func(string) (string, error)) mux.MiddlewareFunc {
metricsMiddleware := middleware.New(middleware.Config{Recorder: restCollector})
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
routeName, err := urlToRoute(req.URL.Path)
if err != nil {
return
Copy link
Contributor

Choose a reason for hiding this comment

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

looks like there are a few REST unittests failing with empty responses. I'm guessing it's related to this return since it effectively drops the request. Can you take a look?

most likely, the tests just need to be updated to use more valid urls/inputs, but not 100% sure.

Copy link
Collaborator Author

@Guitarheroua Guitarheroua Jun 29, 2023

Choose a reason for hiding this comment

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

looks like there are a few REST unit tests failing with empty responses. I'm guessing it's related to this return since it effectively drops the request. Can you take a look?

Most likely, the tests just need to be updated to use more valid urls/inputs, but not 100% sure.

Could I, for example, on error just fill routeName with an "unknown" or "invalid" string? Because this function anyway supposes to return http.Handler

Copy link
Contributor

Choose a reason for hiding this comment

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

yea, that sounds good. Let's go with unknown

}

// This is a custom metric being called on every http request
restCollector.AddTotalRequests(req.Context(), req.Method, req.URL.Path)
restCollector.AddTotalRequests(req.Context(), req.Method, routeName)

// Modify the writer
respWriter := &responseWriter{w, http.StatusOK}
Expand Down
61 changes: 60 additions & 1 deletion engine/access/rest/router.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package rest

import (
"fmt"
"net/http"
"regexp"
"strings"

"github.com/gorilla/mux"
"github.com/rs/zerolog"
Expand All @@ -21,7 +24,7 @@ func newRouter(backend access.API, logger zerolog.Logger, chain flow.Chain, rest
v1SubRouter.Use(middleware.LoggingMiddleware(logger))
v1SubRouter.Use(middleware.QueryExpandable())
v1SubRouter.Use(middleware.QuerySelect())
v1SubRouter.Use(middleware.MetricsMiddleware(restCollector))
v1SubRouter.Use(middleware.MetricsMiddleware(restCollector, URLToRoute))

linkGenerator := models.NewLinkGeneratorImpl(v1SubRouter)

Expand Down Expand Up @@ -114,3 +117,59 @@ var Routes = []route{{
Name: "getNodeVersionInfo",
Handler: GetNodeVersionInfo,
}}

var routeUrlMap = map[string]string{}
var routeRE = regexp.MustCompile(`(?i)/v1/(\w+)(/(\w+)(/(\w+))?)?`)

func init() {
for _, r := range Routes {
routeUrlMap[r.Pattern] = r.Name
}
}

func URLToRoute(url string) (string, error) {
normalized, err := normalizeURL(url)
if err != nil {
return "", err
}

name, ok := routeUrlMap[normalized]
if !ok {
return "", fmt.Errorf("invalid url")
}
return name, nil
}

func normalizeURL(url string) (string, error) {
matches := routeRE.FindAllStringSubmatch(url, -1)
if len(matches) != 1 || len(matches[0]) != 6 {
return "", fmt.Errorf("invalid url")
}

// given a URL like
// /v1/blocks/1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef/payload
// groups [ 1 ] [ 3 ] [ 5 ]
// normalized form like /v1/blocks/{id}/payload

parts := []string{matches[0][1]}

switch len(matches[0][3]) {
case 0:
// top level resource. e.g. /v1/blocks
case 64:
// id based resource. e.g. /v1/blocks/1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
parts = append(parts, "{id}")
case 16:
// address based resource. e.g. /v1/accounts/1234567890abcdef
parts = append(parts, "{address}")
default:
// named resource. e.g. /v1/network/parameters
parts = append(parts, matches[0][3])
}

if matches[0][5] != "" {
parts = append(parts, matches[0][5])
}

return "/" + strings.Join(parts, "/"), nil
}
185 changes: 185 additions & 0 deletions engine/access/rest/router_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package rest

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseURL(t *testing.T) {
tests := []struct {
name string
url string
expected string
}{
{
name: "/v1/transactions",
url: "/v1/transactions",
expected: "createTransaction",
},
{
name: "/v1/transactions/{id}",
url: "/v1/transactions/53730d3f3d2d2f46cb910b16db817d3a62adaaa72fdb3a92ee373c37c5b55a76",
expected: "getTransactionByID",
},
{
name: "/v1/transaction_results/{id}",
url: "/v1/transaction_results/53730d3f3d2d2f46cb910b16db817d3a62adaaa72fdb3a92ee373c37c5b55a76",
expected: "getTransactionResultByID",
},
{
name: "/v1/blocks",
url: "/v1/blocks",
expected: "getBlocksByHeight",
},
{
name: "/v1/blocks/{id}",
url: "/v1/blocks/53730d3f3d2d2f46cb910b16db817d3a62adaaa72fdb3a92ee373c37c5b55a76",
expected: "getBlocksByIDs",
},
{
name: "/v1/blocks/{id}/payload",
url: "/v1/blocks/53730d3f3d2d2f46cb910b16db817d3a62adaaa72fdb3a92ee373c37c5b55a76/payload",
expected: "getBlockPayloadByID",
},
{
name: "/v1/execution_results/{id}",
url: "/v1/execution_results/53730d3f3d2d2f46cb910b16db817d3a62adaaa72fdb3a92ee373c37c5b55a76",
expected: "getExecutionResultByID",
},
{
name: "/v1/execution_results",
url: "/v1/execution_results",
expected: "getExecutionResultByBlockID",
},
{
name: "/v1/collections/{id}",
url: "/v1/collections/53730d3f3d2d2f46cb910b16db817d3a62adaaa72fdb3a92ee373c37c5b55a76",
expected: "getCollectionByID",
},
{
name: "/v1/scripts",
url: "/v1/scripts",
expected: "executeScript",
},
{
name: "/v1/accounts/{address}",
url: "/v1/accounts/6a587be304c1224c",
expected: "getAccount",
},
{
name: "/v1/events",
url: "/v1/events",
expected: "getEvents",
},
{
name: "/v1/network/parameters",
url: "/v1/network/parameters",
expected: "getNetworkParameters",
},
{
name: "/v1/node_version_info",
url: "/v1/node_version_info",
expected: "getNodeVersionInfo",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := URLToRoute(tt.url)
require.NoError(t, err)
assert.Equal(t, tt.expected, got)
})
}
}

func TestBenchmarkParseURL(t *testing.T) {
tests := []struct {
name string
url string
expected string
}{
{
name: "/v1/transactions",
url: "/v1/transactions",
expected: "createTransaction",
},
{
name: "/v1/transactions/{id}",
url: "/v1/transactions/53730d3f3d2d2f46cb910b16db817d3a62adaaa72fdb3a92ee373c37c5b55a76",
expected: "getTransactionByID",
},
{
name: "/v1/transaction_results/{id}",
url: "/v1/transaction_results/53730d3f3d2d2f46cb910b16db817d3a62adaaa72fdb3a92ee373c37c5b55a76",
expected: "getTransactionResultByID",
},
{
name: "/v1/blocks",
url: "/v1/blocks",
expected: "getBlocksByHeight",
},
{
name: "/v1/blocks/{id}",
url: "/v1/blocks/53730d3f3d2d2f46cb910b16db817d3a62adaaa72fdb3a92ee373c37c5b55a76",
expected: "getBlocksByIDs",
},
{
name: "/v1/blocks/{id}/payload",
url: "/v1/blocks/53730d3f3d2d2f46cb910b16db817d3a62adaaa72fdb3a92ee373c37c5b55a76/payload",
expected: "getBlockPayloadByID",
},
{
name: "/v1/execution_results/{id}",
url: "/v1/execution_results/53730d3f3d2d2f46cb910b16db817d3a62adaaa72fdb3a92ee373c37c5b55a76",
expected: "getExecutionResultByID",
},
{
name: "/v1/execution_results",
url: "/v1/execution_results",
expected: "getExecutionResultByBlockID",
},
{
name: "/v1/collections/{id}",
url: "/v1/collections/53730d3f3d2d2f46cb910b16db817d3a62adaaa72fdb3a92ee373c37c5b55a76",
expected: "getCollectionByID",
},
{
name: "/v1/scripts",
url: "/v1/scripts",
expected: "executeScript",
},
{
name: "/v1/accounts/{address}",
url: "/v1/accounts/6a587be304c1224c",
expected: "getAccount",
},
{
name: "/v1/events",
url: "/v1/events",
expected: "getEvents",
},
{
name: "/v1/network/parameters",
url: "/v1/network/parameters",
expected: "getNetworkParameters",
},
{
name: "/v1/node_version_info",
url: "/v1/node_version_info",
expected: "getNodeVersionInfo",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
start := time.Now()
for i := 0; i < 100_000; i++ {
_, _ = URLToRoute(tt.url)
}
t.Logf("%s: %v", tt.name, time.Since(start)/100_000)
})
}
}
2 changes: 1 addition & 1 deletion module/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ type RestMetrics interface {
// Example recorder taken from:
// https://github.com/slok/go-http-metrics/blob/master/metrics/prometheus/prometheus.go
httpmetrics.Recorder
AddTotalRequests(ctx context.Context, service string, id string)
AddTotalRequests(ctx context.Context, method string, routeName string)
}

type GRPCConnectionPoolMetrics interface {
Expand Down
6 changes: 3 additions & 3 deletions module/metrics/rest_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func NewRestCollector(cfg metricsProm.Config) module.RestMetrics {
Subsystem: "http",
Name: "requests_total",
Help: "The number of requests handled over time.",
}, []string{cfg.ServiceLabel, cfg.HandlerIDLabel}),
}, []string{cfg.MethodLabel, cfg.HandlerIDLabel}),
}

cfg.Registry.MustRegister(
Expand All @@ -108,6 +108,6 @@ func (r *RestCollector) AddInflightRequests(_ context.Context, p httpmetrics.HTT
}

// New custom method to track all requests made for every REST API request
func (r *RestCollector) AddTotalRequests(_ context.Context, method string, id string) {
r.httpRequestsTotal.WithLabelValues(method, id).Inc()
func (r *RestCollector) AddTotalRequests(_ context.Context, method string, routeName string) {
r.httpRequestsTotal.WithLabelValues(method, routeName).Inc()
}
6 changes: 3 additions & 3 deletions module/mock/access_metrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions module/mock/rest_metrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.