/*
* Copyright 2013 Sylvain LAURENT
*
* 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 ch.sla.jdbcperflogger.console.ui;
import java.io.File;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.swing.ImageIcon;
import javax.swing.JFileChooser;
import javax.swing.SwingUtilities;
import javax.swing.filechooser.FileNameExtensionFilter;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.sla.jdbcperflogger.StatementType;
import ch.sla.jdbcperflogger.console.db.DetailedViewStatementLog;
import ch.sla.jdbcperflogger.console.db.LogRepositoryRead;
import ch.sla.jdbcperflogger.console.db.LogRepositoryUpdate;
import ch.sla.jdbcperflogger.console.db.LogSearchCriteria;
import ch.sla.jdbcperflogger.console.db.ResultSetAnalyzer;
import ch.sla.jdbcperflogger.console.net.AbstractLogReceiver;
import ch.sla.jdbcperflogger.model.ConnectionInfo;
public class PerfLoggerController {
private final static Logger LOGGER = LoggerFactory.getLogger(PerfLoggerController.class);
private final AbstractLogReceiver logReceiver;
private final LogRepositoryUpdate logRepositoryUpdate;
private final LogRepositoryRead logRepositoryRead;
private final IClientConnectionDelegate clientConnectionDelegate;
private final LogExporter logExporter;
private final PerfLoggerPanel perfLoggerPanel;
private abstract class SelectLogRunner {
abstract void doSelect(ResultSetAnalyzer resultSetAnalyzer);
}
private final SelectLogRunner selectAllLogStatements = new SelectLogRunner() {
@Override
public void doSelect(final ResultSetAnalyzer resultSetAnalyzer) {
logRepositoryRead.getStatements(createSearchCriteria(), resultSetAnalyzer, false);
}
};
private final SelectLogRunner selectAllLogStatementsWithFilledSql = new SelectLogRunner() {
@Override
public void doSelect(final ResultSetAnalyzer resultSetAnalyzer) {
logRepositoryRead.getStatements(createSearchCriteria(), resultSetAnalyzer, true);
}
};
private final SelectLogRunner selectLogStatementsGroupByRawSql = new SelectLogRunner() {
@Override
public void doSelect(final ResultSetAnalyzer resultSetAnalyzer) {
logRepositoryRead.getStatementsGroupByRawSQL(createSearchCriteria(), resultSetAnalyzer);
}
};
private final SelectLogRunner selectLogStatementsGroupByFilledSql = new SelectLogRunner() {
@Override
public void doSelect(final ResultSetAnalyzer resultSetAnalyzer) {
logRepositoryRead.getStatementsGroupByFilledSQL(createSearchCriteria(), resultSetAnalyzer);
}
};
@Nullable
private volatile String txtFilter;
@Nullable
private volatile String sqlPassthroughFilter;
@Nullable
private volatile Long minDurationNanos;
private boolean excludeCommits;
private SelectLogRunner currentSelectLogRunner = selectAllLogStatements;
private boolean tableStructureChanged = true;
private GroupBy groupBy = GroupBy.NONE;
private FilterType filterType = FilterType.HIGHLIGHT;
private final RefreshDataTask refreshDataTask;
private final ScheduledExecutorService refreshDataScheduledExecutorService;
private boolean lastSelectFromRepositoryIsInError = false;
PerfLoggerController(final IClientConnectionDelegate clientConnectionDelegate,
final AbstractLogReceiver logReceiver, final LogRepositoryUpdate logRepositoryUpdate,
final LogRepositoryRead logRepositoryRead) {
this.clientConnectionDelegate = clientConnectionDelegate;
this.logReceiver = logReceiver;
this.logRepositoryUpdate = logRepositoryUpdate;
this.logRepositoryRead = logRepositoryRead;
logExporter = new LogExporter(logRepositoryRead);
perfLoggerPanel = new PerfLoggerPanel(this);
perfLoggerPanel.setCloseEnable(!logReceiver.isServerMode());
refreshDataTask = new RefreshDataTask();
refreshDataScheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
refreshDataScheduledExecutorService.scheduleWithFixedDelay(refreshDataTask, 1, 2, TimeUnit.SECONDS);
}
PerfLoggerPanel getPanel() {
return perfLoggerPanel;
}
void setTextFilter(@Nullable final String filter) {
if (filter == null || filter.isEmpty()) {
txtFilter = null;
} else {
txtFilter = filter;
}
refresh();
}
void setSqlPassThroughFilter(@Nullable final String filter) {
if (filter == null || filter.isEmpty()) {
sqlPassthroughFilter = null;
} else {
sqlPassthroughFilter = filter;
}
refresh();
}
void appendFilter(final String columnName, final @Nullable Object value) {
String filter = sqlPassthroughFilter;
if (filter != null) {
filter += " AND ";
} else {
filter = "";
}
filter += columnName;
if (value != null) {
filter += "='" + value + "'";
} else {
filter += " is null";
}
perfLoggerPanel.setAdvancedFilter(filter);
refresh();
}
void setMinDurationFilter(@Nullable final Long durationMs) {
if (durationMs == null) {
minDurationNanos = null;
} else {
minDurationNanos = TimeUnit.MILLISECONDS.toNanos(durationMs);
}
refresh();
}
void setExcludeCommits(final boolean excludeCommits) {
this.excludeCommits = excludeCommits;
refresh();
}
void setGroupBy(final GroupBy groupBy) {
this.groupBy = groupBy;
switch (groupBy) {
case NONE:
currentSelectLogRunner = selectAllLogStatements;
break;
case RAW_SQL:
currentSelectLogRunner = selectLogStatementsGroupByRawSql;
break;
case FILLED_SQL:
currentSelectLogRunner = selectLogStatementsGroupByFilledSql;
break;
}
tableStructureChanged = true;
perfLoggerPanel.setAdvancedFilter(null);
refresh();
}
void setFilterType(final FilterType filterType) {
this.filterType = filterType;
refresh();
}
void onSelectStatement(final @Nullable Long logId) {
statementSelected(logId);
}
public void onDeleteSelectedStatements(final long... logIds) {
logRepositoryUpdate.deleteStatementLog(logIds);
refresh();
}
void onClear() {
logRepositoryUpdate.clear();
refresh();
statementSelected(null);
}
void onPause() {
if (logReceiver.isPaused()) {
logReceiver.resumeReceivingLogs();
perfLoggerPanel.setPaused(false);
} else {
logReceiver.pauseReceivingLogs();
perfLoggerPanel.setPaused(true);
}
}
void onClose() {
refreshDataScheduledExecutorService.shutdownNow();
try {
refreshDataScheduledExecutorService.awaitTermination(5, TimeUnit.SECONDS);
} catch (final InterruptedException e) {
e.printStackTrace();
}
logReceiver.dispose();
logRepositoryUpdate.dispose();
logRepositoryRead.dispose();
clientConnectionDelegate.close(this);
}
void onExportCsv() {
exportCsv();
}
void onExportSql() {
exportSql();
}
/**
* To be executed in EDT
*/
private void refresh() {
if (filterType == FilterType.FILTER) {
perfLoggerPanel.table.setTxtToHighlight(null);
perfLoggerPanel.table.setMinDurationNanoToHighlight(null);
} else {
perfLoggerPanel.table.setTxtToHighlight(txtFilter);
perfLoggerPanel.table.setMinDurationNanoToHighlight(minDurationNanos);
}
perfLoggerPanel.setTxtToHighlight(txtFilter);
refreshDataTask.forceRefresh();
refreshDataScheduledExecutorService.submit(refreshDataTask);
}
private void statementSelected(@Nullable final Long logId) {
String txt1 = "";
String txt2 = "";
String connectionUrl = null;
String connectionCreationDate = null;
Long connectionCreationDurationMillis = null;
String connectionPropertiesString = null;
DetailedViewStatementLog statementLog = null;
if (logId != null) {
statementLog = logRepositoryRead.getStatementLog(logId);
}
long deltaTimestampBaseMillis = 0;
if (statementLog != null) {
final StatementType statementType = statementLog.getStatementType();
switch (groupBy) {
case NONE:
txt1 = statementLog.getRawSql();
if (statementType != null) {
switch (statementType) {
case NON_PREPARED_BATCH_EXECUTION:
txt1 = logExporter.getBatchedExecutions(statementLog);
txt2 = txt1;
break;
case PREPARED_BATCH_EXECUTION:
txt2 = logExporter.getBatchedExecutions(statementLog);
break;
case BASE_PREPARED_STMT:
case PREPARED_QUERY_STMT:
default:
txt2 = statementLog.getFilledSql();
break;
}
}
deltaTimestampBaseMillis = statementLog.getTimestamp();
final ConnectionInfo connectionInfo = statementLog.getConnectionInfo();
connectionUrl = connectionInfo.getUrl();
connectionPropertiesString = connectionInfo.getConnectionProperties().toString();
final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
connectionCreationDate = format.format(connectionInfo.getCreationDate());
connectionCreationDurationMillis = TimeUnit.NANOSECONDS
.toMillis(connectionInfo.getConnectionCreationDuration());
break;
case RAW_SQL:
if (statementType != null) {
switch (statementType) {
case BASE_NON_PREPARED_STMT:
case BASE_PREPARED_STMT:
case PREPARED_BATCH_EXECUTION:
case PREPARED_QUERY_STMT:
case NON_PREPARED_QUERY_STMT:
case TRANSACTION:
txt1 = statementLog.getRawSql();
break;
case NON_PREPARED_BATCH_EXECUTION:
txt1 = "Cannot display details in \"Group by\" modes";
break;
}
}
break;
case FILLED_SQL:
if (statementType != null) {
switch (statementType) {
case BASE_NON_PREPARED_STMT:
case PREPARED_BATCH_EXECUTION:
case NON_PREPARED_QUERY_STMT:
txt1 = statementLog.getRawSql();
break;
case BASE_PREPARED_STMT:
case PREPARED_QUERY_STMT:
case TRANSACTION:
txt1 = statementLog.getRawSql();
txt2 = statementLog.getFilledSql();
break;
case NON_PREPARED_BATCH_EXECUTION:
txt1 = "Cannot display details in \"Group by\" modes";
break;
}
}
break;
}
final String sqlException = statementLog.getSqlException();
if (sqlException != null) {
txt1 += "\n\n" + sqlException;
txt2 += "\n\n" + sqlException;
}
}
if (!txt1.equals(perfLoggerPanel.txtFieldRawSql.getText())) {
perfLoggerPanel.txtFieldRawSql.setText(txt1);
perfLoggerPanel.txtFieldRawSql.select(0, 0);
}
if (!txt2.equals(perfLoggerPanel.txtFieldFilledSql.getText())) {
perfLoggerPanel.txtFieldFilledSql.setText(txt2);
perfLoggerPanel.txtFieldFilledSql.select(0, 0);
}
perfLoggerPanel.setTxtToHighlight(txtFilter);
perfLoggerPanel.connectionUrlField.setText(connectionUrl);
perfLoggerPanel.connectionCreationDateField.setText(connectionCreationDate);
perfLoggerPanel.connectionCreationDurationField
.setText(connectionCreationDurationMillis != null ? connectionCreationDurationMillis.toString() : "");
perfLoggerPanel.connectionPropertiesField.setText(connectionPropertiesString);
perfLoggerPanel.setDeltaTimestampBaseMillis(deltaTimestampBaseMillis);
}
private void exportSql() {
final JFileChooser fileChooser = new JFileChooser();
fileChooser.setFileFilter(new FileNameExtensionFilter("SQL file", "sql"));
if (fileChooser.showSaveDialog(perfLoggerPanel) == JFileChooser.APPROVE_OPTION) {
File targetFile = fileChooser.getSelectedFile();
if (!targetFile.getName().toLowerCase().endsWith(".sql")) {
targetFile = new File(targetFile.getAbsolutePath() + ".sql");
}
selectAllLogStatementsWithFilledSql.doSelect(logExporter.getSqlLogExporter(targetFile));
}
}
private void exportCsv() {
final JFileChooser fileChooser = new JFileChooser();
fileChooser.setFileFilter(new FileNameExtensionFilter("CSV file", "csv"));
if (fileChooser.showSaveDialog(perfLoggerPanel) == JFileChooser.APPROVE_OPTION) {
File targetFile = fileChooser.getSelectedFile();
if (!targetFile.getName().toLowerCase().endsWith(".csv")) {
targetFile = new File(targetFile.getAbsolutePath() + ".csv");
}
selectAllLogStatementsWithFilledSql.doSelect(logExporter.getCsvLogExporter(targetFile));
}
}
@Nullable
protected String getTxtFilter() {
return filterType == FilterType.FILTER ? txtFilter : null;
}
@Nullable
protected Long getMinDurationNanoFilter() {
return filterType == FilterType.FILTER ? minDurationNanos : null;
}
protected LogSearchCriteria createSearchCriteria() {
final LogSearchCriteria searchCriteria = new LogSearchCriteria();
searchCriteria.setFilter(getTxtFilter());
searchCriteria.setMinDurationNanos(getMinDurationNanoFilter());
searchCriteria.setRemoveTransactionCompletions(excludeCommits);
searchCriteria.setSqlPassThroughFilter(sqlPassthroughFilter);
return searchCriteria;
}
/**
* A task that regularly polls the associated {@link LogRepositoryUpdate} to check for new statements to display. If
* the UI must be refreshed it is later done in the EDT.
*
* @author slaurent
*/
private class RefreshDataTask implements Runnable {
private volatile long lastRefreshTime;
private int connectionsCount;
@Override
public void run() {
try {
doRun();
} catch (final Exception exc) {
// we must catch any exception otherwise the scheduling stops
LOGGER.error("Error in background refresh task", exc);
}
}
private void doRun() {
if (logRepositoryUpdate.getLastModificationTime() <= lastRefreshTime
&& connectionsCount == logReceiver.getConnectionsCount()) {
return;
}
connectionsCount = logReceiver.getConnectionsCount();
lastRefreshTime = logRepositoryUpdate.getLastModificationTime();
doRefreshData(currentSelectLogRunner);
final StringBuilder txt = new StringBuilder();
if (logReceiver.getConnectionsCount() == 0) {
perfLoggerPanel.lblConnectionStatus
.setIcon(new ImageIcon(PerfLoggerController.class.getResource("/icons/network-offline.png")));
final Throwable lastConnectionError = logReceiver.getLastConnectionError();
if (lastConnectionError != null) {
perfLoggerPanel.lblConnectionStatus.setToolTipText(lastConnectionError.toString());
} else {
perfLoggerPanel.lblConnectionStatus.setToolTipText("");
}
} else {
perfLoggerPanel.lblConnectionStatus.setIcon(
new ImageIcon(PerfLoggerController.class.getResource("/icons/network-transmit-receive.png")));
if (logReceiver.isServerMode()) {
perfLoggerPanel.lblConnectionStatus.setToolTipText(connectionsCount + " connection(s)");
} else {
perfLoggerPanel.lblConnectionStatus.setToolTipText("Connected");
}
}
txt.append(logRepositoryRead.countStatements());
txt.append(" statements logged - ");
txt.append(TimeUnit.NANOSECONDS.toMillis(logRepositoryRead.getTotalExecAndFetchTimeNanos()));
txt.append("ms total execution time (with fetch)");
final LogSearchCriteria searchCriteria = createSearchCriteria();
if (searchCriteria.atLeastOneFilterApplied()) {
txt.append(" - ");
txt.append(
TimeUnit.NANOSECONDS.toMillis(logRepositoryRead.getTotalExecAndFetchTimeNanos(searchCriteria)));
txt.append("ms total filtered");
}
final Long lastLostMessageTime = logRepositoryUpdate.getLastLostMessageTime();
if (lastLostMessageTime != null) {
txt.append(" - WARNING: missed statements on ");
txt.append(DateFormat.getTimeInstance().format(new Date(lastLostMessageTime)));
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
perfLoggerPanel.lblStatus.setText(txt.toString());
}
});
}
void forceRefresh() {
lastRefreshTime = -1L;
}
void doRefreshData(final SelectLogRunner selectLogRunner) {
try {
selectLogRunner.doSelect(new ResultSetAnalyzer() {
@Override
public void analyze(final ResultSet resultSet) throws SQLException {
final ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
final int columnCount = resultSetMetaData.getColumnCount();
final List<String> tempColumnNames = new ArrayList<>();
final List<Class<?>> tempColumnTypes = new ArrayList<>();
final List<Object[]> tempRows = new ArrayList<>();
try {
for (int i = 1; i <= columnCount; i++) {
tempColumnNames.add(resultSetMetaData.getColumnLabel(i).toUpperCase());
tempColumnTypes.add(Class.forName(resultSetMetaData.getColumnClassName(i)));
}
while (resultSet.next()) {
final Object[] row = new Object[columnCount];
for (int i = 1; i <= columnCount; i++) {
row[i - 1] = resultSet.getObject(i);
}
tempRows.add(row);
}
} catch (final ClassNotFoundException e) {
throw new RuntimeException(e);
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (lastSelectFromRepositoryIsInError) {
perfLoggerPanel.txtFieldRawSql.setText("");
}
lastSelectFromRepositoryIsInError = false;
perfLoggerPanel.setData(tempRows, tempColumnNames, tempColumnTypes,
tableStructureChanged);
tableStructureChanged = false;
}
});
}
});
} catch (final Exception ex) {
LOGGER.debug("error retrieving log statements", ex);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
lastSelectFromRepositoryIsInError = true;
perfLoggerPanel.txtFieldRawSql.setText(ex.getMessage());
perfLoggerPanel.setData(new ArrayList<Object[]>(), new ArrayList<String>(),
new ArrayList<Class<?>>(), true);
tableStructureChanged = true;
}
});
}
}
}
enum GroupBy {
NONE("-"), RAW_SQL("Raw SQL"), FILLED_SQL("Filled SQL");
final private String title;
GroupBy(final String title) {
this.title = title;
}
@Override
public String toString() {
return title;
}
}
enum FilterType {
HIGHLIGHT("Highlight"), FILTER("Filter");
final private String title;
FilterType(final String title) {
this.title = title;
}
@Override
public String toString() {
return title;
}
}
}