From efa25c3aaa75f4d5e8820f5d10418a741019d612 Mon Sep 17 00:00:00 2001 From: Vincent van Donselaar Date: Sun, 17 Mar 2019 08:02:39 +0100 Subject: [PATCH] Alternative Prometheus metrics Histogram instead of locking Summary. (#1265) * Alternative Prometheus metrics Histogram instead of locking Summary. Add PrometheusHistogramMetricsTracker.java that uses an internal Histogram instead of Summary. A summary is quite lock heavy. See: https://github.com/prometheus/client_java/issues/328 * Fix incorrect magnitudes of buckets. --- .../PrometheusHistogramMetricsTracker.java | 97 ++++++++++++ ...metheusHistogramMetricsTrackerFactory.java | 67 ++++++++ .../PrometheusMetricsTrackerFactory.java | 3 + ...eusHistogramMetricsTrackerFactoryTest.java | 77 ++++++++++ ...PrometheusHistogramMetricsTrackerTest.java | 144 ++++++++++++++++++ 5 files changed, 388 insertions(+) create mode 100644 src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusHistogramMetricsTracker.java create mode 100644 src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusHistogramMetricsTrackerFactory.java create mode 100644 src/test/java/com/zaxxer/hikari/metrics/prometheus/PrometheusHistogramMetricsTrackerFactoryTest.java create mode 100644 src/test/java/com/zaxxer/hikari/metrics/prometheus/PrometheusHistogramMetricsTrackerTest.java diff --git a/src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusHistogramMetricsTracker.java b/src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusHistogramMetricsTracker.java new file mode 100644 index 00000000..4b7befce --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusHistogramMetricsTracker.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2013 Brett Wooldridge + * + * 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 com.zaxxer.hikari.metrics.prometheus; + +import com.zaxxer.hikari.metrics.IMetricsTracker; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.Counter; +import io.prometheus.client.Histogram; + +/** + * Alternative Prometheus metrics tracker using a Histogram instead of Summary + *

+ * This is an alternative metrics tracker that doesn't use a {@link io.prometheus.client.Summary}. Summaries require + * heavy locks that might cause performance issues. Source: https://github.com/prometheus/client_java/issues/328 + * + * @see PrometheusMetricsTracker + */ +class PrometheusHistogramMetricsTracker implements IMetricsTracker +{ + private static final Counter CONNECTION_TIMEOUT_COUNTER = Counter.build() + .name("hikaricp_connection_timeout_total") + .labelNames("pool") + .help("Connection timeout total count") + .create(); + + private static final Histogram ELAPSED_ACQUIRED_HISTOGRAM = + registerHistogram("hikaricp_connection_acquired_nanos", "Connection acquired time (ns)", 1_000); + + private static final Histogram ELAPSED_BORROWED_HISTOGRAM = + registerHistogram("hikaricp_connection_usage_millis", "Connection usage (ms)", 1); + + private static final Histogram ELAPSED_CREATION_HISTOGRAM = + registerHistogram("hikaricp_connection_creation_millis", "Connection creation (ms)", 1); + + private final Counter.Child connectionTimeoutCounterChild; + + private static Histogram registerHistogram(String name, String help, double bucketStart) { + return Histogram.build() + .name(name) + .labelNames("pool") + .help(help) + .exponentialBuckets(bucketStart, 2.0, 11) + .create(); + } + + private final Histogram.Child elapsedAcquiredHistogramChild; + private final Histogram.Child elapsedBorrowedHistogramChild; + private final Histogram.Child elapsedCreationHistogramChild; + + PrometheusHistogramMetricsTracker(String poolName, CollectorRegistry collectorRegistry) { + registerMetrics(collectorRegistry); + this.connectionTimeoutCounterChild = CONNECTION_TIMEOUT_COUNTER.labels(poolName); + this.elapsedAcquiredHistogramChild = ELAPSED_ACQUIRED_HISTOGRAM.labels(poolName); + this.elapsedBorrowedHistogramChild = ELAPSED_BORROWED_HISTOGRAM.labels(poolName); + this.elapsedCreationHistogramChild = ELAPSED_CREATION_HISTOGRAM.labels(poolName); + } + + private void registerMetrics(CollectorRegistry collectorRegistry) { + CONNECTION_TIMEOUT_COUNTER.register(collectorRegistry); + ELAPSED_ACQUIRED_HISTOGRAM.register(collectorRegistry); + ELAPSED_BORROWED_HISTOGRAM.register(collectorRegistry); + ELAPSED_CREATION_HISTOGRAM.register(collectorRegistry); + } + + @Override + public void recordConnectionAcquiredNanos(long elapsedAcquiredNanos) { + elapsedAcquiredHistogramChild.observe(elapsedAcquiredNanos); + } + + @Override + public void recordConnectionUsageMillis(long elapsedBorrowedMillis) { + elapsedBorrowedHistogramChild.observe(elapsedBorrowedMillis); + } + + @Override + public void recordConnectionCreatedMillis(long connectionCreatedMillis) { + elapsedCreationHistogramChild.observe(connectionCreatedMillis); + } + + @Override + public void recordConnectionTimeout() { + connectionTimeoutCounterChild.inc(); + } +} diff --git a/src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusHistogramMetricsTrackerFactory.java b/src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusHistogramMetricsTrackerFactory.java new file mode 100644 index 00000000..6ace160f --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusHistogramMetricsTrackerFactory.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2016 Brett Wooldridge + * + * 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 com.zaxxer.hikari.metrics.prometheus; + +import com.zaxxer.hikari.metrics.IMetricsTracker; +import com.zaxxer.hikari.metrics.MetricsTrackerFactory; +import com.zaxxer.hikari.metrics.PoolStats; +import io.prometheus.client.CollectorRegistry; + +/** + *

{@code
+ * HikariConfig config = new HikariConfig();
+ * config.setMetricsTrackerFactory(new PrometheusHistogramMetricsTrackerFactory());
+ * }
+ */ +public class PrometheusHistogramMetricsTrackerFactory implements MetricsTrackerFactory { + + private HikariCPCollector collector; + + private CollectorRegistry collectorRegistry; + + /** + * Default Constructor. The Hikari metrics are registered to the default + * collector registry ({@code CollectorRegistry.defaultRegistry}). + */ + public PrometheusHistogramMetricsTrackerFactory() { + this.collectorRegistry = CollectorRegistry.defaultRegistry; + } + + /** + * Constructor that allows to pass in a {@link CollectorRegistry} to which the + * Hikari metrics are registered. + */ + public PrometheusHistogramMetricsTrackerFactory(CollectorRegistry collectorRegistry) { + this.collectorRegistry = collectorRegistry; + } + + @Override + public IMetricsTracker create(String poolName, PoolStats poolStats) { + getCollector().add(poolName, poolStats); + return new PrometheusHistogramMetricsTracker(poolName, this.collectorRegistry); + } + + /** + * initialize and register collector if it isn't initialized yet + */ + private HikariCPCollector getCollector() { + if (collector == null) { + collector = new HikariCPCollector().register(this.collectorRegistry); + } + return collector; + } +} diff --git a/src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusMetricsTrackerFactory.java b/src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusMetricsTrackerFactory.java index 0e7737bb..2086d972 100644 --- a/src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusMetricsTrackerFactory.java +++ b/src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusMetricsTrackerFactory.java @@ -26,6 +26,9 @@ import io.prometheus.client.CollectorRegistry; * HikariConfig config = new HikariConfig(); * config.setMetricsTrackerFactory(new PrometheusMetricsTrackerFactory()); * } + * + * Note: the internal {@see io.prometheus.client.Summary} requires heavy locks. Consider using + * {@see PrometheusHistogramMetricsTrackerFactory} if performance plays a role and you don't need the summary per se. */ public class PrometheusMetricsTrackerFactory implements MetricsTrackerFactory { diff --git a/src/test/java/com/zaxxer/hikari/metrics/prometheus/PrometheusHistogramMetricsTrackerFactoryTest.java b/src/test/java/com/zaxxer/hikari/metrics/prometheus/PrometheusHistogramMetricsTrackerFactoryTest.java new file mode 100644 index 00000000..d735dc7b --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/metrics/prometheus/PrometheusHistogramMetricsTrackerFactoryTest.java @@ -0,0 +1,77 @@ +package com.zaxxer.hikari.metrics.prometheus; + +import com.zaxxer.hikari.metrics.PoolStats; +import io.prometheus.client.Collector; +import io.prometheus.client.CollectorRegistry; +import org.junit.After; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class PrometheusHistogramMetricsTrackerFactoryTest { + + @Test + public void registersToProvidedCollectorRegistry() { + CollectorRegistry collectorRegistry = new CollectorRegistry(); + PrometheusHistogramMetricsTrackerFactory factory = + new PrometheusHistogramMetricsTrackerFactory(collectorRegistry); + factory.create("testpool-1", poolStats()); + assertHikariMetricsAreNotPresent(CollectorRegistry.defaultRegistry); + assertHikariMetricsArePresent(collectorRegistry); + } + + @Test + public void registersToDefaultCollectorRegistry() { + PrometheusHistogramMetricsTrackerFactory factory = new PrometheusHistogramMetricsTrackerFactory(); + factory.create("testpool-2", poolStats()); + assertHikariMetricsArePresent(CollectorRegistry.defaultRegistry); + } + + @After + public void clearCollectorRegistry(){ + CollectorRegistry.defaultRegistry.clear(); + } + + private void assertHikariMetricsArePresent(CollectorRegistry collectorRegistry) { + List registeredMetrics = toMetricNames(collectorRegistry.metricFamilySamples()); + assertTrue(registeredMetrics.contains("hikaricp_active_connections")); + assertTrue(registeredMetrics.contains("hikaricp_idle_connections")); + assertTrue(registeredMetrics.contains("hikaricp_pending_threads")); + assertTrue(registeredMetrics.contains("hikaricp_connections")); + assertTrue(registeredMetrics.contains("hikaricp_max_connections")); + assertTrue(registeredMetrics.contains("hikaricp_min_connections")); + } + + private void assertHikariMetricsAreNotPresent(CollectorRegistry collectorRegistry) { + List registeredMetrics = toMetricNames(collectorRegistry.metricFamilySamples()); + assertFalse(registeredMetrics.contains("hikaricp_active_connections")); + assertFalse(registeredMetrics.contains("hikaricp_idle_connections")); + assertFalse(registeredMetrics.contains("hikaricp_pending_threads")); + assertFalse(registeredMetrics.contains("hikaricp_connections")); + assertFalse(registeredMetrics.contains("hikaricp_max_connections")); + assertFalse(registeredMetrics.contains("hikaricp_min_connections")); + } + + private List toMetricNames(Enumeration enumeration) { + List list = new ArrayList<>(); + while (enumeration.hasMoreElements()) { + list.add(enumeration.nextElement().name); + } + return list; + } + + private PoolStats poolStats() { + return new PoolStats(0) { + @Override + protected void update() { + // do nothing + } + }; + } + +} diff --git a/src/test/java/com/zaxxer/hikari/metrics/prometheus/PrometheusHistogramMetricsTrackerTest.java b/src/test/java/com/zaxxer/hikari/metrics/prometheus/PrometheusHistogramMetricsTrackerTest.java new file mode 100644 index 00000000..418c317b --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/metrics/prometheus/PrometheusHistogramMetricsTrackerTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2013, 2014 Brett Wooldridge + * + * 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 com.zaxxer.hikari.metrics.prometheus; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.prometheus.client.CollectorRegistry; +import org.junit.Before; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.SQLTransientConnectionException; + +import static com.zaxxer.hikari.pool.TestElf.newHikariConfig; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +public class PrometheusHistogramMetricsTrackerTest { + + private CollectorRegistry collectorRegistry; + + private static final String POOL_LABEL_NAME = "pool"; + + @Before + public void setupCollectorRegistry(){ + this.collectorRegistry = new CollectorRegistry(); + } + + @Test + public void recordConnectionTimeout() throws Exception { + HikariConfig config = newHikariConfig(); + config.setMetricsTrackerFactory(new PrometheusHistogramMetricsTrackerFactory(collectorRegistry)); + config.setJdbcUrl("jdbc:h2:mem:"); + config.setMaximumPoolSize(2); + config.setConnectionTimeout(250); + + String[] labelNames = {POOL_LABEL_NAME}; + String[] labelValues = {config.getPoolName()}; + + try (HikariDataSource hikariDataSource = new HikariDataSource(config)) { + try (Connection connection1 = hikariDataSource.getConnection(); + Connection connection2 = hikariDataSource.getConnection()) { + try (Connection connection3 = hikariDataSource.getConnection()) { + } catch (SQLTransientConnectionException ignored) { + } + } + + Double total = collectorRegistry.getSampleValue( + "hikaricp_connection_timeout_total", + labelNames, + labelValues + ); + assertThat(total, is(1.0)); + } + } + + @Test + public void connectionAcquisitionMetrics() { + checkSummaryMetricFamily("hikaricp_connection_acquired_nanos"); + } + + @Test + public void connectionUsageMetrics() { + checkSummaryMetricFamily("hikaricp_connection_usage_millis"); + } + + @Test + public void connectionCreationMetrics() { + checkSummaryMetricFamily("hikaricp_connection_creation_millis"); + } + + @Test + public void testMultiplePoolName() throws Exception { + String[] labelNames = {POOL_LABEL_NAME}; + + HikariConfig config = newHikariConfig(); + config.setMetricsTrackerFactory(new PrometheusHistogramMetricsTrackerFactory(collectorRegistry)); + config.setPoolName("first"); + config.setJdbcUrl("jdbc:h2:mem:"); + config.setMaximumPoolSize(2); + config.setConnectionTimeout(250); + String[] labelValues1 = {config.getPoolName()}; + + try (HikariDataSource ignored = new HikariDataSource(config)) { + assertThat(collectorRegistry.getSampleValue( + "hikaricp_connection_timeout_total", + labelNames, + labelValues1), is(0.0)); + + CollectorRegistry collectorRegistry2 = new CollectorRegistry(); + HikariConfig config2 = newHikariConfig(); + config2.setMetricsTrackerFactory(new PrometheusHistogramMetricsTrackerFactory(collectorRegistry2)); + config2.setPoolName("second"); + config2.setJdbcUrl("jdbc:h2:mem:"); + config2.setMaximumPoolSize(4); + config2.setConnectionTimeout(250); + String[] labelValues2 = {config2.getPoolName()}; + + try (HikariDataSource ignored2 = new HikariDataSource(config2)) { + assertThat(collectorRegistry2.getSampleValue( + "hikaricp_connection_timeout_total", + labelNames, + labelValues2), is(0.0)); + } + } + } + + private void checkSummaryMetricFamily(String metricName) { + HikariConfig config = newHikariConfig(); + config.setMetricsTrackerFactory(new PrometheusHistogramMetricsTrackerFactory(collectorRegistry)); + config.setJdbcUrl("jdbc:h2:mem:"); + + try (HikariDataSource ignored = new HikariDataSource(config)) { + Double count = collectorRegistry.getSampleValue( + metricName + "_count", + new String[]{POOL_LABEL_NAME}, + new String[]{config.getPoolName()} + ); + assertNotNull(count); + + Double sum = collectorRegistry.getSampleValue( + metricName + "_sum", + new String[]{POOL_LABEL_NAME}, + new String[]{config.getPoolName()} + ); + assertNotNull(sum); + } + } +}