From 55cda834809ca4e99167a3fbf186352286dc7653 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Wed, 4 Feb 2026 10:20:09 +0100 Subject: [PATCH] Add pool lease metrics and helpers for connection managers --- .../HttpClientObservationSupport.java | 117 +++++++- .../http/observation/ObservingOptions.java | 5 + .../impl/MeteredAsyncConnectionManager.java | 272 ++++++++++++++++++ .../impl/MeteredConnectionManager.java | 235 +++++++++++++++ .../ObservationClassicExecInterceptor.java | 2 +- .../interceptors/IoByteCounterExec.java | 27 +- .../observation/interceptors/TimerExec.java | 31 +- .../HttpClientObservationSupportTest.java | 115 ++++++++ .../AsyncConnectionManagerLeaseDemo.java | 86 ++++++ .../observation/example/AsyncMetricsDemo.java | 7 +- .../example/ConnectionManagerLeaseDemo.java | 85 ++++++ .../observation/example/DnsMetricsDemo.java | 6 +- .../observation/example/PoolGaugesDemo.java | 6 +- .../observation/example/TlsMetricsDemo.java | 5 +- .../impl/MeteredConnectionManagerTest.java | 230 +++++++++++++++ 15 files changed, 1189 insertions(+), 40 deletions(-) create mode 100644 httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/impl/MeteredAsyncConnectionManager.java create mode 100644 httpclient5-observation/src/main/java/org/apache/hc/client5/http/observation/impl/MeteredConnectionManager.java create mode 100644 httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/AsyncConnectionManagerLeaseDemo.java create mode 100644 httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/example/ConnectionManagerLeaseDemo.java create mode 100644 httpclient5-observation/src/test/java/org/apache/hc/client5/http/observation/impl/MeteredConnectionManagerTest.java 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); + } +}