mirror of
https://github.com/overcuriousity/autopsy-flatpak.git
synced 2025-07-12 07:56:16 +00:00
Merge pull request #3823 from APriestman/3879_userMetrics
3879 Add user metrics to health monitor
This commit is contained in:
commit
1bc9932607
@ -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
|
||||
|
@ -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"})
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user