diff options
Diffstat (limited to 'dblib/common/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReport.java')
-rw-r--r-- | dblib/common/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReport.java | 516 |
1 files changed, 516 insertions, 0 deletions
diff --git a/dblib/common/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReport.java b/dblib/common/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReport.java new file mode 100644 index 0000000..23fa3fa --- /dev/null +++ b/dblib/common/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/SlowQueryReport.java @@ -0,0 +1,516 @@ +/*- + * ============LICENSE_START======================================================= + * openecomp + * ================================================================================ + * Copyright (C) 2016 - 2017 AT&T + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomcat.jdbc.pool.interceptor; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; + +import javax.management.openmbean.CompositeDataSupport; +import javax.management.openmbean.CompositeType; +import javax.management.openmbean.OpenDataException; +import javax.management.openmbean.OpenType; +import javax.management.openmbean.SimpleType; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jdbc.pool.ConnectionPool; +import org.apache.tomcat.jdbc.pool.PoolProperties.InterceptorProperty; +import org.apache.tomcat.jdbc.pool.PooledConnection; + +/** + * Slow query report interceptor. Tracks timing of query executions. + * @version 1.0 + */ +public class SlowQueryReport extends AbstractQueryReport { + //logger + private static final Log log = LogFactory.getLog(SlowQueryReport.class); + + /** + * we will be keeping track of query stats on a per pool basis + */ + protected static final ConcurrentHashMap<String,ConcurrentHashMap<String,QueryStats>> perPoolStats = + new ConcurrentHashMap<>(); + /** + * the queries that are used for this interceptor. + */ + protected volatile ConcurrentHashMap<String,QueryStats> queries = null; + /** + * Maximum number of queries we will be storing + */ + protected int maxQueries= 1000; //don't store more than this amount of queries + + /** + * Flag to enable disable logging of slow queries + */ + protected boolean logSlow = true; + + /** + * Flag to enable disable logging of failed queries + */ + protected boolean logFailed = false; + + /** + * Sort QueryStats by last invocation time + */ + protected final Comparator<QueryStats> queryStatsComparator = new QueryStatsComparator(); + + /** + * Returns the query stats for a given pool + * @param poolname - the name of the pool we want to retrieve stats for + * @return a hash map containing statistics for 0 to maxQueries + */ + public static ConcurrentHashMap<String,QueryStats> getPoolStats(String poolname) { + return perPoolStats.get(poolname); + } + + /** + * Creates a slow query report interceptor + */ + public SlowQueryReport() { + super(); + } + + public void setMaxQueries(int maxQueries) { + this.maxQueries = maxQueries; + } + + + @Override + protected String reportFailedQuery(String query, Object[] args, String name, long start, Throwable t) { + String sql = super.reportFailedQuery(query, args, name, start, t); + if (this.maxQueries > 0 ) { + long now = System.currentTimeMillis(); + long delta = now - start; + QueryStats qs = this.getQueryStats(sql); + if (qs != null) { + qs.failure(delta, now); + if (isLogFailed() && log.isWarnEnabled()) { + log.warn("Failed Query Report SQL="+sql+"; time="+delta+" ms;"); + } + } + } + return sql; + } + + @Override + protected String reportQuery(String query, Object[] args, final String name, long start, long delta) { + String sql = super.reportQuery(query, args, name, start, delta); + if (this.maxQueries > 0 ) { + QueryStats qs = this.getQueryStats(sql); + if (qs != null) qs.add(delta, start); + } + return sql; + } + + @Override + protected String reportSlowQuery(String query, Object[] args, String name, long start, long delta) { + String sql = super.reportSlowQuery(query, args, name, start, delta); + if (this.maxQueries > 0 ) { + QueryStats qs = this.getQueryStats(sql); + if (qs != null) { + qs.add(delta, start); + if (isLogSlow() && log.isWarnEnabled()) { + log.warn("Slow Query Report SQL="+sql+"; time="+delta+" ms;"); + } + } + } + return sql; + } + + /** + * invoked when the connection receives the close request + * Not used for now. + */ + @Override + public void closeInvoked() { + // NOOP + } + + @Override + public void prepareStatement(String sql, long time) { + if (this.maxQueries > 0 ) { + QueryStats qs = getQueryStats(sql); + if (qs != null) qs.prepare(time); + } + } + + @Override + public void prepareCall(String sql, long time) { + if (this.maxQueries > 0 ) { + QueryStats qs = getQueryStats(sql); + if (qs != null) qs.prepare(time); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void poolStarted(ConnectionPool pool) { + super.poolStarted(pool); + //see if we already created a map for this pool + queries = SlowQueryReport.perPoolStats.get(pool.getName()); + if (queries==null) { + //create the map to hold our stats + //however TODO we need to improve the eviction + //selection + queries = new ConcurrentHashMap<>(); + if (perPoolStats.putIfAbsent(pool.getName(), queries)!=null) { + //there already was one + queries = SlowQueryReport.perPoolStats.get(pool.getName()); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void poolClosed(ConnectionPool pool) { + perPoolStats.remove(pool.getName()); + super.poolClosed(pool); + } + + protected QueryStats getQueryStats(String sql) { + if (sql==null) sql = ""; + ConcurrentHashMap<String,QueryStats> queries = SlowQueryReport.this.queries; + if (queries==null) { + if (log.isWarnEnabled()) log.warn("Connection has already been closed or abandoned"); + return null; + } + QueryStats qs = queries.get(sql); + if (qs == null) { + qs = new QueryStats(sql); + if (queries.putIfAbsent(sql,qs)!=null) { + qs = queries.get(sql); + } else { + //we added a new element, see if we need to remove the oldest + if (queries.size() > maxQueries) { + removeOldest(queries); + } + } + } + return qs; + } + + /** + * Sort QueryStats by last invocation time + * @param queries The queries map + */ + protected void removeOldest(ConcurrentHashMap<String,QueryStats> queries) { + ArrayList<QueryStats> list = new ArrayList<>(queries.values()); + Collections.sort(list, queryStatsComparator); + int removeIndex = 0; + while (queries.size() > maxQueries) { + String sql = list.get(removeIndex).getQuery(); + queries.remove(sql); + if (log.isDebugEnabled()) log.debug("Removing slow query, capacity reached:"+sql); + removeIndex++; + } + } + + + @Override + public void reset(ConnectionPool parent, PooledConnection con) { + super.reset(parent, con); + if (parent!=null) + queries = SlowQueryReport.perPoolStats.get(parent.getName()); + else + queries = null; + } + + + public boolean isLogSlow() { + return logSlow; + } + + public void setLogSlow(boolean logSlow) { + this.logSlow = logSlow; + } + + public boolean isLogFailed() { + return logFailed; + } + + public void setLogFailed(boolean logFailed) { + this.logFailed = logFailed; + } + + @Override + public void setProperties(Map<String, InterceptorProperty> properties) { + super.setProperties(properties); + final String threshold = "threshold"; + final String maxqueries= "maxQueries"; + final String logslow = "logSlow"; + final String logfailed = "logFailed"; + InterceptorProperty p1 = properties.get(threshold); + InterceptorProperty p2 = properties.get(maxqueries); + InterceptorProperty p3 = properties.get(logslow); + InterceptorProperty p4 = properties.get(logfailed); + if (p1!=null) { + setThreshold(Long.parseLong(p1.getValue())); + } + if (p2!=null) { + setMaxQueries(Integer.parseInt(p2.getValue())); + } + if (p3!=null) { + setLogSlow(Boolean.parseBoolean(p3.getValue())); + } + if (p4!=null) { + setLogFailed(Boolean.parseBoolean(p4.getValue())); + } + } + + + public static class QueryStats { + static final String[] FIELD_NAMES = new String[] { + "query", + "nrOfInvocations", + "maxInvocationTime", + "maxInvocationDate", + "minInvocationTime", + "minInvocationDate", + "totalInvocationTime", + "failures", + "prepareCount", + "prepareTime", + "lastInvocation" + }; + + static final String[] FIELD_DESCRIPTIONS = new String[] { + "The SQL query", + "The number of query invocations, a call to executeXXX", + "The longest time for this query in milliseconds", + "The time and date for when the longest query took place", + "The shortest time for this query in milliseconds", + "The time and date for when the shortest query took place", + "The total amount of milliseconds spent executing this query", + "The number of failures for this query", + "The number of times this query was prepared (prepareStatement/prepareCall)", + "The total number of milliseconds spent preparing this query", + "The date and time of the last invocation" + }; + + static final OpenType<?>[] FIELD_TYPES = new OpenType[] { + SimpleType.STRING, + SimpleType.INTEGER, + SimpleType.LONG, + SimpleType.LONG, + SimpleType.LONG, + SimpleType.LONG, + SimpleType.LONG, + SimpleType.LONG, + SimpleType.INTEGER, + SimpleType.LONG, + SimpleType.LONG + }; + + private final String query; + private volatile int nrOfInvocations; + private volatile long maxInvocationTime = Long.MIN_VALUE; + private volatile long maxInvocationDate; + private volatile long minInvocationTime = Long.MAX_VALUE; + private volatile long minInvocationDate; + private volatile long totalInvocationTime; + private volatile long failures; + private volatile int prepareCount; + private volatile long prepareTime; + private volatile long lastInvocation = 0; + + public static String[] getFieldNames() { + return FIELD_NAMES; + } + + public static String[] getFieldDescriptions() { + return FIELD_DESCRIPTIONS; + } + + public static OpenType<?>[] getFieldTypes() { + return FIELD_TYPES; + } + + @Override + public String toString() { + SimpleDateFormat sdf = + new SimpleDateFormat("d MMM yyyy HH:mm:ss z", Locale.US); + sdf.setTimeZone(TimeZone.getTimeZone("GMT")); + StringBuilder buf = new StringBuilder("QueryStats[query:"); + buf.append(query); + buf.append(", nrOfInvocations:"); + buf.append(nrOfInvocations); + buf.append(", maxInvocationTime:"); + buf.append(maxInvocationTime); + buf.append(", maxInvocationDate:"); + buf.append(sdf.format(new java.util.Date(maxInvocationDate))); + buf.append(", minInvocationTime:"); + buf.append(minInvocationTime); + buf.append(", minInvocationDate:"); + buf.append(sdf.format(new java.util.Date(minInvocationDate))); + buf.append(", totalInvocationTime:"); + buf.append(totalInvocationTime); + buf.append(", averageInvocationTime:"); + buf.append((float)totalInvocationTime / (float)nrOfInvocations); + buf.append(", failures:"); + buf.append(failures); + buf.append(", prepareCount:"); + buf.append(prepareCount); + buf.append(", prepareTime:"); + buf.append(prepareTime); + buf.append("]"); + return buf.toString(); + } + + public CompositeDataSupport getCompositeData(final CompositeType type) throws OpenDataException{ + Object[] values = new Object[] { + query, + Integer.valueOf(nrOfInvocations), + Long.valueOf(maxInvocationTime), + Long.valueOf(maxInvocationDate), + Long.valueOf(minInvocationTime), + Long.valueOf(minInvocationDate), + Long.valueOf(totalInvocationTime), + Long.valueOf(failures), + Integer.valueOf(prepareCount), + Long.valueOf(prepareTime), + Long.valueOf(lastInvocation) + }; + return new CompositeDataSupport(type,FIELD_NAMES,values); + } + + public QueryStats(String query) { + this.query = query; + } + + public void prepare(long invocationTime) { + prepareCount++; + prepareTime+=invocationTime; + + } + + public void add(long invocationTime, long now) { + //not thread safe, but don't sacrifice performance for this kind of stuff + maxInvocationTime = Math.max(invocationTime, maxInvocationTime); + if (maxInvocationTime == invocationTime) { + maxInvocationDate = now; + } + minInvocationTime = Math.min(invocationTime, minInvocationTime); + if (minInvocationTime==invocationTime) { + minInvocationDate = now; + } + nrOfInvocations++; + totalInvocationTime+=invocationTime; + lastInvocation = now; + } + + public void failure(long invocationTime, long now) { + add(invocationTime,now); + failures++; + + } + + public String getQuery() { + return query; + } + + public int getNrOfInvocations() { + return nrOfInvocations; + } + + public long getMaxInvocationTime() { + return maxInvocationTime; + } + + public long getMaxInvocationDate() { + return maxInvocationDate; + } + + public long getMinInvocationTime() { + return minInvocationTime; + } + + public long getMinInvocationDate() { + return minInvocationDate; + } + + public long getTotalInvocationTime() { + return totalInvocationTime; + } + + @Override + public int hashCode() { + return query.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other instanceof QueryStats) { + QueryStats qs = (QueryStats)other; + return qs.query.equals(this.query); + } + return false; + } + + public boolean isOlderThan(QueryStats other) { + return this.lastInvocation < other.lastInvocation; + } + } + + /** Compare QueryStats by their lastInvocation value. QueryStats that + * have never been updated, have a lastInvocation value of {@code 0} + * which should be handled as the newest possible invocation. + */ + private static class QueryStatsComparator implements Comparator<QueryStats> { + + @Override + public int compare(QueryStats stats1, QueryStats stats2) { + return Long.compare(handleZero(stats1.lastInvocation), + handleZero(stats2.lastInvocation)); + } + + private static long handleZero(long value) { + return value == 0 ? Long.MAX_VALUE : value; + } + + } + +} |