/* * (C) Copyright IBM Corp. 2009 * * LICENSE: Eclipse Public License v1.0 * http://www.eclipse.org/legal/epl-v10.html */ package com.ibm.gaiandb.apps; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Hashtable; import java.util.Map; import com.ibm.gaiandb.Logger; import com.ibm.gaiandb.Util; import com.ibm.gaiandb.diags.GDBMessages; /** * The metric monitor adds self-monitoring facilities to GaianDB. It creates a * logical table called GDB_METRICS (which should not be used by other sources). * Anyone can add a monitor by creating an object which implements * <code>MetricMonitor.Monitor<T></code> and adding it to the singleton * object retrieved through the <code>getInstance</code> method. * * @see Monitor * @see #getInstance * * @author Samir Talwar - stalwar@uk.ibm.com */ public class MetricMonitor implements Runnable { // Use PROPRIETARY notice if class contains a main() method, otherwise use COPYRIGHT notice. public static final String COPYRIGHT_NOTICE = "(c) Copyright IBM Corp. 2009"; // Keep this symbol referred to from here - to minimise updates required when eclipse gets upset about mapping this char with UTF8 public static final char TEMPERATURE_SYMBOL = '\u00b0'; /** * An interface, of which implementing objects can be added to * <code>MetricMonitor</code>. The method <code>getValue</code> must return * a value which will then be inserted into the metrics table. * * @param <T> * The type of the value to be returned. * * @author Samir Talwar - stalwar@uk.ibm.com */ public static interface Monitor<T> { /** * Returns a value which will be converted to a string using its * <code>toString</code> method and inserted into the database. * * @return A value which, when converted to a string, has a maximum * length of 255 characters, or null if there is no data to * return. */ public T getValue(); } /** The class logger. */ private static final Logger logger = new Logger("MetricMonitor", 25); /** * The interval, in milliseconds, between subsequent inserts of the return * value of each monitor's <code>getValue</code> method. */ private static final int INTERVAL = 1000; /** * The maximum age of the values in seconds. After they pass this threshold, * they are deleted from the table. */ private static final int OLD_VALUES_THRESHOLD = 60; /** The maximum length of a monitor name. */ private static final int MAX_NAME_LENGTH = 32; /** The maximum length of a value retrieved from a monitor. */ private static final int MAX_VALUE_LENGTH = 255; /** * The name of the physical table. Used primarily for inserts and deletes. */ public static final String PHYSICAL_TABLE_NAME = "GDB_LOCAL_METRICS"; /** The name of the logical table. */ public static final String LOGICAL_TABLE_NAME = "GDB_METRICS"; /** The name of the logical table when using it with provenance. */ public static final String LOGICAL_TABLE_NAME_WITH_PROVENANCE = LOGICAL_TABLE_NAME + "_P"; /** When executed, creates the physical table. */ private static final String CREATE_PHYSICAL_TABLE_SQL = "CREATE TABLE " + PHYSICAL_TABLE_NAME + "(" + " name VARCHAR(" + MAX_NAME_LENGTH + ")," + " value VARCHAR(" + MAX_VALUE_LENGTH + ")," + " received_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP" + ")"; public static String getCreateMetricsTableSQL() { return CREATE_PHYSICAL_TABLE_SQL; } /** When executed, links the logical table name to the physical table. */ private static final String CREATE_LOGICAL_TABLE_SQL = "CALL SETLTFORRDBTABLE(" + " '" + LOGICAL_TABLE_NAME + "'," + " 'LOCALDERBY'," + " '" + PHYSICAL_TABLE_NAME + "'" + ")"; /** Inserts new data into the table. */ public static final String INSERT_SQL = "INSERT INTO " + PHYSICAL_TABLE_NAME + "(name, value)" + " VALUES (?, ?)"; /** Deletes data older than the threshold from the table. */ public static final String DELETE_SQL = "DELETE FROM " + PHYSICAL_TABLE_NAME + " WHERE jSecs(CURRENT_TIMESTAMP) - jSecs(received_timestamp) > " + OLD_VALUES_THRESHOLD; /** A set of instances. One is created per database connection. */ private static final Map<Connection, MetricMonitor> INSTANCES = new Hashtable<Connection, MetricMonitor>(); /** The database connection. */ private final Connection conn; /** The map of monitor names to monitors. */ private final Map<String, Monitor<?>> monitors = new Hashtable<String, Monitor<?>>(); /** * Set to true, ending the infinite loop, when <code>stop</code> is called. * * @see #stop */ private boolean stopped = false; /** * Private constructor that initialises a metric monitor. Only one instance * should exist per connection. * * @param conn The database connection to use. */ private MetricMonitor(Connection conn) { this.conn = conn; } /** * Retrieves an instance of <code>MetricMonitor</code> corresponding to the * given connection. If one does not exist, it creates one and starts it. * * @param conn * The database connection. * * @return A new or existing <code>MetricMonitor</code>. */ public static synchronized MetricMonitor getInstance(Connection conn) { MetricMonitor instance = INSTANCES.get(conn); if (null == instance) { instance = new MetricMonitor(conn); INSTANCES.put(conn, instance); new Thread(instance,"MetricMonitor").start(); } return instance; } /** * Creates the physical and logical tables, and starts inserting data using * the list of monitors. */ public void run() { if (!createTable()) { stopped = true; return; } PreparedStatement insertStatement; PreparedStatement deleteStatement; try { insertStatement = conn.prepareStatement(INSERT_SQL); deleteStatement = conn.prepareStatement(DELETE_SQL); } catch (SQLException e) { logger.logException(GDBMessages.MMON_STATEMENT_PREPARE_ERROR_SQL, "Could not prepare the MetricMonitor insert statement.", e); stopped = true; return; } while (!stopped) { try { deleteStatement.execute(); insertStatement.clearBatch(); for (String name : monitors.keySet()) { Object value = monitors.get(name).getValue(); if (null != value) { String sValue = truncate(value.toString(), MAX_VALUE_LENGTH); insertStatement.clearParameters(); insertStatement.setString(1, name); insertStatement.setString(2, sValue); insertStatement.addBatch(); } } insertStatement.executeBatch(); } catch (SQLException e) { try { if (null == conn || conn.isClosed()) { logger.logImportant("Connection was closed."); break; } else { logger.logException(GDBMessages.MMON_METRICS_INSERT_ERROR_SQL, "Could not insert metrics into the " + LOGICAL_TABLE_NAME + " table.", e); } } catch (SQLException e1) { break; } } try { Thread.sleep(INTERVAL); } catch (InterruptedException e) { logger.logImportant("The metric monitor thread was interrupted."); break; } } if (null != conn) { INSTANCES.remove(conn); } stopped = true; } /** * Stops the monitor. */ public void stop() { stopped = true; } /** * Informs the caller whether the monitor is still running. * * @return True if the monitor is running, or false if it is not. */ public boolean isRunning() { return !stopped; } /** * Adds monitors that record JVM metrics. */ public void addJVMMonitors() { addMonitor("jvm_used_memory", new MetricMonitor.Monitor<Long>() { Runtime runtime = Runtime.getRuntime(); public Long getValue() { return runtime.totalMemory() - runtime.freeMemory(); } }); } /** * Adds a monitor to the list. The name provided will be used when inserting * data into the table. If a monitor with the same name is provided, it will * be overwritten. * * @param <T> * The monitor type. * @param name * The name of the monitor. Maximum length is 32 characters. A * longer name will be truncated. * @param monitor * The monitor itself. */ public <T> void addMonitor(String name, Monitor<T> monitor) { monitors.put(truncate(name, MAX_NAME_LENGTH), monitor); logger.logInfo("Monitor \"" + name + "\" added."); } /** * Removes a monitor with the given name from the list. * * @param name * The name of the monitor. Maximum length is 32 characters. A * longer name will be truncated. */ public void removeMonitor(String name) { monitors.remove(truncate(name, MAX_NAME_LENGTH)); logger.logInfo("Monitor \"" + name + "\" removed."); } /** * Creates the physical and logical tables. * * @return True on success; false on failure. */ private boolean createTable() { Statement stmt; try { stmt = conn.createStatement(); } catch (SQLException e) { logger.logException(GDBMessages.MMON_STATEMENT_CREATE_ERROR_SQL, "Could not create the " + PHYSICAL_TABLE_NAME + " table.", e); return false; } try { Util.executeCreateIfDerbyTableDoesNotExist( stmt, null, PHYSICAL_TABLE_NAME, CREATE_PHYSICAL_TABLE_SQL ); String sql = "CALL LISTLTS()"; ResultSet logicalTables = stmt.executeQuery(sql); boolean found = false; while (logicalTables.next()) { if (logicalTables.getString("LTNAME").equalsIgnoreCase(LOGICAL_TABLE_NAME)) { found = true; break; } } if (!found) { stmt.execute(CREATE_LOGICAL_TABLE_SQL); } } catch (SQLException e) { logger.logException(GDBMessages.MMON_STATEMENT_EXECUTE_ERROR_SQL, "Could not create the " + PHYSICAL_TABLE_NAME + " table.", e); return false; } return true; } /** * Utility function. Truncates the string to the length provided. If the * string is shorter than or of equal length to the maximum length, this * function returns it unchanged. * * @param s * The string to be truncated. * @param maxLength * Its maximum length. Must be greater than or equal to 0. * * @return A truncated string. */ private static String truncate(String s, int maxLength) { if (maxLength < 0) { return s; } if (s.length() > maxLength) { return s.substring(0, maxLength); } else { return s; } } }