shinseongsu commented on PR #906:
URL:
https://github.com/apache/httpcomponents-client/pull/906#issuecomment-6707902063
Hi @rursprung, thanks for working on this! 🙂 I was curious how the new
meters behave with gzip and HTTP/2, so I tried the PR (74d9bc11c) against a
small local server. I'm sharing the numbers in case they help.
Setup: `HttpClientObservationSupport.enable(...)` with `CONTENT_LENGTH`, and
a 100,000 byte body (133 bytes after gzip).
| Case | Wire | Recorded |
|---|---|---|
| plain request | 100000 | 100000 ✅ |
| request via `EntityBuilder.gzipCompressed()` | 133 (chunked) | nothing |
| pre-gzipped request entity | 133 | 133 |
| gzip response, decompressed by the client | 133 | nothing |
| gzip response, content compression off | 133 | 133 |
| async HTTP/2 response with `Content-Length: 100000` | 100000 | nothing |
So I couldn't find a case where the uncompressed size shows up 😅 When
something is recorded, it's the same as the `Content-Length` on the wire. The
existing `IO` counters gave me the same numbers, since they also read
`getContentLength()`.
For the OpenSearch case I think this means the decoded `_bulk` size isn't
visible once gzip is on. And since opensearch-java compresses the request
itself before the exec chain, I'm not sure the client can see the decoded
request size at all. Counting the bytes that actually go through (like Arturo
mentioned) might be the way to get real numbers.
If you want to try it yourself, drop the test below into
`httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/interceptors/`
and run:
```
./mvnw -pl httpclient5-observation -am test
-Dtest=ContentLengthSemanticsTest -Dsurefire.failIfNoSpecifiedTests=false
```
All 4 tests pass on the PR branch. The assertions just describe what happens
today.
Hope this is useful! 🙏
<details>
<summary>ContentLengthSemanticsTest.java</summary>
```java
package org.apache.hc.client5.http.observation.interceptors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPOutputStream;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.entity.EntityBuilder;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.observation.HttpClientObservationSupport;
import org.apache.hc.client5.http.observation.MetricConfig;
import org.apache.hc.client5.http.observation.ObservingOptions;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.EntityDetails;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.Message;
import org.apache.hc.core5.http.URIScheme;
import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer;
import org.apache.hc.core5.http.impl.bootstrap.HttpServer;
import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.message.BasicHttpResponse;
import org.apache.hc.core5.http.nio.AsyncRequestConsumer;
import org.apache.hc.core5.http.nio.AsyncServerRequestHandler;
import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityConsumer;
import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityProducer;
import org.apache.hc.core5.http.nio.support.BasicRequestConsumer;
import org.apache.hc.core5.http.nio.support.BasicResponseProducer;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http2.HttpVersionPolicy;
import org.apache.hc.core5.http2.impl.nio.bootstrap.H2ServerBootstrap;
import org.apache.hc.core5.io.CloseMode;
import org.apache.hc.core5.reactor.ListenerEndpoint;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* What CONTENT_LENGTH records when compression or HTTP/2 is involved.
* Body: 100_000 bytes of 'a' (gzips to 133 bytes). Assertions describe the
observed behaviour.
*/
class ContentLengthSemanticsTest {
private static final String REQUEST = "t.request.content.length";
private static final String RESPONSE = "t.response.content.length";
private static final byte[] BODY = new byte[100_000];
static {
Arrays.fill(BODY, (byte) 'a');
}
private byte[] gzippedBody;
private HttpServer server;
private String base;
private volatile int serverReceivedBytes;
private static ObservingOptions options() {
return
ObservingOptions.builder().metrics(EnumSet.of(ObservingOptions.MetricSet.CONTENT_LENGTH)).build();
}
private static MetricConfig config() {
return MetricConfig.builder().prefix("t").build();
}
private static Double total(final MeterRegistry meters, final String
name) {
final DistributionSummary s = meters.find(name).summary();
return s != null ? s.totalAmount() : null;
}
@BeforeEach
void setUp() throws IOException {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
try (GZIPOutputStream gz = new GZIPOutputStream(out)) {
gz.write(BODY);
}
gzippedBody = out.toByteArray();
server = ServerBootstrap.bootstrap()
.setLocalAddress(InetAddress.getLoopbackAddress())
.setListenerPort(0)
.register("localhost", "/echo", (request, response, context)
-> {
serverReceivedBytes =
EntityUtils.toByteArray(request.getEntity()).length;
response.setCode(200);
})
.register("localhost", "/gzip", (request, response, context)
-> {
response.setCode(200);
response.setEntity(new ByteArrayEntity(gzippedBody,
ContentType.TEXT_PLAIN, "gzip"));
})
.create();
server.start();
base = "http://localhost:" + server.getLocalPort();
}
@AfterEach
void tearDown() {
server.close(CloseMode.IMMEDIATE);
}
private MeterRegistry classic(final HttpPost post) throws IOException {
final MeterRegistry meters = new SimpleMeterRegistry();
final HttpClientBuilder builder = HttpClients.custom();
HttpClientObservationSupport.enable(builder, null, meters,
options(), config());
try (CloseableHttpClient client = builder.build()) {
client.execute(post, r -> {
EntityUtils.consume(r.getEntity());
return null;
});
}
return meters;
}
@Test
void requestCompressedWithEntityBuilderIsNotRecorded() throws Exception {
final HttpPost post = new HttpPost(base + "/echo");
post.setEntity(EntityBuilder.create().setBinary(BODY)
.setContentType(ContentType.APPLICATION_JSON).gzipCompressed().build());
final MeterRegistry meters = classic(post);
assertEquals(gzippedBody.length, serverReceivedBytes, "server
received the gzipped body");
assertNull(total(meters, REQUEST), "neither the decoded nor the wire
size is recorded");
}
@Test
void preEncodedRequestRecordsCompressedLength() throws Exception {
final HttpPost post = new HttpPost(base + "/echo");
post.setEntity(new ByteArrayEntity(gzippedBody,
ContentType.APPLICATION_JSON, "gzip"));
final MeterRegistry meters = classic(post);
assertEquals(gzippedBody.length, serverReceivedBytes);
assertEquals((double) gzippedBody.length, total(meters, REQUEST),
"compressed length, not 100000");
}
@Test
void transparentlyDecompressedResponseIsNotRecorded() throws Exception {
final MeterRegistry meters = classic(new HttpPost(base + "/gzip"));
assertNull(total(meters, RESPONSE), "Content-Length: 133 on the
wire, 100000 read by the app, nothing recorded");
}
@Test
void http2ResponseWithContentLengthIsNotRecorded() throws Exception {
final HttpAsyncServer h2 = H2ServerBootstrap.bootstrap()
.setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_2)
.register("*", new
AsyncServerRequestHandler<Message<HttpRequest, byte[]>>() {
@Override
public AsyncRequestConsumer<Message<HttpRequest,
byte[]>> prepare(
final HttpRequest request, final EntityDetails
details, final HttpContext context) {
return new BasicRequestConsumer<>(details != null ?
new BasicAsyncEntityConsumer() : null);
}
@Override
public void handle(final Message<HttpRequest, byte[]>
message, final ResponseTrigger trigger,
final HttpContext context) throws
HttpException, IOException {
final BasicHttpResponse head = new
BasicHttpResponse(200);
head.addHeader("Content-Length",
Integer.toString(BODY.length));
trigger.submitResponse(new
BasicResponseProducer(head,
new BasicAsyncEntityProducer(BODY,
ContentType.APPLICATION_OCTET_STREAM)), context);
}
})
.create();
h2.start();
final ListenerEndpoint endpoint = h2.listen(
new InetSocketAddress(InetAddress.getLoopbackAddress(), 0),
URIScheme.HTTP).get();
final int port = ((InetSocketAddress)
endpoint.getAddress()).getPort();
final MeterRegistry meters = new SimpleMeterRegistry();
final HttpAsyncClientBuilder builder =
HttpAsyncClients.custom().setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_2);
HttpClientObservationSupport.enable(builder, null, meters,
options(), config());
try (CloseableHttpAsyncClient client = builder.build()) {
client.start();
final SimpleHttpResponse response = client.execute(
SimpleRequestBuilder.get("http://localhost:" + port +
"/").build(), null).get(20, TimeUnit.SECONDS);
assertEquals("100000",
response.getFirstHeader("Content-Length").getValue());
assertNull(total(meters, RESPONSE), "HTTP/2 response with
Content-Length: 100000, nothing recorded");
} finally {
h2.close(CloseMode.IMMEDIATE);
}
}
}
```
</details>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]