-
Notifications
You must be signed in to change notification settings - Fork 12
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
[ffresty] [metric] HTTP Response Time and Complete Gauge Support #160
Changes from all commits
9b99686
fd3c3db
f1ada49
29bfa10
f6fc2ac
c5821a0
4efbf8c
fb8c447
27ea4fe
1b2e68b
4ac2fee
052c797
8180cc5
1d3ce59
e8df07b
665381a
dde1302
63e3df7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,4 +4,6 @@ coverage.txt | |
**/debug.test | ||
.DS_Store | ||
__debug* | ||
.vscode/*.log | ||
.vscode/*.log | ||
*.iml | ||
.idea/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
// Copyright © 2024 Kaleido, Inc. | ||
// Copyright © 2025 Kaleido, Inc. | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
|
@@ -40,7 +40,14 @@ import ( | |
"golang.org/x/time/rate" | ||
) | ||
|
||
const ( | ||
metricsHTTPResponsesTotal = "http_responses_total" | ||
metricsNetworkErrorsTotal = "network_errors_total" | ||
metricsHTTPResponseTime = "http_response_time_seconds" | ||
) | ||
|
||
type retryCtxKey struct{} | ||
type hostCtxKey struct{} | ||
|
||
type retryCtx struct { | ||
id string | ||
|
@@ -98,27 +105,21 @@ func EnableClientMetrics(ctx context.Context, metricsRegistry metric.MetricsRegi | |
if err != nil { | ||
return err | ||
} | ||
metricsManager.NewCounterMetricWithLabels(ctx, "http_response", "HTTP response", []string{"status", "error", "host", "method"}, false) | ||
metricsManager.NewCounterMetricWithLabels(ctx, "network_error", "Network error", []string{"host", "method"}, false) | ||
metricsManager.NewCounterMetricWithLabels(ctx, metricsHTTPResponsesTotal, "HTTP response", []string{"status", "error", "host", "method"}, false) | ||
metricsManager.NewCounterMetricWithLabels(ctx, metricsNetworkErrorsTotal, "Network error", []string{"host", "method"}, false) | ||
metricsManager.NewSummaryMetricWithLabels(ctx, metricsHTTPResponseTime, "HTTP response time", []string{"status", "host", "method"}, false) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @onelapahead what's your consideration between There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Histogram is more costly in terms of cardinality bc of all the buckets you make. So this felt light enough to only calculate average response time across a few basic dimensions. Would love to toggle btwn histogram and summaries based on certain settings eventually, but that was a bigger change than I had an appetite for. |
||
|
||
// create hooks | ||
onErrorMetricsHook := func(req *resty.Request, _ error) { | ||
method := req.Method | ||
u, _ := url.Parse(req.URL) | ||
host := u.Host | ||
// whilst there it is a possibility to get an response returned in the error here (and resty doc for OnError shows this) it seems to be a special case and the statuscode in such cases was not set. | ||
// therefore we log all cases as network_error we may in future find reason to extract more detail from the error | ||
metricsManager.IncCounterMetricWithLabels(ctx, "network_error", map[string]string{"host": host, "method": method}, nil) | ||
metricsManager.IncCounterMetricWithLabels(ctx, metricsNetworkErrorsTotal, map[string]string{"host": req.Context().Value(hostCtxKey{}).(string), "method": req.Method}, nil) | ||
} | ||
RegisterGlobalOnError(onErrorMetricsHook) | ||
|
||
onSuccessMetricsHook := func(_ *resty.Client, resp *resty.Response) { | ||
method := resp.Request.Method | ||
u, _ := url.Parse(resp.Request.URL) | ||
host := u.Host | ||
code := resp.RawResponse.StatusCode | ||
metricsManager.IncCounterMetricWithLabels(ctx, "http_response", map[string]string{"status": fmt.Sprintf("%d", code), "error": "false", "host": host, "method": method}, nil) | ||
metricsManager.IncCounterMetricWithLabels(ctx, metricsHTTPResponsesTotal, map[string]string{"status": fmt.Sprintf("%d", resp.RawResponse.StatusCode), "error": "false", "host": resp.Request.Context().Value(hostCtxKey{}).(string), "method": resp.Request.Method}, nil) | ||
} | ||
RegisterGlobalOnSuccess(onSuccessMetricsHook) | ||
return nil | ||
|
@@ -142,13 +143,17 @@ func OnAfterResponse(c *resty.Client, resp *resty.Response) { | |
} | ||
rCtx := resp.Request.Context() | ||
rc := rCtx.Value(retryCtxKey{}).(*retryCtx) | ||
elapsed := float64(time.Since(rc.start)) / float64(time.Millisecond) | ||
elapsed := time.Since(rc.start) | ||
level := logrus.DebugLevel | ||
status := resp.StatusCode() | ||
if status >= 300 { | ||
level = logrus.ErrorLevel | ||
} | ||
log.L(rCtx).Logf(level, "<== %s %s [%d] (%.2fms)", resp.Request.Method, resp.Request.URL, status, elapsed) | ||
log.L(rCtx).Logf(level, "<== %s %s [%d] (%dms)", resp.Request.Method, resp.Request.URL, status, time.Since(rc.start).Milliseconds()) | ||
if metricsManager != nil { | ||
metricsManager.ObserveSummaryMetricWithLabels(rCtx, metricsHTTPResponseTime, elapsed.Seconds(), map[string]string{"status": fmt.Sprintf("%d", status), "host": rCtx.Value(hostCtxKey{}).(string), "method": resp.Request.Method}, nil) | ||
} | ||
// TODO use req.TraceInfo() for richer metrics at the DNS and transport layer | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. leaving this todo here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. correct, dont think its worth of an issue (unless you feel otherwise) but a good reminder for future contributors reading code that more is possible with the metrics and where to get started with it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sounds good |
||
|
||
func OnError(req *resty.Request, err error) { | ||
|
@@ -193,7 +198,7 @@ func GetRateLimiter(rps, burst int) *rate.Limiter { | |
// | ||
// You can use the normal Resty builder pattern, to set per-instance configuration | ||
// as required. | ||
func NewWithConfig(ctx context.Context, ffrestyConfig Config) (client *resty.Client) { | ||
func NewWithConfig(ctx context.Context, ffrestyConfig Config) (client *resty.Client) { //nolint:gocyclo | ||
if ffrestyConfig.HTTPCustomClient != nil { | ||
if httpClient, ok := ffrestyConfig.HTTPCustomClient.(*http.Client); ok { | ||
client = resty.NewWithClient(httpClient) | ||
|
@@ -232,10 +237,10 @@ func NewWithConfig(ctx context.Context, ffrestyConfig Config) (client *resty.Cli | |
|
||
rateLimiterMap[client] = GetRateLimiter(ffrestyConfig.ThrottleRequestsPerSecond, ffrestyConfig.ThrottleBurst) | ||
|
||
url := strings.TrimSuffix(ffrestyConfig.URL, "/") | ||
if url != "" { | ||
client.SetBaseURL(url) | ||
log.L(ctx).Debugf("Created REST client to %s", url) | ||
_url := strings.TrimSuffix(ffrestyConfig.URL, "/") | ||
if _url != "" { | ||
client.SetBaseURL(_url) | ||
log.L(ctx).Debugf("Created REST client to %s", _url) | ||
} | ||
|
||
if ffrestyConfig.ProxyURL != "" { | ||
|
@@ -244,7 +249,7 @@ func NewWithConfig(ctx context.Context, ffrestyConfig Config) (client *resty.Cli | |
|
||
client.SetTimeout(time.Duration(ffrestyConfig.HTTPRequestTimeout)) | ||
|
||
client.OnBeforeRequest(func(_ *resty.Client, req *resty.Request) error { | ||
client.OnBeforeRequest(func(c *resty.Client, req *resty.Request) error { | ||
if rateLimiterMap[client] != nil { | ||
// Wait for permission to proceed with the request | ||
err := rateLimiterMap[client].Wait(req.Context()) | ||
|
@@ -253,6 +258,26 @@ func NewWithConfig(ctx context.Context, ffrestyConfig Config) (client *resty.Cli | |
} | ||
} | ||
rCtx := req.Context() | ||
// Record host in context to avoid redundant parses in hooks | ||
var u *url.URL | ||
if req.URL != "" { | ||
u, _ = url.Parse(req.URL) | ||
} | ||
// The req.URL might have only set a path i.e. /home, fallbacking to the base URL of the client. | ||
// So if the URL is nil, that's likely the case and we'll derive the host from the configured | ||
// base instead. | ||
if (u == nil || u.Host == "") && _url != "" { | ||
u, _ = url.Parse(_url) | ||
} | ||
if (u == nil || u.Host == "") && c.BaseURL != "" { | ||
u, _ = url.Parse(c.BaseURL) | ||
} | ||
if u != nil && u.Host != "" { | ||
host := u.Host | ||
rCtx = context.WithValue(rCtx, hostCtxKey{}, host) | ||
} else { | ||
rCtx = context.WithValue(rCtx, hostCtxKey{}, "unknown") | ||
} | ||
rc := rCtx.Value(retryCtxKey{}) | ||
if rc == nil { | ||
// First attempt | ||
|
@@ -289,8 +314,9 @@ func NewWithConfig(ctx context.Context, ffrestyConfig Config) (client *resty.Cli | |
} | ||
} | ||
|
||
log.L(rCtx).Debugf("==> %s %s%s", req.Method, url, req.URL) | ||
log.L(rCtx).Debugf("==> %s %s%s", req.Method, _url, req.URL) | ||
log.L(rCtx).Tracef("==> (body) %+v", req.Body) | ||
|
||
return nil | ||
}) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some size metrics for data transmitted would be useful to add as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Chengxuan still required? or follow up PR for @onelapahead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@EnriqueL8 this is a suggestion, it should not block the merge of this PR. thanks for checking.