-
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
HTTP client round trippers logging, metrics and request id #18
base: master
Are you sure you want to change the base?
HTTP client round trippers logging, metrics and request id #18
Conversation
httpclient/config_test.go
Outdated
func TestConfigWithLoader(t *testing.T) { | ||
yamlData := []byte(` | ||
retries: | ||
max_retry_attempts: 30 |
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.
Configuration parameters should follow camelCase naming conventions to maintain consistency with other packages in go-appkit. Let’s rename them accordingly.
Additionally, the current implementation doesn’t allow enabling the retry mechanism without also enabling rate limiting. I suggest introducing an enabled: true|false
flag for both features to provide more flexibility. Alternative option: use rate-limiting round tripper only if the rate limit > 0 and retryable round tripper - only if max retry attempts > 0.
httpclient/httpclient.go
Outdated
|
||
// MustHTTPClient wraps delegate transports with logging, metrics, rate limiting, retryable, user agent, request id | ||
// and panics if any error occurs. | ||
func MustHTTPClient(cfg *Config, userAgent, reqType string, delegate http.RoundTripper) *http.Client { |
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.
Let’s provide both methods:
NewHTTPClient
: This method returns an error, allowing callers to handle it gracefully.MustNewHTTPClient
: This method panics if an error occurs, providing a convenient option for cases where error handling is not necessary.
This approach ensures flexibility for different use cases.
Additionally, I propose introducing two more methods:
NewHTTPClientWithOpts
MustNewHTTPClientWithOpts
In these methods, the delegate
can be within the HTTPClientOpts
struct for better extensibility and clarity in configuration.
httpclient/httpclient.go
Outdated
// and panics if any error occurs. | ||
func MustHTTPClient(cfg *Config, userAgent, reqType string, delegate http.RoundTripper) *http.Client { | ||
if delegate == nil { | ||
delegate = http.DefaultTransport |
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.
Please use http.DefaultTransport.(*http.Transport).Clone()
to avoid any potential side effects.
httpclient/config.go
Outdated
|
||
const ( | ||
// DefaultLimit is the default limit for rate limiting. | ||
DefaultLimit = 10 |
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.
The name DefaultLimit seems too generic for a package-level constant.
Additionally, I’m not convinced that default values are necessary in this case. Please refer to the second part of my next comment for further details.
httpclient/config.go
Outdated
DefaultWaitTimeout = 10 * time.Second | ||
|
||
// configuration properties | ||
cfgKeyRetriesMax = "retries.max_retry_attempts" |
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.
How about renaming retries.max_retry_attempts
to retries.maxAttempts
? The word "retry" in the middle seems redundant and can be omitted for better readability.
Additionally, consider supporting multiple retry backoff types, such as exponential and constant. You can refer to the BackoffPolicy
field in the RetryableRoundTripperOpts
struct for implementation details.
if err != nil { | ||
return err | ||
} | ||
c.Limit = limit |
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.
Let's do more validation and return an error for example if the limit is negative.
httpclient/config.go
Outdated
} | ||
|
||
// Config represents options for HTTP client configuration. | ||
type Config struct { |
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.
Please consider the following suggestions:
- We usually store internal config structs by value, not by pointer. I.e.:
type Config struct {
Retries RetriesConfig `mapstructure:"retries"`
RateLimits RateLimitConfig `mapstructure:"rate_limits"`
Timeout time.Duration `mapstructure:"timeout"`
}
-
All existing
Config
structs have 2 constructors:NewConfig
andNewConfigWithKeyPrefix
. Let's do the same for thehttpclient.Config
too. -
Let's add a compile-time check that
httpclient.Config
implementsconfig.Config
andconfig.KeyPrefixProvider
interfaces.
var _ config.Config = (*Config)(nil)
var _ config.KeyPrefixProvider = (*Config)(nil)
httpclient/httpclient.go
Outdated
delegate = NewMetricsRoundTripper(delegate, reqType) | ||
|
||
delegate, err := NewRateLimitingRoundTripperWithOpts( | ||
delegate, cfg.RateLimits.Limit, cfg.RateLimits.TransportOpts(), |
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.
It would be a good idea to accept LoggerProvider func(ctx context.Context) log.FieldLogger
as an optional parameter in the constructors (NewHTTPClient
and MustNewHTTPClient
). This provider can then be passed as an option to NewRateLimitingRoundTripperWithOpts
.
By default, the LoggerProvider
should be set to middleware.GetLoggerFromContext
.
Additionally, this LoggerProvider
should also be utilized for the LoggingRoundTripper
.
httpclient/httpclient.go
Outdated
delegate = http.DefaultTransport | ||
} | ||
|
||
delegate = NewLoggingRoundTripper(delegate, reqType) |
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.
I’m not sure it’s a good idea to log all requests unconditionally without providing a way to configure it.
Could we consider adding a configuration like the following?
log:
slowRequestThreshold: 1s # Log requests taking 1s or longer (warn level).
mode: none | failed_requests | all_requests # Log modes:
# - none: Disable logging entirely.
# - failed_requests: Log only failed requests (4xx/5xx) at error level.
# - all_requests: Log all requests (2xx at info level, 4xx/5xx at error level).
# Default: failed_requests or none (to be decided).
@andrewesteves, what’s your opinion on this approach?
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.
@vasayxtx i liked the approach and implemented it, just the namings i suggest none
, failed
and all
since it's a logger round tripper for http clients the "requests" suffix could be omitted. is it okay for you?
|
||
if resp != nil { | ||
args = []interface{}{r.Method, r.URL.String(), rt.ReqType, resp.StatusCode, elapsed.Seconds(), err} | ||
message = common + "status code %d, time taken %.3f, err %+v" |
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.
Let’s differentiate logs for successfully completed and failed requests. Logging them separately will make it much easier to search for errors in the logs.
For additional suggestions, please refer to my previous comment.
httpclient/metrics_round_tripper.go
Outdated
ExternalHTTPRequestDuration = prometheus.NewHistogramVec( | ||
prometheus.HistogramOpts{ | ||
Namespace: namespace, | ||
Name: "external_http_request_duration", |
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.
The name external_http_request_duration
could be ambiguous, as "external" might imply either incoming requests from the internet or outgoing requests. I suggest renaming the Prometheus metric to http_client_request_duration_seconds
for better clarity.
Additionally, this aligns with the naming convention already used in go-authkit.
httpclient/metrics_round_tripper.go
Outdated
if err == nil && resp != nil { | ||
status = strconv.Itoa(resp.StatusCode) | ||
if resp.StatusCode >= http.StatusBadRequest && logger != nil { | ||
logger.Warnf("external request %s %s completed with HTTP status %d", r.Method, r.URL.String(), status) |
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.
I suggest avoiding log production in the MetricsRoundTripper
that is responsible for collecting metrics. Instead, we should use a separate LoggingRoundTripper
specifically for logging purposes. This separation of concerns will keep the implementation cleaner and more modular.
|
||
// RoundTrip adds X-Request-ID header to the request. | ||
func (rt *RequestIDRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { | ||
requestID := middleware.GetRequestIDFromContext(r.Context()) |
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.
Let’s add support for a RequestIDProvider func(ctx context.Context)
string to enable customization of how request IDs are generated or retrieved. This should be included as part of the HTTPClientOpts
struct, with a default implementation of middleware.GetRequestIDFromContext
.
Use case: This is particularly useful for scenarios like outgoing requests from background workers, where the context is not tied to an incoming HTTP request.
No description provided.