diff --git a/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/HttpClientObservationSupport.java b/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/HttpClientObservationSupport.java
index 044540e36c..6880f594b3 100644
--- a/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/HttpClientObservationSupport.java
+++ b/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/HttpClientObservationSupport.java
@@ -33,16 +33,24 @@
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
import org.apache.hc.client5.http.impl.cache.CachingHttpAsyncClientBuilder;
import org.apache.hc.client5.http.impl.cache.CachingHttpClientBuilder;
+import org.apache.hc.client5.http.DnsResolver;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.observation.binder.ConnPoolMeters;
import org.apache.hc.client5.http.observation.binder.ConnPoolMetersAsync;
+import org.apache.hc.client5.http.observation.impl.MeteredAsyncConnectionManager;
+import org.apache.hc.client5.http.observation.impl.MeteredConnectionManager;
import org.apache.hc.client5.http.observation.impl.ObservationAsyncExecInterceptor;
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.AsyncIoByteCounterExec;
import org.apache.hc.client5.http.observation.interceptors.AsyncTimerExec;
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;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionManager;
import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
/**
* Utility class that wires Micrometer / OpenTelemetry instrumentation into
@@ -86,7 +94,9 @@
* that surrounds each execution with a start/stop span.
*
Metric interceptors according to {@link ObservingOptions.MetricSet}:
* BASIC (latency timer + response counter), IO (bytes in/out counters),
- * and CONN_POOL (pool gauges; classic and async variants).
+ * and CONN_POOL (pool gauges; classic and async variants). Pool lease
+ * timing is available when using {@link #meteredConnectionManager} or
+ * {@link #meteredAsyncConnectionManager}.
*
*
* Thread safety: This class is stateless. Methods may be
@@ -461,6 +471,111 @@ 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.CONN_POOL)) {
+ ConnPoolMetersAsync.bindTo(builder, meterReg, config);
+ }
+ }
+
+ /**
+ * Wraps a classic connection manager with Micrometer pool-lease metrics if
+ * {@link ObservingOptions.MetricSet#CONN_POOL} is enabled. Otherwise returns
+ * the delegate unchanged.
+ *
+ * @param delegate connection manager to wrap
+ * @param meterReg meter registry to register meters with (must not be {@code null})
+ * @param opts observation/metric options; when {@code null} {@link ObservingOptions#DEFAULT} is used
+ * @param mc metric configuration; when {@code null} {@link MetricConfig#DEFAULT} is used
+ * @return metered connection manager or original delegate
+ * @since 5.7
+ */
+ public static HttpClientConnectionManager meteredConnectionManager(final HttpClientConnectionManager delegate,
+ final MeterRegistry meterReg,
+ final ObservingOptions opts,
+ final MetricConfig mc) {
+ Args.notNull(delegate, "delegate");
+ Args.notNull(meterReg, "meterRegistry");
+ final ObservingOptions o = opts != null ? opts : ObservingOptions.DEFAULT;
+ final MetricConfig config = mc != null ? mc : MetricConfig.DEFAULT;
+ if (!o.metricSets.contains(ObservingOptions.MetricSet.CONN_POOL)) {
+ return delegate;
+ }
+ return new MeteredConnectionManager(delegate, meterReg, config, o);
+ }
+
+ /**
+ * Wraps an async connection manager with Micrometer pool-lease metrics if
+ * {@link ObservingOptions.MetricSet#CONN_POOL} is enabled. Otherwise returns
+ * the delegate unchanged.
+ *
+ * @param delegate connection manager to wrap
+ * @param meterReg meter registry to register meters with (must not be {@code null})
+ * @param opts observation/metric options; when {@code null} {@link ObservingOptions#DEFAULT} is used
+ * @param mc metric configuration; when {@code null} {@link MetricConfig#DEFAULT} is used
+ * @return metered connection manager or original delegate
+ * @since 5.7
+ */
+ public static AsyncClientConnectionManager meteredAsyncConnectionManager(final AsyncClientConnectionManager delegate,
+ final MeterRegistry meterReg,
+ final ObservingOptions opts,
+ final MetricConfig mc) {
+ Args.notNull(delegate, "delegate");
+ Args.notNull(meterReg, "meterRegistry");
+ final ObservingOptions o = opts != null ? opts : ObservingOptions.DEFAULT;
+ final MetricConfig config = mc != null ? mc : MetricConfig.DEFAULT;
+ if (!o.metricSets.contains(ObservingOptions.MetricSet.CONN_POOL)) {
+ return delegate;
+ }
+ return new MeteredAsyncConnectionManager(delegate, meterReg, config, o);
+ }
+
+ /**
+ * Wraps a DNS resolver with Micrometer metrics if {@link ObservingOptions.MetricSet#DNS}
+ * is enabled. Otherwise returns the delegate unchanged.
+ *
+ * @param delegate underlying DNS resolver
+ * @param meterReg meter registry to register meters with (must not be {@code null})
+ * @param opts observation/metric options; when {@code null} {@link ObservingOptions#DEFAULT} is used
+ * @param mc metric configuration; when {@code null} {@link MetricConfig#DEFAULT} is used
+ * @return metered resolver or original delegate
+ * @since 5.7
+ */
+ public static DnsResolver meteredDnsResolver(final DnsResolver delegate,
+ final MeterRegistry meterReg,
+ final ObservingOptions opts,
+ final MetricConfig mc) {
+ Args.notNull(delegate, "delegate");
+ Args.notNull(meterReg, "meterRegistry");
+ final ObservingOptions o = opts != null ? opts : ObservingOptions.DEFAULT;
+ final MetricConfig config = mc != null ? mc : MetricConfig.DEFAULT;
+ if (!o.metricSets.contains(ObservingOptions.MetricSet.DNS)) {
+ return delegate;
+ }
+ return new MeteredDnsResolver(delegate, meterReg, config, o);
+ }
+
+ /**
+ * Wraps a TLS strategy with Micrometer metrics if {@link ObservingOptions.MetricSet#TLS}
+ * is enabled. Otherwise returns the delegate unchanged.
+ *
+ * @param delegate TLS strategy to wrap
+ * @param meterReg meter registry to register meters with (must not be {@code null})
+ * @param opts observation/metric options; when {@code null} {@link ObservingOptions#DEFAULT} is used
+ * @param mc metric configuration; when {@code null} {@link MetricConfig#DEFAULT} is used
+ * @return metered strategy or original delegate
+ * @since 5.7
+ */
+ public static TlsStrategy meteredTlsStrategy(final TlsStrategy delegate,
+ final MeterRegistry meterReg,
+ final ObservingOptions opts,
+ final MetricConfig mc) {
+ Args.notNull(delegate, "delegate");
+ Args.notNull(meterReg, "meterRegistry");
+ final ObservingOptions o = opts != null ? opts : ObservingOptions.DEFAULT;
+ final MetricConfig config = mc != null ? mc : MetricConfig.DEFAULT;
+ if (!o.metricSets.contains(ObservingOptions.MetricSet.TLS)) {
+ return delegate;
+ }
+ return new MeteredTlsStrategy(delegate, meterReg, config, o);
}
/**
diff --git a/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/ObservingOptions.java b/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/ObservingOptions.java
index a6f8c6c856..d6c3d6501e 100644
--- a/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/ObservingOptions.java
+++ b/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/ObservingOptions.java
@@ -43,6 +43,11 @@ public final class ObservingOptions {
/**
* Which metric groups to enable.
+ *
+ * {@link MetricSet#TLS} and {@link MetricSet#DNS} are applied when using
+ * {@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.
*/
public enum MetricSet { BASIC, IO, CONN_POOL, TLS, DNS }
diff --git a/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/impl/MeteredAsyncConnectionManager.java b/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/impl/MeteredAsyncConnectionManager.java
new file mode 100644
index 0000000000..62eac64778
--- /dev/null
+++ b/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/impl/MeteredAsyncConnectionManager.java
@@ -0,0 +1,272 @@
+/*
+ * ====================================================================
+ * 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
+ * .
+ *
+ */
+package org.apache.hc.client5.http.observation.impl;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Timer;
+import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionManager;
+import org.apache.hc.client5.http.nio.AsyncConnectionEndpoint;
+import org.apache.hc.client5.http.observation.MetricConfig;
+import org.apache.hc.client5.http.observation.ObservingOptions;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.pool.ConnPoolControl;
+import org.apache.hc.core5.pool.PoolStats;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+
+/**
+ * Async connection manager wrapper that records pool lease wait time via Micrometer.
+ *
+ * @since 5.7
+ */
+public final class MeteredAsyncConnectionManager implements AsyncClientConnectionManager, ConnPoolControl {
+
+ private final AsyncClientConnectionManager delegate;
+ private final MeterRegistry registry;
+ private final MetricConfig mc;
+ private final ObservingOptions opts;
+ private final ConnPoolControl poolControl;
+
+ public MeteredAsyncConnectionManager(final AsyncClientConnectionManager delegate,
+ final MeterRegistry registry,
+ final MetricConfig mc,
+ final ObservingOptions opts) {
+ this.delegate = Args.notNull(delegate, "delegate");
+ this.registry = Args.notNull(registry, "registry");
+ this.mc = mc != null ? mc : MetricConfig.builder().build();
+ this.opts = opts != null ? opts : ObservingOptions.DEFAULT;
+ @SuppressWarnings("unchecked") final ConnPoolControl pc =
+ delegate instanceof ConnPoolControl ? (ConnPoolControl) delegate : null;
+ this.poolControl = pc;
+ }
+
+ @Override
+ public Future lease(final String id,
+ final HttpRoute route,
+ final Object state,
+ final Timeout requestTimeout,
+ final FutureCallback callback) {
+ final long start = System.nanoTime();
+ final AtomicBoolean recorded = new AtomicBoolean(false);
+ final FutureCallback wrapped = new FutureCallback() {
+ @Override
+ public void completed(final AsyncConnectionEndpoint result) {
+ recordOnce(recorded, "ok", route, start);
+ if (callback != null) {
+ callback.completed(result);
+ }
+ }
+
+ @Override
+ public void failed(final Exception ex) {
+ recordOnce(recorded, "error", route, start);
+ if (callback != null) {
+ callback.failed(ex);
+ }
+ }
+
+ @Override
+ public void cancelled() {
+ recordOnce(recorded, "cancel", route, start);
+ if (callback != null) {
+ callback.cancelled();
+ }
+ }
+ };
+
+ final Future future = delegate.lease(id, route, state, requestTimeout, wrapped);
+ return new Future() {
+ @Override
+ public boolean cancel(final boolean mayInterruptIfRunning) {
+ final boolean cancelled = future.cancel(mayInterruptIfRunning);
+ if (cancelled) {
+ recordOnce(recorded, "cancel", route, start);
+ }
+ return cancelled;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return future.isCancelled();
+ }
+
+ @Override
+ public boolean isDone() {
+ return future.isDone();
+ }
+
+ @Override
+ public AsyncConnectionEndpoint get() throws InterruptedException, ExecutionException {
+ return future.get();
+ }
+
+ @Override
+ public AsyncConnectionEndpoint get(final long timeout, final TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ return future.get(timeout, unit);
+ }
+ };
+ }
+
+ @Override
+ public void release(final AsyncConnectionEndpoint endpoint, final Object newState, final TimeValue validDuration) {
+ delegate.release(endpoint, newState, validDuration);
+ }
+
+ @Override
+ public Future connect(final AsyncConnectionEndpoint endpoint,
+ final ConnectionInitiator connectionInitiator,
+ final Timeout connectTimeout,
+ final Object attachment,
+ final HttpContext context,
+ final FutureCallback callback) {
+ return delegate.connect(endpoint, connectionInitiator, connectTimeout, attachment, context, callback);
+ }
+
+ @Override
+ public void upgrade(final AsyncConnectionEndpoint endpoint, final Object attachment, final HttpContext context) {
+ delegate.upgrade(endpoint, attachment, context);
+ }
+
+ @Override
+ public void close() throws java.io.IOException {
+ delegate.close();
+ }
+
+ @Override
+ public void close(final org.apache.hc.core5.io.CloseMode closeMode) {
+ delegate.close(closeMode);
+ }
+
+ @Override
+ public PoolStats getTotalStats() {
+ return poolControl != null ? poolControl.getTotalStats() : new PoolStats(0, 0, 0, 0);
+ }
+
+ @Override
+ public PoolStats getStats(final HttpRoute route) {
+ return poolControl != null ? poolControl.getStats(route) : new PoolStats(0, 0, 0, 0);
+ }
+
+ @Override
+ public void setMaxTotal(final int max) {
+ if (poolControl != null) {
+ poolControl.setMaxTotal(max);
+ }
+ }
+
+ @Override
+ public int getMaxTotal() {
+ return poolControl != null ? poolControl.getMaxTotal() : 0;
+ }
+
+ @Override
+ public void setDefaultMaxPerRoute(final int max) {
+ if (poolControl != null) {
+ poolControl.setDefaultMaxPerRoute(max);
+ }
+ }
+
+ @Override
+ public int getDefaultMaxPerRoute() {
+ return poolControl != null ? poolControl.getDefaultMaxPerRoute() : 0;
+ }
+
+ @Override
+ public void setMaxPerRoute(final HttpRoute route, final int max) {
+ if (poolControl != null) {
+ poolControl.setMaxPerRoute(route, max);
+ }
+ }
+
+ @Override
+ public int getMaxPerRoute(final HttpRoute route) {
+ return poolControl != null ? poolControl.getMaxPerRoute(route) : 0;
+ }
+
+ @Override
+ public void closeIdle(final TimeValue idleTime) {
+ if (poolControl != null) {
+ poolControl.closeIdle(idleTime);
+ }
+ }
+
+ @Override
+ public void closeExpired() {
+ if (poolControl != null) {
+ poolControl.closeExpired();
+ }
+ }
+
+ @Override
+ public Set getRoutes() {
+ return poolControl != null ? poolControl.getRoutes() : Collections.emptySet();
+ }
+
+ private void recordOnce(final AtomicBoolean recorded,
+ final String result,
+ final HttpRoute route,
+ final long startNanos) {
+ if (recorded.compareAndSet(false, true)) {
+ record(result, route, startNanos);
+ }
+ }
+
+ private void record(final String result, final HttpRoute route, final long startNanos) {
+ final List tags = new ArrayList<>(3);
+ tags.add(Tag.of("result", result));
+ if (opts.tagLevel == ObservingOptions.TagLevel.EXTENDED && route != null && route.getTargetHost() != null) {
+ tags.add(Tag.of("target", route.getTargetHost().getHostName()));
+ }
+ Timer.builder(mc.prefix + ".pool.lease")
+ .tags(mc.commonTags)
+ .tags(tags)
+ .register(registry)
+ .record(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
+ Counter.builder(mc.prefix + ".pool.leases")
+ .tags(mc.commonTags)
+ .tags(tags)
+ .register(registry)
+ .increment();
+ }
+}
diff --git a/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/impl/MeteredConnectionManager.java b/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/impl/MeteredConnectionManager.java
new file mode 100644
index 0000000000..a4dc2ba8d6
--- /dev/null
+++ b/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/impl/MeteredConnectionManager.java
@@ -0,0 +1,235 @@
+/*
+ * ====================================================================
+ * 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
+ * .
+ *
+ */
+package org.apache.hc.client5.http.observation.impl;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Timer;
+import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.client5.http.io.ConnectionEndpoint;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.io.LeaseRequest;
+import org.apache.hc.client5.http.observation.MetricConfig;
+import org.apache.hc.client5.http.observation.ObservingOptions;
+import org.apache.hc.core5.pool.ConnPoolControl;
+import org.apache.hc.core5.pool.PoolStats;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+
+/**
+ * Connection manager wrapper that records pool lease wait time via Micrometer.
+ *
+ * @since 5.7
+ */
+public final class MeteredConnectionManager implements HttpClientConnectionManager, ConnPoolControl {
+
+ private final HttpClientConnectionManager delegate;
+ private final MeterRegistry registry;
+ private final MetricConfig mc;
+ private final ObservingOptions opts;
+ private final ConnPoolControl poolControl;
+
+ public MeteredConnectionManager(final HttpClientConnectionManager delegate,
+ final MeterRegistry registry,
+ final MetricConfig mc,
+ final ObservingOptions opts) {
+ this.delegate = Args.notNull(delegate, "delegate");
+ this.registry = Args.notNull(registry, "registry");
+ this.mc = mc != null ? mc : MetricConfig.builder().build();
+ this.opts = opts != null ? opts : ObservingOptions.DEFAULT;
+ @SuppressWarnings("unchecked") final ConnPoolControl pc =
+ delegate instanceof ConnPoolControl ? (ConnPoolControl) delegate : null;
+ this.poolControl = pc;
+ }
+
+ @Override
+ public LeaseRequest lease(final String id, final HttpRoute route, final Timeout requestTimeout, final Object state) {
+ final long start = System.nanoTime();
+ final LeaseRequest leaseRequest = delegate.lease(id, route, requestTimeout, state);
+ final AtomicBoolean recorded = new AtomicBoolean(false);
+ return new LeaseRequest() {
+ @Override
+ public ConnectionEndpoint get(final Timeout timeout)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ try {
+ final ConnectionEndpoint endpoint = leaseRequest.get(timeout);
+ recordOnce(recorded, "ok", route, start);
+ return endpoint;
+ } catch (final TimeoutException ex) {
+ recordOnce(recorded, "timeout", route, start);
+ throw ex;
+ } catch (final InterruptedException ex) {
+ recordOnce(recorded, "cancel", route, start);
+ throw ex;
+ } catch (final ExecutionException ex) {
+ recordOnce(recorded, "error", route, start);
+ throw ex;
+ }
+ }
+
+ @Override
+ public boolean cancel() {
+ final boolean cancelled = leaseRequest.cancel();
+ if (cancelled) {
+ recordOnce(recorded, "cancel", route, start);
+ }
+ return cancelled;
+ }
+ };
+ }
+
+ @Override
+ public void release(final ConnectionEndpoint endpoint, final Object newState, final TimeValue validDuration) {
+ delegate.release(endpoint, newState, validDuration);
+ }
+
+ @Override
+ public void connect(final ConnectionEndpoint endpoint, final TimeValue connectTimeout,
+ final org.apache.hc.core5.http.protocol.HttpContext context) throws IOException {
+ delegate.connect(endpoint, connectTimeout, context);
+ }
+
+ @Override
+ public void upgrade(final ConnectionEndpoint endpoint,
+ final org.apache.hc.core5.http.protocol.HttpContext context) throws IOException {
+ delegate.upgrade(endpoint, context);
+ }
+
+ @Override
+ public void close() throws IOException {
+ delegate.close();
+ }
+
+ @Override
+ public void close(final org.apache.hc.core5.io.CloseMode closeMode) {
+ delegate.close(closeMode);
+ }
+
+ @Override
+ public PoolStats getTotalStats() {
+ return poolControl != null ? poolControl.getTotalStats() : new PoolStats(0, 0, 0, 0);
+ }
+
+ @Override
+ public PoolStats getStats(final HttpRoute route) {
+ return poolControl != null ? poolControl.getStats(route) : new PoolStats(0, 0, 0, 0);
+ }
+
+ @Override
+ public void setMaxTotal(final int max) {
+ if (poolControl != null) {
+ poolControl.setMaxTotal(max);
+ }
+ }
+
+ @Override
+ public int getMaxTotal() {
+ return poolControl != null ? poolControl.getMaxTotal() : 0;
+ }
+
+ @Override
+ public void setDefaultMaxPerRoute(final int max) {
+ if (poolControl != null) {
+ poolControl.setDefaultMaxPerRoute(max);
+ }
+ }
+
+ @Override
+ public int getDefaultMaxPerRoute() {
+ return poolControl != null ? poolControl.getDefaultMaxPerRoute() : 0;
+ }
+
+ @Override
+ public void setMaxPerRoute(final HttpRoute route, final int max) {
+ if (poolControl != null) {
+ poolControl.setMaxPerRoute(route, max);
+ }
+ }
+
+ @Override
+ public int getMaxPerRoute(final HttpRoute route) {
+ return poolControl != null ? poolControl.getMaxPerRoute(route) : 0;
+ }
+
+ @Override
+ public void closeIdle(final TimeValue idleTime) {
+ if (poolControl != null) {
+ poolControl.closeIdle(idleTime);
+ }
+ }
+
+ @Override
+ public void closeExpired() {
+ if (poolControl != null) {
+ poolControl.closeExpired();
+ }
+ }
+
+ @Override
+ public Set getRoutes() {
+ return poolControl != null ? poolControl.getRoutes() : Collections.emptySet();
+ }
+
+ private void recordOnce(final AtomicBoolean recorded,
+ final String result,
+ final HttpRoute route,
+ final long startNanos) {
+ if (recorded.compareAndSet(false, true)) {
+ record(result, route, startNanos);
+ }
+ }
+
+ private void record(final String result, final HttpRoute route, final long startNanos) {
+ final List tags = new ArrayList<>(3);
+ tags.add(Tag.of("result", result));
+ if (opts.tagLevel == ObservingOptions.TagLevel.EXTENDED && route != null && route.getTargetHost() != null) {
+ tags.add(Tag.of("target", route.getTargetHost().getHostName()));
+ }
+ Timer.builder(mc.prefix + ".pool.lease")
+ .tags(mc.commonTags)
+ .tags(tags)
+ .register(registry)
+ .record(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
+ Counter.builder(mc.prefix + ".pool.leases")
+ .tags(mc.commonTags)
+ .tags(tags)
+ .register(registry)
+ .increment();
+ }
+}
diff --git a/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/impl/ObservationClassicExecInterceptor.java b/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/impl/ObservationClassicExecInterceptor.java
index 705370ae88..230a8fcc01 100644
--- a/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/impl/ObservationClassicExecInterceptor.java
+++ b/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/impl/ObservationClassicExecInterceptor.java
@@ -72,7 +72,7 @@ public ClassicHttpResponse execute(final ClassicHttpRequest request,
final String method = request.getMethod();
final String uriForName = safeUriForName(request);
- final String peer = request.getAuthority().getHostName();
+ final String peer = scope.route.getTargetHost().getHostName();
final Observation obs = Observation
.createNotStarted("http.client.request", registry)
diff --git a/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/interceptors/IoByteCounterExec.java b/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/interceptors/IoByteCounterExec.java
index 4bca10abf2..8cad4b0eb1 100644
--- a/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/interceptors/IoByteCounterExec.java
+++ b/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/interceptors/IoByteCounterExec.java
@@ -64,9 +64,6 @@ public final class IoByteCounterExec implements ExecChainHandler {
private final ObservingOptions opts;
private final MetricConfig mc;
- private final Counter.Builder reqBuilder;
- private final Counter.Builder respBuilder;
-
public IoByteCounterExec(final MeterRegistry meterRegistry,
final ObservingOptions opts,
final MetricConfig mc) {
@@ -74,13 +71,7 @@ public IoByteCounterExec(final MeterRegistry meterRegistry,
this.opts = Args.notNull(opts, "observingOptions");
this.mc = Args.notNull(mc, "metricConfig");
- this.reqBuilder = Counter.builder(mc.prefix + ".request.bytes")
- .baseUnit("bytes")
- .description("HTTP request payload size");
-
- this.respBuilder = Counter.builder(mc.prefix + ".response.bytes")
- .baseUnit("bytes")
- .description("HTTP response payload size");
+ // builders are created per request to avoid tag accumulation
}
@Override
@@ -108,10 +99,22 @@ public ClassicHttpResponse execute(final ClassicHttpRequest request,
final List tags = buildTags(request.getMethod(), status, protocol, target, uri);
if (reqBytes >= 0) {
- reqBuilder.tags(tags).tags(mc.commonTags).register(meterRegistry).increment(reqBytes);
+ Counter.builder(mc.prefix + ".request.bytes")
+ .baseUnit("bytes")
+ .description("HTTP request payload size")
+ .tags(mc.commonTags)
+ .tags(tags)
+ .register(meterRegistry)
+ .increment(reqBytes);
}
if (respBytes >= 0) {
- respBuilder.tags(tags).tags(mc.commonTags).register(meterRegistry).increment(respBytes);
+ Counter.builder(mc.prefix + ".response.bytes")
+ .baseUnit("bytes")
+ .description("HTTP response payload size")
+ .tags(mc.commonTags)
+ .tags(tags)
+ .register(meterRegistry)
+ .increment(respBytes);
}
}
}
diff --git a/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/interceptors/TimerExec.java b/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/interceptors/TimerExec.java
index 918486a773..4d1465b0d3 100644
--- a/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/interceptors/TimerExec.java
+++ b/httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/interceptors/TimerExec.java
@@ -68,9 +68,6 @@ public final class TimerExec implements ExecChainHandler {
private final ObservingOptions cfg;
private final MetricConfig mc;
- private final Timer.Builder timerBuilder;
- private final Counter.Builder counterBuilder;
-
private final AtomicInteger inflight = new AtomicInteger(0);
/**
@@ -88,17 +85,6 @@ public TimerExec(final MeterRegistry reg, final ObservingOptions cfg, final Metr
this.cfg = Args.notNull(cfg, "config");
this.mc = mc != null ? mc : MetricConfig.builder().build();
- final String base = this.mc.prefix + ".";
- this.timerBuilder = Timer.builder(base + "request").tags(this.mc.commonTags);
- if (this.mc.percentiles != null && this.mc.percentiles.length > 0) {
- this.timerBuilder.publishPercentiles(this.mc.percentiles);
- }
- if (this.mc.slo != null) {
- this.timerBuilder.serviceLevelObjectives(this.mc.slo);
- }
-
- this.counterBuilder = Counter.builder(base + "response").tags(this.mc.commonTags);
-
// Tag-aware guard: only register once per (name + tags)
if (registry.find(this.mc.prefix + ".inflight")
.tags("kind", "classic")
@@ -141,11 +127,20 @@ public ClassicHttpResponse execute(final ClassicHttpRequest request,
tags.add(Tag.of("target", scope.route.getTargetHost().getHostName()));
}
- timerBuilder.tags(tags)
- .register(registry)
- .record(durNanos, TimeUnit.NANOSECONDS);
+ Timer.Builder tb = Timer.builder(mc.prefix + ".request")
+ .tags(mc.commonTags)
+ .tags(tags);
+ if (mc.slo != null) {
+ tb = tb.serviceLevelObjectives(mc.slo);
+ }
+ if (mc.percentiles != null && mc.percentiles.length > 0) {
+ tb = tb.publishPercentiles(mc.percentiles);
+ }
+ tb.register(registry).record(durNanos, TimeUnit.NANOSECONDS);
- counterBuilder.tags(tags)
+ Counter.builder(mc.prefix + ".response")
+ .tags(mc.commonTags)
+ .tags(tags)
.register(registry)
.increment();
} finally {
diff --git a/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/HttpClientObservationSupportTest.java b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/HttpClientObservationSupportTest.java
index 152818fe5d..3a9d541979 100644
--- a/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/HttpClientObservationSupportTest.java
+++ b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/HttpClientObservationSupportTest.java
@@ -28,18 +28,29 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.net.InetAddress;
+import java.net.SocketAddress;
import java.util.EnumSet;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.micrometer.observation.ObservationRegistry;
+import org.apache.hc.client5.http.DnsResolver;
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.impl.cache.CachingHttpAsyncClientBuilder;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionManager;
+import org.apache.hc.client5.http.observation.impl.MeteredAsyncConnectionManager;
+import org.apache.hc.client5.http.observation.impl.MeteredConnectionManager;
+import org.apache.hc.client5.http.observation.impl.MeteredDnsResolver;
+import org.apache.hc.client5.http.observation.impl.MeteredTlsStrategy;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpHost;
@@ -49,6 +60,11 @@
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.net.NamedEndpoint;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.util.Timeout;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
@@ -56,6 +72,42 @@ class HttpClientObservationSupportTest {
private HttpServer server;
+ private static final class NoopDnsResolver implements DnsResolver {
+ @Override
+ public InetAddress[] resolve(final String host) {
+ return new InetAddress[0];
+ }
+
+ @Override
+ public String resolveCanonicalHostname(final String host) {
+ return host;
+ }
+ }
+
+ private static final class NoopTlsStrategy implements TlsStrategy {
+ @Override
+ public void upgrade(final TransportSecurityLayer sessionLayer,
+ final NamedEndpoint endpoint,
+ final Object attachment,
+ final Timeout handshakeTimeout,
+ final FutureCallback callback) {
+ if (callback != null) {
+ callback.completed(sessionLayer);
+ }
+ }
+
+ @Deprecated
+ @Override
+ public boolean upgrade(final TransportSecurityLayer sessionLayer,
+ final HttpHost host,
+ final SocketAddress localAddress,
+ final SocketAddress remoteAddress,
+ final Object attachment,
+ final Timeout handshakeTimeout) {
+ return true;
+ }
+ }
+
@AfterEach
void shutDown() {
if (this.server != null) {
@@ -124,4 +176,67 @@ void basicIoAndPoolMetricsRecorded() throws Exception {
assertNotNull(meters.find(mc.prefix + ".pool.available").gauge());
assertNotNull(meters.find(mc.prefix + ".pool.pending").gauge());
}
+
+ @Test
+ void meteredHelpersRespectMetricSets() {
+ final MeterRegistry meters = new SimpleMeterRegistry();
+ final ObservingOptions noDnsOrTls = ObservingOptions.builder()
+ .metrics(EnumSet.of(ObservingOptions.MetricSet.BASIC))
+ .build();
+ final ObservingOptions dnsAndTls = ObservingOptions.builder()
+ .metrics(EnumSet.of(ObservingOptions.MetricSet.DNS, ObservingOptions.MetricSet.TLS, ObservingOptions.MetricSet.CONN_POOL))
+ .build();
+
+ final DnsResolver dns = new NoopDnsResolver();
+ final DnsResolver dnsSame = HttpClientObservationSupport.meteredDnsResolver(dns, meters, noDnsOrTls, MetricConfig.DEFAULT);
+ assertSame(dns, dnsSame);
+
+ final DnsResolver dnsWrapped = HttpClientObservationSupport.meteredDnsResolver(dns, meters, dnsAndTls, MetricConfig.DEFAULT);
+ assertTrue(dnsWrapped instanceof MeteredDnsResolver);
+
+ final TlsStrategy tls = new NoopTlsStrategy();
+ final TlsStrategy tlsSame = HttpClientObservationSupport.meteredTlsStrategy(tls, meters, noDnsOrTls, MetricConfig.DEFAULT);
+ assertSame(tls, tlsSame);
+
+ final TlsStrategy tlsWrapped = HttpClientObservationSupport.meteredTlsStrategy(tls, meters, dnsAndTls, MetricConfig.DEFAULT);
+ assertTrue(tlsWrapped instanceof MeteredTlsStrategy);
+
+ final HttpClientConnectionManager rawClassic = PoolingHttpClientConnectionManagerBuilder.create().build();
+ final HttpClientConnectionManager classicSame =
+ HttpClientObservationSupport.meteredConnectionManager(rawClassic, meters, noDnsOrTls, MetricConfig.DEFAULT);
+ assertSame(rawClassic, classicSame);
+
+ final HttpClientConnectionManager classicWrapped =
+ HttpClientObservationSupport.meteredConnectionManager(rawClassic, meters, dnsAndTls, MetricConfig.DEFAULT);
+ assertTrue(classicWrapped instanceof MeteredConnectionManager);
+
+ final AsyncClientConnectionManager rawAsync = PoolingAsyncClientConnectionManagerBuilder.create().build();
+ final AsyncClientConnectionManager asyncSame =
+ HttpClientObservationSupport.meteredAsyncConnectionManager(rawAsync, meters, noDnsOrTls, MetricConfig.DEFAULT);
+ assertSame(rawAsync, asyncSame);
+
+ final AsyncClientConnectionManager asyncWrapped =
+ HttpClientObservationSupport.meteredAsyncConnectionManager(rawAsync, meters, dnsAndTls, MetricConfig.DEFAULT);
+ assertTrue(asyncWrapped instanceof MeteredAsyncConnectionManager);
+ }
+
+ @Test
+ void cachingAsyncRegistersConnPoolMeters() {
+ final MeterRegistry meters = new SimpleMeterRegistry();
+ final ObservationRegistry observations = ObservationRegistry.create();
+ final MetricConfig mc = MetricConfig.builder().prefix("async").build();
+ final ObservingOptions opts = ObservingOptions.builder()
+ .metrics(EnumSet.of(ObservingOptions.MetricSet.CONN_POOL))
+ .build();
+
+ final AsyncClientConnectionManager cm = PoolingAsyncClientConnectionManagerBuilder.create().build();
+ final CachingHttpAsyncClientBuilder builder = CachingHttpAsyncClientBuilder.create();
+ builder.setConnectionManager(cm);
+
+ HttpClientObservationSupport.enable(builder, observations, meters, opts, mc);
+
+ assertNotNull(meters.find(mc.prefix + ".pool.leased").gauge());
+ assertNotNull(meters.find(mc.prefix + ".pool.available").gauge());
+ assertNotNull(meters.find(mc.prefix + ".pool.pending").gauge());
+ }
}
diff --git a/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/AsyncConnectionManagerLeaseDemo.java b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/AsyncConnectionManagerLeaseDemo.java
new file mode 100644
index 0000000000..68e82ef15c
--- /dev/null
+++ b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/AsyncConnectionManagerLeaseDemo.java
@@ -0,0 +1,86 @@
+/*
+ * ====================================================================
+ * 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
+ * .
+ *
+ */
+package org.apache.hc.client5.http.observation.example;
+
+import java.util.EnumSet;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import io.micrometer.core.instrument.Metrics;
+import io.micrometer.core.instrument.search.Search;
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.prometheusmetrics.PrometheusConfig;
+import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
+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.nio.PoolingAsyncClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionManager;
+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.client5.http.observation.impl.MeteredAsyncConnectionManager;
+
+public final class AsyncConnectionManagerLeaseDemo {
+
+ public static void main(final String[] args) throws Exception {
+ final PrometheusMeterRegistry reg = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
+ Metrics.addRegistry(reg);
+
+ final ObservationRegistry obs = ObservationRegistry.create();
+ final MetricConfig mc = MetricConfig.builder().prefix("demo").build();
+ final ObservingOptions opts = ObservingOptions.builder()
+ .metrics(EnumSet.of(ObservingOptions.MetricSet.BASIC, ObservingOptions.MetricSet.CONN_POOL))
+ .build();
+
+ final AsyncClientConnectionManager rawCm = PoolingAsyncClientConnectionManagerBuilder.create().build();
+ final AsyncClientConnectionManager cm = new MeteredAsyncConnectionManager(rawCm, reg, mc, opts);
+
+ final HttpAsyncClientBuilder builder = HttpAsyncClients.custom().setConnectionManager(cm);
+ HttpClientObservationSupport.enable(builder, obs, reg, opts, mc);
+
+ try (final CloseableHttpAsyncClient client = builder.build()) {
+ client.start();
+
+ final SimpleHttpRequest req = SimpleRequestBuilder.get("http://httpbin.org/get").build();
+ final Future fut = client.execute(req, null);
+ final SimpleHttpResponse rsp = fut.get(20, TimeUnit.SECONDS);
+ System.out.println("status=" + rsp.getCode());
+ }
+
+ System.out.println("pool.lease present? " + (Search.in(reg).name("demo.pool.lease").timer() != null));
+ System.out.println("pool.leases present? " + (Search.in(reg).name("demo.pool.leases").counter() != null));
+ System.out.println("--- scrape ---");
+ System.out.println(reg.scrape());
+ }
+
+ private AsyncConnectionManagerLeaseDemo() {
+ }
+}
diff --git a/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/AsyncMetricsDemo.java b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/AsyncMetricsDemo.java
index 9cb6101e00..d800dfd5d6 100644
--- a/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/AsyncMetricsDemo.java
+++ b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/AsyncMetricsDemo.java
@@ -44,6 +44,8 @@
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.nio.PoolingAsyncClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionManager;
import org.apache.hc.client5.http.observation.HttpClientObservationSupport;
import org.apache.hc.client5.http.observation.MetricConfig;
import org.apache.hc.client5.http.observation.ObservingOptions;
@@ -68,7 +70,10 @@ public static void main(final String[] args) throws Exception {
.tagLevel(ObservingOptions.TagLevel.EXTENDED)
.build();
- final HttpAsyncClientBuilder b = HttpAsyncClients.custom();
+ final AsyncClientConnectionManager rawCm = PoolingAsyncClientConnectionManagerBuilder.create().build();
+ final AsyncClientConnectionManager cm =
+ HttpClientObservationSupport.meteredAsyncConnectionManager(rawCm, reg, opts, mc);
+ final HttpAsyncClientBuilder b = HttpAsyncClients.custom().setConnectionManager(cm);
HttpClientObservationSupport.enable(b, obs, reg, opts, mc);
final CloseableHttpAsyncClient client = b.build();
diff --git a/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/ConnectionManagerLeaseDemo.java b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/ConnectionManagerLeaseDemo.java
new file mode 100644
index 0000000000..ed8197cfd4
--- /dev/null
+++ b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/ConnectionManagerLeaseDemo.java
@@ -0,0 +1,85 @@
+/*
+ * ====================================================================
+ * 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
+ * .
+ *
+ */
+package org.apache.hc.client5.http.observation.example;
+
+import java.util.EnumSet;
+
+import io.micrometer.core.instrument.Metrics;
+import io.micrometer.core.instrument.search.Search;
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.prometheusmetrics.PrometheusConfig;
+import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
+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.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+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.client5.http.observation.impl.MeteredConnectionManager;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
+
+public final class ConnectionManagerLeaseDemo {
+
+ public static void main(final String[] args) throws Exception {
+ final PrometheusMeterRegistry reg = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
+ Metrics.addRegistry(reg);
+
+ final ObservationRegistry obs = ObservationRegistry.create();
+ final MetricConfig mc = MetricConfig.builder().prefix("demo").build();
+ final ObservingOptions opts = ObservingOptions.builder()
+ .metrics(EnumSet.of(ObservingOptions.MetricSet.BASIC, ObservingOptions.MetricSet.CONN_POOL))
+ .build();
+
+ final HttpClientConnectionManager rawCm = PoolingHttpClientConnectionManagerBuilder.create().build();
+ final HttpClientConnectionManager cm = new MeteredConnectionManager(rawCm, reg, mc, opts);
+
+ final HttpClientBuilder builder = HttpClients.custom().setConnectionManager(cm);
+ HttpClientObservationSupport.enable(builder, obs, reg, opts, mc);
+
+ try (final CloseableHttpClient client = builder.build()) {
+ final HttpHost target = new HttpHost("http", "httpbin.org", 80);
+ final ClassicHttpResponse rsp = client.executeOpen(
+ target,
+ ClassicRequestBuilder.get("/get").build(),
+ null);
+ System.out.println("status=" + rsp.getCode());
+ rsp.close();
+ }
+
+ System.out.println("pool.lease present? " + (Search.in(reg).name("demo.pool.lease").timer() != null));
+ System.out.println("pool.leases present? " + (Search.in(reg).name("demo.pool.leases").counter() != null));
+ System.out.println("--- scrape ---");
+ System.out.println(reg.scrape());
+ }
+
+ private ConnectionManagerLeaseDemo() {
+ }
+}
diff --git a/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/DnsMetricsDemo.java b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/DnsMetricsDemo.java
index 3712569d4d..ac2b686e07 100644
--- a/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/DnsMetricsDemo.java
+++ b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/DnsMetricsDemo.java
@@ -42,7 +42,6 @@
import org.apache.hc.client5.http.observation.MetricConfig;
import org.apache.hc.client5.http.observation.ObservingOptions;
-import org.apache.hc.client5.http.observation.impl.MeteredDnsResolver;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
@@ -75,8 +74,9 @@ public static void main(final String[] args) throws Exception {
.build();
// 4) Classic client + real DNS resolver wrapped with metrics
- final MeteredDnsResolver meteredResolver =
- new MeteredDnsResolver(SystemDefaultDnsResolver.INSTANCE, registry, mc, opts);
+ final org.apache.hc.client5.http.DnsResolver meteredResolver =
+ HttpClientObservationSupport.meteredDnsResolver(
+ SystemDefaultDnsResolver.INSTANCE, registry, opts, mc);
final HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create()
.setDnsResolver(meteredResolver)
diff --git a/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/PoolGaugesDemo.java b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/PoolGaugesDemo.java
index ca4493b8e1..ed9f9bf463 100644
--- a/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/PoolGaugesDemo.java
+++ b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/PoolGaugesDemo.java
@@ -63,7 +63,8 @@ public static void main(final String[] args) throws Exception {
.build();
// Ensure a pooling manager is used so pool gauges exist
- final HttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
+ final HttpClientConnectionManager rawCm = new PoolingHttpClientConnectionManager();
+ final HttpClientConnectionManager cm = HttpClientObservationSupport.meteredConnectionManager(rawCm, reg, opts, mc);
final HttpClientBuilder b = HttpClients.custom().setConnectionManager(cm);
HttpClientObservationSupport.enable(b, obs, reg, opts, mc);
@@ -77,6 +78,9 @@ public static void main(final String[] args) throws Exception {
System.out.println("pool.available present? " + (Search.in(reg).name("http.client.pool.available").gauge() != null));
System.out.println("pool.pending present? " + (Search.in(reg).name("http.client.pool.pending").gauge() != null));
+ System.out.println("pool.lease present? " + (Search.in(reg).name("http.client.pool.lease").timer() != null));
+ System.out.println("pool.leases present? " + (Search.in(reg).name("http.client.pool.leases").counter() != null));
+
System.out.println("--- scrape ---");
System.out.println(reg.scrape());
}
diff --git a/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/TlsMetricsDemo.java b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/TlsMetricsDemo.java
index 9f81babcb8..b114f61ff8 100644
--- a/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/TlsMetricsDemo.java
+++ b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/TlsMetricsDemo.java
@@ -44,7 +44,6 @@
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.client5.http.observation.impl.MeteredTlsStrategy;
import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder;
import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
@@ -61,7 +60,7 @@ public static void main(final String[] args) throws Exception {
// 3) Metric knobs
final MetricConfig mc = MetricConfig.builder()
.prefix("demo")
- .percentiles(2) // p90 + p99
+ .percentiles(0.90, 0.99) // p90 + p99
.build();
final ObservingOptions opts = ObservingOptions.builder()
@@ -75,7 +74,7 @@ public static void main(final String[] args) throws Exception {
// 4) Build a CM with a metered TLS strategy, then give it to the async builder
final TlsStrategy realTls = ClientTlsStrategyBuilder.create().buildAsync();
- final TlsStrategy meteredTls = new MeteredTlsStrategy(realTls, registry, mc, opts);
+ final TlsStrategy meteredTls = HttpClientObservationSupport.meteredTlsStrategy(realTls, registry, opts, mc);
// TLS strategy goes on the *connection manager* (not on the builder)
final org.apache.hc.client5.http.nio.AsyncClientConnectionManager cm =
diff --git a/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/impl/MeteredConnectionManagerTest.java b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/impl/MeteredConnectionManagerTest.java
new file mode 100644
index 0000000000..c81ca9c7bb
--- /dev/null
+++ b/httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/impl/MeteredConnectionManagerTest.java
@@ -0,0 +1,230 @@
+/*
+ * ====================================================================
+ * 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
+ * .
+ *
+ */
+package org.apache.hc.client5.http.observation.impl;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.client5.http.io.ConnectionEndpoint;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.io.LeaseRequest;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionManager;
+import org.apache.hc.client5.http.nio.AsyncConnectionEndpoint;
+import org.apache.hc.client5.http.observation.MetricConfig;
+import org.apache.hc.client5.http.observation.ObservingOptions;
+import org.apache.hc.core5.concurrent.BasicFuture;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Test;
+
+class MeteredConnectionManagerTest {
+
+ private static final class DummyEndpoint extends ConnectionEndpoint {
+ @Deprecated
+ @Override
+ public org.apache.hc.core5.http.ClassicHttpResponse execute(
+ final String id,
+ final org.apache.hc.core5.http.ClassicHttpRequest request,
+ final org.apache.hc.core5.http.impl.io.HttpRequestExecutor executor,
+ final HttpContext context) {
+ return null;
+ }
+
+ @Override
+ public boolean isConnected() {
+ return false;
+ }
+
+ @Override
+ public void setSocketTimeout(final Timeout timeout) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public void close(final CloseMode closeMode) {
+ }
+ }
+
+ private static final class DummyConnectionManager implements HttpClientConnectionManager {
+ @Override
+ public LeaseRequest lease(final String id, final HttpRoute route, final Timeout requestTimeout, final Object state) {
+ return new LeaseRequest() {
+ @Override
+ public ConnectionEndpoint get(final Timeout timeout) {
+ return new DummyEndpoint();
+ }
+
+ @Override
+ public boolean cancel() {
+ return false;
+ }
+ };
+ }
+
+ @Override
+ public void release(final ConnectionEndpoint endpoint, final Object newState, final TimeValue validDuration) {
+ }
+
+ @Override
+ public void connect(final ConnectionEndpoint endpoint, final TimeValue connectTimeout, final HttpContext context) {
+ }
+
+ @Override
+ public void upgrade(final ConnectionEndpoint endpoint, final HttpContext context) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public void close(final CloseMode closeMode) {
+ }
+ }
+
+ private static final class DummyAsyncEndpoint extends AsyncConnectionEndpoint {
+ @Override
+ public void execute(final String id,
+ final org.apache.hc.core5.http.nio.AsyncClientExchangeHandler exchangeHandler,
+ final org.apache.hc.core5.http.nio.HandlerFactory pushHandlerFactory,
+ final HttpContext context) {
+ }
+
+ @Override
+ public boolean isConnected() {
+ return false;
+ }
+
+ @Override
+ public void setSocketTimeout(final Timeout timeout) {
+ }
+
+ @Override
+ public void close(final CloseMode closeMode) {
+ }
+ }
+
+ private static final class DummyAsyncConnectionManager implements AsyncClientConnectionManager {
+ @Override
+ public java.util.concurrent.Future lease(final String id,
+ final HttpRoute route,
+ final Object state,
+ final Timeout requestTimeout,
+ final FutureCallback callback) {
+ final BasicFuture future = new BasicFuture<>(callback);
+ future.completed(new DummyAsyncEndpoint());
+ return future;
+ }
+
+ @Override
+ public void release(final AsyncConnectionEndpoint endpoint, final Object newState, final TimeValue validDuration) {
+ }
+
+ @Override
+ public java.util.concurrent.Future connect(final AsyncConnectionEndpoint endpoint,
+ final ConnectionInitiator connectionInitiator,
+ final Timeout connectTimeout,
+ final Object attachment,
+ final HttpContext context,
+ final FutureCallback callback) {
+ final BasicFuture future = new BasicFuture<>(callback);
+ future.completed(endpoint);
+ return future;
+ }
+
+ @Override
+ public void upgrade(final AsyncConnectionEndpoint endpoint, final Object attachment, final HttpContext context) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public void close(final CloseMode closeMode) {
+ }
+ }
+
+ @Test
+ void recordsClassicLeaseTime() throws Exception {
+ final MeterRegistry registry = new SimpleMeterRegistry();
+ final MetricConfig mc = MetricConfig.builder().prefix("classic").build();
+ final ObservingOptions opts = ObservingOptions.DEFAULT;
+
+ final HttpClientConnectionManager metered =
+ new MeteredConnectionManager(new DummyConnectionManager(), registry, mc, opts);
+
+ final HttpRoute route = new HttpRoute(new HttpHost("http", "example.com", 80));
+ try {
+ metered.lease("id", route, Timeout.ofSeconds(1), null).get(Timeout.ofSeconds(1));
+ } finally {
+ metered.close();
+ }
+
+ assertNotNull(registry.find("classic.pool.lease").timer());
+ assertTrue(registry.find("classic.pool.lease").timer().count() >= 1L);
+ assertNotNull(registry.find("classic.pool.leases").counter());
+ assertTrue(registry.find("classic.pool.leases").counter().count() >= 1.0d);
+ }
+
+ @Test
+ void recordsAsyncLeaseTime() throws InterruptedException, ExecutionException, TimeoutException, java.io.IOException {
+ final MeterRegistry registry = new SimpleMeterRegistry();
+ final MetricConfig mc = MetricConfig.builder().prefix("async").build();
+ final ObservingOptions opts = ObservingOptions.DEFAULT;
+
+ final AsyncClientConnectionManager metered =
+ new MeteredAsyncConnectionManager(new DummyAsyncConnectionManager(), registry, mc, opts);
+
+ final HttpRoute route = new HttpRoute(new HttpHost("http", "example.com", 80));
+ try {
+ metered.lease("id", route, null, Timeout.ofSeconds(1), null).get(1, TimeUnit.SECONDS);
+ } finally {
+ metered.close();
+ }
+
+ assertNotNull(registry.find("async.pool.lease").timer());
+ assertTrue(registry.find("async.pool.lease").timer().count() >= 1L);
+ assertNotNull(registry.find("async.pool.leases").counter());
+ assertTrue(registry.find("async.pool.leases").counter().count() >= 1.0d);
+ }
+}