Merge pull request #3823 from APriestman/3879_userMetrics

3879 Add user metrics to health monitor
This commit is contained in:
Richard Cordovano 2018-06-12 10:45:02 -04:00 committed by GitHub
commit 1bc9932607
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1004 additions and 107 deletions

View File

@ -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;
@ -67,18 +65,16 @@ 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<String, TimingInfo> timingInfoMap;
private final List<UserData> userInfoList;
private static final int CONN_POOL_SIZE = 10;
private BasicDataSource connectionPool = null;
private CaseDbConnectionInfo connectionSettingsInUse = null;
@ -90,8 +86,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 +153,70 @@ 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();
}
/**
* 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," +
"case_name text 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);
}
}
}
/**
@ -190,7 +245,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
stopTimer();
healthMonitorOutputTimer = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat("health_monitor_timer").build());
healthMonitorOutputTimer.scheduleWithFixedDelay(new PeriodicHealthMonitorTask(false), DATABASE_WRITE_INTERVAL, DATABASE_WRITE_INTERVAL, TimeUnit.MINUTES);
healthMonitorOutputTimer.scheduleWithFixedDelay(new PeriodicHealthMonitorTask(), DATABASE_WRITE_INTERVAL, DATABASE_WRITE_INTERVAL, TimeUnit.MINUTES);
}
/**
@ -207,7 +262,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 +383,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
@ -356,16 +432,10 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
/**
* Collect metrics at a scheduled time.
* @param caseIsClosing True if this was triggered from a case closed event
* @throws HealthMonitorException
*/
private void gatherTimerBasedMetrics(boolean caseIsClosing) throws HealthMonitorException {
// Time a database query. If this was triggered from a case close event
// it will fail - since we're on a new thread the case database will
// be in the process of closing. In that case, skip collecting the metric.
if( ! caseIsClosing) {
performDatabaseQuery();
}
private void gatherTimerBasedMetrics() throws HealthMonitorException {
performDatabaseQuery();
}
/**
@ -375,6 +445,7 @@ public final class EnterpriseHealthMonitor implements PropertyChangeListener {
private void writeCurrentStateToDatabase() throws HealthMonitorException {
Map<String, TimingInfo> timingMapCopy;
List<UserData> userDataCopy;
// Do as little as possible within the synchronized block since it will
// block threads attempting to record metrics.
@ -387,10 +458,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;
}
@ -407,22 +481,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, case_name) 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.setString(5, userInfo.getCaseName());
userStatement.execute();
}
} catch (SQLException ex) {
@ -762,9 +847,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," +
@ -773,16 +857,23 @@ 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," +
"case_name text 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() + "')");
@ -807,31 +898,32 @@ 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 {
boolean caseIsClosing;
PeriodicHealthMonitorTask(boolean caseIsClosing) {
this.caseIsClosing = caseIsClosing;
}
/**
* Perform all periodic tasks:
* - Check if monitoring has been enabled / disabled in the database
* - Calculate any final metrics
* - Write current metric data to the database
*/
@Override
public void run() {
try {
getInstance().updateFromGlobalEnabledStatus();
getInstance().gatherTimerBasedMetrics(caseIsClosing);
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();
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
}
}
@ -842,8 +934,12 @@ 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(true));
// Case is closing
addUserEvent(UserEvent.CASE_CLOSE);
} else if((null == evt.getOldValue()) && (evt.getNewValue() instanceof Case)) {
// Case is opening
addUserEvent(UserEvent.CASE_OPEN);
}
break;
}
@ -1057,6 +1153,47 @@ 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<UserData> 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");
}
List<UserData> resultList = new ArrayList<>();
try (Connection conn = connect();
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);
}
} 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.
@ -1096,6 +1233,164 @@ 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;
private String caseName;
/**
* 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 = "";
// 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 = "";
}
}
/**
* 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.caseName = resultSet.getString("case_name");
}
/**
* 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;
}
/**
* 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;
}
}
/**
* Internal class for collecting timing metrics.
* Instead of storing each TimingMetric, we only store the min and max

View File

@ -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<String, List<EnterpriseHealthMonitor.DatabaseTimingResult>> timingData;
List<EnterpriseHealthMonitor.UserData> userData;
private JComboBox<String> dateComboBox = null;
private JComboBox<String> hostComboBox = null;
private JCheckBox hostCheckBox = null;
private JCheckBox showTrendLineCheckBox = null;
private JCheckBox skipOutliersCheckBox = null;
private JPanel graphPanel = null;
private JComboBox<String> timingDateComboBox = null;
private JComboBox<String> timingHostComboBox = null;
private JCheckBox timingHostCheckBox = null;
private JCheckBox timingShowTrendLineCheckBox = null;
private JCheckBox timingSkipOutliersCheckBox = null;
private JPanel timingGraphPanel = null;
private JComboBox<String> 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<EnterpriseHealthMonitor.DatabaseTimingResult> 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,150 @@ 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()) {
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());
// 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 user 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 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;
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"})

View File

@ -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);
}
}
}

View File

@ -0,0 +1,455 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2018 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> 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.Color;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.util.Collections;
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.HashSet;
import java.util.TreeSet;
import java.util.Calendar;
import java.util.GregorianCalendar;
import javax.swing.JPanel;
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 user metric data
*/
class UserMetricGraphPanel extends JPanel {
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 static final int pointWidth = 4;
private static final int numberYDivisions = 10;
private final List<UserCount> 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 maxTimestamp;
private final long minTimestamp;
private int maxCount;
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"
})
UserMetricGraphPanel(List<UserData> 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();
}
// Comparator for the set of UserData objects
Comparator<UserData> sortOnTimestamp = new Comparator<UserData>() {
@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<String, TreeSet<UserData>> userDataMap = new HashMap<>();
for(UserData result:userResults) {
if(userDataMap.containsKey(result.getHostname())) {
userDataMap.get(result.getHostname()).add(result);
} else {
TreeSet<UserData> resultTreeSet = new TreeSet<>(sortOnTimestamp);
resultTreeSet.add(result);
userDataMap.put(result.getHostname(), resultTreeSet);
}
}
// 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<>();
dataInterval = MILLISECONDS_PER_HOUR;
maxCount = Integer.MIN_VALUE;
for (long timestamp = maxTimestamp;timestamp > minTimestamp;timestamp -= dataInterval) {
// 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<String> openCaseNames = new HashSet<>();
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) {
// 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());
}
}
// Update the logged in user count
if (lastRecord.getEventType().userIsLoggedIn()) {
if(lastRecord.isExaminerNode()) {
loggedInUserCount.addExaminer();
} else {
loggedInUserCount.addAutoIngestNode();
}
}
}
}
// 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 {
dataToPlot.add(loggedInUserCount);
}
}
}
/**
* 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 (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));
}
int maxValueOnYAxis = Integer.max(maxCount, 5);
int minValueOnYAxis = minCount;
// 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;
Map<Integer, Integer> countToGraphPosition = new HashMap<>();
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));
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;
}
}
}
// 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);
// Sort dataToPlot on timestamp
Collections.sort(dataToPlot, new Comparator<UserCount>(){
@Override
public int compare(UserCount o1, UserCount o2){
return Long.compare(o1.getTimestamp(), o2.getTimestamp());
}
});
// 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 {
yTopOfExaminerBox = (int) ((maxValueOnYAxis - userCount.getTotalNodeCount()) * yScale + topGraphPadding);
}
// 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;
// Plot the auto ingest bar
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);
}
/**
* 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;
}
}
}

View File

@ -1,5 +1,5 @@
#Updated by build script
#Tue, 08 May 2018 10:29:55 -0600
#Mon, 19 Mar 2018 11:17:11 -0700
LBL_splash_window_title=Starting Autopsy
SPLASH_HEIGHT=314
SPLASH_WIDTH=538
@ -8,4 +8,4 @@ SplashRunningTextBounds=0,289,538,18
SplashRunningTextColor=0x0
SplashRunningTextFontSize=19
currentVersion=Autopsy 4.7.0
currentVersion=Autopsy 4.6.0

View File

@ -1,4 +1,4 @@
#Updated by build script
#Tue, 08 May 2018 10:29:55 -0600
CTL_MainWindow_Title=Autopsy 4.7.0
CTL_MainWindow_Title_No_Project=Autopsy 4.7.0
#Fri, 09 Mar 2018 13:03:41 -0700
CTL_MainWindow_Title=Autopsy 4.6.0
CTL_MainWindow_Title_No_Project=Autopsy 4.6.0