diff --git a/simpleclient_hibernate/pom.xml b/simpleclient_hibernate/pom.xml index 9a78d4295..8a32f4fe9 100644 --- a/simpleclient_hibernate/pom.xml +++ b/simpleclient_hibernate/pom.xml @@ -45,7 +45,7 @@ org.hibernate hibernate-core - 4.2.0.Final + 5.2.0.Final provided diff --git a/simpleclient_hibernate/src/main/java/io/prometheus/client/hibernate/HibernateStatisticsCollector.java b/simpleclient_hibernate/src/main/java/io/prometheus/client/hibernate/HibernateStatisticsCollector.java index cbff1faa5..32f07f8f9 100644 --- a/simpleclient_hibernate/src/main/java/io/prometheus/client/hibernate/HibernateStatisticsCollector.java +++ b/simpleclient_hibernate/src/main/java/io/prometheus/client/hibernate/HibernateStatisticsCollector.java @@ -3,6 +3,7 @@ import io.prometheus.client.Collector; import io.prometheus.client.CollectorRegistry; import io.prometheus.client.CounterMetricFamily; +import io.prometheus.client.GaugeMetricFamily; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -32,6 +33,10 @@ * SessionFactory sessionFactory = * entityManagerFactory.unwrap(SessionFactory.class); * + *

+ * When {@code enablePerQueryMetrics()} has been called, certain metrics like execution + * time are collected per query. This may create a lot of monitoring data, so it should + * be used with caution. * * @author Christian Kaltepoth */ @@ -39,8 +44,12 @@ public class HibernateStatisticsCollector extends Collector { private static final List LABEL_NAMES = Collections.singletonList("unit"); + private static final List LABEL_NAMES_PER_QUERY = Arrays.asList("unit", "query"); + private final Map sessionFactories = new ConcurrentHashMap(); + private boolean perQueryMetricsEnabled; + /** * Creates an empty collector. If you use this constructor, you have to add one or more * session factories to the collector by calling the {@link #add(SessionFactory, String)} @@ -74,6 +83,19 @@ public HibernateStatisticsCollector add(SessionFactory sessionFactory, String na return this; } + /** + * Enables collection of per-query metrics. Produces a lot of monitoring data, so use with caution. + *

+ * Per-query metrics have a label "query" with the actual HQL query as value. The query will contain + * placeholders ("?") instead of the real parameter values (example: {@code select u from User u where id=?}). + * + * @return Returns the collector + */ + public HibernateStatisticsCollector enablePerQueryMetrics() { + this.perQueryMetricsEnabled = true; + return this; + } + @Override public List collect() { List metrics = new ArrayList(); @@ -82,6 +104,9 @@ public List collect() { metrics.addAll(getCacheMetrics()); metrics.addAll(getEntityMetrics()); metrics.addAll(getQueryExecutionMetrics()); + if (perQueryMetricsEnabled) { + metrics.addAll(getPerQueryMetrics()); + } return metrics; } @@ -448,14 +473,104 @@ public double getValue(Statistics statistics) { ); } + private List getPerQueryMetrics() { + List metrics = new ArrayList(); + + metrics.addAll(Arrays.asList( + + createCounterForQuery("hibernate_per_query_cache_hit_total", + "Global number of cache hits for query (getCacheHitCount)", + new ValueProviderPerQuery() { + @Override + public double getValue(Statistics statistics, String query) { + return statistics.getQueryStatistics(query) + .getCacheHitCount(); + } + } + ), + createCounterForQuery("hibernate_per_query_cache_miss_total", + "Global number of cache misses for query (getCacheMissCount)", + new ValueProviderPerQuery() { + @Override + public double getValue(Statistics statistics, String query) { + return statistics.getQueryStatistics(query) + .getCacheMissCount(); + } + } + ), + createCounterForQuery("hibernate_per_query_cache_put_total", + "Global number of cache puts for query (getCachePutCount)", + new ValueProviderPerQuery() { + @Override + public double getValue(Statistics statistics, String query) { + return statistics.getQueryStatistics(query) + .getCachePutCount(); + } + } + ), + createCounterForQuery("hibernate_per_query_execution_total", + "Global number of executions for query (getExecutionCount)", + new ValueProviderPerQuery() { + @Override + public double getValue(Statistics statistics, String query) { + return statistics.getQueryStatistics(query) + .getExecutionCount(); + } + } + ), + createCounterForQuery("hibernate_per_query_execution_rows_total", + "Global number of rows for all executions of query (getExecutionRowCount)", + new ValueProviderPerQuery() { + @Override + public double getValue(Statistics statistics, String query) { + return statistics.getQueryStatistics(query) + .getExecutionRowCount(); + } + } + ), + createGaugeForQuery("hibernate_per_query_execution_min_seconds", + "Minimum execution time of query in seconds (based on getExecutionMinTime)", + new ValueProviderPerQuery() { + @Override + public double getValue(Statistics statistics, String query) { + return toSeconds(statistics.getQueryStatistics(query) + .getExecutionMinTime()); + } + } + ), + createGaugeForQuery("hibernate_per_query_execution_max_seconds", + "Maximum execution time of query in seconds (based on getExecutionMaxTime)", + new ValueProviderPerQuery() { + @Override + public double getValue(Statistics statistics, String query) { + return toSeconds(statistics.getQueryStatistics(query) + .getExecutionMaxTime()); + } + } + ), + createCounterForQuery("hibernate_per_query_execution_seconds_total", + "Accumulated execution time of query in seconds (based on getExecutionTotalTime)", + new ValueProviderPerQuery() { + @Override + public double getValue(Statistics statistics, String query) { + return toSeconds(statistics.getQueryStatistics(query) + .getExecutionTotalTime()); + } + } + ) + )); + + return metrics; + } + private CounterMetricFamily createCounter(String metric, String help, ValueProvider provider) { CounterMetricFamily metricFamily = new CounterMetricFamily(metric, help, LABEL_NAMES); for (Entry entry : sessionFactories.entrySet()) { metricFamily.addMetric( - Collections.singletonList(entry.getKey()), - provider.getValue(entry.getValue().getStatistics()) + Collections.singletonList(entry.getKey()), + provider.getValue(entry.getValue().getStatistics()) ); } @@ -463,10 +578,71 @@ private CounterMetricFamily createCounter(String metric, String help, ValueProvi } + private CounterMetricFamily createCounterForQuery(String metric, String help, ValueProviderPerQuery provider) { + + final CounterMetricFamily counters = new CounterMetricFamily(metric, help, LABEL_NAMES_PER_QUERY); + + addMetricsForQuery(new PerQuerySamples() { + @Override + public void addMetric(List labelValues, double value) { + counters.addMetric(labelValues, value); + } + }, provider); + + return counters; + + } + + private GaugeMetricFamily createGaugeForQuery(String metric, String help, ValueProviderPerQuery provider) { + + final GaugeMetricFamily gauges = new GaugeMetricFamily(metric, help, LABEL_NAMES_PER_QUERY); + + addMetricsForQuery(new PerQuerySamples() { + @Override + public void addMetric(List labelValues, double value) { + gauges.addMetric(labelValues, value); + } + }, provider); + + return gauges; + + } + + private void addMetricsForQuery(PerQuerySamples samples, ValueProviderPerQuery provider) { + + for (Entry entry : sessionFactories.entrySet()) { + SessionFactory sessionFactory = entry.getValue(); + Statistics stats = sessionFactory.getStatistics(); + String unitName = entry.getKey(); + + for (String query : stats.getQueries()) { + samples.addMetric(Arrays.asList(unitName, query), provider.getValue(stats, query)); + } + } + } + + private double toSeconds(long milliseconds){ + return milliseconds / 1000d; + } + + private interface PerQuerySamples { + + void addMetric(List labelValues, double value); + + } + + private interface ValueProvider { double getValue(Statistics statistics); } + private interface ValueProviderPerQuery { + + double getValue(Statistics statistics, String query); + + } + + } diff --git a/simpleclient_hibernate/src/test/java/io/prometheus/client/hibernate/HibernateStatisticsCollectorTest.java b/simpleclient_hibernate/src/test/java/io/prometheus/client/hibernate/HibernateStatisticsCollectorTest.java index 770fab84f..b148ee108 100644 --- a/simpleclient_hibernate/src/test/java/io/prometheus/client/hibernate/HibernateStatisticsCollectorTest.java +++ b/simpleclient_hibernate/src/test/java/io/prometheus/client/hibernate/HibernateStatisticsCollectorTest.java @@ -1,12 +1,15 @@ package io.prometheus.client.hibernate; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import io.prometheus.client.CollectorRegistry; import org.hibernate.SessionFactory; +import org.hibernate.stat.QueryStatistics; import org.hibernate.stat.Statistics; import org.junit.Before; import org.junit.Rule; @@ -20,6 +23,7 @@ public class HibernateStatisticsCollectorTest { private SessionFactory sessionFactory; private Statistics statistics; + private QueryStatistics queryStatistics; private CollectorRegistry registry; @Before @@ -27,6 +31,7 @@ public void before() { registry = new CollectorRegistry(); sessionFactory = mock(SessionFactory.class); statistics = mock(Statistics.class); + queryStatistics = mock(QueryStatistics.class); when(sessionFactory.getStatistics()).thenReturn(statistics); } @@ -159,6 +164,60 @@ public void shouldPublishQueryExecutionMetrics() { } + @Test + public void shouldPublishPerQueryMetricsWhenEnabled() { + String query = "query"; + mockQueryStatistics(query); + + new HibernateStatisticsCollector() + .add(sessionFactory, "factory6") + .enablePerQueryMetrics() + .register(registry); + + assertThat(getSampleForQuery("hibernate_per_query_cache_hit_total", "factory6", query), is(1.0)); + assertThat(getSampleForQuery("hibernate_per_query_cache_miss_total", "factory6", query), is(2.0)); + assertThat(getSampleForQuery("hibernate_per_query_cache_put_total", "factory6", query), is(3.0)); + assertThat(getSampleForQuery("hibernate_per_query_execution_max_seconds", "factory6", query), is(0.555d)); + assertThat(getSampleForQuery("hibernate_per_query_execution_min_seconds", "factory6", query), is(0.123d)); + assertThat(getSampleForQuery("hibernate_per_query_execution_rows_total", "factory6", query), is(7.0)); + assertThat(getSampleForQuery("hibernate_per_query_execution_total", "factory6", query), is(8.0)); + assertThat(getSampleForQuery("hibernate_per_query_execution_seconds_total", "factory6", query), is(102.540d)); + + } + + @Test + public void shouldNotPublishPerQueryMetricsByDefault() { + String query = "query"; + mockQueryStatistics(query); + + new HibernateStatisticsCollector() + .add(sessionFactory, "factory7") + .register(registry); + + assertThat(getSampleForQuery("hibernate_per_query_cache_hit_total", "factory7", query), nullValue()); + assertThat(getSampleForQuery("hibernate_per_query_cache_miss_total", "factory7", query), nullValue()); + assertThat(getSampleForQuery("hibernate_per_query_cache_put_total", "factory7", query), nullValue()); + assertThat(getSampleForQuery("hibernate_per_query_execution_max_seconds", "factory7", query), nullValue()); + assertThat(getSampleForQuery("hibernate_per_query_execution_min_seconds", "factory7", query), nullValue()); + assertThat(getSampleForQuery("hibernate_per_query_execution_rows_total", "factory7", query), nullValue()); + assertThat(getSampleForQuery("hibernate_per_query_execution_total", "factory7", query), nullValue()); + assertThat(getSampleForQuery("hibernate_per_query_execution_seconds", "factory7", query), nullValue()); + + } + + private void mockQueryStatistics(String query) { + when(statistics.getQueries()).thenReturn(new String[]{query}); + when(statistics.getQueryStatistics(eq(query))).thenReturn(queryStatistics); + when(queryStatistics.getCacheHitCount()).thenReturn(1L); + when(queryStatistics.getCacheMissCount()).thenReturn(2L); + when(queryStatistics.getCachePutCount()).thenReturn(3L); + when(queryStatistics.getExecutionMaxTime()).thenReturn(555L); + when(queryStatistics.getExecutionMinTime()).thenReturn(123L); + when(queryStatistics.getExecutionRowCount()).thenReturn(7L); + when(queryStatistics.getExecutionCount()).thenReturn(8L); + when(queryStatistics.getExecutionTotalTime()).thenReturn(102540L); + } + @Test public void shouldFailIfNoSessionFactoriesAreRegistered() { @@ -175,4 +234,10 @@ private Double getSample(String metric, String factory) { ); } + private Double getSampleForQuery(String metric, String factory, String query) { + return registry.getSampleValue( + metric, new String[]{"unit", "query"}, new String[]{factory, query} + ); + } + } \ No newline at end of file