2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright (C) 2016 - 2017 AT&T
6 * ================================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 * ============LICENSE_END=========================================================
22 * Licensed to the Apache Software Foundation (ASF) under one or more
23 * contributor license agreements. See the NOTICE file distributed with
24 * this work for additional information regarding copyright ownership.
25 * The ASF licenses this file to You under the Apache License, Version 2.0
26 * (the "License"); you may not use this file except in compliance with
27 * the License. You may obtain a copy of the License at
29 * http://www.apache.org/licenses/LICENSE-2.0
31 * Unless required by applicable law or agreed to in writing, software
32 * distributed under the License is distributed on an "AS IS" BASIS,
33 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
34 * See the License for the specific language governing permissions and
35 * limitations under the License.
38 package org.apache.tomcat.jdbc.pool.interceptor;
40 import java.lang.reflect.Constructor;
41 import java.lang.reflect.InvocationHandler;
42 import java.lang.reflect.InvocationTargetException;
43 import java.lang.reflect.Method;
44 import java.lang.reflect.Proxy;
45 import java.sql.CallableStatement;
46 import java.sql.PreparedStatement;
47 import java.sql.SQLException;
48 import java.sql.Statement;
50 import org.apache.juli.logging.Log;
51 import org.apache.juli.logging.LogFactory;
52 import org.apache.tomcat.jdbc.pool.JdbcInterceptor;
54 * Abstract class that wraps statements and intercepts query executions.
57 public abstract class AbstractQueryReport extends AbstractCreateStatementInterceptor {
59 private static final Log log = LogFactory.getLog(AbstractQueryReport.class);
62 * The threshold in milliseconds. If the query is faster than this, we don't measure it
64 protected long threshold = 1000; //don't report queries less than this
67 * the constructors that are used to create statement proxies
69 protected static final Constructor<?>[] constructors =
70 new Constructor[AbstractCreateStatementInterceptor.STATEMENT_TYPE_COUNT];
73 public AbstractQueryReport() {
78 * Invoked when prepareStatement has been called and completed.
79 * @param sql - the string used to prepare the statement with
80 * @param time - the time it took to invoke prepare
82 protected abstract void prepareStatement(String sql, long time);
85 * Invoked when prepareCall has been called and completed.
86 * @param query - the string used to prepare the statement with
87 * @param time - the time it took to invoke prepare
89 protected abstract void prepareCall(String query, long time);
92 * Invoked when a query execution, a call to execute/executeQuery or executeBatch failed.
93 * @param query the query that was executed and failed
94 * @param args the arguments to the execution
95 * @param name the name of the method used to execute {@link AbstractCreateStatementInterceptor#isExecute(Method, boolean)}
96 * @param start the time the query execution started
97 * @param t the exception that happened
98 * @return - the SQL that was executed or the string "batch" if it was a batch execution
100 protected String reportFailedQuery(String query, Object[] args, final String name, long start, Throwable t) {
101 //extract the query string
102 String sql = (query==null && args!=null && args.length>0)?(String)args[0]:query;
103 //if we do batch execution, then we name the query 'batch'
104 if (sql==null && compare(EXECUTE_BATCH,name)) {
111 * Invoked when a query execution, a call to execute/executeQuery or executeBatch succeeded and was within the timing threshold
112 * @param query the query that was executed and failed
113 * @param args the arguments to the execution
114 * @param name the name of the method used to execute {@link AbstractCreateStatementInterceptor#isExecute(Method, boolean)}
115 * @param start the time the query execution started
116 * @param delta the time the execution took
117 * @return - the SQL that was executed or the string "batch" if it was a batch execution
119 protected String reportQuery(String query, Object[] args, final String name, long start, long delta) {
120 //extract the query string
121 String sql = (query==null && args!=null && args.length>0)?(String)args[0]:query;
122 //if we do batch execution, then we name the query 'batch'
123 if (sql==null && compare(EXECUTE_BATCH,name)) {
130 * Invoked when a query execution, a call to execute/executeQuery or executeBatch succeeded and was exceeded the timing threshold
131 * @param query the query that was executed and failed
132 * @param args the arguments to the execution
133 * @param name the name of the method used to execute {@link AbstractCreateStatementInterceptor#isExecute(Method, boolean)}
134 * @param start the time the query execution started
135 * @param delta the time the execution took
136 * @return - the SQL that was executed or the string "batch" if it was a batch execution
138 protected String reportSlowQuery(String query, Object[] args, final String name, long start, long delta) {
139 //extract the query string
140 String sql = (query==null && args!=null && args.length>0)?(String)args[0]:query;
141 //if we do batch execution, then we name the query 'batch'
142 if (sql==null && compare(EXECUTE_BATCH,name)) {
149 * returns the query measure threshold.
150 * This value is in milliseconds. If the query is faster than this threshold than it wont be accounted for
151 * @return the threshold in milliseconds
153 public long getThreshold() {
158 * Sets the query measurement threshold. The value is in milliseconds.
159 * If the query goes faster than this threshold it will not be recorded.
160 * @param threshold set to -1 to record every query. Value is in milliseconds.
162 public void setThreshold(long threshold) {
163 this.threshold = threshold;
167 * Creates a constructor for a proxy class, if one doesn't already exist
168 * @param idx - the index of the constructor
169 * @param clazz - the interface that the proxy will implement
170 * @return - returns a constructor used to create new instances
171 * @throws NoSuchMethodException Constructor not found
173 protected Constructor<?> getConstructor(int idx, Class<?> clazz) throws NoSuchMethodException {
174 if (constructors[idx]==null) {
175 Class<?> proxyClass = Proxy.getProxyClass(SlowQueryReport.class.getClassLoader(), new Class[] {clazz});
176 constructors[idx] = proxyClass.getConstructor(new Class[] { InvocationHandler.class });
178 return constructors[idx];
182 * Creates a statement interceptor to monitor query response times
185 public Object createStatement(Object proxy, Method method, Object[] args, Object statement, long time) {
187 Object result = null;
188 String name = method.getName();
190 Constructor<?> constructor = null;
191 if (compare(CREATE_STATEMENT,name)) {
193 constructor = getConstructor(CREATE_STATEMENT_IDX,Statement.class);
194 }else if (compare(PREPARE_STATEMENT,name)) {
196 sql = (String)args[0];
197 constructor = getConstructor(PREPARE_STATEMENT_IDX,PreparedStatement.class);
199 prepareStatement(sql, time);
201 }else if (compare(PREPARE_CALL,name)) {
203 sql = (String)args[0];
204 constructor = getConstructor(PREPARE_CALL_IDX,CallableStatement.class);
205 prepareCall(sql,time);
207 //do nothing, might be a future unsupported method
208 //so we better bail out and let the system continue
211 result = constructor.newInstance(new Object[] { new StatementProxy(statement,sql) });
213 }catch (Exception x) {
214 log.warn("Unable to create statement proxy for slow query report.",x);
221 * Class to measure query execute time
224 protected class StatementProxy implements InvocationHandler {
225 protected boolean closed = false;
226 protected Object delegate;
227 protected final String query;
228 public StatementProxy(Object parent, String query) {
229 this.delegate = parent;
234 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
235 //get the name of the method for comparison
236 final String name = method.getName();
238 boolean close = compare(JdbcInterceptor.CLOSE_VAL,name);
239 //allow close to be called multiple times
240 if (close && closed) return null;
241 //are we calling isClosed?
242 if (compare(JdbcInterceptor.ISCLOSED_VAL,name)) return Boolean.valueOf(closed);
243 //if we are calling anything else, bail out
244 if (closed) throw new SQLException("Statement closed.");
245 boolean process = false;
246 //check to see if we are about to execute a query
247 process = isExecute( method, process);
248 //if we are executing, get the current time
249 long start = (process)?System.currentTimeMillis():0;
250 Object result = null;
253 result = method.invoke(delegate,args);
254 }catch (Throwable t) {
255 reportFailedQuery(query,args,name,start,t);
256 if (t instanceof InvocationTargetException
257 && t.getCause() != null) {
264 long delta = (process)?(System.currentTimeMillis()-start):Long.MIN_VALUE;
265 //see if we meet the requirements to measure
266 if (delta>threshold) {
268 //report the slow query
269 reportSlowQuery(query, args, name, start, delta);
270 }catch (Exception t) {
271 if (log.isWarnEnabled()) log.warn("Unable to process slow query",t);
273 } else if (process) {
274 reportQuery(query, args, name, start, delta);
276 //perform close cleanup