Skip to content

Conversation

@mustansir14
Copy link
Contributor

Description:

This PR adds a new HTTP response body size metric to InstrumentedTransport, which is the current Transport for SaneHTTPClient. InstrumentedTransport is also set to be used as the Transport for RetryableHTTPClient.

Challenge with Response Body Size Calculation:

  • The Response object has a ContentLength field that may have exactly what we're looking for, but it is not guaranteed to be provided by the server. Also it is never available for a chunked response.
  • A simple approach would be to read the full body in memory and get it's size, but this is inefficient as responses can be large. Also it may add a delay to the RoundTrip() call, and would require restoring the body so that downstream code can read it again.

Solution:

To address this, a responseSizeCounterReadCloser is introduced to measure the size of the response body WHILE the downstream code reads it. This ensures no overhead is added in the RoundTrip() call. This however will not work if the downstream code never reads the body (e.g. detectors where only the StatusCode is used to verify), so to handle this, we add a check in the Close() method of the ReadCloser to drain the body if it's not consumed.

Checklist:

  • Tests passing (make test-community)?
  • Lint passing (make lint this requires golangci-lint)?

@mustansir14 mustansir14 requested a review from a team November 7, 2025 14:46
@mustansir14 mustansir14 requested a review from a team as a code owner November 7, 2025 14:46
Comment on lines 138 to 146

// wrap the response body to count bytes read
resp.Body = &responseSizeCounterReadCloser{
ReadCloser: resp.Body,
recordSizeFunc: func(size int) {
// Record response body size
recordResponseBodySize(sanitizedURL, size)
},
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This would also start generating metrics for SaneHTTPClient. Just wanna make sure if this is intentional.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this is intended. We also want to add this metric to SaneHTTPClient

Copy link
Contributor

@rosecodym rosecodym left a comment

Choose a reason for hiding this comment

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

This is pretty interesting, but I did leave an inline question.

Also, please add doc comments for your new exported functions. Thanks!

if r.recordSizeFunc != nil {
if r.bytesRead == nil {
// this means downstream code never consumed the body, we must drain the body to get its size
_, err := io.Copy(io.Discard, r)
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this cause Read to be called? If so, can you put that in a comment?


func (r *responseSizeCounterReadCloser) Close() error {
if r.recordSizeFunc != nil {
if r.bytesRead == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

If a previous call to Read didn't read the entire payload, then the leftover size won't be counted, right? E.g.

buf := make([]byte, 10)
reader.Read(buf) // bytesRead is now 10
reader.Close() // bytesRead is not updated

This is kind of a niche case, so I'm not sure how much of a problem it is, but if I'm correct, this should at least be called out in a comment somewhere.

Copy link
Contributor Author

@mustansir14 mustansir14 Nov 11, 2025

Choose a reason for hiding this comment

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

Hmm, that's an interesting case. You're right, a partial read will cause us to not have a correct size. But do you think this is a problem? I can't think of any case where a detector or any piece of code would partially read a response.

I can surely call this out in a comment though.

name string
path string
expectedStatusCode int
expectsNon200 bool
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this field for? It looks unused.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, good catch. I basically copy-pasted the test for SaneHTTPClient so this got copied over as well. Will remove it


// Get initial metric values
sanitizedURL := sanitizeURL(requestURL)
initialRequestsTotal := testutil.ToFloat64(httpRequestsTotal.WithLabelValues(sanitizedURL))
Copy link
Contributor

Choose a reason for hiding this comment

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

Whoa I didn't know we could test Prometheus metrics like this!

Copy link
Contributor

@camgunz camgunz left a comment

Choose a reason for hiding this comment

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

I really like how this is super accurate, which wasn't easy to do! But instead of counting what we read (and potentially manually reading), I think we should start out by just using ContentLength from the Response object. It's definitely less reliable, but I don't want to get out the big hammer until we see how much less reliable it really is.

@mustansir14
Copy link
Contributor Author

I really like how this is super accurate, which wasn't easy to do! But instead of counting what we read (and potentially manually reading), I think we should start out by just using ContentLength from the Response object. It's definitely less reliable, but I don't want to get out the big hammer until we see how much less reliable it really is.

I really liked that code 🥹. But I guess you're right, it is overkill for just metrics. I'll scrap it

@mustansir14 mustansir14 requested a review from camgunz November 21, 2025 09:28
Comment on lines +115 to +118
// Record response body size if known
if contentLength >= 0 {
httpResponseBodySizeBytes.WithLabelValues(sanitizedURL).Observe(float64(contentLength))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

this if will always be true?

Copy link
Contributor

Choose a reason for hiding this comment

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

I assume this check to ensure that the contentLength value being passed into the function is valid (i.e. >0)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's what net/http documentation says about the ContentLength field:

// ContentLength records the length of the associated content. The
// value -1 indicates that the length is unknown. Unless Request.Method
// is "HEAD", values >= 0 indicate that the given number of bytes may
// be read from Body.
ContentLength int64

So there is a possibility of the value being -1

Copy link
Contributor

@nabeelalam nabeelalam left a comment

Choose a reason for hiding this comment

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

Thanks @mustansir14

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants