Repository navigation
Conversation
arturobernalg
left a comment
There was a problem hiding this comment.
I do not think getContentLength() is sufficient for these metrics. It reports the declared entity length, not necessarily the number of bytes transferred.
The tests even record a positive request body size when the connection is refused and no body is sent.
If we use the OpenTelemetry metric names, the recorded values should follow their semantics. Otherwise, these should be exposed as declared entity lengths under different names.
65ebeda to
536bab3
Compare
|
Thanks, you are right.
Why the declared, uncompressed length is still useful: servers often limit If you wish, I can add transferred body sizes under the OpenTelemetry names in a |
arturobernalg
left a comment
There was a problem hiding this comment.
Thanks for the update. The metric semantics are now clear and the interceptors themselves look fine.
There are still a couple of limitations worth addressing.
The caching builder overloads currently do not install the interceptors because CACHING is added after the custom interceptor entries are processed. This is a pre-existing issue, which I will address separately. We should not claim caching support until it is covered by integration tests.
Also, HttpCore reports an unknown response entity length over HTTP/2 even when Content-Length is present. This should be documented.
Finally, getContentLength() does not necessarily represent the uncompressed entity length. I would avoid making that claim unconditionally.
Add `ObservingOptions.MetricSet.CONTENT_LENGTH`, which installs the new `ContentLengthExec` (classic) and `AsyncContentLengthExec` (async) interceptors. They record `<prefix>.request.content.length` and `<prefix>.response.content.length` as `DistributionSummary` meters in bytes. The values are the declared entity lengths as the application sees them: `HttpEntity#getContentLength()` (classic), `AsyncEntityProducer#getContentLength()` and `EntityDetails#getContentLength()` (async). The interceptors run before the client encodes the request and after it decodes the response, so when the client handles content coding the values are the uncompressed lengths. They are the encoded lengths if the application sends an already encoded entity or if content compression is disabled. They are not the number of bytes transferred, so the OpenTelemetry names `http.client.request.body.size` / `http.client.response.body.size` (defined as transferred, possibly compressed bytes) are not used. The uncompressed length matters because servers often limit the decoded size of a request, not its size on the wire. For example, OpenSearch rejects requests with `413 Request Entity Too Large` when the body exceeds `http.max_content_length` (default `100mb`). Netty's `HttpObjectAggregator` enforces this limit after `HttpContentDecompressor`, so a small gzip body that inflates past the limit is rejected. This metric shows how close clients get to such a limit, for example with large `_bulk` requests. See https://github.com/opensearch-project/OpenSearch/blob/56bfaa9cc8c5ba4ee49a92e3476dd9898b76e6e3/modules/transport-netty4/src/main/java/org/opensearch/http/netty4/Netty4HttpServerTransport.java#L500-L506 Nothing is recorded if no response was received, for entities of unknown length (chunked, transparently decompressed) or for messages without an entity. Over HTTP/2, HttpCore reports the response entity length as unknown, so no response length is recorded there. Entities are never wrapped or buffered. Tags match the request timer (`method`, `status`, plus `protocol` and `target` at `TagLevel.EXTENDED`, common tags, tag customizer). The existing `IO` byte counters and the default metric set are unchanged; the new meters are opt-in. See micrometer-metrics/micrometer#880 and micrometer-metrics/micrometer#7158. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Thanks! I have addressed the points:
|
536bab3 to
74d9bc1
Compare
|
@rursprung Could you demonstrate how this metric captures the uncompressed request size when using |
|
oh, i didn't even notice that because so for our setup (no gzip enabled on our side) the functionality from this PR is still useful and i presume there are many setups out there where this would also be the case. does this PR still make sense to you? (i'd of course change the javadoc & commit-msg to reflect the new facts) |
(full disclosure: AI-generated, reviewed by me)
What
Adds opt-in metrics for the declared request and response entity lengths to
httpclient5-observation, for classic and async clients:<prefix>.request.content.lengthDistributionSummary<prefix>.response.content.lengthDistributionSummaryEnable with the new
ObservingOptions.MetricSet.CONTENT_LENGTH:The classic and async
enable(...)variants install the newContentLengthExec/AsyncContentLengthExecinterceptors. The interceptorsare public, so they can also be added by hand. The caching
enable(...)variants contain the same wiring as for
BASICandIO, but the cachingbuilders currently add
CACHINGafter custom interceptors are processed, sonone of these interceptors are installed there yet. That is a pre-existing
issue that will be fixed separately; this PR does not claim caching support.
Why the declared length
Many servers limit the size of a request after decoding, not its size on
the wire. Our use case is OpenSearch, which is used through HttpClient 5 by
opensearch-java:413 Request Entity Too Largeif itsbody exceeds
http.max_content_length(default100mb)(setting).
decoder -> HttpContentDecompressor -> HttpObjectAggregator(maxContentLength).The aggregator enforces the limit after decompression, so it counts the
inflated bytes. A small gzip body that inflates past the limit is rejected
(pipeline).
So to see how close large
_bulkrequests get to the limit, users need theuncompressed size. When the client handles content coding, the declared
entity length that the client sees before compression is exactly that. The
wire size would not show this.
Background:
metrics. The Micrometer team said
(comment)
that this must be done per HTTP library, because
Content-Lengthismissing for chunked bodies and compression makes body size differ from
bytes on the network.
(since 1.17.0) in favor of
httpclient5-observation, so new HttpClient 5instrumentation belongs here.
Design
IOcounters(
<prefix>.request.bytes/<prefix>.response.bytes) are unchanged.ObservingOptions.DEFAULT(BASIC only) is unchanged. Note:ObservingOptions.allMetricSets()now includesCONTENT_LENGTH. japicmpreports the enum change as compatible.
<prefix>.request.content.length/<prefix>.response.content.length. These are deliberately not theOpenTelemetry names
http.client.request.body.size/http.client.response.body.size: OTel defines those as bytes transferred,compressed if a content coding is used. There is no current OTel name for
the uncompressed size (the old
http.*_content_length_uncompressedattributes are deprecated).
DistributionSummary, base unitbytes.MetricConfig.percentilesare published if configured.
method,status, and withTagLevel.EXTENDEDalsoprotocolandtarget.MetricConfig.commonTagsandObservingOptions.tagCustomizerapply.HttpEntity#getContentLength()of the request andresponse entities. Async:
AsyncEntityProducer#getContentLength()andEntityDetails#getContentLength(). The interceptors sit at the start ofthe chain, so they see the request before the client compresses it and the
response after the client decompresses it. The value is therefore
uncompressed when the client handles content coding. It is the encoded
length if the application sends an already encoded entity (for example
with its own
Content-Encoding: gzip) or if content compression isdisabled.
wrapped or buffered.
spanSamplingis honored.What is and is not recorded
that (for example connection refused), nothing is recorded.
Transfer-Encoding: chunked,getContentLength() == -1), including responses that the clientdecompresses transparently (the decompressing wrappers report
-1), andmessages without an entity.
if
Content-Lengthis present, so no response length is recorded forHTTP/2 exchanges.
transferred: it excludes headers and framing, and is
recorded even if the body was not fully sent or received (for example a
server that rejects a request before reading its body).
Possible follow-ups (separate PRs)
http.client.request.body.size/http.client.response.body.size:interceptor just before
MAIN_TRANSPORT(after compression) plus countingof the bytes actually written and read. This also covers chunked bodies.
wrapping entities.
Tests
ContentLengthExecTest(classic) andAsyncContentLengthExecTest(async): known length, chunked/unknown length, no entity (
GET/204),GETwith response entity, error response (500), no response(connection refused, nothing recorded), extended and common tags,
spanSamplingskip, default options not installing the meters../mvnw -pl httpclient5-observation verify javadoc:javadocpasses: tests,checkstyle, japicmp (against 5.6), Javadoc and apache-rat.
Docs
observation.md) lives inapache/httpcomponents-websiteand has no 5.7.x version yet. I'll raise a follow-up PR there once this PR here is approved/merged.