From 49dc707a31887f7f1dcc511c92b7b2f9c754eaf5 Mon Sep 17 00:00:00 2001 From: Benjamin Hubert Date: Tue, 1 Oct 2019 05:32:30 +0200 Subject: [PATCH] Add instrumentation for pools of Apache HttpComponents HttpClient (#1223) Provides metrics for the underlying connection pool total stats. See #533 --- ...pClientConnectionManagerMetricsBinder.java | 128 ++++++++++++++++++ ...entConnectionManagerMetricsBinderTest.java | 113 ++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinder.java create mode 100644 micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinderTest.java diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinder.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinder.java new file mode 100644 index 0000000000..d1fb293051 --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinder.java @@ -0,0 +1,128 @@ +/* + * Copyright 2019 Pivotal Software, Inc. + *

+ * Licensed 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. + */ +package io.micrometer.core.instrument.binder.httpcomponents; + +import io.micrometer.core.instrument.*; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNull; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; + +/** + * Collects metrics from a {@link PoolingHttpClientConnectionManager}. + * + * It monitors the overall connection pool state. + * + * @author Benjamin Hubert (benjamin.hubert@willhaben.at) + * @since 1.3.0 + */ +public class PoolingHttpClientConnectionManagerMetricsBinder implements MeterBinder { + + private static final String METER_TOTAL_MAX_DESC = "The configured maximum number of allowed persistent connections for all routes."; + private static final String METER_TOTAL_MAX = "httpcomponents.httpclient.pool.total.max"; + private static final String METER_TOTAL_CONNECTIONS_DESC = "The number of persistent and leased connections for all routes."; + private static final String METER_TOTAL_CONNECTIONS = "httpcomponents.httpclient.pool.total.connections"; + private static final String METER_TOTAL_PENDING_DESC = "The number of connection requests being blocked awaiting a free connection for all routes."; + private static final String METER_TOTAL_PENDING = "httpcomponents.httpclient.pool.total.pending"; + private static final String METER_DEFAULT_MAX_PER_ROUTE_DESC = "The configured default maximum number of allowed persistent connections per route."; + private static final String METER_DEFAULT_MAX_PER_ROUTE = "httpcomponents.httpclient.pool.route.max.default"; + private static final String TAG_CONNECTIONS_STATE = "state"; + + private final PoolingHttpClientConnectionManager connectionManager; + private final Iterable tags; + + /** + * Creates a metrics binder for the given pooling connection manager. + * + * For convenience this constructor will take care of casting the given + * {@link HttpClientConnectionManager} to the required {@link + * PoolingHttpClientConnectionManager}. An {@link IllegalArgumentException} + * is thrown, if the given {@code connectionManager} is not an instance of + * {@link PoolingHttpClientConnectionManager}. + * + * @param connectionManager The connection manager to monitor. + * @param name Name of the connection manager. Will be added as tag with the + * key "httpclient". + * @param tags Tags to apply to all recorded metrics. Must be an even number + * of arguments representing key/value pairs of tags. + */ + @SuppressWarnings("WeakerAccess") + public PoolingHttpClientConnectionManagerMetricsBinder(HttpClientConnectionManager connectionManager, String name, String... tags) { + this(connectionManager, name, Tags.of(tags)); + } + + /** + * Creates a metrics binder for the given pooling connection manager. + * + * For convenience this constructor will take care of casting the given + * {@link HttpClientConnectionManager} to the required {@link + * PoolingHttpClientConnectionManager}. An {@link IllegalArgumentException} + * is thrown, if the given {@code connectionManager} is not an instance of + * {@link PoolingHttpClientConnectionManager}. + * + * @param connectionManager The connection manager to monitor. + * @param name Name of the connection manager. Will be added as tag with the + * key "httpclient". + * @param tags Tags to apply to all recorded metrics. + */ + @SuppressWarnings("WeakerAccess") + public PoolingHttpClientConnectionManagerMetricsBinder(HttpClientConnectionManager connectionManager, String name, Iterable tags) { + if (!(connectionManager instanceof PoolingHttpClientConnectionManager)) { + throw new IllegalArgumentException("The given connectionManager is not an instance of PoolingHttpClientConnectionManager."); + } + this.connectionManager = (PoolingHttpClientConnectionManager) connectionManager; + this.tags = Tags.concat(tags, "httpclient", name); + } + + @Override + public void bindTo(@NonNull MeterRegistry registry) { + registerTotalMetrics(registry); + } + + private void registerTotalMetrics(MeterRegistry registry) { + Gauge.builder(METER_TOTAL_MAX, + connectionManager, + (connectionManager) -> connectionManager.getTotalStats().getMax()) + .description(METER_TOTAL_MAX_DESC) + .tags(tags) + .register(registry); + Gauge.builder(METER_TOTAL_CONNECTIONS, + connectionManager, + (connectionManager) -> connectionManager.getTotalStats().getAvailable()) + .description(METER_TOTAL_CONNECTIONS_DESC) + .tags(tags).tag(TAG_CONNECTIONS_STATE, "available") + .register(registry); + Gauge.builder(METER_TOTAL_CONNECTIONS, + connectionManager, + (connectionManager) -> connectionManager.getTotalStats().getLeased()) + .description(METER_TOTAL_CONNECTIONS_DESC) + .tags(tags).tag(TAG_CONNECTIONS_STATE, "leased") + .register(registry); + Gauge.builder(METER_TOTAL_PENDING, + connectionManager, + (connectionManager) -> connectionManager.getTotalStats().getPending()) + .description(METER_TOTAL_PENDING_DESC) + .tags(tags) + .register(registry); + Gauge.builder(METER_DEFAULT_MAX_PER_ROUTE, + connectionManager, + PoolingHttpClientConnectionManager::getDefaultMaxPerRoute) + .description(METER_DEFAULT_MAX_PER_ROUTE_DESC) + .tags(tags) + .register(registry); + } + +} diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinderTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinderTest.java new file mode 100644 index 0000000000..94f33537b8 --- /dev/null +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinderTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 2017 Pivotal Software, Inc. + *

+ * Licensed 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. + */ +package io.micrometer.core.instrument.binder.httpcomponents; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.pool.PoolStats; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link PoolingHttpClientConnectionManagerMetricsBinder}. + * + * @author Benjamin Hubert (benjamin.hubert@willhaben.at) + */ +class PoolingHttpClientConnectionManagerMetricsBinderTest { + + private MeterRegistry registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + private PoolingHttpClientConnectionManager connectionManager; + private PoolingHttpClientConnectionManagerMetricsBinder binder; + + @BeforeEach + void setup() { + connectionManager = mock(PoolingHttpClientConnectionManager.class); + binder = new PoolingHttpClientConnectionManagerMetricsBinder(connectionManager, "test"); + binder.bindTo(registry); + } + + @Test + void creationWithNonPoolingHttpClientThrowsException() { + assertThrows(IllegalArgumentException.class, () -> { + HttpClientConnectionManager connectionManager = mock(HttpClientConnectionManager.class); + new PoolingHttpClientConnectionManagerMetricsBinder(connectionManager, "test"); + }); + } + + @Test + void creationWithPoolingHttpClientIsOk() { + HttpClientConnectionManager connectionManager = mock(PoolingHttpClientConnectionManager.class); + new PoolingHttpClientConnectionManagerMetricsBinder(connectionManager, "test"); + } + + @Test + void totalMax() { + PoolStats poolStats = mock(PoolStats.class); + when(poolStats.getMax()).thenReturn(13); + when(connectionManager.getTotalStats()).thenReturn(poolStats); + assertThat(registry.get("httpcomponents.httpclient.pool.total.max") + .tags("httpclient", "test") + .gauge().value()).isEqualTo(13.0); + } + + @Test + void totalAvailable() { + PoolStats poolStats = mock(PoolStats.class); + when(poolStats.getAvailable()).thenReturn(17); + when(connectionManager.getTotalStats()).thenReturn(poolStats); + assertThat(registry.get("httpcomponents.httpclient.pool.total.connections") + .tags("httpclient", "test", "state", "available") + .gauge().value()).isEqualTo(17.0); + } + + @Test + void totalLeased() { + PoolStats poolStats = mock(PoolStats.class); + when(poolStats.getLeased()).thenReturn(23); + when(connectionManager.getTotalStats()).thenReturn(poolStats); + assertThat(registry.get("httpcomponents.httpclient.pool.total.connections") + .tags("httpclient", "test", "state", "leased") + .gauge().value()).isEqualTo(23.0); + } + + @Test + void totalPending() { + PoolStats poolStats = mock(PoolStats.class); + when(poolStats.getPending()).thenReturn(37); + when(connectionManager.getTotalStats()).thenReturn(poolStats); + assertThat(registry.get("httpcomponents.httpclient.pool.total.pending") + .tags("httpclient", "test") + .gauge().value()).isEqualTo(37.0); + } + + @Test + void routeMaxDefault() { + when(connectionManager.getDefaultMaxPerRoute()).thenReturn(7); + assertThat(registry.get("httpcomponents.httpclient.pool.route.max.default") + .tags("httpclient", "test") + .gauge().value()).isEqualTo(7.0); + } + +}