001package io.prometheus.client.hibernate;
002
003import io.prometheus.client.Collector;
004import io.prometheus.client.CollectorRegistry;
005import io.prometheus.client.CounterMetricFamily;
006import io.prometheus.client.GaugeMetricFamily;
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.Collections;
010import java.util.List;
011import java.util.Map;
012import java.util.Map.Entry;
013import java.util.concurrent.ConcurrentHashMap;
014import org.hibernate.SessionFactory;
015import org.hibernate.stat.Statistics;
016
017/**
018 * Collect metrics from one or more Hibernate SessionFactory instances.
019 * <p>
020 * Usage example for a single session factory:
021 * <pre>
022 * new HibernateStatisticsCollector(sessionFactory, "myapp").register();
023 * </pre>
024 * Usage example for multiple session factories:
025 * <pre>
026 * new HibernateStatisticsCollector()
027 *     .add(sessionFactory1, "myapp1")
028 *     .add(sessionFactory2, "myapp2")
029 *     .register();
030 * </pre>
031 * If you are in a JPA environment, you can obtain the SessionFactory like this:
032 * <pre>
033 * SessionFactory sessionFactory =
034 *     entityManagerFactory.unwrap(SessionFactory.class);
035 * </pre>
036 * <p>
037 * When {@code enablePerQueryMetrics()} has been called, certain metrics like execution
038 * time are collected per query. This may create a lot of monitoring data, so it should
039 * be used with caution.
040 *
041 * @author Christian Kaltepoth
042 */
043public class HibernateStatisticsCollector extends Collector {
044
045  private static final List<String> LABEL_NAMES = Collections.singletonList("unit");
046
047  private static final List<String> LABEL_NAMES_PER_QUERY = Arrays.asList("unit", "query");
048
049  private final Map<String, SessionFactory> sessionFactories = new ConcurrentHashMap<String, SessionFactory>();
050
051  private boolean perQueryMetricsEnabled;
052
053  /**
054   * Creates an empty collector. If you use this constructor, you have to add one or more
055   * session factories to the collector by calling the {@link #add(SessionFactory, String)}
056   * method.
057   */
058  public HibernateStatisticsCollector() {
059    // nothing
060  }
061
062  /**
063   * Creates a new collector for the given session factory. Calling this constructor
064   * has the same effect as creating an empty collector and adding the session factory
065   * using {@link #add(SessionFactory, String)}.
066   *
067   * @param sessionFactory The Hibernate SessionFactory to collect metrics for
068   * @param name A unique name for this SessionFactory
069   */
070  public HibernateStatisticsCollector(SessionFactory sessionFactory, String name) {
071    add(sessionFactory, name);
072  }
073
074  /**
075   * Registers a Hibernate SessionFactory with this collector.
076   *
077   * @param sessionFactory The Hibernate SessionFactory to collect metrics for
078   * @param name A unique name for this SessionFactory
079   * @return Returns the collector
080   */
081  public HibernateStatisticsCollector add(SessionFactory sessionFactory, String name) {
082    sessionFactories.put(name, sessionFactory);
083    return this;
084  }
085
086  /**
087   * Enables collection of per-query metrics. Produces a lot of monitoring data, so use with caution.
088   * <p>
089   * Per-query metrics have a label "query" with the actual HQL query as value. The query will contain
090   * placeholders ("?") instead of the real parameter values (example: {@code select u from User u where id=?}).
091   *
092   * @return Returns the collector
093   */
094  public HibernateStatisticsCollector enablePerQueryMetrics() {
095    this.perQueryMetricsEnabled = true;
096    return this;
097  }
098
099  @Override
100  public List<MetricFamilySamples> collect() {
101    List<MetricFamilySamples> metrics = new ArrayList<MetricFamilySamples>();
102    metrics.addAll(getSessionMetrics());
103    metrics.addAll(getConnectionMetrics());
104    metrics.addAll(getCacheMetrics());
105    metrics.addAll(getEntityMetrics());
106    metrics.addAll(getQueryExecutionMetrics());
107    if (perQueryMetricsEnabled) {
108      metrics.addAll(getPerQueryMetrics());
109    }
110    return metrics;
111  }
112
113  @Override
114  public <T extends Collector> T register(CollectorRegistry registry) {
115    if (sessionFactories.isEmpty()) {
116      throw new IllegalStateException("You must register at least one SessionFactory.");
117    }
118    return super.register(registry);
119  }
120
121  private List<MetricFamilySamples> getSessionMetrics() {
122    return Arrays.<MetricFamilySamples>asList(
123        createCounter(
124            "hibernate_session_opened_total",
125            "Global number of sessions opened (getSessionOpenCount)",
126            new ValueProvider() {
127              @Override
128              public double getValue(Statistics statistics) {
129                return statistics.getSessionOpenCount();
130              }
131            }
132        ),
133        createCounter(
134            "hibernate_session_closed_total",
135            "Global number of sessions closed (getSessionCloseCount)",
136            new ValueProvider() {
137              @Override
138              public double getValue(Statistics statistics) {
139                return statistics.getSessionCloseCount();
140              }
141            }
142        ),
143        createCounter(
144            "hibernate_flushed_total",
145            "The global number of flushes executed by sessions (getFlushCount)",
146            new ValueProvider() {
147              @Override
148              public double getValue(Statistics statistics) {
149                return statistics.getFlushCount();
150              }
151            }
152        ),
153        createCounter(
154            "hibernate_connect_total",
155            "The global number of connections requested by the sessions (getConnectCount)",
156            new ValueProvider() {
157              @Override
158              public double getValue(Statistics statistics) {
159                return statistics.getConnectCount();
160              }
161            }
162        ),
163        createCounter(
164            "hibernate_optimistic_failure_total",
165            "The number of StaleObjectStateExceptions that occurred (getOptimisticFailureCount)",
166            new ValueProvider() {
167              @Override
168              public double getValue(Statistics statistics) {
169                return statistics.getOptimisticFailureCount();
170              }
171            }
172        )
173    );
174  }
175
176  private List<MetricFamilySamples> getConnectionMetrics() {
177    return Arrays.<MetricFamilySamples>asList(
178        createCounter(
179            "hibernate_statement_prepared_total",
180            "The number of prepared statements that were acquired (getPrepareStatementCount)",
181            new ValueProvider() {
182              @Override
183              public double getValue(Statistics statistics) {
184                return statistics.getPrepareStatementCount();
185              }
186            }
187        ),
188        createCounter(
189            "hibernate_statement_closed_total",
190            "The number of prepared statements that were released (getCloseStatementCount)",
191            new ValueProvider() {
192              @Override
193              public double getValue(Statistics statistics) {
194                return statistics.getCloseStatementCount();
195              }
196            }
197        ),
198        createCounter(
199            "hibernate_transaction_total",
200            "The number of transactions we know to have completed (getTransactionCount)",
201            new ValueProvider() {
202              @Override
203              public double getValue(Statistics statistics) {
204                return statistics.getTransactionCount();
205              }
206            }
207        ),
208        createCounter(
209            "hibernate_transaction_success_total",
210            "The number of transactions we know to have been successful (getSuccessfulTransactionCount)",
211            new ValueProvider() {
212              @Override
213              public double getValue(Statistics statistics) {
214                return statistics.getSuccessfulTransactionCount();
215              }
216            }
217        )
218    );
219  }
220
221  private List<MetricFamilySamples> getCacheMetrics() {
222    return Arrays.<MetricFamilySamples>asList(
223        createCounter(
224            "hibernate_second_level_cache_hit_total",
225            "Global number of cacheable entities/collections successfully retrieved from the cache (getSecondLevelCacheHitCount)",
226            new ValueProvider() {
227              @Override
228              public double getValue(Statistics statistics) {
229                return statistics.getSecondLevelCacheHitCount();
230              }
231            }
232        ),
233        createCounter(
234            "hibernate_second_level_cache_miss_total",
235            "Global number of cacheable entities/collections not found in the cache and loaded from the database (getSecondLevelCacheMissCount)",
236            new ValueProvider() {
237              @Override
238              public double getValue(Statistics statistics) {
239                return statistics.getSecondLevelCacheMissCount();
240              }
241            }
242        ),
243        createCounter(
244            "hibernate_second_level_cache_put_total",
245            "Global number of cacheable entities/collections put in the cache (getSecondLevelCachePutCount)",
246            new ValueProvider() {
247              @Override
248              public double getValue(Statistics statistics) {
249                return statistics.getSecondLevelCachePutCount();
250              }
251            }
252        ),
253        createCounter(
254            "hibernate_query_cache_hit_total",
255            "The global number of cached queries successfully retrieved from cache (getQueryCacheHitCount)",
256            new ValueProvider() {
257              @Override
258              public double getValue(Statistics statistics) {
259                return statistics.getQueryCacheHitCount();
260              }
261            }
262        ),
263        createCounter(
264            "hibernate_query_cache_miss_total",
265            "The global number of cached queries not found in cache (getQueryCacheMissCount)",
266            new ValueProvider() {
267              @Override
268              public double getValue(Statistics statistics) {
269                return statistics.getQueryCacheMissCount();
270              }
271            }
272        ),
273        createCounter(
274            "hibernate_query_cache_put_total",
275            "The global number of cacheable queries put in cache (getQueryCachePutCount)",
276            new ValueProvider() {
277              @Override
278              public double getValue(Statistics statistics) {
279                return statistics.getQueryCachePutCount();
280              }
281            }
282        ),
283        createCounter(
284            "hibernate_natural_id_cache_hit_total",
285            "The global number of cached naturalId lookups successfully retrieved from cache (getNaturalIdCacheHitCount)",
286            new ValueProvider() {
287              @Override
288              public double getValue(Statistics statistics) {
289                return statistics.getNaturalIdCacheHitCount();
290              }
291            }
292        ),
293        createCounter(
294            "hibernate_natural_id_cache_miss_total",
295            "The global number of cached naturalId lookups not found in cache (getNaturalIdCacheMissCount)",
296            new ValueProvider() {
297              @Override
298              public double getValue(Statistics statistics) {
299                return statistics.getNaturalIdCacheMissCount();
300              }
301            }
302        ),
303        createCounter(
304            "hibernate_natural_id_cache_put_total",
305            "The global number of cacheable naturalId lookups put in cache (getNaturalIdCachePutCount)",
306            new ValueProvider() {
307              @Override
308              public double getValue(Statistics statistics) {
309                return statistics.getNaturalIdCachePutCount();
310              }
311            }
312        ),
313        createCounter(
314            "hibernate_update_timestamps_cache_hit_total",
315            "The global number of timestamps successfully retrieved from cache (getUpdateTimestampsCacheHitCount)",
316            new ValueProvider() {
317              @Override
318              public double getValue(Statistics statistics) {
319                return statistics.getUpdateTimestampsCacheHitCount();
320              }
321            }
322        ),
323        createCounter(
324            "hibernate_update_timestamps_cache_miss_total",
325            "The global number of tables for which no update timestamps was not found in cache (getUpdateTimestampsCacheMissCount)",
326            new ValueProvider() {
327              @Override
328              public double getValue(Statistics statistics) {
329                return statistics.getUpdateTimestampsCacheMissCount();
330              }
331            }
332        ),
333        createCounter(
334            "hibernate_update_timestamps_cache_put_total",
335            "The global number of timestamps put in cache (getUpdateTimestampsCachePutCount)",
336            new ValueProvider() {
337              @Override
338              public double getValue(Statistics statistics) {
339                return statistics.getUpdateTimestampsCachePutCount();
340              }
341            }
342        )
343    );
344  }
345
346  private List<MetricFamilySamples> getEntityMetrics() {
347    return Arrays.<MetricFamilySamples>asList(
348        createCounter(
349            "hibernate_entity_delete_total",
350            "Global number of entity deletes (getEntityDeleteCount)",
351            new ValueProvider() {
352              @Override
353              public double getValue(Statistics statistics) {
354                return statistics.getEntityDeleteCount();
355              }
356            }
357        ),
358        createCounter(
359            "hibernate_entity_insert_total",
360            "Global number of entity inserts (getEntityInsertCount)",
361            new ValueProvider() {
362              @Override
363              public double getValue(Statistics statistics) {
364                return statistics.getEntityInsertCount();
365              }
366            }
367        ),
368        createCounter(
369            "hibernate_entity_load_total",
370            "Global number of entity loads (getEntityLoadCount)",
371            new ValueProvider() {
372              @Override
373              public double getValue(Statistics statistics) {
374                return statistics.getEntityLoadCount();
375              }
376            }
377        ),
378        createCounter(
379            "hibernate_entity_fetch_total",
380            "Global number of entity fetches (getEntityFetchCount)",
381            new ValueProvider() {
382              @Override
383              public double getValue(Statistics statistics) {
384                return statistics.getEntityFetchCount();
385              }
386            }
387        ),
388        createCounter(
389            "hibernate_entity_update_total",
390            "Global number of entity updates (getEntityUpdateCount)",
391            new ValueProvider() {
392              @Override
393              public double getValue(Statistics statistics) {
394                return statistics.getEntityUpdateCount();
395              }
396            }
397        ),
398        createCounter(
399            "hibernate_collection_load_total",
400            "Global number of collections loaded (getCollectionLoadCount)",
401            new ValueProvider() {
402              @Override
403              public double getValue(Statistics statistics) {
404                return statistics.getCollectionLoadCount();
405              }
406            }
407        ),
408        createCounter(
409            "hibernate_collection_fetch_total",
410            "Global number of collections fetched (getCollectionFetchCount)",
411            new ValueProvider() {
412              @Override
413              public double getValue(Statistics statistics) {
414                return statistics.getCollectionFetchCount();
415              }
416            }
417        ),
418        createCounter(
419            "hibernate_collection_update_total",
420            "Global number of collections updated (getCollectionUpdateCount)",
421            new ValueProvider() {
422              @Override
423              public double getValue(Statistics statistics) {
424                return statistics.getCollectionUpdateCount();
425              }
426            }
427        ),
428        createCounter(
429            "hibernate_collection_remove_total",
430            "Global number of collections removed (getCollectionRemoveCount)",
431            new ValueProvider() {
432              @Override
433              public double getValue(Statistics statistics) {
434                return statistics.getCollectionRemoveCount();
435              }
436            }
437        ),
438        createCounter(
439            "hibernate_collection_recreate_total",
440            "Global number of collections recreated (getCollectionRecreateCount)",
441            new ValueProvider() {
442              @Override
443              public double getValue(Statistics statistics) {
444                return statistics.getCollectionRecreateCount();
445              }
446            }
447        )
448    );
449  }
450
451  private List<MetricFamilySamples> getQueryExecutionMetrics() {
452    return Arrays.<MetricFamilySamples>asList(
453        createCounter(
454            "hibernate_query_execution_total",
455            "Global number of executed queries (getQueryExecutionCount)",
456            new ValueProvider() {
457              @Override
458              public double getValue(Statistics statistics) {
459                return statistics.getQueryExecutionCount();
460              }
461            }
462        ),
463        createCounter(
464            "hibernate_natural_id_query_execution_total",
465            "The global number of naturalId queries executed against the database (getNaturalIdQueryExecutionCount)",
466            new ValueProvider() {
467              @Override
468              public double getValue(Statistics statistics) {
469                return statistics.getNaturalIdQueryExecutionCount();
470              }
471            }
472        )
473    );
474  }
475
476  private List<MetricFamilySamples> getPerQueryMetrics() {
477    List<MetricFamilySamples> metrics = new ArrayList<MetricFamilySamples>();
478
479    metrics.addAll(Arrays.asList(
480
481        createCounterForQuery("hibernate_per_query_cache_hit_total",
482                "Global number of cache hits for query (getCacheHitCount)",
483            new ValueProviderPerQuery() {
484              @Override
485              public double getValue(Statistics statistics, String query) {
486                return statistics.getQueryStatistics(query)
487                        .getCacheHitCount();
488              }
489            }
490        ),
491        createCounterForQuery("hibernate_per_query_cache_miss_total",
492                "Global number of cache misses for query (getCacheMissCount)",
493            new ValueProviderPerQuery() {
494              @Override
495              public double getValue(Statistics statistics, String query) {
496                return statistics.getQueryStatistics(query)
497                        .getCacheMissCount();
498              }
499            }
500        ),
501        createCounterForQuery("hibernate_per_query_cache_put_total",
502                "Global number of cache puts for query (getCachePutCount)",
503            new ValueProviderPerQuery() {
504              @Override
505              public double getValue(Statistics statistics, String query) {
506                return statistics.getQueryStatistics(query)
507                        .getCachePutCount();
508              }
509            }
510        ),
511        createCounterForQuery("hibernate_per_query_execution_total",
512                "Global number of executions for query (getExecutionCount)",
513            new ValueProviderPerQuery() {
514              @Override
515              public double getValue(Statistics statistics, String query) {
516                return statistics.getQueryStatistics(query)
517                        .getExecutionCount();
518              }
519            }
520        ),
521        createCounterForQuery("hibernate_per_query_execution_rows_total",
522                "Global number of rows for all executions of query (getExecutionRowCount)",
523            new ValueProviderPerQuery() {
524              @Override
525              public double getValue(Statistics statistics, String query) {
526                return statistics.getQueryStatistics(query)
527                        .getExecutionRowCount();
528              }
529            }
530        ),
531        createGaugeForQuery("hibernate_per_query_execution_min_seconds",
532                "Minimum execution time of query in seconds (based on getExecutionMinTime)",
533            new ValueProviderPerQuery() {
534              @Override
535              public double getValue(Statistics statistics, String query) {
536                return toSeconds(statistics.getQueryStatistics(query)
537                        .getExecutionMinTime());
538              }
539            }
540        ),
541        createGaugeForQuery("hibernate_per_query_execution_max_seconds",
542                "Maximum execution time of query in seconds (based on getExecutionMaxTime)",
543            new ValueProviderPerQuery() {
544              @Override
545              public double getValue(Statistics statistics, String query) {
546                return toSeconds(statistics.getQueryStatistics(query)
547                        .getExecutionMaxTime());
548              }
549            }
550        ),
551        createCounterForQuery("hibernate_per_query_execution_seconds_total",
552            "Accumulated execution time of query in seconds (based on getExecutionTotalTime)",
553            new ValueProviderPerQuery() {
554              @Override
555              public double getValue(Statistics statistics, String query) {
556                return toSeconds(statistics.getQueryStatistics(query)
557                    .getExecutionTotalTime());
558              }
559            }
560        )
561    ));
562
563    return metrics;
564  }
565
566  private CounterMetricFamily createCounter(String metric, String help, ValueProvider provider) {
567
568    CounterMetricFamily metricFamily = new CounterMetricFamily(metric, help, LABEL_NAMES);
569
570    for (Entry<String, SessionFactory> entry : sessionFactories.entrySet()) {
571      metricFamily.addMetric(
572              Collections.singletonList(entry.getKey()),
573              provider.getValue(entry.getValue().getStatistics())
574      );
575    }
576
577    return metricFamily;
578
579  }
580
581  private CounterMetricFamily createCounterForQuery(String metric, String help, ValueProviderPerQuery provider) {
582
583    final CounterMetricFamily counters = new CounterMetricFamily(metric, help, LABEL_NAMES_PER_QUERY);
584
585    addMetricsForQuery(new PerQuerySamples() {
586      @Override
587      public void addMetric(List<String> labelValues, double value) {
588        counters.addMetric(labelValues, value);
589      }
590    }, provider);
591
592    return counters;
593
594  }
595
596  private GaugeMetricFamily createGaugeForQuery(String metric, String help, ValueProviderPerQuery provider) {
597
598    final GaugeMetricFamily gauges = new GaugeMetricFamily(metric, help, LABEL_NAMES_PER_QUERY);
599
600    addMetricsForQuery(new PerQuerySamples() {
601      @Override
602      public void addMetric(List<String> labelValues, double value) {
603        gauges.addMetric(labelValues, value);
604      }
605    }, provider);
606
607    return gauges;
608
609  }
610
611  private void addMetricsForQuery(PerQuerySamples samples, ValueProviderPerQuery provider) {
612
613    for (Entry<String, SessionFactory> entry : sessionFactories.entrySet()) {
614      SessionFactory sessionFactory = entry.getValue();
615      Statistics stats = sessionFactory.getStatistics();
616      String unitName = entry.getKey();
617
618      for (String query : stats.getQueries()) {
619        samples.addMetric(Arrays.asList(unitName, query), provider.getValue(stats, query));
620      }
621    }
622  }
623
624  private double toSeconds(long milliseconds){
625    return milliseconds / 1000d;
626  }
627
628  private interface PerQuerySamples {
629
630    void addMetric(List<String> labelValues, double value);
631
632  }
633
634
635  private interface ValueProvider {
636
637    double getValue(Statistics statistics);
638
639  }
640
641  private interface ValueProviderPerQuery {
642
643    double getValue(Statistics statistics, String query);
644
645  }
646
647
648}