/* This file is part of VoltDB. * Copyright (C) 2008-2017 VoltDB Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with VoltDB. If not, see <http://www.gnu.org/licenses/>. */ package org.voltdb; import java.io.IOException; import java.net.InetSocketAddress; import java.sql.CallableStatement; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; import java.sql.Types; import java.util.ArrayList; import org.voltcore.network.ReverseDNSCache; import org.voltdb.client.Client; import org.voltdb.client.ClientConfig; import org.voltdb.client.ClientFactory; /** * Polls a Volt cluster via the statistics sysproc and Exports the results to a database via JDBC. * Can be embedded in an application or invoked from the command line * */ public class ClusterMonitor { private static final String tablePrefix = "ma_"; private static final String initiatorsTable = tablePrefix + "initiators"; private static final String instancesTable = tablePrefix + "instances"; private static final String iostatsTable = tablePrefix + "iostats"; private static final String proceduresTable = tablePrefix + "procedures"; private static final String tablestatsTable = tablePrefix + "tablestats"; private static final String retrieveInstanceId = " select instanceId from " + instancesTable + " where startTime = ? and leaderAddress = ?;"; private static final String createInstanceStatement = "insert into " + instancesTable + " ( startTime, leaderAddress, applicationName, subApplicationName, username, " + " versionString, tag, numHosts, " + "numPartitionsPerHost, numTotalPartitions, numKSafety) " + "values ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; private static final String insertInitiatorStatement = "insert into " + initiatorsTable + " ( instanceId, tsEvent, hostId, hostName, siteId, connectionId, connectionHostname, " + " procedureName, numInvocations, avgExecutionTime, minExecutionTime, maxExecutionTime, " + " numAborts, numFailures )" + " values ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; private static final String insertIOStatsStatement = "insert into " + iostatsTable + " ( instanceId, tsEvent, hostId, hostName, connectionId, connectionHostname, " + " numBytesRead, numMessagesRead, numBytesWritten, numMessagesWritten )" + " values ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; private static final String insertProceduresStatement = "insert into " + proceduresTable + " (instanceId, tsEvent, hostId, hostName, siteId, procedureName, numInvocations, " + " numTimedInvocations, avgExecutionTime, minExecutionTime, maxExecutionTime, numAborts, numFailures )" + " values ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; private static final String insertTableStatsStatement = "insert into " + tablestatsTable + " (instanceId, tsEvent, hostId, hostName, siteId, partitionId, " + " tableName, tableType, numActiveTuples, numAllocatedTuples, numDeletedTuples )" + " values ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; private final Thread m_loadThread = new Thread(new Loader(), "Client stats loader"); public static void main(String args[]) throws Exception { String application = ""; String subApplication = null; int hosts = -1; int partitionsPerHost = -1; int totalPartitions = -1; int kFactor = -1; String databaseURL = ""; String voltUsername = ""; String voltPassword = ""; long pollInterval = 10000; String tag = null; ArrayList<InetSocketAddress> voltHosts = new ArrayList<InetSocketAddress>(); for (String arg : args) { String[] parts = arg.split("=",2); if (parts.length == 1) { continue; } else if (parts[1].startsWith("${")) { continue; } else if (parts[0].equals("application")) { application = parts[1]; } else if (parts[0].equals("subApplication")) { subApplication = parts[1]; } else if (parts[0].equals("hosts")) { hosts = Integer.parseInt(parts[1]); if (hosts < 1) { System.err.println("hosts can't be less than 1"); System.exit(-1); } } else if (parts[0].equals("partitionsPerHost")) { partitionsPerHost = Integer.parseInt(parts[1]); if (partitionsPerHost < 1) { System.err.println("partitionsPerHost can't be less than 1"); System.exit(-1); } } else if (parts[0].equals("kFactor")) { kFactor = Integer.parseInt(parts[1]); if (kFactor < 0) { System.err.println("kFactor can't be less than 0"); System.exit(-1); } } else if (parts[0].startsWith("voltHost")) { String hostnport[] = parts[1].split("\\:",2); String host = hostnport[0]; int port; if (hostnport.length < 2) { port = VoltDB.DEFAULT_PORT; } else { port = Integer.valueOf(hostnport[1]); } if (host.isEmpty()) { System.err.println("voltHost can't be empty"); System.exit(-1); } voltHosts.add(new InetSocketAddress(host, port)); } else if (parts[0].equals("voltUsername")) { voltUsername = parts[1]; } else if (parts[0].equals("voltPassword")) { voltPassword = parts[1]; } else if (parts[0].equals("databaseURL")) { databaseURL = parts[1]; if (databaseURL.isEmpty()) { System.err.println("databaseURL cannot be empty"); System.exit(-1); } } else if (parts[0].equals("pollInterval")) { pollInterval = Long.parseLong(parts[1]); if (pollInterval < 200) { System.err.println("Poll interval is excessive, try 1 second"); System.exit(-1); } if (pollInterval < 1000) { System.err.println("Warning, an excessively frequent poll interval incurs overhead"); } } else if (parts[1].equals("tag")) { tag = parts[1]; } } if (partitionsPerHost != -1 && hosts != -1) { totalPartitions = partitionsPerHost * hosts; } boolean err = false; if (voltHosts.isEmpty()) { System.err.println("No volt hosts specified"); System.err.println("Usage: voltHost1=host1 voltHost2=host2 ... "); err = true; } if (application.isEmpty()) { System.err.println("No application name specified"); System.err.println("Usage: application=bingo"); err = true; } if (databaseURL.isEmpty()) { System.err.println("No database specified. Be sure to incude username and password in URL."); System.err.println("Usage: databaseURL=jdbc:mysql://[host][,failoverhost...]" + "[:port]/[database][?propertyName1][=propertyValue1][&propertyName2][=propertyValue2]..."); err = true; } if (err) { System.exit(-1); } final ClusterMonitor cm; try { cm = new ClusterMonitor( application, subApplication, tag, hosts, partitionsPerHost, totalPartitions, kFactor, voltHosts, voltUsername, voltPassword, databaseURL, pollInterval); } catch (Exception e) { System.err.printf(e.getMessage()); return; } cm.start(); cm.m_loadThread.join(); } private final Connection m_conn; private final Client m_client; private final Integer m_instanceId; private final long m_pollInterval; private final PreparedStatement insertInitiator; private final PreparedStatement insertIOStats; private final PreparedStatement insertProcedures; private final PreparedStatement insertTableStats; public ClusterMonitor( String application, String subApplication, String tag, int hosts, int partitionsPerHost, int totalPartitions, int kFactor, ArrayList<InetSocketAddress> voltHosts, String voltUsername, String voltPassword, String databaseURL, long pollInterval) throws SQLException { if ((databaseURL == null) || (databaseURL.isEmpty())) { String msg = "Not connecting to SQL reporting server as connection URL is null or missing."; throw new RuntimeException(msg); } try { m_conn = DriverManager.getConnection(databaseURL); // safest thing possible m_conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); // commit everything or nothing m_conn.setAutoCommit(false); } catch (Exception e) { String msg = "Failed to connect to SQL reporting server with message:\n "; msg += e.getMessage(); throw new RuntimeException(msg); } m_pollInterval = pollInterval; ClientConfig clientConfig = new ClientConfig(voltUsername, voltPassword); m_client = ClientFactory.createClient(clientConfig); int successfulConnections = 0; for (InetSocketAddress host : voltHosts) { try { m_client.createConnection(ReverseDNSCache.hostnameOrAddress(host.getAddress())); successfulConnections++; } catch (IOException e) { e.printStackTrace(); } } if (successfulConnections == 0) { throw new RuntimeException("Unable to open any connections to the cluster"); } String versionString = m_client.getBuildString(); if (versionString.equals("?revision=")) { versionString = null; } int instanceId = -1; CallableStatement retrieveStmt = m_conn.prepareCall(retrieveInstanceId); retrieveStmt.setLong( 1, (Long)m_client.getInstanceId()[0]); retrieveStmt.setInt( 2, (Integer)m_client.getInstanceId()[1]); retrieveStmt.execute(); ResultSet instanceIdSet = retrieveStmt.getResultSet(); while (instanceIdSet.next()) { instanceId = instanceIdSet.getInt(1); } instanceIdSet.close(); retrieveStmt.close(); insertInitiator = m_conn.prepareStatement( insertInitiatorStatement); insertIOStats = m_conn.prepareStatement( insertIOStatsStatement); insertProcedures = m_conn.prepareStatement( insertProceduresStatement); insertTableStats = m_conn.prepareStatement( insertTableStatsStatement); final String username = System.getProperty("user.name"); if (instanceId < 0) { boolean success = false; final PreparedStatement statement = m_conn.prepareStatement( createInstanceStatement, Statement.RETURN_GENERATED_KEYS); try { int index = 1; statement.setLong( index++, (Long)m_client.getInstanceId()[0]); statement.setInt( index++, (Integer)m_client.getInstanceId()[1]); statement.setString( index++, application); if (subApplication != null) { statement.setString( index++, subApplication); } else { statement.setNull( index++, Types.VARCHAR); } statement.setString( index++, username); if (versionString != null) { statement.setString( index++, versionString); } else { statement.setNull( index++, Types.VARCHAR); } if (tag != null) { statement.setString( index++, tag); } else { statement.setNull( index++, Types.VARCHAR); } if (hosts > 0) { statement.setInt( index++, hosts); } else { statement.setNull( index++, Types.INTEGER); } if (partitionsPerHost > 0) { statement.setInt( index++, partitionsPerHost); } else { statement.setNull( index++, Types.INTEGER); } if (totalPartitions > 0) { statement.setInt( index++, totalPartitions); } else { statement.setNull( index++, Types.INTEGER); } if (kFactor >= 0) { statement.setInt( index++, kFactor); } else { statement.setNull( index++, Types.INTEGER); } statement.execute(); final ResultSet generatedKeys = statement.getGeneratedKeys(); if (generatedKeys.first()) { instanceId = generatedKeys.getInt(1); success = true; } else { generatedKeys.close(); throw new SQLException("Create instance statement didn't return an instance key"); } generatedKeys.close(); } finally { statement.close(); if (success) { m_conn.commit(); } else { m_conn.close(); } } } m_instanceId = instanceId; } public void start() { m_loadThread.start(); } public synchronized void stop() throws InterruptedException { m_shouldStop = true; notifyAll(); while (!m_stopped) { wait(); } } private boolean m_shouldStop = false; private volatile boolean m_stopped = false; private class Loader implements Runnable { @Override public void run() { synchronized (ClusterMonitor.this) { try { while (true) { VoltTable stats[] = m_client.callProcedure("@Statistics", "management", (byte)1).getResults(); final VoltTable initiatorResults = stats[1]; final VoltTable procedureResults = stats[2]; final VoltTable ioResults = stats[3]; final VoltTable tableResults = stats[4]; sendInitiatorResults(initiatorResults); sendProcedureResults(procedureResults); sendIOResults(ioResults); sendTableResults(tableResults); m_conn.commit(); if (m_shouldStop) { break; } ClusterMonitor.this.wait(m_pollInterval); } } catch (Exception e) { throw new RuntimeException(e); } finally { m_stopped = true; ClusterMonitor.this.notifyAll(); } } } private void sendInitiatorResults(VoltTable initiatorResults) throws Exception { boolean first = true; try { while (initiatorResults.advanceRow()) { int index = 1; if (first) { first = false; insertInitiator.setInt( index++, m_instanceId); insertInitiator.setTimestamp( index++, new Timestamp(initiatorResults.getLong("TIMESTAMP"))); } else { index += 2; } insertInitiator.setLong( index++, initiatorResults.getLong("HOST_ID")); insertInitiator.setString( index++, initiatorResults.getString("HOSTNAME")); insertInitiator.setLong( index++, initiatorResults.getLong("SITE_ID")); insertInitiator.setLong( index++, initiatorResults.getLong("CONNECTION_ID")); insertInitiator.setString( index++, initiatorResults.getString("CONNECTION_HOSTNAME")); insertInitiator.setString( index++, initiatorResults.getString("PROCEDURE_NAME")); insertInitiator.setLong( index++, initiatorResults.getLong("INVOCATIONS")); insertInitiator.setLong( index++, initiatorResults.getLong("AVG_EXECUTION_TIME")); insertInitiator.setLong( index++, initiatorResults.getLong("MIN_EXECUTION_TIME")); insertInitiator.setLong( index++, initiatorResults.getLong("MAX_EXECUTION_TIME")); insertInitiator.setLong( index++, initiatorResults.getLong("ABORTS")); insertInitiator.setLong( index++, initiatorResults.getLong("FAILURES")); insertInitiator.addBatch(); } } finally { try { insertInitiator.executeBatch(); } catch (SQLException e) { e.printStackTrace(); } } } private void sendTableResults(VoltTable tableResults) throws SQLException { boolean first = true; try { while (tableResults.advanceRow()) { int index = 1; if (first) { first = false; insertTableStats.setInt( index++, m_instanceId); insertTableStats.setTimestamp( index++, new Timestamp(tableResults.getLong("TIMESTAMP"))); } else { index += 2; } insertTableStats.setLong( index++, tableResults.getLong("HOST_ID")); insertTableStats.setString( index++, tableResults.getString("HOSTNAME")); insertTableStats.setLong( index++, tableResults.getLong("SITE_ID")); insertTableStats.setLong( index++, tableResults.getLong("PARTITION_ID")); insertTableStats.setString( index++, tableResults.getString("TABLE_NAME")); insertTableStats.setString( index++, tableResults.getString("TABLE_TYPE")); insertTableStats.setLong( index++, tableResults.getLong("TUPLE_COUNT")); insertTableStats.setLong( index++, tableResults.getLong("TUPLE_COUNT")); insertTableStats.setLong( index++, tableResults.getLong("TUPLE_COUNT")); insertTableStats.addBatch(); } } finally { try { insertTableStats.executeBatch(); } catch (SQLException e) { e.printStackTrace(); } } } private void sendIOResults(VoltTable ioResults) throws SQLException { boolean first = true; try { while (ioResults.advanceRow()) { int index = 1; if (first) { first = false; insertIOStats.setInt( index++, m_instanceId); insertIOStats.setTimestamp( index++, new Timestamp(ioResults.getLong("TIMESTAMP"))); } else { index += 2; } insertIOStats.setLong( index++, ioResults.getLong("HOST_ID")); insertIOStats.setString( index++, ioResults.getString("HOSTNAME")); insertIOStats.setLong( index++, ioResults.getLong("CONNECTION_ID")); insertIOStats.setString( index++, ioResults.getString("CONNECTION_HOSTNAME")); insertIOStats.setLong( index++, ioResults.getLong("BYTES_READ")); insertIOStats.setLong( index++, ioResults.getLong("MESSAGES_READ")); insertIOStats.setLong( index++, ioResults.getLong("BYTES_WRITTEN")); insertIOStats.setLong( index++, ioResults.getLong("MESSAGES_WRITTEN")); insertIOStats.addBatch(); } } finally { try { insertIOStats.executeBatch(); } catch (SQLException e) { e.printStackTrace(); } } } private void sendProcedureResults(VoltTable procedureResults) throws SQLException { boolean first = true; try { while (procedureResults.advanceRow()) { int index = 1; if (first) { first = false; insertProcedures.setInt( index++, m_instanceId); insertProcedures.setTimestamp( index++, new Timestamp(procedureResults.getLong("TIMESTAMP"))); } else { index += 2; } insertProcedures.setLong( index++, procedureResults.getLong("HOST_ID")); insertProcedures.setString( index++, procedureResults.getString("HOSTNAME")); insertProcedures.setLong( index++, procedureResults.getLong("SITE_ID")); insertProcedures.setString( index++, procedureResults.getString("PROCEDURE")); insertProcedures.setLong( index++, procedureResults.getLong("INVOCATIONS")); insertProcedures.setLong( index++, procedureResults.getLong("TIMED_INVOCATIONS")); insertProcedures.setLong( index++, procedureResults.getLong("AVG_EXECUTION_TIME")); insertProcedures.setLong( index++, procedureResults.getLong("MIN_EXECUTION_TIME")); insertProcedures.setLong( index++, procedureResults.getLong("MAX_EXECUTION_TIME")); insertProcedures.setLong( index++, procedureResults.getLong("ABORTS")); insertProcedures.setLong( index++, procedureResults.getLong("FAILURES")); insertProcedures.addBatch(); } } finally { try { insertProcedures.executeBatch(); } catch (SQLException e) { e.printStackTrace(); } } } } }