From 663315b44c8a9f50e698e67322ddf429a1e61708 Mon Sep 17 00:00:00 2001 From: Ann Priestman Date: Fri, 1 Jun 2018 12:21:02 -0400 Subject: [PATCH 1/7] Added user metrics and bar graphs. --- .../EnterpriseHealthMonitor.java | 489 ++++++++++++++++-- .../healthmonitor/HealthMonitorDashboard.java | 228 ++++++-- .../autopsy/healthmonitor/Installer.java | 9 + 3 files changed, 651 insertions(+), 75 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java index d28ef1b475..3c821af75d 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java @@ -67,18 +67,18 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { private final static Logger logger = Logger.getLogger(EnterpriseHealthMonitor.class.getName()); private final static String DATABASE_NAME = "EnterpriseHealthMonitor"; - private final static long DATABASE_WRITE_INTERVAL = 60; // Minutes + private final static long DATABASE_WRITE_INTERVAL = 1; // Minutes public static final CaseDbSchemaVersionNumber CURRENT_DB_SCHEMA_VERSION - = new CaseDbSchemaVersionNumber(1, 0); + = new CaseDbSchemaVersionNumber(1, 1); private static final AtomicBoolean isEnabled = new AtomicBoolean(false); private static EnterpriseHealthMonitor instance; - private final ExecutorService healthMonitorExecutor; private static final String HEALTH_MONITOR_EVENT_THREAD_NAME = "Health-Monitor-Event-Listener-%d"; private ScheduledThreadPoolExecutor healthMonitorOutputTimer; private final Map timingInfoMap; + private final List userInfoList; private static final int CONN_POOL_SIZE = 10; private BasicDataSource connectionPool = null; private CaseDbConnectionInfo connectionSettingsInUse = null; @@ -90,8 +90,9 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { // of whether the monitor is enabled. timingInfoMap = new HashMap<>(); - // Set up the executor to handle case events - healthMonitorExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat(HEALTH_MONITOR_EVENT_THREAD_NAME).build()); + // Create the list to hold user information. The list will exist regardless + // of whether the monitor is enabled. + userInfoList = new ArrayList<>(); // Get the host name try { @@ -156,12 +157,189 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { initializeDatabaseSchema(); } + if( ! CURRENT_DB_SCHEMA_VERSION.equals(getVersion())) { + upgradeDatabaseSchema(); + } + } catch (CoordinationService.CoordinationServiceException ex) { throw new HealthMonitorException("Error releasing database lock", ex); } // Clear out any old data timingInfoMap.clear(); + userInfoList.clear(); + + // TEMP TEMP + if(false) { + // Write to the database + try (CoordinationService.Lock lock = getSharedDbLock()) { + if(lock == null) { + throw new HealthMonitorException("Error getting database lock"); + } + + Connection conn = connect(); + if(conn == null) { + throw new HealthMonitorException("Error getting database connection"); + } + + // Add metrics to the database + String addUserInfoSql = "INSERT INTO user_data (host, timestamp, event_type, is_examiner) VALUES (?, ?, ?, ?)"; + try (PreparedStatement userStatement = conn.prepareStatement(addUserInfoSql)) { + + long msPerDay = 24 * 60 * 60 * 1000; + long msPerHour = 60 * 60 * 1000; + long currentTime = System.currentTimeMillis(); + + // ain1 has had a case open for a month + userStatement.setString(1, "ain1"); + userStatement.setLong(2, currentTime - msPerDay * 30); + userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); + userStatement.setBoolean(4, false); + userStatement.execute(); + + // ain2 has had a case open for two weeks + userStatement.setString(1, "ain2"); + userStatement.setLong(2, currentTime - msPerDay * 14); + userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); + userStatement.setBoolean(4, false); + userStatement.execute(); + + // ain3 has a case open half the time over the last week + int count = 0; + for(long i = currentTime - msPerDay * 7;i < currentTime;i += msPerHour * 3) { + userStatement.setString(1, "ain3"); + userStatement.setLong(2, i); + if(count % 2 == 0){ + userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); + } else { + userStatement.setInt(3, UserEvent.CASE_CLOSE.getEventValue()); + } + userStatement.setBoolean(4, false); + userStatement.execute(); + count++; + } + + // ex1 is logged in and has a case open part of the time + for(long i = currentTime - msPerDay * 30;i < currentTime;i += msPerDay) { + userStatement.setString(1, "ex1"); + userStatement.setLong(2, i - 5000); + userStatement.setInt(3, UserEvent.LOG_ON.getEventValue()); + userStatement.setBoolean(4, true); + userStatement.execute(); + + userStatement.setString(1, "ex1"); + userStatement.setLong(2, i); + userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); + userStatement.setBoolean(4, true); + userStatement.execute(); + + userStatement.setString(1, "ex1"); + userStatement.setLong(2, i + msPerHour * 8); + userStatement.setInt(3, UserEvent.CASE_CLOSE.getEventValue()); + userStatement.setBoolean(4, true); + userStatement.execute(); + + userStatement.setString(1, "ex1"); + userStatement.setLong(2, i + msPerHour * 8 + 5000); + userStatement.setInt(3, UserEvent.LOG_OFF.getEventValue()); + userStatement.setBoolean(4, true); + userStatement.execute(); + } + + // ex2 and ex3 are always logged in and open and close cases periodically + for(long i = currentTime - msPerDay * 9;i < currentTime;i += msPerDay) { + for(long j = 0;j < 10;j+=3){ + userStatement.setString(1, "ex2"); + userStatement.setLong(2, i + j * msPerHour); + userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); + userStatement.setBoolean(4, true); + userStatement.execute(); + + userStatement.setString(1, "ex2"); + userStatement.setLong(2, i + j * msPerHour + msPerHour + 5000); + userStatement.setInt(3, UserEvent.CASE_CLOSE.getEventValue()); + userStatement.setBoolean(4, true); + userStatement.execute(); + + userStatement.setString(1, "ex3"); + userStatement.setLong(2, i + j * msPerHour - 40000); + userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); + userStatement.setBoolean(4, true); + userStatement.execute(); + + userStatement.setString(1, "ex3"); + userStatement.setLong(2, i + j * msPerHour + msPerHour + 5000 - 40000); + userStatement.setInt(3, UserEvent.CASE_CLOSE.getEventValue()); + userStatement.setBoolean(4, true); + userStatement.execute(); + } + } + + } catch (SQLException ex) { + throw new HealthMonitorException("Error saving metric data to database", ex); + } finally { + try { + conn.close(); + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Error closing Connection.", ex); + } + } + } catch (CoordinationService.CoordinationServiceException ex) { + throw new HealthMonitorException("Error releasing database lock", ex); + } + } + } + + /** + * Upgrade an older database + */ + private void upgradeDatabaseSchema() throws HealthMonitorException { + + logger.log(Level.INFO, "Upgrading Health Monitor database"); + CaseDbSchemaVersionNumber currentSchema = getVersion(); + + Connection conn = connect(); + if(conn == null) { + throw new HealthMonitorException("Error getting database connection"); + } + + try (Statement statement = conn.createStatement()) { + conn.setAutoCommit(false); + + // Upgrade from 1.0 to 1.1 + // Changes: user_data table added + if(currentSchema.compareTo(new CaseDbSchemaVersionNumber(1,1)) < 0) { + + // Add the user_data table + statement.execute("CREATE TABLE IF NOT EXISTS user_data ("+ + "id SERIAL PRIMARY KEY," + + "host text NOT NULL," + + "timestamp bigint NOT NULL," + + "event_type int NOT NULL," + + "is_examiner BOOLEAN NOT NULL" + + ")"); + } + + // Update the schema version + statement.execute("UPDATE db_info SET value='" + CURRENT_DB_SCHEMA_VERSION.getMajor() + "' WHERE name='SCHEMA_VERSION'"); + statement.execute("UPDATE db_info SET value='" + CURRENT_DB_SCHEMA_VERSION.getMinor() + "' WHERE name='SCHEMA_MINOR_VERSION'"); + + conn.commit(); + logger.log(Level.INFO, "Health Monitor database upgraded to version {0}", CURRENT_DB_SCHEMA_VERSION.toString()); + } catch (SQLException ex) { + try { + conn.rollback(); + } catch (SQLException ex2) { + logger.log(Level.SEVERE, "Rollback error"); + } + throw new HealthMonitorException("Error upgrading database", ex); + } finally { + try { + conn.close(); + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Error closing connection.", ex); + } + } } /** @@ -207,7 +385,17 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { * @throws HealthMonitorException */ static synchronized void startUpIfEnabled() throws HealthMonitorException { - getInstance(); + getInstance().addUserEvent(UserEvent.LOG_ON); + } + + /** + * Called when the application is closing. + * Create a log off event and write all existing metrics to the database + * @throws HealthMonitorException + */ + static synchronized void shutdown() throws HealthMonitorException { + getInstance().addUserEvent(UserEvent.LOG_OFF); + recordMetrics(); } /** @@ -318,6 +506,17 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { } } + /** + * Add a user event to the list. + * @param eventType + */ + private void addUserEvent(UserEvent eventType) { + UserData userInfo = new UserData(eventType); + synchronized(this) { + userInfoList.add(userInfo); + } + } + /** * Time a database query. * Database queries are hard to test in normal processing because the time @@ -370,6 +569,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { private void writeCurrentStateToDatabase() throws HealthMonitorException { Map timingMapCopy; + List userDataCopy; // Do as little as possible within the synchronized block since it will // block threads attempting to record metrics. @@ -382,10 +582,13 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { // per metric name. timingMapCopy = new HashMap<>(timingInfoMap); timingInfoMap.clear(); + + userDataCopy = new ArrayList<>(userInfoList); + userInfoList.clear(); } - // Check if there's anything to report (right now we only have the timing map) - if(timingMapCopy.keySet().isEmpty()) { + // Check if there's anything to report + if(timingMapCopy.keySet().isEmpty() && userDataCopy.isEmpty()) { return; } @@ -402,22 +605,33 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { throw new HealthMonitorException("Error getting database connection"); } - // Add timing metrics to the database + // Add metrics to the database String addTimingInfoSql = "INSERT INTO timing_data (name, host, timestamp, count, average, max, min) VALUES (?, ?, ?, ?, ?, ?, ?)"; - try (PreparedStatement statement = conn.prepareStatement(addTimingInfoSql)) { + String addUserInfoSql = "INSERT INTO user_data (host, timestamp, event_type, is_examiner) VALUES (?, ?, ?, ?)"; + try (PreparedStatement timingStatement = conn.prepareStatement(addTimingInfoSql); + PreparedStatement userStatement = conn.prepareStatement(addUserInfoSql)) { for(String name:timingMapCopy.keySet()) { TimingInfo info = timingMapCopy.get(name); - statement.setString(1, name); - statement.setString(2, hostName); - statement.setLong(3, System.currentTimeMillis()); - statement.setLong(4, info.getCount()); - statement.setDouble(5, info.getAverage()); - statement.setDouble(6, info.getMax()); - statement.setDouble(7, info.getMin()); + timingStatement.setString(1, name); + timingStatement.setString(2, hostName); + timingStatement.setLong(3, System.currentTimeMillis()); + timingStatement.setLong(4, info.getCount()); + timingStatement.setDouble(5, info.getAverage()); + timingStatement.setDouble(6, info.getMax()); + timingStatement.setDouble(7, info.getMin()); - statement.execute(); + timingStatement.execute(); + } + + for(UserData userInfo:userDataCopy) { + userStatement.setString(1, hostName); + userStatement.setLong(2, userInfo.getTimestamp()); + userStatement.setInt(3, userInfo.getEventType().getEventValue()); + userStatement.setBoolean(4, userInfo.isExaminerNode()); + + userStatement.execute(); } } catch (SQLException ex) { @@ -757,9 +971,8 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { try (Statement statement = conn.createStatement()) { conn.setAutoCommit(false); - - String createTimingTable = - "CREATE TABLE IF NOT EXISTS timing_data (" + + + statement.execute("CREATE TABLE IF NOT EXISTS timing_data (" + "id SERIAL PRIMARY KEY," + "name text NOT NULL," + "host text NOT NULL," + @@ -768,16 +981,22 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { "average double precision NOT NULL," + "max double precision NOT NULL," + "min double precision NOT NULL" + - ")"; - statement.execute(createTimingTable); + ")"); - String createDbInfoTable = - "CREATE TABLE IF NOT EXISTS db_info (" + + statement.execute("CREATE TABLE IF NOT EXISTS db_info (" + "id SERIAL PRIMARY KEY NOT NULL," + "name text NOT NULL," + "value text NOT NULL" + - ")"; - statement.execute(createDbInfoTable); + ")"); + + statement.execute("CREATE TABLE IF NOT EXISTS user_data ("+ + "id SERIAL PRIMARY KEY," + + "host text NOT NULL," + + "timestamp bigint NOT NULL," + + "event_type int NOT NULL," + + "is_examiner BOOLEAN NOT NULL" + + ")"); + statement.execute("INSERT INTO db_info (name, value) VALUES ('SCHEMA_VERSION', '" + CURRENT_DB_SCHEMA_VERSION.getMajor() + "')"); statement.execute("INSERT INTO db_info (name, value) VALUES ('SCHEMA_MINOR_VERSION', '" + CURRENT_DB_SCHEMA_VERSION.getMinor() + "')"); @@ -814,13 +1033,19 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { */ @Override public void run() { - try { - getInstance().updateFromGlobalEnabledStatus(); + recordMetrics(); + } + } + + private static void recordMetrics() { + try { + getInstance().updateFromGlobalEnabledStatus(); + if(monitorIsEnabled()) { getInstance().gatherTimerBasedMetrics(); getInstance().writeCurrentStateToDatabase(); - } catch (HealthMonitorException ex) { - logger.log(Level.SEVERE, "Error performing periodic task", ex); //NON-NLS } + } catch (HealthMonitorException ex) { + logger.log(Level.SEVERE, "Error performing periodic task", ex); //NON-NLS } } @@ -831,8 +1056,15 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { case CURRENT_CASE: if ((null == evt.getNewValue()) && (evt.getOldValue() instanceof Case)) { - // When a case is closed, write the current metrics to the database - healthMonitorExecutor.submit(new EnterpriseHealthMonitor.PeriodicHealthMonitorTask()); + // Case is closing + addUserEvent(UserEvent.CASE_CLOSE); + + // Write the current metrics to the database + // TEMP DON'T DO IT + //healthMonitorExecutor.submit(new EnterpriseHealthMonitor.PeriodicHealthMonitorTask()); + } else if((null == evt.getOldValue()) && (evt.getNewValue() instanceof Case)) { + // Case is opening + addUserEvent(UserEvent.CASE_OPEN); } break; } @@ -1046,6 +1278,57 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { } } + /** + * Get user metrics currently stored in the database. + * @param timeRange Maximum age for returned metrics (in milliseconds) + * @return A list of user metrics + * @throws HealthMonitorException + */ + List getUserMetricsFromDatabase(long timeRange) throws HealthMonitorException { + + // Make sure the monitor is enabled. It could theoretically get disabled after this + // check but it doesn't seem worth holding a lock to ensure that it doesn't since that + // may slow down ingest. + if(! isEnabled.get()) { + throw new HealthMonitorException("Health Monitor is not enabled"); + } + + // Calculate the smallest timestamp we should return + long minimumTimestamp = System.currentTimeMillis() - timeRange; + + try (CoordinationService.Lock lock = getSharedDbLock()) { + if(lock == null) { + throw new HealthMonitorException("Error getting database lock"); + } + + Connection conn = connect(); + if(conn == null) { + throw new HealthMonitorException("Error getting database connection"); + } + + List resultList = new ArrayList<>(); + + try (Statement statement = conn.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT * FROM user_data WHERE timestamp > " + minimumTimestamp)) { + + while (resultSet.next()) { + resultList.add(new UserData(resultSet)); + } + return resultList; + } catch (SQLException ex) { + throw new HealthMonitorException("Error reading user metrics from database", ex); + } finally { + try { + conn.close(); + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Error closing Connection.", ex); + } + } + } catch (CoordinationService.CoordinationServiceException ex) { + throw new HealthMonitorException("Error getting database lock", ex); + } + } + /** * Get an exclusive lock for the health monitor database. * Acquire this before creating, initializing, or updating the database schema. @@ -1085,6 +1368,146 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { } } + /** + * Types of user events being logged + */ + enum UserEvent { + LOG_ON(0), + LOG_OFF(1), + CASE_OPEN(2), + CASE_CLOSE(3); + + int value; + + UserEvent(int value) { + this.value = value; + } + + /** + * Get the integer value of the event to store in the database. + * @return value corresponding to the event + */ + int getEventValue() { + return value; + } + + /** + * Get the UserEvent from the value stored in the database + * @param value + * @return the corresponding UserEvent object + * @throws HealthMonitorException + */ + static UserEvent valueOf(int value) throws HealthMonitorException { + for (UserEvent v : UserEvent.values()) { + if (v.value == value) { + return v; + } + } + throw new HealthMonitorException("Can not create UserEvent from unknown value " + value); + } + + /** + * Return whether a case is considered to be open given this event + * as the last recorded event. + * @return true if a case is open, false otherwise + */ + boolean caseIsOpen() { + return(this.equals(CASE_OPEN)); + } + + /** + * Return whether a user is considered to be logged in given this event + * as the last recorded event. + * @return true if a the user is logged in, false otherwise + */ + boolean userIsLoggedIn() { + // LOG_ON, CASE_OPEN, and CASE_CLOSED events all imply that the user + // is logged in + return( ! this.equals(LOG_OFF)); + } + } + + /** + * Class holding user metric data. + * Can be used for storing new events or retrieving + * events out of the database. + */ + static class UserData { + private final UserEvent eventType; + private long timestamp; + private final boolean isExaminer; + private final String hostname; + + /** + * Create a new UserData object using the given event type + * and the current settings. + * @param eventType The type of event being recorded + */ + private UserData(UserEvent eventType) { + this.eventType = eventType; + this.timestamp = System.currentTimeMillis(); + this.isExaminer = (UserPreferences.SelectedMode.STANDALONE == UserPreferences.getMode()); + this.hostname = ""; + } + + /** + * Create a UserData object from a database result set. + * @param resultSet The result set containing the data + * @throws SQLException + * @throws HealthMonitorException + */ + UserData(ResultSet resultSet) throws SQLException, HealthMonitorException { + this.timestamp = resultSet.getLong("timestamp"); + this.hostname = resultSet.getString("host"); + this.eventType = UserEvent.valueOf(resultSet.getInt("event_type")); + this.isExaminer = resultSet.getBoolean("is_examiner"); + } + + /** + * This should only be used to make a dummy object to use for timestamp + * comparisons. + * @param timestamp + * @return A UserData object with the given timestamp + */ + static UserData createDummyUserData(long timestamp) { + UserData userData = new UserData(UserEvent.CASE_CLOSE); + userData.timestamp = timestamp; + return userData; + } + + /** + * Get the timestamp for the event + * @return Timestamp in milliseconds + */ + long getTimestamp() { + return timestamp; + } + + /** + * Get the host that created the metric + * @return the host name + */ + String getHostname() { + return hostname; + } + + /** + * Get the type of event + * @return the event type + */ + UserEvent getEventType() { + return eventType; + } + + /** + * Check whether this node is an examiner node or an auto ingest node + * @return true if it is an examiner node + */ + boolean isExaminerNode() { + return isExaminer; + } + } + /** * Internal class for collecting timing metrics. * Instead of storing each TimingMetric, we only store the min and max diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java index 9dbd08233e..7fef9b508e 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java @@ -25,6 +25,7 @@ import java.util.Set; import java.util.HashSet; import java.util.HashMap; import java.util.Arrays; +import java.util.ArrayList; import java.util.List; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -61,13 +62,16 @@ public class HealthMonitorDashboard { private final static String ADMIN_ACCESS_FILE_PATH = Paths.get(Places.getUserDirectory().getAbsolutePath(), ADMIN_ACCESS_FILE_NAME).toString(); Map> timingData; + List userData; - private JComboBox dateComboBox = null; - private JComboBox hostComboBox = null; - private JCheckBox hostCheckBox = null; - private JCheckBox showTrendLineCheckBox = null; - private JCheckBox skipOutliersCheckBox = null; - private JPanel graphPanel = null; + private JComboBox timingDateComboBox = null; + private JComboBox timingHostComboBox = null; + private JCheckBox timingHostCheckBox = null; + private JCheckBox timingShowTrendLineCheckBox = null; + private JCheckBox timingSkipOutliersCheckBox = null; + private JPanel timingGraphPanel = null; + private JComboBox userDateComboBox = null; + private JPanel userGraphPanel = null; private JDialog dialog = null; private final Container parentWindow; @@ -78,6 +82,7 @@ public class HealthMonitorDashboard { */ public HealthMonitorDashboard(Container parent) { timingData = new HashMap<>(); + userData = new ArrayList<>(); parentWindow = parent; } @@ -91,10 +96,12 @@ public class HealthMonitorDashboard { // Update the enabled status and get the timing data, then create all // the sub panels. JPanel timingPanel; + JPanel userPanel; JPanel adminPanel; try { updateData(); timingPanel = createTimingPanel(); + userPanel = createUserPanel(); adminPanel = createAdminPanel(); } catch (HealthMonitorException ex) { logger.log(Level.SEVERE, "Error creating panels for health monitor dashboard", ex); @@ -109,6 +116,9 @@ public class HealthMonitorDashboard { // Add the timing panel mainPanel.add(timingPanel); + // Add the user panel + mainPanel.add(userPanel); + // Add the admin panel if the admin file is present File adminFile = new File(ADMIN_ACCESS_FILE_PATH); if(adminFile.exists()) { @@ -148,6 +158,9 @@ public class HealthMonitorDashboard { if(EnterpriseHealthMonitor.monitorIsEnabled()) { // Get a copy of the timing data from the database timingData = EnterpriseHealthMonitor.getInstance().getTimingMetricsFromDatabase(DateRange.getMaximumTimestampRange()); + + // Get a copy of the user data from the database + userData = EnterpriseHealthMonitor.getInstance().getUserMetricsFromDatabase(DateRange.getMaximumTimestampRange()); } } @@ -185,12 +198,12 @@ public class HealthMonitorDashboard { timingMetricPanel.add(new JSeparator()); // Create panel to hold graphs - graphPanel = new JPanel(); - graphPanel.setLayout(new GridLayout(0,2)); + timingGraphPanel = new JPanel(); + timingGraphPanel.setLayout(new GridLayout(0,2)); // Update the graph panel, put it in a scroll pane, and add to the timing metric panel updateTimingMetricGraphs(); - JScrollPane scrollPane = new JScrollPane(graphPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + JScrollPane scrollPane = new JScrollPane(timingGraphPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); timingMetricPanel.add(scrollPane); timingMetricPanel.revalidate(); timingMetricPanel.repaint(); @@ -216,11 +229,11 @@ public class HealthMonitorDashboard { // Create the combo box for selecting how much data to display String[] dateOptionStrings = Arrays.stream(DateRange.values()).map(e -> e.getLabel()).toArray(String[]::new); - dateComboBox = new JComboBox<>(dateOptionStrings); - dateComboBox.setSelectedItem(DateRange.ONE_DAY.getLabel()); + timingDateComboBox = new JComboBox<>(dateOptionStrings); + timingDateComboBox.setSelectedItem(DateRange.ONE_DAY.getLabel()); // Set up the listener on the date combo box - dateComboBox.addActionListener(new ActionListener() { + timingDateComboBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { try { @@ -240,14 +253,14 @@ public class HealthMonitorDashboard { } // Load the host names into the combo box - hostComboBox = new JComboBox<>(hostNameSet.toArray(new String[hostNameSet.size()])); + timingHostComboBox = new JComboBox<>(hostNameSet.toArray(new String[hostNameSet.size()])); // Set up the listener on the combo box - hostComboBox.addActionListener(new ActionListener() { + timingHostComboBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { try { - if((hostCheckBox != null) && hostCheckBox.isSelected()) { + if((timingHostCheckBox != null) && timingHostCheckBox.isSelected()) { updateTimingMetricGraphs(); } } catch (HealthMonitorException ex) { @@ -257,16 +270,16 @@ public class HealthMonitorDashboard { }); // Create the host checkbox - hostCheckBox = new JCheckBox(Bundle.HealthMonitorDashboard_createTimingControlPanel_filterByHost()); - hostCheckBox.setSelected(false); - hostComboBox.setEnabled(false); + timingHostCheckBox = new JCheckBox(Bundle.HealthMonitorDashboard_createTimingControlPanel_filterByHost()); + timingHostCheckBox.setSelected(false); + timingHostComboBox.setEnabled(false); // Set up the listener on the checkbox - hostCheckBox.addActionListener(new ActionListener() { + timingHostCheckBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { try { - hostComboBox.setEnabled(hostCheckBox.isSelected()); + timingHostComboBox.setEnabled(timingHostCheckBox.isSelected()); updateTimingMetricGraphs(); } catch (HealthMonitorException ex) { logger.log(Level.SEVERE, "Error populating timing metric panel", ex); @@ -275,11 +288,11 @@ public class HealthMonitorDashboard { }); // Create the checkbox for showing the trend line - showTrendLineCheckBox = new JCheckBox(Bundle.HealthMonitorDashboard_createTimingControlPanel_showTrendLine()); - showTrendLineCheckBox.setSelected(true); + timingShowTrendLineCheckBox = new JCheckBox(Bundle.HealthMonitorDashboard_createTimingControlPanel_showTrendLine()); + timingShowTrendLineCheckBox.setSelected(true); // Set up the listener on the checkbox - showTrendLineCheckBox.addActionListener(new ActionListener() { + timingShowTrendLineCheckBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { try { @@ -291,11 +304,11 @@ public class HealthMonitorDashboard { }); // Create the checkbox for omitting outliers - skipOutliersCheckBox = new JCheckBox(Bundle.HealthMonitorDashboard_createTimingControlPanel_skipOutliers()); - skipOutliersCheckBox.setSelected(false); + timingSkipOutliersCheckBox = new JCheckBox(Bundle.HealthMonitorDashboard_createTimingControlPanel_skipOutliers()); + timingSkipOutliersCheckBox.setSelected(false); // Set up the listener on the checkbox - skipOutliersCheckBox.addActionListener(new ActionListener() { + timingSkipOutliersCheckBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { try { @@ -308,26 +321,26 @@ public class HealthMonitorDashboard { // Add the date range combo box and label to the panel timingControlPanel.add(new JLabel(Bundle.HealthMonitorDashboard_createTimingControlPanel_maxDays())); - timingControlPanel.add(dateComboBox); + timingControlPanel.add(timingDateComboBox); // Put some space between the elements timingControlPanel.add(Box.createHorizontalStrut(100)); // Add the host combo box and checkbox to the panel - timingControlPanel.add(hostCheckBox); - timingControlPanel.add(hostComboBox); + timingControlPanel.add(timingHostCheckBox); + timingControlPanel.add(timingHostComboBox); // Put some space between the elements timingControlPanel.add(Box.createHorizontalStrut(100)); // Add the skip outliers checkbox - timingControlPanel.add(this.showTrendLineCheckBox); + timingControlPanel.add(this.timingShowTrendLineCheckBox); // Put some space between the elements timingControlPanel.add(Box.createHorizontalStrut(100)); // Add the skip outliers checkbox - timingControlPanel.add(this.skipOutliersCheckBox); + timingControlPanel.add(this.timingSkipOutliersCheckBox); return timingControlPanel; } @@ -340,11 +353,11 @@ public class HealthMonitorDashboard { private void updateTimingMetricGraphs() throws HealthMonitorException { // Clear out any old graphs - graphPanel.removeAll(); + timingGraphPanel.removeAll(); if(timingData.keySet().isEmpty()) { // There are no timing metrics in the database - graphPanel.add(new JLabel(Bundle.HealthMonitorDashboard_updateTimingMetricGraphs_noData())); + timingGraphPanel.add(new JLabel(Bundle.HealthMonitorDashboard_updateTimingMetricGraphs_noData())); return; } @@ -352,8 +365,8 @@ public class HealthMonitorDashboard { // If necessary, trim down the list of results to fit the selected time range List intermediateTimingDataForDisplay; - if(dateComboBox.getSelectedItem() != null) { - DateRange selectedDateRange = DateRange.fromLabel(dateComboBox.getSelectedItem().toString()); + if(timingDateComboBox.getSelectedItem() != null) { + DateRange selectedDateRange = DateRange.fromLabel(timingDateComboBox.getSelectedItem().toString()); long threshold = System.currentTimeMillis() - selectedDateRange.getTimestampRange(); intermediateTimingDataForDisplay = timingData.get(metricName).stream() .filter(t -> t.getTimestamp() > threshold) @@ -366,25 +379,156 @@ public class HealthMonitorDashboard { // The graph always uses the data from all hosts to generate the x and y scales // so we don't filter anything out here. String hostToDisplay = null; - if(hostCheckBox.isSelected() && (hostComboBox.getSelectedItem() != null)) { - hostToDisplay = hostComboBox.getSelectedItem().toString(); + if(timingHostCheckBox.isSelected() && (timingHostComboBox.getSelectedItem() != null)) { + hostToDisplay = timingHostComboBox.getSelectedItem().toString(); } // Generate the graph TimingMetricGraphPanel singleTimingGraphPanel = new TimingMetricGraphPanel(intermediateTimingDataForDisplay, - hostToDisplay, true, metricName, skipOutliersCheckBox.isSelected(), showTrendLineCheckBox.isSelected()); + hostToDisplay, true, metricName, timingSkipOutliersCheckBox.isSelected(), timingShowTrendLineCheckBox.isSelected()); singleTimingGraphPanel.setPreferredSize(new Dimension(700,200)); - graphPanel.add(singleTimingGraphPanel); + timingGraphPanel.add(singleTimingGraphPanel); } - graphPanel.revalidate(); - graphPanel.repaint(); + timingGraphPanel.revalidate(); + timingGraphPanel.repaint(); + } + + /** + * Create the user panel. + * This displays cases open and users logged in + * @return the user panel + */ + @NbBundle.Messages({"HealthMonitorDashboard.createUserPanel.noData=No data to display - monitor is not enabled", + "HealthMonitorDashboard.createUserPanel.userMetricsTitle=User Metrics"}) + private JPanel createUserPanel() throws HealthMonitorException { + // If the monitor isn't enabled, just add a message + if(! EnterpriseHealthMonitor.monitorIsEnabled()) { + //timingMetricPanel.setPreferredSize(new Dimension(400,100)); + JPanel emptyUserMetricPanel = new JPanel(); + emptyUserMetricPanel.add(new JLabel(Bundle.HealthMonitorDashboard_createUserPanel_userMetricsTitle())); + emptyUserMetricPanel.add(new JLabel(" ")); + emptyUserMetricPanel.add(new JLabel(Bundle.HealthMonitorDashboard_createUserPanel_noData())); + + return emptyUserMetricPanel; + } + + JPanel userMetricPanel = new JPanel(); + userMetricPanel.setLayout(new BoxLayout(userMetricPanel, BoxLayout.PAGE_AXIS)); + userMetricPanel.setBorder(BorderFactory.createEtchedBorder()); + + // Add title + JLabel userMetricTitle = new JLabel(Bundle.HealthMonitorDashboard_createUserPanel_userMetricsTitle()); + userMetricPanel.add(userMetricTitle); + userMetricPanel.add(new JSeparator()); + + System.out.println("#########\nUser data"); + for(EnterpriseHealthMonitor.UserData d:this.userData) { + System.out.println(" " + d.getHostname() + " " + d.getTimestamp() + " " + d.getEventType().toString()); + } + + // Add the controls + userMetricPanel.add(createUserControlPanel()); + userMetricPanel.add(new JSeparator()); + + // Create panel to hold graphs + userGraphPanel = new JPanel(); + userGraphPanel.setLayout(new GridLayout(0,2)); + + // Update the graph panel, put it in a scroll pane, and add to the timing metric panel + updateUserMetricGraphs(); + JScrollPane scrollPane = new JScrollPane(userGraphPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + userMetricPanel.add(scrollPane); + userMetricPanel.revalidate(); + userMetricPanel.repaint(); + + return userMetricPanel; + } + + /** + * Create the panel with controls for the user panel + * @return the control panel + */ + @NbBundle.Messages({"HealthMonitorDashboard.createUserControlPanel.maxDays=Max days to display"}) + private JPanel createUserControlPanel() { + JPanel userControlPanel = new JPanel(); + + // If the monitor is not enabled, don't add any components + if(! EnterpriseHealthMonitor.monitorIsEnabled()) { + return userControlPanel; + } + + // Create the combo box for selecting how much data to display + String[] dateOptionStrings = Arrays.stream(DateRange.values()).map(e -> e.getLabel()).toArray(String[]::new); + userDateComboBox = new JComboBox<>(dateOptionStrings); + userDateComboBox.setSelectedItem(DateRange.ONE_DAY.getLabel()); + + // Set up the listener on the date combo box + userDateComboBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + try { + updateUserMetricGraphs(); + } catch (HealthMonitorException ex) { + logger.log(Level.SEVERE, "Error updating user metric panel", ex); + } + } + }); + + // Add the date range combo box and label to the panel + userControlPanel.add(new JLabel(Bundle.HealthMonitorDashboard_createUserControlPanel_maxDays())); + userControlPanel.add(userDateComboBox); + + return userControlPanel; + } + + /** + * Update the timing graphs. + * @throws HealthMonitorException + */ + @NbBundle.Messages({"HealthMonitorDashboard.updateUserMetricGraphs.noData=No data to display"}) + private void updateUserMetricGraphs() throws HealthMonitorException { + + // Clear out any old graphs + userGraphPanel.removeAll(); + + if(userData.isEmpty()) { + // There are no user metrics in the database + userGraphPanel.add(new JLabel(Bundle.HealthMonitorDashboard_updateUserMetricGraphs_noData())); + return; + } + + // Calculate the minimum timestamp for the graph. + // Unlike the timing graphs, we have to pass in the full user data list here to + // use for the graph. This is because even if we're only displaying one day, the + // last metric for a host may be that it logged on two days ago, so we would want + // to show that node as logged on. + long timestampThreshold; + if(userDateComboBox.getSelectedItem() != null) { + DateRange selectedDateRange = DateRange.fromLabel(userDateComboBox.getSelectedItem().toString()); + timestampThreshold = System.currentTimeMillis() - selectedDateRange.getTimestampRange(); + + } else { + timestampThreshold = System.currentTimeMillis() - DateRange.getMaximumTimestampRange(); + } + + // Generate the graphs + UserMetricGraphPanel caseGraphPanel = new UserMetricGraphPanel(userData, timestampThreshold, true); + caseGraphPanel.setPreferredSize(new Dimension(700,200)); + + UserMetricGraphPanel logonGraphPanel = new UserMetricGraphPanel(userData, timestampThreshold, false); + logonGraphPanel.setPreferredSize(new Dimension(700,200)); + + userGraphPanel.add(caseGraphPanel); + userGraphPanel.add(logonGraphPanel); + userGraphPanel.revalidate(); + userGraphPanel.repaint(); } /** * Create the admin panel. * This allows the health monitor to be enabled and disabled. - * @return + * @return the admin panel */ @NbBundle.Messages({"HealthMonitorDashboard.createAdminPanel.enableButton=Enable monitor", "HealthMonitorDashboard.createAdminPanel.disableButton=Disable monitor"}) diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/Installer.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/Installer.java index 61ea5a5244..187513a84a 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/Installer.java @@ -50,4 +50,13 @@ public class Installer extends ModuleInstall { logger.log(Level.SEVERE, "Error starting health services monitor", ex); } } + + @Override + public void close() { + try { + EnterpriseHealthMonitor.shutdown(); + } catch (HealthMonitorException ex) { + logger.log(Level.SEVERE, "Error stopping health services monitor", ex); + } + } } \ No newline at end of file From 519df52ff376a9012f7266626c1b286061b073b1 Mon Sep 17 00:00:00 2001 From: Ann Priestman Date: Fri, 1 Jun 2018 13:02:41 -0400 Subject: [PATCH 2/7] Added user metric graph panel --- .../healthmonitor/UserMetricGraphPanel.java | 446 ++++++++++++++++++ 1 file changed, 446 insertions(+) create mode 100644 Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java new file mode 100644 index 0000000000..dd785939cd --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java @@ -0,0 +1,446 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * 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 org.sleuthkit.autopsy.healthmonitor; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.RenderingHints; +import java.awt.Stroke; +import java.util.Collections; +import java.util.stream.Collectors; +import java.util.Comparator; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.HashMap; +import java.util.TreeSet; +import java.util.Calendar; +import java.util.GregorianCalendar; +import javax.swing.JPanel; +import org.sleuthkit.autopsy.coreutils.Logger; +import java.util.logging.Level; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.healthmonitor.EnterpriseHealthMonitor.UserData; + +/** + * Creates graphs using the given users metric data + */ +class UserMetricGraphPanel extends JPanel { + + private final static Logger logger = Logger.getLogger(TimingMetricGraphPanel.class.getName()); + + private final int padding = 25; + private final int labelPadding = 25; + private final Color examinerColor = new Color(0x12, 0x20, 0xdb, 180); + private final Color autoIngestColor = new Color(0x12, 0x80, 0x20, 180); + private final Color gridColor = new Color(200, 200, 200, 200); + private static final Stroke GRAPH_STROKE = new BasicStroke(2f); + private static final Stroke NARROW_STROKE = new BasicStroke(1f); + private final int pointWidth = 4; + private final int numberYDivisions = 10; + private final boolean doLineGraph = false; + private final boolean doBarGraph = true; + private final List dataToPlot; + private String graphLabel; + private final long dataInterval; + private String yUnitString; + private final long MILLISECONDS_PER_HOUR = 1000 * 60 * 60; + private final long MILLISECONDS_PER_DAY = MILLISECONDS_PER_HOUR * 24; + private final long NANOSECONDS_PER_MILLISECOND = 1000 * 1000; + private long maxTimestamp; + private long minTimestamp; + private long maxCount; + private final long minCount = 0; + + @NbBundle.Messages({"UserMetricGraphPanel.constructor.casesOpen=Cases open", + "UserMetricGraphPanel.constructor.loggedIn=Users logged in", + "UserMetricGraphPanel.constructor.restOfLabel= - Examiner nodes in blue, auto ingest nodes in green" + }) + UserMetricGraphPanel(List userResults, long timestampThreshold, boolean plotCases) { + + maxTimestamp = System.currentTimeMillis(); + minTimestamp = timestampThreshold; + + // Make the label + if (plotCases) { + graphLabel = Bundle.UserMetricGraphPanel_constructor_casesOpen(); + } else { + graphLabel = Bundle.UserMetricGraphPanel_constructor_loggedIn(); + } + graphLabel += Bundle.UserMetricGraphPanel_constructor_restOfLabel(); + + // Comparator for the set of UserData objects + Comparator sortOnTimestamp = new Comparator() { + @Override + public int compare(UserData o1, UserData o2) { + return Long.compare(o1.getTimestamp(), o2.getTimestamp()); + } + }; + + // Create a map from host name to data and get the timestamp bounds. + // We're using TreeSets here because they support the floor function. + Map> userDataMap = new HashMap<>(); + for(UserData result:userResults) { + if(userDataMap.containsKey(result.getHostname())) { + userDataMap.get(result.getHostname()).add(result); + } else { + TreeSet resultTreeSet = new TreeSet<>(sortOnTimestamp); + resultTreeSet.add(result); + userDataMap.put(result.getHostname(), resultTreeSet); + } + // TODO test what happens if two identical timestamps come in for the same host + } + + // Create a list of data points to plot + // The idea here is that starting at maxTimestamp, we go backwards in increments, + // see what the state of each node was at that time and make the counts of nodes + // that are logged in/ have a case open. + // A case is open if the last event was "case open"; closed otherwise + // A user is logged in if the last event was anything but "log out";logged out otherwise + dataToPlot = new ArrayList<>(); + System.out.println("Min timestamp: " + minTimestamp + ", max timestamp: " + maxTimestamp); + dataInterval = MILLISECONDS_PER_HOUR; + for (long timestamp = maxTimestamp;timestamp > minTimestamp;timestamp -= dataInterval) { + UserCount userCount = new UserCount(timestamp); + UserData timestampUserData = UserData.createDummyUserData(timestamp); + + for (String hostname:userDataMap.keySet()) { + // Get the most recent record before this timestamp + UserData lastRecord = userDataMap.get(hostname).floor(timestampUserData); + + if (lastRecord != null) { + if (plotCases) { + if (lastRecord.getEventType().caseIsOpen()) { + if(lastRecord.isExaminerNode()) { + userCount.addExaminer(); + } else { + userCount.addAutoIngestNode(); + } + } + } else { + if (lastRecord.getEventType().userIsLoggedIn()) { + if(lastRecord.isExaminerNode()) { + userCount.addExaminer(); + } else { + userCount.addAutoIngestNode(); + } + } + } + } + } + + dataToPlot.add(userCount); + } + + // Get the maximum count + maxCount = Long.MIN_VALUE; + for (UserCount userCount:dataToPlot) { + if(doBarGraph) { + maxCount = Long.max(maxCount, userCount.getExaminerNodeCount() + userCount.getAutoIngestNodeCount()); + } else { + maxCount = Long.max(maxCount, userCount.getAutoIngestNodeCount()); + maxCount = Long.max(maxCount, userCount.getExaminerNodeCount()); + } + } + } + + private class UserCount { + private final long timestamp; + private long examinerCount; + private long autoIngestCount; + + UserCount(long timestamp) { + this.timestamp = timestamp; + this.examinerCount = 0; + this.autoIngestCount = 0; + } + + void addExaminer() { + examinerCount++; + } + + void addAutoIngestNode() { + autoIngestCount++; + } + + long getExaminerNodeCount() { + return examinerCount; + } + + long getAutoIngestNodeCount() { + return autoIngestCount; + } + + long getTimestamp() { + return timestamp; + } + } + + /** + * Setup of the graphics panel: + * Origin (0,0) is at the top left corner + * + * Horizontally (from the left): (padding)(label padding)(the graph)(padding) + * For plotting data on the x-axis, we scale it to the size of the graph and then add the padding and label padding + * + * Vertically (from the top): (padding)(the graph)(label padding)(padding) + * For plotting data on the y-axis, we subtract from the max value in the graph and then scale to the size of the graph + * @param g + */ + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // Get the max and min timestamps to create the x-axis. + // We add a small buffer to each side so the data won't overwrite the axes. + double maxValueOnXAxis = maxTimestamp + TimeUnit.HOURS.toMillis(2); // Two hour buffer + double minValueOnXAxis = minTimestamp - TimeUnit.HOURS.toMillis(2); // Two hour buffer + + // Get the max and min times to create the y-axis + // To make the intervals even, make sure the maximum is a multiple of five + if((maxCount % 5) != 0) { + maxCount += (5 - (maxCount % 5)); + } + double maxValueOnYAxis = maxCount; + double minValueOnYAxis = minCount; + //minValueOnYAxis = Math.max(0, minValueOnYAxis - (maxValueOnYAxis * 0.1)); + //maxValueOnYAxis = maxValueOnYAxis * 1.1; + + // The graph itself has the following corners: + // (padding + label padding, padding + font height) -> top left + // (padding + label padding, getHeight() - label padding - padding) -> bottom left + // (getWidth() - padding, padding + font height) -> top right + // (padding + label padding, getHeight() - label padding - padding) -> bottom right + int leftGraphPadding = padding + labelPadding; + int rightGraphPadding = padding; + int topGraphPadding = padding + g2.getFontMetrics().getHeight(); + int bottomGraphPadding = labelPadding; + + // Calculate the scale for each axis. + // The size of the graph area is the width/height of the panel minus any padding. + // The scale is calculated based on this size of the graph compared to the data range. + // For example: + // getWidth() = 575 => graph width = 500 + // If our max x value to plot is 10000 and our min is 0, then the xScale would be 0.05 - i.e., + // our original x values will be multipled by 0.05 to translate them to an x-coordinate in the + // graph (plus the padding) + int graphWidth = getWidth() - leftGraphPadding - rightGraphPadding; + int graphHeight = getHeight() - topGraphPadding - bottomGraphPadding; + double xScale = ((double) graphWidth) / (maxValueOnXAxis - minValueOnXAxis); + double yScale = ((double) graphHeight) / (maxValueOnYAxis - minValueOnYAxis); + + // Draw white background + g2.setColor(Color.WHITE); + g2.fillRect(leftGraphPadding, topGraphPadding, graphWidth, graphHeight); + + // Create hatch marks and grid lines for y axis. + int labelWidth; + int positionForMetricNameLabel = 0; + for (int i = 0; i < numberYDivisions + 1; i++) { + int x0 = leftGraphPadding; + int x1 = pointWidth + leftGraphPadding; + int y0 = getHeight() - ((i * graphHeight) / numberYDivisions + bottomGraphPadding); + int y1 = y0; + + if ( ! dataToPlot.isEmpty()) { + // Draw the grid line + g2.setColor(gridColor); + g2.drawLine(leftGraphPadding + 1 + pointWidth, y0, getWidth() - rightGraphPadding, y1); + + // Create the label + g2.setColor(Color.BLACK); + double yValue = minValueOnYAxis + ((maxValueOnYAxis - minValueOnYAxis) * ((i * 1.0) / numberYDivisions)); + String yLabel = Double.toString(((int) (yValue * 100)) / 100.0); + FontMetrics fontMetrics = g2.getFontMetrics(); + labelWidth = fontMetrics.stringWidth(yLabel); + g2.drawString(yLabel, x0 - labelWidth - 5, y0 + (fontMetrics.getHeight() / 2) - 3); + + // The nicest looking alignment for this label seems to be left-aligned with the top + // y-axis label. Save this position to be used to write the label later. + if (i == numberYDivisions) { + positionForMetricNameLabel = x0 - labelWidth - 5; + } + } + + // Draw the small hatch mark + g2.setColor(Color.BLACK); + g2.drawLine(x0, y0, x1, y1); + } + + // On the x-axis, the farthest right grid line should represent midnight preceding the last recorded value + Calendar maxDate = new GregorianCalendar(); + maxDate.setTimeInMillis(maxTimestamp); + maxDate.set(Calendar.HOUR_OF_DAY, 0); + maxDate.set(Calendar.MINUTE, 0); + maxDate.set(Calendar.SECOND, 0); + maxDate.set(Calendar.MILLISECOND, 0); + long maxMidnightInMillis = maxDate.getTimeInMillis(); + + // We don't want to display more than 20 grid lines. If we have more + // data then that, put multiple days within one division + long totalDays = (maxMidnightInMillis - (long)minValueOnXAxis) / MILLISECONDS_PER_DAY; + long daysPerDivision; + if(totalDays <= 20) { + daysPerDivision = 1; + } else { + daysPerDivision = (totalDays / 20); + if((totalDays % 20) != 0) { + daysPerDivision++; + } + } + + // Draw the vertical grid lines and labels + // The vertical grid lines will be at midnight, and display the date underneath them + // At present we use GMT because of some complications with daylight savings time. + for (long currentDivision = maxMidnightInMillis; currentDivision >= minValueOnXAxis; currentDivision -= MILLISECONDS_PER_DAY * daysPerDivision) { + + int x0 = (int) ((currentDivision - minValueOnXAxis) * xScale + leftGraphPadding); + int x1 = x0; + int y0 = getHeight() - bottomGraphPadding; + int y1 = y0 - pointWidth; + + // Draw the light grey grid line + g2.setColor(gridColor); + g2.drawLine(x0, getHeight() - bottomGraphPadding - 1 - pointWidth, x1, topGraphPadding); + + // Draw the hatch mark + g2.setColor(Color.BLACK); + g2.drawLine(x0, y0, x1, y1); + + // Draw the label + Calendar thisDate = new GregorianCalendar(); + thisDate.setTimeZone(TimeZone.getTimeZone("GMT")); // Stick with GMT to avoid daylight savings issues + thisDate.setTimeInMillis(currentDivision); + int month = thisDate.get(Calendar.MONTH) + 1; + int day = thisDate.get(Calendar.DAY_OF_MONTH); + + String xLabel = month + "/" + day; + FontMetrics metrics = g2.getFontMetrics(); + labelWidth = metrics.stringWidth(xLabel); + g2.drawString(xLabel, x0 - labelWidth / 2, y0 + metrics.getHeight() + 3); + } + + // Create x and y axes + g2.setColor(Color.BLACK); + g2.drawLine(leftGraphPadding, getHeight() - bottomGraphPadding, leftGraphPadding, topGraphPadding); + g2.drawLine(leftGraphPadding, getHeight() - bottomGraphPadding, getWidth() - rightGraphPadding, getHeight() - bottomGraphPadding); + + if(! doBarGraph) { + // Create the points to plot + List graphPoints = new ArrayList<>(); + for(UserCount userCount:dataToPlot) { + int x1 = (int) ((userCount.getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding); + int y1 = (int) ((maxValueOnYAxis - userCount.getExaminerNodeCount()) * yScale + topGraphPadding); + //int x1 = (int) ((timingResults.get(i).getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding); + //int y1 = (int) ((maxValueOnYAxis - metricTime) * yScale + topGraphPadding); + graphPoints.add(new Point(x1, y1)); + } + + // Sort the points + Collections.sort(graphPoints, new Comparator() { + @Override + public int compare(Point o1, Point o2) { + if(o1.getX() > o2.getX()) { + return 1; + } else if (o1.getX() < o2.getX()) { + return -1; + } + return 0; + } + }); + + // Draw the selected type of graph. If there's only one data point, + // draw that single point. + g2.setStroke(NARROW_STROKE); + g2.setColor(examinerColor); + if(doLineGraph && graphPoints.size() > 1) { + for (int i = 0; i < graphPoints.size() - 1; i++) { + int x1 = graphPoints.get(i).x; + int y1 = graphPoints.get(i).y; + int x2 = graphPoints.get(i + 1).x; + int y2 = graphPoints.get(i + 1).y; + g2.drawLine(x1, y1, x2, y2); + } + } else { + for (int i = 0; i < graphPoints.size(); i++) { + int x = graphPoints.get(i).x - pointWidth / 2; + int y = graphPoints.get(i).y - pointWidth / 2; + int ovalW = pointWidth; + int ovalH = pointWidth; + g2.fillOval(x, y, ovalW, ovalH); + } + } + } else { + + // Sort dataToPlot on timestamp + Collections.sort(dataToPlot, new Comparator(){ + @Override + public int compare(UserCount o1, UserCount o2){ + return Long.compare(o1.getTimestamp(), o2.getTimestamp()); + } + }); + + for(int i = 0;i < dataToPlot.size();i++) { + UserCount userCount = dataToPlot.get(i); + int x = (int) ((userCount.getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding); + int yTopOfExaminerBox = (int) ((maxValueOnYAxis - userCount.getExaminerNodeCount() + - userCount.getAutoIngestNodeCount()) * yScale + topGraphPadding); + int width; + if(i < dataToPlot.size() - 1) { + width = Integer.max((int)((dataToPlot.get(i + 1).getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding) - x - 1, + 1); + } else { + width = Integer.max((int)(dataInterval * xScale), 1); + } + int heightExaminerBox = (int)(userCount.getExaminerNodeCount() * yScale); + + g2.setColor(examinerColor); + g2.fillRect(x, yTopOfExaminerBox, width, heightExaminerBox); + + int yTopOfAutoIngestBox = yTopOfExaminerBox + heightExaminerBox; + int heightAutoIngestBox = (int)(userCount.getAutoIngestNodeCount() * yScale); + + g2.setColor(autoIngestColor); + g2.fillRect(x, yTopOfAutoIngestBox, width, heightAutoIngestBox); + + } + } + + // The graph lines may have extended up past the bounds of the graph. Overwrite that + // area with the original background color. + g2.setColor(this.getBackground()); + g2.fillRect(leftGraphPadding, 0, graphWidth, topGraphPadding); + + // Write the scale. Do this after we erase the top block of the graph. + g2.setColor(Color.BLACK); + String titleStr = graphLabel; + g2.drawString(titleStr, positionForMetricNameLabel, padding); + } + + +} From 6bd41fbb12cc6e67995a73d102e6f3ae85026d58 Mon Sep 17 00:00:00 2001 From: Ann Priestman Date: Mon, 4 Jun 2018 13:29:47 -0400 Subject: [PATCH 3/7] Improving bar graph --- .../EnterpriseHealthMonitor.java | 41 ++++- .../healthmonitor/HealthMonitorDashboard.java | 5 - .../healthmonitor/UserMetricGraphPanel.java | 155 +++++++++++------- 3 files changed, 132 insertions(+), 69 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java index 3c821af75d..e1b27c3426 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java @@ -183,7 +183,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { } // Add metrics to the database - String addUserInfoSql = "INSERT INTO user_data (host, timestamp, event_type, is_examiner) VALUES (?, ?, ?, ?)"; + String addUserInfoSql = "INSERT INTO user_data (host, timestamp, event_type, is_examiner, case_name) VALUES (?, ?, ?, ?, ?)"; try (PreparedStatement userStatement = conn.prepareStatement(addUserInfoSql)) { long msPerDay = 24 * 60 * 60 * 1000; @@ -195,6 +195,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { userStatement.setLong(2, currentTime - msPerDay * 30); userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); userStatement.setBoolean(4, false); + userStatement.setString(5, "case1"); userStatement.execute(); // ain2 has had a case open for two weeks @@ -202,6 +203,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { userStatement.setLong(2, currentTime - msPerDay * 14); userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); userStatement.setBoolean(4, false); + userStatement.setString(5, "case1"); userStatement.execute(); // ain3 has a case open half the time over the last week @@ -215,6 +217,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { userStatement.setInt(3, UserEvent.CASE_CLOSE.getEventValue()); } userStatement.setBoolean(4, false); + userStatement.setString(5, "case2"); userStatement.execute(); count++; } @@ -225,24 +228,28 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { userStatement.setLong(2, i - 5000); userStatement.setInt(3, UserEvent.LOG_ON.getEventValue()); userStatement.setBoolean(4, true); + userStatement.setString(5, ""); userStatement.execute(); userStatement.setString(1, "ex1"); userStatement.setLong(2, i); userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); userStatement.setBoolean(4, true); + userStatement.setString(5, "case3"); userStatement.execute(); userStatement.setString(1, "ex1"); userStatement.setLong(2, i + msPerHour * 8); userStatement.setInt(3, UserEvent.CASE_CLOSE.getEventValue()); userStatement.setBoolean(4, true); + userStatement.setString(5, ""); userStatement.execute(); userStatement.setString(1, "ex1"); userStatement.setLong(2, i + msPerHour * 8 + 5000); userStatement.setInt(3, UserEvent.LOG_OFF.getEventValue()); userStatement.setBoolean(4, true); + userStatement.setString(5, ""); userStatement.execute(); } @@ -253,24 +260,28 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { userStatement.setLong(2, i + j * msPerHour); userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); userStatement.setBoolean(4, true); + userStatement.setString(5, "case3"); userStatement.execute(); userStatement.setString(1, "ex2"); userStatement.setLong(2, i + j * msPerHour + msPerHour + 5000); userStatement.setInt(3, UserEvent.CASE_CLOSE.getEventValue()); userStatement.setBoolean(4, true); + userStatement.setString(5, ""); userStatement.execute(); userStatement.setString(1, "ex3"); userStatement.setLong(2, i + j * msPerHour - 40000); userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); userStatement.setBoolean(4, true); + userStatement.setString(5, "case4"); userStatement.execute(); userStatement.setString(1, "ex3"); userStatement.setLong(2, i + j * msPerHour + msPerHour + 5000 - 40000); userStatement.setInt(3, UserEvent.CASE_CLOSE.getEventValue()); userStatement.setBoolean(4, true); + userStatement.setString(5, ""); userStatement.execute(); } } @@ -316,7 +327,8 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { "host text NOT NULL," + "timestamp bigint NOT NULL," + "event_type int NOT NULL," + - "is_examiner BOOLEAN NOT NULL" + + "is_examiner boolean NOT NULL," + + "case_name text NOT NULL" + ")"); } @@ -607,7 +619,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { // Add metrics to the database String addTimingInfoSql = "INSERT INTO timing_data (name, host, timestamp, count, average, max, min) VALUES (?, ?, ?, ?, ?, ?, ?)"; - String addUserInfoSql = "INSERT INTO user_data (host, timestamp, event_type, is_examiner) VALUES (?, ?, ?, ?)"; + String addUserInfoSql = "INSERT INTO user_data (host, timestamp, event_type, is_examiner, case_name) VALUES (?, ?, ?, ?, ?)"; try (PreparedStatement timingStatement = conn.prepareStatement(addTimingInfoSql); PreparedStatement userStatement = conn.prepareStatement(addUserInfoSql)) { @@ -630,7 +642,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { userStatement.setLong(2, userInfo.getTimestamp()); userStatement.setInt(3, userInfo.getEventType().getEventValue()); userStatement.setBoolean(4, userInfo.isExaminerNode()); - + userStatement.setString(5, userInfo.getCaseName()); userStatement.execute(); } @@ -994,7 +1006,8 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { "host text NOT NULL," + "timestamp bigint NOT NULL," + "event_type int NOT NULL," + - "is_examiner BOOLEAN NOT NULL" + + "is_examiner BOOLEAN NOT NULL," + + "case_name text NOT NULL" + ")"); @@ -1437,6 +1450,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { private long timestamp; private final boolean isExaminer; private final String hostname; + private String caseName; /** * Create a new UserData object using the given event type @@ -1448,6 +1462,14 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { this.timestamp = System.currentTimeMillis(); this.isExaminer = (UserPreferences.SelectedMode.STANDALONE == UserPreferences.getMode()); this.hostname = ""; + + // If there's a case open, record the name + try { + this.caseName = Case.getCurrentCaseThrows().getDisplayName(); + } catch (NoCurrentCaseException ex) { + // It's not an error if there's no case open + this.caseName = ""; + } } /** @@ -1461,6 +1483,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { this.hostname = resultSet.getString("host"); this.eventType = UserEvent.valueOf(resultSet.getInt("event_type")); this.isExaminer = resultSet.getBoolean("is_examiner"); + this.caseName = resultSet.getString("case_name"); } /** @@ -1506,6 +1529,14 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { boolean isExaminerNode() { return isExaminer; } + + /** + * Get the name of the case for this metric + * @return the case name. Will be the empty string if no case was open. + */ + String getCaseName() { + return caseName; + } } /** diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java index 7fef9b508e..4bbb14558c 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java @@ -422,11 +422,6 @@ public class HealthMonitorDashboard { userMetricPanel.add(userMetricTitle); userMetricPanel.add(new JSeparator()); - System.out.println("#########\nUser data"); - for(EnterpriseHealthMonitor.UserData d:this.userData) { - System.out.println(" " + d.getHostname() + " " + d.getTimestamp() + " " + d.getEventType().toString()); - } - // Add the controls userMetricPanel.add(createUserControlPanel()); userMetricPanel.add(new JSeparator()); diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java index dd785939cd..cb87f34446 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.HashMap; +import java.util.HashSet; import java.util.TreeSet; import java.util.Calendar; import java.util.GregorianCalendar; @@ -54,8 +55,8 @@ class UserMetricGraphPanel extends JPanel { private final int padding = 25; private final int labelPadding = 25; - private final Color examinerColor = new Color(0x12, 0x20, 0xdb, 180); - private final Color autoIngestColor = new Color(0x12, 0x80, 0x20, 180); + private final Color examinerColor = new Color(0x12, 0x20, 0xdb, 255); + private final Color autoIngestColor = new Color(0x12, 0x80, 0x20, 255); private final Color gridColor = new Color(200, 200, 200, 200); private static final Stroke GRAPH_STROKE = new BasicStroke(2f); private static final Stroke NARROW_STROKE = new BasicStroke(1f); @@ -64,20 +65,18 @@ class UserMetricGraphPanel extends JPanel { private final boolean doLineGraph = false; private final boolean doBarGraph = true; private final List dataToPlot; - private String graphLabel; + private final String graphLabel; private final long dataInterval; - private String yUnitString; private final long MILLISECONDS_PER_HOUR = 1000 * 60 * 60; private final long MILLISECONDS_PER_DAY = MILLISECONDS_PER_HOUR * 24; private final long NANOSECONDS_PER_MILLISECOND = 1000 * 1000; private long maxTimestamp; private long minTimestamp; - private long maxCount; - private final long minCount = 0; + private int maxCount; + private final int minCount = 0; @NbBundle.Messages({"UserMetricGraphPanel.constructor.casesOpen=Cases open", - "UserMetricGraphPanel.constructor.loggedIn=Users logged in", - "UserMetricGraphPanel.constructor.restOfLabel= - Examiner nodes in blue, auto ingest nodes in green" + "UserMetricGraphPanel.constructor.loggedIn=Users logged in - examiner nodes in blue, auto ingest nodes in green" }) UserMetricGraphPanel(List userResults, long timestampThreshold, boolean plotCases) { @@ -90,7 +89,6 @@ class UserMetricGraphPanel extends JPanel { } else { graphLabel = Bundle.UserMetricGraphPanel_constructor_loggedIn(); } - graphLabel += Bundle.UserMetricGraphPanel_constructor_restOfLabel(); // Comparator for the set of UserData objects Comparator sortOnTimestamp = new Comparator() { @@ -121,10 +119,16 @@ class UserMetricGraphPanel extends JPanel { // A case is open if the last event was "case open"; closed otherwise // A user is logged in if the last event was anything but "log out";logged out otherwise dataToPlot = new ArrayList<>(); - System.out.println("Min timestamp: " + minTimestamp + ", max timestamp: " + maxTimestamp); dataInterval = MILLISECONDS_PER_HOUR; + maxCount = Integer.MIN_VALUE; for (long timestamp = maxTimestamp;timestamp > minTimestamp;timestamp -= dataInterval) { - UserCount userCount = new UserCount(timestamp); + + // Collect both counts so that we can use the same scale in the open case graph and + // the logged in users graph + UserCount openCaseCount = new UserCount(timestamp); + UserCount loggedInUserCount = new UserCount(timestamp); + + Set openCaseNames = new HashSet<>(); UserData timestampUserData = UserData.createDummyUserData(timestamp); for (String hostname:userDataMap.keySet()) { @@ -132,45 +136,55 @@ class UserMetricGraphPanel extends JPanel { UserData lastRecord = userDataMap.get(hostname).floor(timestampUserData); if (lastRecord != null) { - if (plotCases) { - if (lastRecord.getEventType().caseIsOpen()) { - if(lastRecord.isExaminerNode()) { - userCount.addExaminer(); - } else { - userCount.addAutoIngestNode(); - } + + // Update the case count. + if (lastRecord.getEventType().caseIsOpen()) { + + // Only add each case once regardless of how many users have it open + if ( ! openCaseNames.contains(lastRecord.getCaseName())) { + + // Store everything as examiner nodes. The graph will represent + // the number of distinct cases open, not anything about the + // nodes that have them open. + openCaseCount.addExaminer(); + openCaseNames.add(lastRecord.getCaseName()); } - } else { - if (lastRecord.getEventType().userIsLoggedIn()) { - if(lastRecord.isExaminerNode()) { - userCount.addExaminer(); - } else { - userCount.addAutoIngestNode(); - } + } + + // Update the logged in user count + if (lastRecord.getEventType().userIsLoggedIn()) { + if(lastRecord.isExaminerNode()) { + loggedInUserCount.addExaminer(); + } else { + loggedInUserCount.addAutoIngestNode(); } } } } - dataToPlot.add(userCount); - } - - // Get the maximum count - maxCount = Long.MIN_VALUE; - for (UserCount userCount:dataToPlot) { + // Check if this is a new maximum if(doBarGraph) { - maxCount = Long.max(maxCount, userCount.getExaminerNodeCount() + userCount.getAutoIngestNodeCount()); + maxCount = Integer.max(maxCount, openCaseCount.getTotalNodeCount()); + maxCount = Integer.max(maxCount, loggedInUserCount.getTotalNodeCount()); } else { - maxCount = Long.max(maxCount, userCount.getAutoIngestNodeCount()); - maxCount = Long.max(maxCount, userCount.getExaminerNodeCount()); + maxCount = Integer.max(maxCount, openCaseCount.getAutoIngestNodeCount()); + maxCount = Integer.max(maxCount, openCaseCount.getExaminerNodeCount()); + maxCount = Integer.max(maxCount, loggedInUserCount.getAutoIngestNodeCount()); + maxCount = Integer.max(maxCount, loggedInUserCount.getExaminerNodeCount()); + } + + if(plotCases) { + dataToPlot.add(openCaseCount); + } else { + dataToPlot.add(loggedInUserCount); } } } private class UserCount { private final long timestamp; - private long examinerCount; - private long autoIngestCount; + private int examinerCount; + private int autoIngestCount; UserCount(long timestamp) { this.timestamp = timestamp; @@ -186,14 +200,18 @@ class UserMetricGraphPanel extends JPanel { autoIngestCount++; } - long getExaminerNodeCount() { + int getExaminerNodeCount() { return examinerCount; } - long getAutoIngestNodeCount() { + int getAutoIngestNodeCount() { return autoIngestCount; } + int getTotalNodeCount() { + return examinerCount + autoIngestCount; + } + long getTimestamp() { return timestamp; } @@ -218,18 +236,16 @@ class UserMetricGraphPanel extends JPanel { // Get the max and min timestamps to create the x-axis. // We add a small buffer to each side so the data won't overwrite the axes. - double maxValueOnXAxis = maxTimestamp + TimeUnit.HOURS.toMillis(2); // Two hour buffer - double minValueOnXAxis = minTimestamp - TimeUnit.HOURS.toMillis(2); // Two hour buffer + double maxValueOnXAxis = maxTimestamp + TimeUnit.HOURS.toMillis(2); // Two hour buffer (the last bar graph will take up one of the hours) + double minValueOnXAxis = minTimestamp - TimeUnit.HOURS.toMillis(1); // One hour buffer // Get the max and min times to create the y-axis // To make the intervals even, make sure the maximum is a multiple of five if((maxCount % 5) != 0) { maxCount += (5 - (maxCount % 5)); } - double maxValueOnYAxis = maxCount; - double minValueOnYAxis = minCount; - //minValueOnYAxis = Math.max(0, minValueOnYAxis - (maxValueOnYAxis * 0.1)); - //maxValueOnYAxis = maxValueOnYAxis * 1.1; + int maxValueOnYAxis = maxCount; + int minValueOnYAxis = minCount; // The graph itself has the following corners: // (padding + label padding, padding + font height) -> top left @@ -261,6 +277,7 @@ class UserMetricGraphPanel extends JPanel { // Create hatch marks and grid lines for y axis. int labelWidth; int positionForMetricNameLabel = 0; + Map countToGraphPosition = new HashMap<>(); for (int i = 0; i < numberYDivisions + 1; i++) { int x0 = leftGraphPadding; int x1 = pointWidth + leftGraphPadding; @@ -275,15 +292,19 @@ class UserMetricGraphPanel extends JPanel { // Create the label g2.setColor(Color.BLACK); double yValue = minValueOnYAxis + ((maxValueOnYAxis - minValueOnYAxis) * ((i * 1.0) / numberYDivisions)); - String yLabel = Double.toString(((int) (yValue * 100)) / 100.0); - FontMetrics fontMetrics = g2.getFontMetrics(); - labelWidth = fontMetrics.stringWidth(yLabel); - g2.drawString(yLabel, x0 - labelWidth - 5, y0 + (fontMetrics.getHeight() / 2) - 3); - - // The nicest looking alignment for this label seems to be left-aligned with the top - // y-axis label. Save this position to be used to write the label later. - if (i == numberYDivisions) { - positionForMetricNameLabel = x0 - labelWidth - 5; + int intermediateLabelVal = (int) (yValue * 100); + if ((i == numberYDivisions) || ((intermediateLabelVal % 100) == 0)) { + countToGraphPosition.put(intermediateLabelVal / 100, y0); + String yLabel = Integer.toString(intermediateLabelVal / 100); + FontMetrics fontMetrics = g2.getFontMetrics(); + labelWidth = fontMetrics.stringWidth(yLabel); + g2.drawString(yLabel, x0 - labelWidth - 5, y0 + (fontMetrics.getHeight() / 2) - 3); + + // The nicest looking alignment for this label seems to be left-aligned with the top + // y-axis label. Save this position to be used to write the label later. + if (i == numberYDivisions) { + positionForMetricNameLabel = x0 - labelWidth - 5; + } } } @@ -356,8 +377,6 @@ class UserMetricGraphPanel extends JPanel { for(UserCount userCount:dataToPlot) { int x1 = (int) ((userCount.getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding); int y1 = (int) ((maxValueOnYAxis - userCount.getExaminerNodeCount()) * yScale + topGraphPadding); - //int x1 = (int) ((timingResults.get(i).getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding); - //int y1 = (int) ((maxValueOnYAxis - metricTime) * yScale + topGraphPadding); graphPoints.add(new Point(x1, y1)); } @@ -408,8 +427,17 @@ class UserMetricGraphPanel extends JPanel { for(int i = 0;i < dataToPlot.size();i++) { UserCount userCount = dataToPlot.get(i); int x = (int) ((userCount.getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding); - int yTopOfExaminerBox = (int) ((maxValueOnYAxis - userCount.getExaminerNodeCount() + int yTopOfExaminerBox; + int totalCount = userCount.getExaminerNodeCount() + userCount.getAutoIngestNodeCount(); + if(countToGraphPosition.containsKey(totalCount)) { + // If we've drawn a grid line for this count, use the recorded value. Otherwise rounding differences + // lead to the bar graph not quite lining up with the existing grid. + yTopOfExaminerBox = countToGraphPosition.get(totalCount); + } else { + yTopOfExaminerBox = (int) ((maxValueOnYAxis - userCount.getExaminerNodeCount() - userCount.getAutoIngestNodeCount()) * yScale + topGraphPadding); + } + int width; if(i < dataToPlot.size() - 1) { width = Integer.max((int)((dataToPlot.get(i + 1).getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding) - x - 1, @@ -417,13 +445,22 @@ class UserMetricGraphPanel extends JPanel { } else { width = Integer.max((int)(dataInterval * xScale), 1); } - int heightExaminerBox = (int)(userCount.getExaminerNodeCount() * yScale); + + // It's easiest here to draw the rectangle going all the way to the bottom of the graph. + // The bottom will be overwritten by the auto ingest box. + int heightExaminerBox = (getHeight() - bottomGraphPadding) - yTopOfExaminerBox; g2.setColor(examinerColor); g2.fillRect(x, yTopOfExaminerBox, width, heightExaminerBox); - int yTopOfAutoIngestBox = yTopOfExaminerBox + heightExaminerBox; - int heightAutoIngestBox = (int)(userCount.getAutoIngestNodeCount() * yScale); + int yTopOfAutoIngestBox; + if(countToGraphPosition.containsKey(userCount.getAutoIngestNodeCount())) { + yTopOfAutoIngestBox =countToGraphPosition.get(userCount.getAutoIngestNodeCount()); + } else { + yTopOfAutoIngestBox = yTopOfExaminerBox + heightExaminerBox; + } + int heightAutoIngestBox = (getHeight() - bottomGraphPadding) - yTopOfAutoIngestBox; + //int heightAutoIngestBox = (int)(userCount.getAutoIngestNodeCount() * yScale); g2.setColor(autoIngestColor); g2.fillRect(x, yTopOfAutoIngestBox, width, heightAutoIngestBox); From e8dc844166289ea3b3e2fc92a70b65b60ae8a0e3 Mon Sep 17 00:00:00 2001 From: Ann Priestman Date: Mon, 4 Jun 2018 15:45:03 -0400 Subject: [PATCH 4/7] Removed unnecessary graphing code --- .../healthmonitor/UserMetricGraphPanel.java | 260 ++++++++---------- 1 file changed, 118 insertions(+), 142 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java index cb87f34446..dd945192ff 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java @@ -18,16 +18,12 @@ */ package org.sleuthkit.autopsy.healthmonitor; -import java.awt.BasicStroke; import java.awt.Color; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; -import java.awt.Point; import java.awt.RenderingHints; -import java.awt.Stroke; import java.util.Collections; -import java.util.stream.Collectors; import java.util.Comparator; import java.util.ArrayList; import java.util.List; @@ -40,14 +36,13 @@ import java.util.Calendar; import java.util.GregorianCalendar; import javax.swing.JPanel; import org.sleuthkit.autopsy.coreutils.Logger; -import java.util.logging.Level; import java.util.TimeZone; import java.util.concurrent.TimeUnit; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.healthmonitor.EnterpriseHealthMonitor.UserData; /** - * Creates graphs using the given users metric data + * Creates graphs using the given user metric data */ class UserMetricGraphPanel extends JPanel { @@ -58,22 +53,17 @@ class UserMetricGraphPanel extends JPanel { private final Color examinerColor = new Color(0x12, 0x20, 0xdb, 255); private final Color autoIngestColor = new Color(0x12, 0x80, 0x20, 255); private final Color gridColor = new Color(200, 200, 200, 200); - private static final Stroke GRAPH_STROKE = new BasicStroke(2f); - private static final Stroke NARROW_STROKE = new BasicStroke(1f); private final int pointWidth = 4; private final int numberYDivisions = 10; - private final boolean doLineGraph = false; - private final boolean doBarGraph = true; private final List dataToPlot; private final String graphLabel; private final long dataInterval; private final long MILLISECONDS_PER_HOUR = 1000 * 60 * 60; private final long MILLISECONDS_PER_DAY = MILLISECONDS_PER_HOUR * 24; - private final long NANOSECONDS_PER_MILLISECOND = 1000 * 1000; private long maxTimestamp; private long minTimestamp; private int maxCount; - private final int minCount = 0; + private final int minCount = 0; // The bottom of the graph will always be zero @NbBundle.Messages({"UserMetricGraphPanel.constructor.casesOpen=Cases open", "UserMetricGraphPanel.constructor.loggedIn=Users logged in - examiner nodes in blue, auto ingest nodes in green" @@ -162,17 +152,13 @@ class UserMetricGraphPanel extends JPanel { } } - // Check if this is a new maximum - if(doBarGraph) { - maxCount = Integer.max(maxCount, openCaseCount.getTotalNodeCount()); - maxCount = Integer.max(maxCount, loggedInUserCount.getTotalNodeCount()); - } else { - maxCount = Integer.max(maxCount, openCaseCount.getAutoIngestNodeCount()); - maxCount = Integer.max(maxCount, openCaseCount.getExaminerNodeCount()); - maxCount = Integer.max(maxCount, loggedInUserCount.getAutoIngestNodeCount()); - maxCount = Integer.max(maxCount, loggedInUserCount.getExaminerNodeCount()); - } - + // Check if this is a new maximum. + // Assuming we log all the events, there should never be more cases open than + // there are logged in users, but it could happen if we lose data. + maxCount = Integer.max(maxCount, openCaseCount.getTotalNodeCount()); + maxCount = Integer.max(maxCount, loggedInUserCount.getTotalNodeCount()); + + // Add the count to be plotted if(plotCases) { dataToPlot.add(openCaseCount); } else { @@ -180,42 +166,6 @@ class UserMetricGraphPanel extends JPanel { } } } - - private class UserCount { - private final long timestamp; - private int examinerCount; - private int autoIngestCount; - - UserCount(long timestamp) { - this.timestamp = timestamp; - this.examinerCount = 0; - this.autoIngestCount = 0; - } - - void addExaminer() { - examinerCount++; - } - - void addAutoIngestNode() { - autoIngestCount++; - } - - int getExaminerNodeCount() { - return examinerCount; - } - - int getAutoIngestNodeCount() { - return autoIngestCount; - } - - int getTotalNodeCount() { - return examinerCount + autoIngestCount; - } - - long getTimestamp() { - return timestamp; - } - } /** * Setup of the graphics panel: @@ -370,101 +320,63 @@ class UserMetricGraphPanel extends JPanel { g2.setColor(Color.BLACK); g2.drawLine(leftGraphPadding, getHeight() - bottomGraphPadding, leftGraphPadding, topGraphPadding); g2.drawLine(leftGraphPadding, getHeight() - bottomGraphPadding, getWidth() - rightGraphPadding, getHeight() - bottomGraphPadding); - - if(! doBarGraph) { - // Create the points to plot - List graphPoints = new ArrayList<>(); - for(UserCount userCount:dataToPlot) { - int x1 = (int) ((userCount.getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding); - int y1 = (int) ((maxValueOnYAxis - userCount.getExaminerNodeCount()) * yScale + topGraphPadding); - graphPoints.add(new Point(x1, y1)); + + // Sort dataToPlot on timestamp + Collections.sort(dataToPlot, new Comparator(){ + @Override + public int compare(UserCount o1, UserCount o2){ + return Long.compare(o1.getTimestamp(), o2.getTimestamp()); } - - // Sort the points - Collections.sort(graphPoints, new Comparator() { - @Override - public int compare(Point o1, Point o2) { - if(o1.getX() > o2.getX()) { - return 1; - } else if (o1.getX() < o2.getX()) { - return -1; - } - return 0; - } - }); - - // Draw the selected type of graph. If there's only one data point, - // draw that single point. - g2.setStroke(NARROW_STROKE); - g2.setColor(examinerColor); - if(doLineGraph && graphPoints.size() > 1) { - for (int i = 0; i < graphPoints.size() - 1; i++) { - int x1 = graphPoints.get(i).x; - int y1 = graphPoints.get(i).y; - int x2 = graphPoints.get(i + 1).x; - int y2 = graphPoints.get(i + 1).y; - g2.drawLine(x1, y1, x2, y2); - } + }); + + // Create the bars + for(int i = 0;i < dataToPlot.size();i++) { + UserCount userCount = dataToPlot.get(i); + int x = (int) ((userCount.getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding); + int yTopOfExaminerBox; + if(countToGraphPosition.containsKey(userCount.getTotalNodeCount())) { + // If we've drawn a grid line for this count, use the recorded value. If we don't do + // this, rounding differences lead to the bar graph not quite lining up with the existing grid. + yTopOfExaminerBox = countToGraphPosition.get(userCount.getTotalNodeCount()); } else { - for (int i = 0; i < graphPoints.size(); i++) { - int x = graphPoints.get(i).x - pointWidth / 2; - int y = graphPoints.get(i).y - pointWidth / 2; - int ovalW = pointWidth; - int ovalH = pointWidth; - g2.fillOval(x, y, ovalW, ovalH); - } + yTopOfExaminerBox = (int) ((maxValueOnYAxis - userCount.getTotalNodeCount()) * yScale + topGraphPadding); } - } else { - - // Sort dataToPlot on timestamp - Collections.sort(dataToPlot, new Comparator(){ - @Override - public int compare(UserCount o1, UserCount o2){ - return Long.compare(o1.getTimestamp(), o2.getTimestamp()); - } - }); - - for(int i = 0;i < dataToPlot.size();i++) { - UserCount userCount = dataToPlot.get(i); - int x = (int) ((userCount.getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding); - int yTopOfExaminerBox; - int totalCount = userCount.getExaminerNodeCount() + userCount.getAutoIngestNodeCount(); - if(countToGraphPosition.containsKey(totalCount)) { - // If we've drawn a grid line for this count, use the recorded value. Otherwise rounding differences - // lead to the bar graph not quite lining up with the existing grid. - yTopOfExaminerBox = countToGraphPosition.get(totalCount); - } else { - yTopOfExaminerBox = (int) ((maxValueOnYAxis - userCount.getExaminerNodeCount() - - userCount.getAutoIngestNodeCount()) * yScale + topGraphPadding); - } - - int width; - if(i < dataToPlot.size() - 1) { - width = Integer.max((int)((dataToPlot.get(i + 1).getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding) - x - 1, - 1); - } else { - width = Integer.max((int)(dataInterval * xScale), 1); - } - - // It's easiest here to draw the rectangle going all the way to the bottom of the graph. - // The bottom will be overwritten by the auto ingest box. - int heightExaminerBox = (getHeight() - bottomGraphPadding) - yTopOfExaminerBox; - - g2.setColor(examinerColor); - g2.fillRect(x, yTopOfExaminerBox, width, heightExaminerBox); + // Calculate the width. If this isn't the last column, set this to one less than + // the distance to the next column starting point. + int width; + if(i < dataToPlot.size() - 1) { + width = Integer.max((int)((dataToPlot.get(i + 1).getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding) - x - 1, + 1); + } else { + width = Integer.max((int)(dataInterval * xScale), 1); + } + + // The examiner bar goes all the way to the bottom of the graph. + // The bottom will be overwritten by the auto ingest bar for displaying + // logged in users. + int heightExaminerBox = (getHeight() - bottomGraphPadding) - yTopOfExaminerBox; + + // Plot the examiner bar + g2.setColor(examinerColor); + g2.fillRect(x, yTopOfExaminerBox, width, heightExaminerBox); + + // Check that there is an auto ingest node count before plotting its bar. + // For the cases open graph, this will always be empty. + if (userCount.getAutoIngestNodeCount() > 0) { int yTopOfAutoIngestBox; if(countToGraphPosition.containsKey(userCount.getAutoIngestNodeCount())) { + // As above, if we've drawn a grid line for this count, use the recorded value. If we don't do + // this, rounding differences lead to the bar graph not quite lining up with the existing grid. yTopOfAutoIngestBox =countToGraphPosition.get(userCount.getAutoIngestNodeCount()); } else { yTopOfAutoIngestBox = yTopOfExaminerBox + heightExaminerBox; } int heightAutoIngestBox = (getHeight() - bottomGraphPadding) - yTopOfAutoIngestBox; - //int heightAutoIngestBox = (int)(userCount.getAutoIngestNodeCount() * yScale); - + + // Plot the auto ingest bar g2.setColor(autoIngestColor); g2.fillRect(x, yTopOfAutoIngestBox, width, heightAutoIngestBox); - } } @@ -479,5 +391,69 @@ class UserMetricGraphPanel extends JPanel { g2.drawString(titleStr, positionForMetricNameLabel, padding); } - + /** + * Utility class to keep track of the data to be graphed. + * Used for tracking logging in users and open cases. + */ + private class UserCount { + private final long timestamp; + private int examinerCount; + private int autoIngestCount; + + /** + * Create a UserCount object with counts initialized to zero. + * @param timestamp + */ + UserCount(long timestamp) { + this.timestamp = timestamp; + this.examinerCount = 0; + this.autoIngestCount = 0; + } + + /** + * Add one examiner node to the count. + */ + void addExaminer() { + examinerCount++; + } + + /** + * Add one auto ingest node to the count. + */ + void addAutoIngestNode() { + autoIngestCount++; + } + + /** + * Get the number of examiner nodes. + * @return number of examiner nodes + */ + int getExaminerNodeCount() { + return examinerCount; + } + + /** + * Get the number of auto ingest nodes. + * @return number of auto ingest nodes + */ + int getAutoIngestNodeCount() { + return autoIngestCount; + } + + /** + * Get the total number of nodes + * @return the sum of the examiner and auto ingest nodes + */ + int getTotalNodeCount() { + return examinerCount + autoIngestCount; + } + + /** + * Get the timestamp for this metric + * @return the timestamp + */ + long getTimestamp() { + return timestamp; + } + } } From 8227d42f1bc4377fb5316dc02a6cb2aae5b4e0b3 Mon Sep 17 00:00:00 2001 From: Ann Priestman Date: Wed, 6 Jun 2018 09:47:28 -0400 Subject: [PATCH 5/7] Cleanup --- .../EnterpriseHealthMonitor.java | 136 +----------------- .../healthmonitor/HealthMonitorDashboard.java | 10 +- .../healthmonitor/UserMetricGraphPanel.java | 1 - 3 files changed, 6 insertions(+), 141 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java index e1b27c3426..7293e88edd 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java @@ -67,7 +67,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { private final static Logger logger = Logger.getLogger(EnterpriseHealthMonitor.class.getName()); private final static String DATABASE_NAME = "EnterpriseHealthMonitor"; - private final static long DATABASE_WRITE_INTERVAL = 1; // Minutes + private final static long DATABASE_WRITE_INTERVAL = 60; // Minutes public static final CaseDbSchemaVersionNumber CURRENT_DB_SCHEMA_VERSION = new CaseDbSchemaVersionNumber(1, 1); @@ -168,137 +168,6 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { // Clear out any old data timingInfoMap.clear(); userInfoList.clear(); - - // TEMP TEMP - if(false) { - // Write to the database - try (CoordinationService.Lock lock = getSharedDbLock()) { - if(lock == null) { - throw new HealthMonitorException("Error getting database lock"); - } - - Connection conn = connect(); - if(conn == null) { - throw new HealthMonitorException("Error getting database connection"); - } - - // Add metrics to the database - String addUserInfoSql = "INSERT INTO user_data (host, timestamp, event_type, is_examiner, case_name) VALUES (?, ?, ?, ?, ?)"; - try (PreparedStatement userStatement = conn.prepareStatement(addUserInfoSql)) { - - long msPerDay = 24 * 60 * 60 * 1000; - long msPerHour = 60 * 60 * 1000; - long currentTime = System.currentTimeMillis(); - - // ain1 has had a case open for a month - userStatement.setString(1, "ain1"); - userStatement.setLong(2, currentTime - msPerDay * 30); - userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); - userStatement.setBoolean(4, false); - userStatement.setString(5, "case1"); - userStatement.execute(); - - // ain2 has had a case open for two weeks - userStatement.setString(1, "ain2"); - userStatement.setLong(2, currentTime - msPerDay * 14); - userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); - userStatement.setBoolean(4, false); - userStatement.setString(5, "case1"); - userStatement.execute(); - - // ain3 has a case open half the time over the last week - int count = 0; - for(long i = currentTime - msPerDay * 7;i < currentTime;i += msPerHour * 3) { - userStatement.setString(1, "ain3"); - userStatement.setLong(2, i); - if(count % 2 == 0){ - userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); - } else { - userStatement.setInt(3, UserEvent.CASE_CLOSE.getEventValue()); - } - userStatement.setBoolean(4, false); - userStatement.setString(5, "case2"); - userStatement.execute(); - count++; - } - - // ex1 is logged in and has a case open part of the time - for(long i = currentTime - msPerDay * 30;i < currentTime;i += msPerDay) { - userStatement.setString(1, "ex1"); - userStatement.setLong(2, i - 5000); - userStatement.setInt(3, UserEvent.LOG_ON.getEventValue()); - userStatement.setBoolean(4, true); - userStatement.setString(5, ""); - userStatement.execute(); - - userStatement.setString(1, "ex1"); - userStatement.setLong(2, i); - userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); - userStatement.setBoolean(4, true); - userStatement.setString(5, "case3"); - userStatement.execute(); - - userStatement.setString(1, "ex1"); - userStatement.setLong(2, i + msPerHour * 8); - userStatement.setInt(3, UserEvent.CASE_CLOSE.getEventValue()); - userStatement.setBoolean(4, true); - userStatement.setString(5, ""); - userStatement.execute(); - - userStatement.setString(1, "ex1"); - userStatement.setLong(2, i + msPerHour * 8 + 5000); - userStatement.setInt(3, UserEvent.LOG_OFF.getEventValue()); - userStatement.setBoolean(4, true); - userStatement.setString(5, ""); - userStatement.execute(); - } - - // ex2 and ex3 are always logged in and open and close cases periodically - for(long i = currentTime - msPerDay * 9;i < currentTime;i += msPerDay) { - for(long j = 0;j < 10;j+=3){ - userStatement.setString(1, "ex2"); - userStatement.setLong(2, i + j * msPerHour); - userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); - userStatement.setBoolean(4, true); - userStatement.setString(5, "case3"); - userStatement.execute(); - - userStatement.setString(1, "ex2"); - userStatement.setLong(2, i + j * msPerHour + msPerHour + 5000); - userStatement.setInt(3, UserEvent.CASE_CLOSE.getEventValue()); - userStatement.setBoolean(4, true); - userStatement.setString(5, ""); - userStatement.execute(); - - userStatement.setString(1, "ex3"); - userStatement.setLong(2, i + j * msPerHour - 40000); - userStatement.setInt(3, UserEvent.CASE_OPEN.getEventValue()); - userStatement.setBoolean(4, true); - userStatement.setString(5, "case4"); - userStatement.execute(); - - userStatement.setString(1, "ex3"); - userStatement.setLong(2, i + j * msPerHour + msPerHour + 5000 - 40000); - userStatement.setInt(3, UserEvent.CASE_CLOSE.getEventValue()); - userStatement.setBoolean(4, true); - userStatement.setString(5, ""); - userStatement.execute(); - } - } - - } catch (SQLException ex) { - throw new HealthMonitorException("Error saving metric data to database", ex); - } finally { - try { - conn.close(); - } catch (SQLException ex) { - logger.log(Level.SEVERE, "Error closing Connection.", ex); - } - } - } catch (CoordinationService.CoordinationServiceException ex) { - throw new HealthMonitorException("Error releasing database lock", ex); - } - } } /** @@ -1072,9 +941,6 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { // Case is closing addUserEvent(UserEvent.CASE_CLOSE); - // Write the current metrics to the database - // TEMP DON'T DO IT - //healthMonitorExecutor.submit(new EnterpriseHealthMonitor.PeriodicHealthMonitorTask()); } else if((null == evt.getOldValue()) && (evt.getNewValue() instanceof Case)) { // Case is opening addUserEvent(UserEvent.CASE_OPEN); diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java index 4bbb14558c..e774ff7371 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java @@ -160,7 +160,7 @@ public class HealthMonitorDashboard { timingData = EnterpriseHealthMonitor.getInstance().getTimingMetricsFromDatabase(DateRange.getMaximumTimestampRange()); // Get a copy of the user data from the database - userData = EnterpriseHealthMonitor.getInstance().getUserMetricsFromDatabase(DateRange.getMaximumTimestampRange()); + userData = EnterpriseHealthMonitor.getInstance().getUserMetricsFromDatabase(DateRange.getMaximumTimestampRange()); } } @@ -477,8 +477,8 @@ public class HealthMonitorDashboard { return userControlPanel; } - /** - * Update the timing graphs. + /** + * Update the user graphs. * @throws HealthMonitorException */ @NbBundle.Messages({"HealthMonitorDashboard.updateUserMetricGraphs.noData=No data to display"}) @@ -494,8 +494,8 @@ public class HealthMonitorDashboard { } // Calculate the minimum timestamp for the graph. - // Unlike the timing graphs, we have to pass in the full user data list here to - // use for the graph. This is because even if we're only displaying one day, the + // Unlike the timing graphs, we do not filter the list of user metrics here. + // This is because even if we're only displaying one day, the // last metric for a host may be that it logged on two days ago, so we would want // to show that node as logged on. long timestampThreshold; diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java index dd945192ff..4326c9a45d 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java @@ -99,7 +99,6 @@ class UserMetricGraphPanel extends JPanel { resultTreeSet.add(result); userDataMap.put(result.getHostname(), resultTreeSet); } - // TODO test what happens if two identical timestamps come in for the same host } // Create a list of data points to plot From 9c9b0c8306f1ce08d61cc791fd026fe07940f947 Mon Sep 17 00:00:00 2001 From: Ann Priestman Date: Wed, 6 Jun 2018 11:47:48 -0400 Subject: [PATCH 6/7] Cleanup --- .../EnterpriseHealthMonitor.java | 29 +++++++------------ .../healthmonitor/HealthMonitorDashboard.java | 1 - 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java index 7293e88edd..36d5bdcefc 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java @@ -903,22 +903,23 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { /** * The task called by the ScheduledThreadPoolExecutor to handle - * the database checks/writes. + * the periodic database update */ static final class PeriodicHealthMonitorTask implements Runnable { - /** - * Perform all periodic tasks: - * - Check if monitoring has been enabled / disabled in the database - * - Gather any additional metrics - * - Write current metric data to the database - */ @Override public void run() { recordMetrics(); } } + /** + * Perform all periodic tasks: + * - Check if monitoring has been enabled / disabled in the database + * - Gather any additional metrics + * - Write current metric data to the database + * Do not run this from a new thread if the case/application is closing. + */ private static void recordMetrics() { try { getInstance().updateFromGlobalEnabledStatus(); @@ -1179,15 +1180,11 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { if(lock == null) { throw new HealthMonitorException("Error getting database lock"); } - - Connection conn = connect(); - if(conn == null) { - throw new HealthMonitorException("Error getting database connection"); - } List resultList = new ArrayList<>(); - try (Statement statement = conn.createStatement(); + try (Connection conn = connect(); + Statement statement = conn.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT * FROM user_data WHERE timestamp > " + minimumTimestamp)) { while (resultSet.next()) { @@ -1196,12 +1193,6 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { return resultList; } catch (SQLException ex) { throw new HealthMonitorException("Error reading user metrics from database", ex); - } finally { - try { - conn.close(); - } catch (SQLException ex) { - logger.log(Level.SEVERE, "Error closing Connection.", ex); - } } } catch (CoordinationService.CoordinationServiceException ex) { throw new HealthMonitorException("Error getting database lock", ex); diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java index e774ff7371..07f8fcced7 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java @@ -404,7 +404,6 @@ public class HealthMonitorDashboard { private JPanel createUserPanel() throws HealthMonitorException { // If the monitor isn't enabled, just add a message if(! EnterpriseHealthMonitor.monitorIsEnabled()) { - //timingMetricPanel.setPreferredSize(new Dimension(400,100)); JPanel emptyUserMetricPanel = new JPanel(); emptyUserMetricPanel.add(new JLabel(Bundle.HealthMonitorDashboard_createUserPanel_userMetricsTitle())); emptyUserMetricPanel.add(new JLabel(" ")); From d78cab81c4a396ac7f565acedcf06ca21874d3d7 Mon Sep 17 00:00:00 2001 From: Ann Priestman Date: Wed, 6 Jun 2018 12:28:02 -0400 Subject: [PATCH 7/7] Don't let the user graphs have a max height of zero. codacy fixes --- .../EnterpriseHealthMonitor.java | 4 ---- .../healthmonitor/UserMetricGraphPanel.java | 19 ++++++++----------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java index 36d5bdcefc..fd3b3ee20e 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/EnterpriseHealthMonitor.java @@ -34,8 +34,6 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -74,8 +72,6 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener { private static final AtomicBoolean isEnabled = new AtomicBoolean(false); private static EnterpriseHealthMonitor instance; - private static final String HEALTH_MONITOR_EVENT_THREAD_NAME = "Health-Monitor-Event-Listener-%d"; - private ScheduledThreadPoolExecutor healthMonitorOutputTimer; private final Map timingInfoMap; private final List userInfoList; diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java index 4326c9a45d..d8da7afe97 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/UserMetricGraphPanel.java @@ -35,7 +35,6 @@ import java.util.TreeSet; import java.util.Calendar; import java.util.GregorianCalendar; import javax.swing.JPanel; -import org.sleuthkit.autopsy.coreutils.Logger; import java.util.TimeZone; import java.util.concurrent.TimeUnit; import org.openide.util.NbBundle; @@ -46,24 +45,22 @@ import org.sleuthkit.autopsy.healthmonitor.EnterpriseHealthMonitor.UserData; */ class UserMetricGraphPanel extends JPanel { - private final static Logger logger = Logger.getLogger(TimingMetricGraphPanel.class.getName()); - - private final int padding = 25; - private final int labelPadding = 25; + private static final int padding = 25; + private static final int labelPadding = 25; private final Color examinerColor = new Color(0x12, 0x20, 0xdb, 255); private final Color autoIngestColor = new Color(0x12, 0x80, 0x20, 255); private final Color gridColor = new Color(200, 200, 200, 200); - private final int pointWidth = 4; - private final int numberYDivisions = 10; + private static final int pointWidth = 4; + private static final int numberYDivisions = 10; private final List dataToPlot; private final String graphLabel; private final long dataInterval; private final long MILLISECONDS_PER_HOUR = 1000 * 60 * 60; private final long MILLISECONDS_PER_DAY = MILLISECONDS_PER_HOUR * 24; - private long maxTimestamp; - private long minTimestamp; + private final long maxTimestamp; + private final long minTimestamp; private int maxCount; - private final int minCount = 0; // The bottom of the graph will always be zero + private static final int minCount = 0; // The bottom of the graph will always be zero @NbBundle.Messages({"UserMetricGraphPanel.constructor.casesOpen=Cases open", "UserMetricGraphPanel.constructor.loggedIn=Users logged in - examiner nodes in blue, auto ingest nodes in green" @@ -193,7 +190,7 @@ class UserMetricGraphPanel extends JPanel { if((maxCount % 5) != 0) { maxCount += (5 - (maxCount % 5)); } - int maxValueOnYAxis = maxCount; + int maxValueOnYAxis = Integer.max(maxCount, 5); int minValueOnYAxis = minCount; // The graph itself has the following corners: