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}