001package nl.nlighten.prometheus.tomcat; 002 003import java.util.Map; 004import io.prometheus.client.Gauge; 005import io.prometheus.client.Histogram; 006import org.apache.tomcat.jdbc.pool.ConnectionPool; 007import org.apache.tomcat.jdbc.pool.PoolProperties.InterceptorProperty; 008import org.apache.tomcat.jdbc.pool.PooledConnection; 009import org.apache.tomcat.jdbc.pool.interceptor.AbstractQueryReport; 010 011/** 012 * A Tomcat <a href="http://tomcat.apache.org/tomcat-8.5-doc/jdbc-pool.html#JDBC_interceptors">JDBC interceptor</a> that tracks query statistics for 013 * applications using the Tomcat <a href="http://tomcat.apache.org/tomcat-8.5-doc/jdbc-pool.html">jdbc-pool</a>. This interceptor will NOT work for 014 * any other connection pool (eg DBCP2). 015 * 016 * The interceptor will create the following metrics: 017 * 018 * - A histogram with global query response times 019 * - A histogram with per query response times for slow queries (optional) 020 * - A gauge with per query error counts (optional) 021 * 022 * <p> 023 * Example usage: 024 * <pre> 025 * {@code 026 * <Resource name="jdbc/TestDB" 027 * auth="Container" 028 * type="javax.sql.DataSource" 029 * factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" 030 * jdbcInterceptors="nl.nlighten.prometheus.TomcatJdbcInterceptor(logFailed=true,logSlow=true,threshold=1000,buckets=.01|.05,|.1|1|10,slowQueryBuckets=1|10|30)" 031 * username="root" 032 * password="password" 033 * driverClassName="com.mysql.jdbc.Driver" 034 * url="jdbc:mysql://localhost:3306/mysql"/> 035 * } 036 * </pre> 037 * 038 * Configuration options are as shown above an have the following meaning: 039 * - logFailed: if set to 'true' provide metrics on failed queries 040 * - logSlow: if set to 'true' provide metrics on metrics exceeding threshold 041 * - threshold: the threshold in ms above which metrics will be provided if logSlow=true 042 * - buckets: the buckets separated by a pipe ("|") symbol to be used for the global query response times, defaults to .01|.05|.1|.25|.5|1|2.5|10 043 * - slowQueryBuckets: the buckets separated by a pipe ("|") symbol to be used for the global query response times, defaults to 1|2.5|10|30 044 * 045 * NOTE: enabling logFailed and logSlow may lead to a lot of additional metrics., so be careful !!! 046 * 047 * Example metrics being exported: 048 * <pre> 049 * tomcat_jdbc_query_seconds_bucket{le="0.005",} 48950.0 050 * ..... 051 * tomcat_jdbc_query_seconds_bucket{le="+Inf",} 301.0 052 * tomcat_jdbc_query_seconds_count 353501.0 053 * tomcat_jdbc_query_seconds_sum 331875.0 054 * tomcat_jdbc_slowquery_seconds{query="SELECT 1 from DUAL", } 055 * </pre> 056 */ 057public class TomcatJdbcInterceptor extends AbstractQueryReport { 058 059 private static Histogram globalQueryStats; 060 private static Histogram slowQueryStats; 061 private static Gauge failedQueryStats; 062 private boolean slowQueryStatsEnabled; 063 private boolean failedQueryStatsEnabled; 064 private long slowQueryThreshold = 1000; 065 066 public final static String SUCCESS_QUERY_STATUS = "success"; 067 public final static String FAILED_QUERY_STATUS = "error"; 068 069 070 071 @Override 072 public void setProperties(Map<String, InterceptorProperty> properties) { 073 // super.setProperties(properties); 074 075 InterceptorProperty bucketsProperty = properties.get("buckets"); 076 double[] buckets; 077 if (bucketsProperty != null) { 078 String[] bucketParams = bucketsProperty.getValue().split("\\|"); 079 buckets = new double[bucketParams.length]; 080 for (int i = 0; i < bucketParams.length; i++) { 081 buckets[i] = Double.parseDouble(bucketParams[i]); 082 } 083 } else { 084 buckets = new double[] {.01, .05, .1, .25, .5, 1, 2.5, 10}; 085 } 086 087 if (globalQueryStats == null) { 088 Histogram.Builder builder = Histogram.build() 089 .help("JDBC query duration") 090 .name("tomcat_jdbc_query_seconds") 091 .buckets(buckets) 092 .labelNames("status"); 093 globalQueryStats = builder.register(); 094 } 095 096 InterceptorProperty slowQueryBucketsProperty = properties.get("slowQueryBuckets"); 097 double[] slowQueryBuckets; 098 if (slowQueryBucketsProperty != null) { 099 String[] bucketParams = slowQueryBucketsProperty.getValue().split("\\|"); 100 slowQueryBuckets = new double[bucketParams.length]; 101 for (int i = 0; i < bucketParams.length; i++) { 102 slowQueryBuckets[i] = Double.parseDouble(bucketParams[i]); 103 } 104 } else { 105 slowQueryBuckets = new double[] { 1, 2.5, 10, 30}; 106 } 107 108 InterceptorProperty slowQueryStatsProperty = properties.get("logSlow"); 109 if (slowQueryStatsProperty != null && slowQueryStatsProperty.getValue().equals("true")) { 110 slowQueryStatsEnabled = true; 111 if (slowQueryStats == null) { 112 Histogram.Builder builder = Histogram.build() 113 .help("JDBC slow query duration in seconds") 114 .name("tomcat_jdbc_slowquery_seconds") 115 .buckets(slowQueryBuckets) 116 .labelNames("query"); 117 slowQueryStats = builder.register(); 118 } 119 } 120 121 InterceptorProperty slowQueryThresholdProperty = properties.get("threshold"); 122 if (slowQueryThresholdProperty != null) { 123 slowQueryThreshold = Long.parseLong(slowQueryThresholdProperty.getValue()); 124 } 125 126 InterceptorProperty failedQueryStatsProperty = properties.get("logFailed"); 127 if (failedQueryStatsProperty != null && failedQueryStatsProperty.getValue().equals("true")) { 128 failedQueryStatsEnabled = true; 129 if (failedQueryStats == null) { 130 Gauge.Builder builder = Gauge.build() 131 .help("Number of errors for give JDBC query") 132 .name("tomcat_jdbc_failedquery_total") 133 .labelNames("query"); 134 failedQueryStats = builder.register(); 135 } 136 } 137 } 138 139 @Override 140 protected String reportFailedQuery(String query, Object[] args, String name, long start, Throwable t) { 141 String sql = super.reportFailedQuery(query, args, name, start, t); 142 long now = System.currentTimeMillis(); 143 long delta = now - start; 144 globalQueryStats.labels(FAILED_QUERY_STATUS).observe(delta/1000); 145 if (failedQueryStatsEnabled) { 146 failedQueryStats.labels(sql).inc(); 147 } 148 return sql; 149 } 150 151 @Override 152 protected String reportQuery(String query, Object[] args, final String name, long start, long delta) { 153 String sql = super.reportQuery(query, args, name, start, delta); 154 globalQueryStats.labels(SUCCESS_QUERY_STATUS).observe(delta/1000); 155 if (slowQueryStatsEnabled && delta >= slowQueryThreshold) { 156 slowQueryStats.labels(sql).observe(delta/1000); 157 } 158 return sql; 159 } 160 161 @Override 162 protected String reportSlowQuery(String query, Object[] args, String name, long start, long delta) { 163 String sql = super.reportSlowQuery(query, args, name, start, delta); 164 globalQueryStats.labels(SUCCESS_QUERY_STATUS).observe(delta/1000); 165 if (slowQueryStatsEnabled && delta >= slowQueryThreshold) { 166 slowQueryStats.labels(sql).observe(delta/1000); 167 } 168 return sql; 169 } 170 171 @Override 172 public void closeInvoked() { 173 // NOOP 174 } 175 176 @Override 177 public void prepareStatement(String sql, long time) { 178 // NOOP 179 } 180 181 @Override 182 public void prepareCall(String sql, long time) { 183 // NOOP 184 } 185 186 @Override 187 public void poolStarted(ConnectionPool pool) { 188 super.poolStarted(pool); 189 } 190 191 @Override 192 public void poolClosed(ConnectionPool pool) { 193 super.poolClosed(pool); 194 } 195 196 @Override 197 public void reset(ConnectionPool parent, PooledConnection con) { 198 super.reset(parent, con); 199 } 200}