Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@
import org.apache.hc.client5.http.observation.impl.ObservationClassicExecInterceptor;
import org.apache.hc.client5.http.observation.impl.MeteredDnsResolver;
import org.apache.hc.client5.http.observation.impl.MeteredTlsStrategy;
import org.apache.hc.client5.http.observation.interceptors.AsyncContentLengthExec;
import org.apache.hc.client5.http.observation.interceptors.AsyncIoByteCounterExec;
import org.apache.hc.client5.http.observation.interceptors.AsyncTimerExec;
import org.apache.hc.client5.http.observation.interceptors.ContentLengthExec;
import org.apache.hc.client5.http.observation.interceptors.IoByteCounterExec;
import org.apache.hc.client5.http.observation.interceptors.TimerExec;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
Expand Down Expand Up @@ -94,6 +96,8 @@
* that surrounds each execution with a start/stop span.</li>
* <li>Metric interceptors according to {@link ObservingOptions.MetricSet}:
* BASIC (latency timer + response counter), IO (bytes in/out counters),
* CONTENT_LENGTH (declared request/response entity length distribution
* summaries, opt-in),
* and CONN_POOL (pool gauges; classic and async variants). Pool lease
* timing is available when using {@link #meteredConnectionManager} or
* {@link #meteredAsyncConnectionManager}.</li>
Expand All @@ -118,6 +122,10 @@ public final class HttpClientObservationSupport {
* Internal ID for I/O byte counters interceptor.
*/
private static final String IO_ID = "metric-io";
/**
* Internal ID for content length distribution summaries interceptor.
*/
private static final String CONTENT_LENGTH_ID = "metric-content-length";

/* ====================== Classic ====================== */

Expand Down Expand Up @@ -203,6 +211,9 @@ public static void enable(final HttpClientBuilder builder,
if (o.metricSets.contains(ObservingOptions.MetricSet.IO)) {
builder.addExecInterceptorFirst(IO_ID, new IoByteCounterExec(meterReg, o, config));
}
if (o.metricSets.contains(ObservingOptions.MetricSet.CONTENT_LENGTH)) {
builder.addExecInterceptorFirst(CONTENT_LENGTH_ID, new ContentLengthExec(meterReg, o, config));
}
if (o.metricSets.contains(ObservingOptions.MetricSet.CONN_POOL)) {
ConnPoolMeters.bindTo(builder, meterReg, config);
}
Expand Down Expand Up @@ -294,6 +305,9 @@ public static void enable(final CachingHttpClientBuilder builder,
if (o.metricSets.contains(ObservingOptions.MetricSet.IO)) {
builder.addExecInterceptorAfter(ChainElement.CACHING.name(), IO_ID, new IoByteCounterExec(meterReg, o, config));
}
if (o.metricSets.contains(ObservingOptions.MetricSet.CONTENT_LENGTH)) {
builder.addExecInterceptorAfter(ChainElement.CACHING.name(), CONTENT_LENGTH_ID, new ContentLengthExec(meterReg, o, config));
}
if (o.metricSets.contains(ObservingOptions.MetricSet.CONN_POOL)) {
ConnPoolMeters.bindTo(builder, meterReg, config);
}
Expand Down Expand Up @@ -381,6 +395,9 @@ public static void enable(final HttpAsyncClientBuilder builder,
if (o.metricSets.contains(ObservingOptions.MetricSet.IO)) {
builder.addExecInterceptorFirst(IO_ID, new AsyncIoByteCounterExec(meterReg, o, config));
}
if (o.metricSets.contains(ObservingOptions.MetricSet.CONTENT_LENGTH)) {
builder.addExecInterceptorFirst(CONTENT_LENGTH_ID, new AsyncContentLengthExec(meterReg, o, config));
}
if (o.metricSets.contains(ObservingOptions.MetricSet.CONN_POOL)) {
ConnPoolMetersAsync.bindTo(builder, meterReg, config);
}
Expand Down Expand Up @@ -471,6 +488,9 @@ public static void enable(final CachingHttpAsyncClientBuilder builder,
if (o.metricSets.contains(ObservingOptions.MetricSet.IO)) {
builder.addExecInterceptorAfter(ChainElement.CACHING.name(), IO_ID, new AsyncIoByteCounterExec(meterReg, o, config));
}
if (o.metricSets.contains(ObservingOptions.MetricSet.CONTENT_LENGTH)) {
builder.addExecInterceptorAfter(ChainElement.CACHING.name(), CONTENT_LENGTH_ID, new AsyncContentLengthExec(meterReg, o, config));
}
if (o.metricSets.contains(ObservingOptions.MetricSet.CONN_POOL)) {
ConnPoolMetersAsync.bindTo(builder, meterReg, config);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,25 @@ public final class ObservingOptions {
* {@link org.apache.hc.client5.http.observation.HttpClientObservationSupport#meteredTlsStrategy}
* and {@link org.apache.hc.client5.http.observation.HttpClientObservationSupport#meteredDnsResolver}
* to wrap the underlying TLS strategy or DNS resolver.
* <p>
* {@link MetricSet#CONTENT_LENGTH} records the declared request and response
* entity lengths as distribution summaries. See
* {@link org.apache.hc.client5.http.observation.interceptors.ContentLengthExec}
* for details.
*/
public enum MetricSet { BASIC, IO, CONN_POOL, TLS, DNS }
public enum MetricSet {
BASIC,
IO,
CONN_POOL,
TLS,
DNS,
/**
* Distributions of the declared request and response entity lengths, in bytes.
*
* @since 5.7
*/
CONTENT_LENGTH
}

/**
* How many tags each metric/trace should get.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.observation.interceptors;

import java.io.IOException;

import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;
import org.apache.hc.client5.http.async.AsyncExecCallback;
import org.apache.hc.client5.http.async.AsyncExecChain;
import org.apache.hc.client5.http.async.AsyncExecChainHandler;
import org.apache.hc.client5.http.observation.MetricConfig;
import org.apache.hc.client5.http.observation.ObservingOptions;
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.HttpResponse;
import org.apache.hc.core5.http.nio.AsyncDataConsumer;
import org.apache.hc.core5.http.nio.AsyncEntityProducer;
import org.apache.hc.core5.util.Args;

/**
* Records the <b>declared</b> request / response entity lengths for
* <b>asynchronous</b> clients as {@link DistributionSummary distribution summaries}.
* <p>
* Meters, tags and semantics are the same as for {@link ContentLengthExec}. The
* request length is taken from {@link AsyncEntityProducer#getContentLength()} and
* the response length from {@link EntityDetails#getContentLength()}. They are the
* declared entity lengths as the application sees them, not the number of bytes
* transferred, and they are uncompressed only if no content coding is visible to
* the application. Nothing is recorded if no response was received, for entities
* of unknown length (for example chunked) or for messages without an entity. The
* data stream is never wrapped or buffered.
* <p>
* Over HTTP/2, HttpCore reports the response entity length as unknown even if
* a {@code Content-Length} header is present, so no response length is recorded
* for HTTP/2 exchanges.
* <p>
* Enabled by {@link ObservingOptions.MetricSet#CONTENT_LENGTH}.
*
* @see ContentLengthExec
* @since 5.7
*/
public final class AsyncContentLengthExec implements AsyncExecChainHandler {

private final ObservingOptions opts;
private final ContentLengthRecorder recorder;

/**
* Creates a new instance.
*
* @param meterRegistry meter registry to register meters with
* @param opts observation/metric options
* @param mc metric configuration
*/
public AsyncContentLengthExec(final MeterRegistry meterRegistry,
final ObservingOptions opts,
final MetricConfig mc) {
Args.notNull(meterRegistry, "meterRegistry");
this.opts = Args.notNull(opts, "observingOptions");
Args.notNull(mc, "metricConfig");
this.recorder = new ContentLengthRecorder(meterRegistry, opts, mc);
}

@Override
public void execute(final HttpRequest request,
final AsyncEntityProducer entityProducer,
final AsyncExecChain.Scope scope,
final AsyncExecChain chain,
final AsyncExecCallback callback) throws HttpException, IOException {

if (!opts.spanSampling.test(request.getRequestUri())) {
chain.proceed(request, entityProducer, scope, callback);
return;
}

final long requestContentLength = entityProducer != null
? ContentLengthRecorder.normalize(entityProducer.getContentLength())
: -1L;

final AsyncExecCallback wrapped = new AsyncExecCallback() {

private volatile HttpResponse response;
private volatile long responseContentLength = -1L;

@Override
public AsyncDataConsumer handleResponse(final HttpResponse response,
final EntityDetails entityDetails) throws HttpException, IOException {
this.response = response;
if (entityDetails != null) {
responseContentLength = ContentLengthRecorder.normalize(entityDetails.getContentLength());
}
return callback.handleResponse(response, entityDetails);
}

@Override
public void handleInformationResponse(final HttpResponse response) throws HttpException, IOException {
callback.handleInformationResponse(response);
}

@Override
public void completed() {
record();
callback.completed();
}

@Override
public void failed(final Exception cause) {
record();
callback.failed(cause);
}

private void record() {
final HttpResponse rsp = response;
if (rsp != null) {
recorder.record(request.getMethod(), rsp.getCode(), scope.route.getTargetHost(),
request.getRequestUri(), requestContentLength, responseContentLength);
}
}
};

chain.proceed(request, entityProducer, scope, wrapped);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.observation.interceptors;

import java.io.IOException;

import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;
import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecChainHandler;
import org.apache.hc.client5.http.observation.MetricConfig;
import org.apache.hc.client5.http.observation.ObservingOptions;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.util.Args;

/**
* Records the <b>declared</b> request / response entity lengths for <b>classic</b>
* clients as {@link DistributionSummary distribution summaries}.
* <p>
* Meters:
* <ul>
* <li>{@code <prefix>.request.content.length} (distribution summary, baseUnit=bytes)</li>
* <li>{@code <prefix>.response.content.length} (distribution summary, baseUnit=bytes)</li>
* </ul>
* <p>
* Tags are the same as for the request timer: {@code method}, {@code status},
* and when {@link ObservingOptions.TagLevel#EXTENDED} also {@code protocol} and
* {@code target}. {@link ObservingOptions#tagCustomizer} is applied and
* {@link MetricConfig#commonTags} are appended. {@link MetricConfig#percentiles}
* are published if configured.
* <p>
* <b>What is measured.</b> The values are taken from {@link HttpEntity#getContentLength()}
* of the request and response entities as the application sees them: before the
* client applies any content coding to the request and after the client
* transparently decodes the response. They are the declared entity lengths, not
* the number of bytes transferred: they exclude headers and transfer framing,
* and they are recorded even if the body is not (fully) transmitted, for example
* when the server rejects a request before reading its body.
* <p>
* The declared length is the uncompressed length only if no content coding is
* visible to the application. It is the encoded length if the application sends
* an entity that is already encoded (for example with its own
* {@code Content-Encoding: gzip}) or if content compression is disabled and the
* response is not decoded. When the client handles content coding, this metric
* is useful where limits apply to the decoded size, for example servers that
* reject requests whose decoded body exceeds a maximum content length.
* <p>
* <b>What is not measured.</b> Nothing is recorded if no response was received
* (for example the connection could not be established), for an entity whose
* length is not known up front (for example {@code Transfer-Encoding: chunked}
* or a response the client decodes transparently), or for a message without
* an entity. Entities are never wrapped or buffered, so the instrumentation adds
* no I/O overhead.
* <p>
* Enabled by {@link ObservingOptions.MetricSet#CONTENT_LENGTH}.
*
* @see AsyncContentLengthExec
* @since 5.7
*/
public final class ContentLengthExec implements ExecChainHandler {

private final ObservingOptions opts;
private final ContentLengthRecorder recorder;

/**
* Creates a new instance.
*
* @param meterRegistry meter registry to register meters with
* @param opts observation/metric options
* @param mc metric configuration
*/
public ContentLengthExec(final MeterRegistry meterRegistry,
final ObservingOptions opts,
final MetricConfig mc) {
Args.notNull(meterRegistry, "meterRegistry");
this.opts = Args.notNull(opts, "observingOptions");
Args.notNull(mc, "metricConfig");
this.recorder = new ContentLengthRecorder(meterRegistry, opts, mc);
}

@Override
public ClassicHttpResponse execute(final ClassicHttpRequest request,
final ExecChain.Scope scope,
final ExecChain chain) throws IOException, HttpException {

if (!opts.spanSampling.test(request.getRequestUri())) {
return chain.proceed(request, scope);
}

final long requestContentLength = contentLength(request.getEntity());
final ClassicHttpResponse response = chain.proceed(request, scope);
recorder.record(request.getMethod(), response.getCode(), scope.route.getTargetHost(),
request.getRequestUri(), requestContentLength, contentLength(response.getEntity()));
return response;
}

private static long contentLength(final HttpEntity entity) {
return entity != null ? ContentLengthRecorder.normalize(entity.getContentLength()) : -1L;
}

}
Loading