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.
37 package org.apache.tomcat.jdbc.pool.interceptor;
39 import java.text.SimpleDateFormat;
40 import java.util.ArrayList;
41 import java.util.Collections;
42 import java.util.Comparator;
43 import java.util.Locale;
45 import java.util.TimeZone;
46 import java.util.concurrent.ConcurrentHashMap;
48 import javax.management.openmbean.CompositeDataSupport;
49 import javax.management.openmbean.CompositeType;
50 import javax.management.openmbean.OpenDataException;
51 import javax.management.openmbean.OpenType;
52 import javax.management.openmbean.SimpleType;
54 import org.apache.juli.logging.Log;
55 import org.apache.juli.logging.LogFactory;
56 import org.apache.tomcat.jdbc.pool.ConnectionPool;
57 import org.apache.tomcat.jdbc.pool.PoolProperties.InterceptorProperty;
58 import org.apache.tomcat.jdbc.pool.PooledConnection;
61 * Slow query report interceptor. Tracks timing of query executions.
64 public class SlowQueryReport extends AbstractQueryReport {
66 private static final Log log = LogFactory.getLog(SlowQueryReport.class);
69 * we will be keeping track of query stats on a per pool basis
71 protected static final ConcurrentHashMap<String,ConcurrentHashMap<String,QueryStats>> perPoolStats =
72 new ConcurrentHashMap<>();
74 * the queries that are used for this interceptor.
76 protected volatile ConcurrentHashMap<String,QueryStats> queries = null;
78 * Maximum number of queries we will be storing
80 protected int maxQueries= 1000; //don't store more than this amount of queries
83 * Flag to enable disable logging of slow queries
85 protected boolean logSlow = true;
88 * Flag to enable disable logging of failed queries
90 protected boolean logFailed = false;
93 * Sort QueryStats by last invocation time
95 protected final Comparator<QueryStats> queryStatsComparator = new QueryStatsComparator();
98 * Returns the query stats for a given pool
99 * @param poolname - the name of the pool we want to retrieve stats for
100 * @return a hash map containing statistics for 0 to maxQueries
102 public static ConcurrentHashMap<String,QueryStats> getPoolStats(String poolname) {
103 return perPoolStats.get(poolname);
107 * Creates a slow query report interceptor
109 public SlowQueryReport() {
113 public void setMaxQueries(int maxQueries) {
114 this.maxQueries = maxQueries;
119 protected String reportFailedQuery(String query, Object[] args, String name, long start, Throwable t) {
120 String sql = super.reportFailedQuery(query, args, name, start, t);
121 if (this.maxQueries > 0 ) {
122 long now = System.currentTimeMillis();
123 long delta = now - start;
124 QueryStats qs = this.getQueryStats(sql);
126 qs.failure(delta, now);
127 if (isLogFailed() && log.isWarnEnabled()) {
128 log.warn("Failed Query Report SQL="+sql+"; time="+delta+" ms;");
136 protected String reportQuery(String query, Object[] args, final String name, long start, long delta) {
137 String sql = super.reportQuery(query, args, name, start, delta);
138 if (this.maxQueries > 0 ) {
139 QueryStats qs = this.getQueryStats(sql);
140 if (qs != null) qs.add(delta, start);
146 protected String reportSlowQuery(String query, Object[] args, String name, long start, long delta) {
147 String sql = super.reportSlowQuery(query, args, name, start, delta);
148 if (this.maxQueries > 0 ) {
149 QueryStats qs = this.getQueryStats(sql);
151 qs.add(delta, start);
152 if (isLogSlow() && log.isWarnEnabled()) {
153 log.warn("Slow Query Report SQL="+sql+"; time="+delta+" ms;");
161 * invoked when the connection receives the close request
165 public void closeInvoked() {
170 public void prepareStatement(String sql, long time) {
171 if (this.maxQueries > 0 ) {
172 QueryStats qs = getQueryStats(sql);
173 if (qs != null) qs.prepare(time);
178 public void prepareCall(String sql, long time) {
179 if (this.maxQueries > 0 ) {
180 QueryStats qs = getQueryStats(sql);
181 if (qs != null) qs.prepare(time);
189 public void poolStarted(ConnectionPool pool) {
190 super.poolStarted(pool);
191 //see if we already created a map for this pool
192 queries = SlowQueryReport.perPoolStats.get(pool.getName());
194 //create the map to hold our stats
195 //however TODO we need to improve the eviction
197 queries = new ConcurrentHashMap<>();
198 if (perPoolStats.putIfAbsent(pool.getName(), queries)!=null) {
199 //there already was one
200 queries = SlowQueryReport.perPoolStats.get(pool.getName());
209 public void poolClosed(ConnectionPool pool) {
210 perPoolStats.remove(pool.getName());
211 super.poolClosed(pool);
214 protected QueryStats getQueryStats(String sql) {
215 if (sql==null) sql = "";
216 ConcurrentHashMap<String,QueryStats> queries = SlowQueryReport.this.queries;
218 if (log.isWarnEnabled()) log.warn("Connection has already been closed or abandoned");
221 QueryStats qs = queries.get(sql);
223 qs = new QueryStats(sql);
224 if (queries.putIfAbsent(sql,qs)!=null) {
225 qs = queries.get(sql);
227 //we added a new element, see if we need to remove the oldest
228 if (queries.size() > maxQueries) {
229 removeOldest(queries);
237 * Sort QueryStats by last invocation time
238 * @param queries The queries map
240 protected void removeOldest(ConcurrentHashMap<String,QueryStats> queries) {
241 ArrayList<QueryStats> list = new ArrayList<>(queries.values());
242 Collections.sort(list, queryStatsComparator);
244 while (queries.size() > maxQueries) {
245 String sql = list.get(removeIndex).getQuery();
247 if (log.isDebugEnabled()) log.debug("Removing slow query, capacity reached:"+sql);
254 public void reset(ConnectionPool parent, PooledConnection con) {
255 super.reset(parent, con);
257 queries = SlowQueryReport.perPoolStats.get(parent.getName());
263 public boolean isLogSlow() {
267 public void setLogSlow(boolean logSlow) {
268 this.logSlow = logSlow;
271 public boolean isLogFailed() {
275 public void setLogFailed(boolean logFailed) {
276 this.logFailed = logFailed;
280 public void setProperties(Map<String, InterceptorProperty> properties) {
281 super.setProperties(properties);
282 final String threshold = "threshold";
283 final String maxqueries= "maxQueries";
284 final String logslow = "logSlow";
285 final String logfailed = "logFailed";
286 InterceptorProperty p1 = properties.get(threshold);
287 InterceptorProperty p2 = properties.get(maxqueries);
288 InterceptorProperty p3 = properties.get(logslow);
289 InterceptorProperty p4 = properties.get(logfailed);
291 setThreshold(Long.parseLong(p1.getValue()));
294 setMaxQueries(Integer.parseInt(p2.getValue()));
297 setLogSlow(Boolean.parseBoolean(p3.getValue()));
300 setLogFailed(Boolean.parseBoolean(p4.getValue()));
305 public static class QueryStats {
306 static final String[] FIELD_NAMES = new String[] {
313 "totalInvocationTime",
320 static final String[] FIELD_DESCRIPTIONS = new String[] {
322 "The number of query invocations, a call to executeXXX",
323 "The longest time for this query in milliseconds",
324 "The time and date for when the longest query took place",
325 "The shortest time for this query in milliseconds",
326 "The time and date for when the shortest query took place",
327 "The total amount of milliseconds spent executing this query",
328 "The number of failures for this query",
329 "The number of times this query was prepared (prepareStatement/prepareCall)",
330 "The total number of milliseconds spent preparing this query",
331 "The date and time of the last invocation"
334 static final OpenType<?>[] FIELD_TYPES = new OpenType[] {
348 private final String query;
349 private volatile int nrOfInvocations;
350 private volatile long maxInvocationTime = Long.MIN_VALUE;
351 private volatile long maxInvocationDate;
352 private volatile long minInvocationTime = Long.MAX_VALUE;
353 private volatile long minInvocationDate;
354 private volatile long totalInvocationTime;
355 private volatile long failures;
356 private volatile int prepareCount;
357 private volatile long prepareTime;
358 private volatile long lastInvocation = 0;
360 public static String[] getFieldNames() {
364 public static String[] getFieldDescriptions() {
365 return FIELD_DESCRIPTIONS;
368 public static OpenType<?>[] getFieldTypes() {
373 public String toString() {
374 SimpleDateFormat sdf =
375 new SimpleDateFormat("d MMM yyyy HH:mm:ss z", Locale.US);
376 sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
377 StringBuilder buf = new StringBuilder("QueryStats[query:");
379 buf.append(", nrOfInvocations:");
380 buf.append(nrOfInvocations);
381 buf.append(", maxInvocationTime:");
382 buf.append(maxInvocationTime);
383 buf.append(", maxInvocationDate:");
384 buf.append(sdf.format(new java.util.Date(maxInvocationDate)));
385 buf.append(", minInvocationTime:");
386 buf.append(minInvocationTime);
387 buf.append(", minInvocationDate:");
388 buf.append(sdf.format(new java.util.Date(minInvocationDate)));
389 buf.append(", totalInvocationTime:");
390 buf.append(totalInvocationTime);
391 buf.append(", averageInvocationTime:");
392 buf.append((float)totalInvocationTime / (float)nrOfInvocations);
393 buf.append(", failures:");
394 buf.append(failures);
395 buf.append(", prepareCount:");
396 buf.append(prepareCount);
397 buf.append(", prepareTime:");
398 buf.append(prepareTime);
400 return buf.toString();
403 public CompositeDataSupport getCompositeData(final CompositeType type) throws OpenDataException{
404 Object[] values = new Object[] {
406 Integer.valueOf(nrOfInvocations),
407 Long.valueOf(maxInvocationTime),
408 Long.valueOf(maxInvocationDate),
409 Long.valueOf(minInvocationTime),
410 Long.valueOf(minInvocationDate),
411 Long.valueOf(totalInvocationTime),
412 Long.valueOf(failures),
413 Integer.valueOf(prepareCount),
414 Long.valueOf(prepareTime),
415 Long.valueOf(lastInvocation)
417 return new CompositeDataSupport(type,FIELD_NAMES,values);
420 public QueryStats(String query) {
424 public void prepare(long invocationTime) {
426 prepareTime+=invocationTime;
430 public void add(long invocationTime, long now) {
431 //not thread safe, but don't sacrifice performance for this kind of stuff
432 maxInvocationTime = Math.max(invocationTime, maxInvocationTime);
433 if (maxInvocationTime == invocationTime) {
434 maxInvocationDate = now;
436 minInvocationTime = Math.min(invocationTime, minInvocationTime);
437 if (minInvocationTime==invocationTime) {
438 minInvocationDate = now;
441 totalInvocationTime+=invocationTime;
442 lastInvocation = now;
445 public void failure(long invocationTime, long now) {
446 add(invocationTime,now);
451 public String getQuery() {
455 public int getNrOfInvocations() {
456 return nrOfInvocations;
459 public long getMaxInvocationTime() {
460 return maxInvocationTime;
463 public long getMaxInvocationDate() {
464 return maxInvocationDate;
467 public long getMinInvocationTime() {
468 return minInvocationTime;
471 public long getMinInvocationDate() {
472 return minInvocationDate;
475 public long getTotalInvocationTime() {
476 return totalInvocationTime;
480 public int hashCode() {
481 return query.hashCode();
485 public boolean equals(Object other) {
486 if (other instanceof QueryStats) {
487 QueryStats qs = (QueryStats)other;
488 return qs.query.equals(this.query);
493 public boolean isOlderThan(QueryStats other) {
494 return this.lastInvocation < other.lastInvocation;
498 /** Compare QueryStats by their lastInvocation value. QueryStats that
499 * have never been updated, have a lastInvocation value of {@code 0}
500 * which should be handled as the newest possible invocation.
502 private static class QueryStatsComparator implements Comparator<QueryStats> {
505 public int compare(QueryStats stats1, QueryStats stats2) {
506 return Long.compare(handleZero(stats1.lastInvocation),
507 handleZero(stats2.lastInvocation));
510 private static long handleZero(long value) {
511 return value == 0 ? Long.MAX_VALUE : value;