/** * Copyright (C) 2015 Orange * 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. */ package com.francetelecom.clara.cloud.commons; import com.p6spy.engine.logging.appender.P6Logger; import org.junit.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; /** * Utility class to programmatically intercept JDBC calls, record some usefull stats, to make some assertions * on them during unit tests (e.g. number of joins or sql queries exec duration) * * To use it, reference it in the spy.properties file: * <pre> appender=com.francetelecom.clara.cloud.commons.P6SpyAppender * </pre> * then invoke {@link #reset()} and {@link #checkStats(boolean)} */ public class P6SpyAppender implements P6Logger { protected final static Logger LOG = LoggerFactory.getLogger(P6SpyAppender.class.getName()); /** * Minimum JOIN count to log the queries. This correspond to the orange-fr best pratices to not go * over the specified join count */ int thresholdJoin = 4; /** * Minimum duration time in ms to log the slowest queries */ int thresholdDuration = 250; // // API for unit tests // /** * Used by Unit tests to asserts on collected JDBC stats: * - number of joins should be lower than 61 (to support running on mysql engine which has this built-in limitation) * - eacj JDBC query should be faster than 10s * @param enableAssert set to true to actually fail Junit asserts when */ public synchronized void checkStats(boolean enableAssert) { if (getSlowestCount() > 0) { LOG.debug("Top slowest queries:"); for (JDBCEntry entry : slowestQueries) { LOG.debug(" " + entry.getElapsed() + "ms ("+entry.getJoinCount()+" join): " + entry.getSql()); if (enableAssert) { Assert.assertTrue("Too slow query: " + entry.getElapsed() + "ms. Consider this as a bug and correct it ASAP! SQL=" + entry.getSql(), entry.getElapsed() <= (1000 * 10)); } } if (getSlowestCount() > slowestQueries.size()) { LOG.debug((getSlowestCount() - slowestQueries.size()) + " more slow (>= "+getThresholdDuration()+"ms) queries..."); } } if (getMaxJoinCount() > 0) { LOG.debug("Top JOIN queries:"); for (JDBCEntry entry : maxJoinQueries) { LOG.debug(" " + entry.getElapsed() + "ms ("+entry.getJoinCount()+" join): " + entry.getSql()); if (enableAssert) { Assert.assertTrue("Too many JOIN: " + entry.getJoinCount() + ". MySQL do not support over 61 JOIN. SQL=" + entry.getSql(), entry.getJoinCount() <= 61); } } if (getMaxJoinCount() > maxJoinQueries.size()) { LOG.debug((getMaxJoinCount() - maxJoinQueries.size()) + " more JOIN (>= "+getThresholdJoin()+" join) queries..."); } } } /** * Clears the previously collected stats. */ public synchronized void reset() { slowestCount = 0; slowestQueries.clear(); maxJoinCount = 0; maxJoinQueries.clear(); } // // Internals // private static P6SpyAppender currentInstance = null; protected String lastEntry; /** * Utility class used to extract number of Joins in a JDBC statement, and duration ("elapsed") */ public static class JDBCEntry { int connectionId; String now; long elapsed; String category; String prepared; String sql; int joinCount; public int getJoinCount() { return joinCount; } public JDBCEntry(int connectionId, String now, long elapsed, String category, String prepared, String sql) { super(); this.connectionId = connectionId; this.now = now; this.elapsed = elapsed; this.category = category; this.prepared = prepared; this.sql = sql; int index = -1; this.joinCount = 0; do { index = sql.toLowerCase().indexOf(" join ", index + 1); if (index >= 0) { this.joinCount++; } } while (index >= 0); } public int getConnectionId() { return connectionId; } public String getNow() { return now; } public long getElapsed() { return elapsed; } public String getCategory() { return category; } public String getPrepared() { return prepared; } public String getSql() { return sql; } @Override public String toString() { return now + "|" + elapsed + "|" + (connectionId == -1 ? "" : String.valueOf(connectionId)) + "|" + category + "|" + prepared + "|" + sql; } } /** * Used to sort the JDBCEntry by nb of joins , to collect the queries with highest nb of joins */ public static final class JoinComparator implements Comparator<JDBCEntry> { @Override public int compare(JDBCEntry o1, JDBCEntry o2) { int cmp = o2.getJoinCount() - o1.getJoinCount(); if (cmp == 0) { cmp = (int) (o2.getElapsed() - o1.getElapsed()); } return cmp; } } /** * Used to sort the JDBCEntry by execution duration ("elapsed"), to collect the N slowest queries */ public static final class DurationComparator implements Comparator<JDBCEntry> { @Override public int compare(JDBCEntry o1, JDBCEntry o2) { return (int) (o2.getElapsed() - o1.getElapsed()); } } List<JDBCEntry> slowestQueries; int slowestCount = 0; Comparator<JDBCEntry> durationComparator = new DurationComparator(); List<JDBCEntry> maxJoinQueries; int maxJoinCount = 0; Comparator<JDBCEntry> joinComparator = new JoinComparator(); public P6SpyAppender() { super(); currentInstance = this; slowestQueries = new ArrayList<JDBCEntry>(); maxJoinQueries = new ArrayList<JDBCEntry>(); } public static P6SpyAppender getCurrentInstance() { return currentInstance; } public int getMaxJoinCount() { return maxJoinCount; } public int getThresholdDuration() { return thresholdDuration; } public int getThresholdJoin() { return thresholdJoin; } public int getSlowestCount() { return slowestCount; } // // P6Logger Impl // @Override public String getLastEntry() { return lastEntry; } @Override public void logException(Exception e) { LOG.debug(e.getMessage()); } @Override public void logText(String formattedLine) { LOG.debug(formattedLine); } @Override public synchronized void logSQL(int connectionId, String now, long elapsed, String category, String prepared, String sql) { JDBCEntry jdbcEntry = new JDBCEntry(connectionId, now, elapsed, category, prepared, sql); //LOG.debug(jdbcEntry.toString()); if (thresholdDuration <= elapsed) { slowestQueries.add(jdbcEntry); slowestCount++; Collections.sort(maxJoinQueries, durationComparator); if (slowestQueries.size() > 10) { slowestQueries.remove(slowestQueries.size() - 1); } } if (thresholdJoin <= jdbcEntry.getJoinCount()) { maxJoinQueries.add(jdbcEntry); maxJoinCount++; Collections.sort(maxJoinQueries, joinComparator); if (maxJoinQueries.size() > 10) { maxJoinQueries.remove(maxJoinQueries.size() - 1); } } } }