70 private final static AtomicBoolean
isEnabled =
new AtomicBoolean(
false);
94 hostName = java.net.InetAddress.getLocalHost().getHostName();
95 }
catch (java.net.UnknownHostException ex) {
97 hostName = UUID.randomUUID().toString();
98 logger.log(Level.SEVERE,
"Unable to look up host name - falling back to UUID " +
hostName, ex);
102 username = System.getProperty(
"user.name");
105 updateFromGlobalEnabledStatus();
118 synchronized static HealthMonitor getInstance() throws HealthMonitorException {
135 logger.log(Level.INFO,
"Activating Servies Health Monitor");
141 throw new HealthMonitorException(
"Multi user mode is not enabled - can not activate health monitor");
147 throw new HealthMonitorException(
"Error getting database lock");
166 throw new HealthMonitorException(
"Error releasing database lock", ex);
179 logger.log(Level.INFO,
"Upgrading Health Monitor database");
180 CaseDbSchemaVersionNumber currentSchema =
getVersion();
184 throw new HealthMonitorException(
"Error getting database connection");
186 ResultSet resultSet =
null;
188 try (Statement statement = conn.createStatement()) {
189 conn.setAutoCommit(
false);
197 if (currentSchema.compareTo(
new CaseDbSchemaVersionNumber(1, 1)) < 0) {
200 statement.execute(
"CREATE TABLE IF NOT EXISTS user_data ("
201 +
"id SERIAL PRIMARY KEY,"
202 +
"host text NOT NULL,"
203 +
"timestamp bigint NOT NULL,"
204 +
"event_type int NOT NULL,"
205 +
"is_examiner boolean NOT NULL,"
206 +
"case_name text NOT NULL"
212 if (currentSchema.compareTo(
new CaseDbSchemaVersionNumber(1, 2)) < 0) {
214 resultSet = statement.executeQuery(
"SELECT column_name " +
215 "FROM information_schema.columns " +
216 "WHERE table_name='user_data' and column_name='username'");
217 if (! resultSet.next()) {
219 statement.execute(
"ALTER TABLE user_data ADD COLUMN username text");
224 statement.execute(
"UPDATE db_info SET value='" +
CURRENT_DB_SCHEMA_VERSION.getMajor() +
"' WHERE name='SCHEMA_VERSION'");
225 statement.execute(
"UPDATE db_info SET value='" +
CURRENT_DB_SCHEMA_VERSION.getMinor() +
"' WHERE name='SCHEMA_MINOR_VERSION'");
229 }
catch (SQLException ex) {
232 }
catch (SQLException ex2) {
233 logger.log(Level.SEVERE,
"Rollback error");
235 throw new HealthMonitorException(
"Error upgrading database", ex);
237 if (resultSet !=
null) {
240 }
catch (SQLException ex2) {
241 logger.log(Level.SEVERE,
"Error closing result set");
246 }
catch (SQLException ex) {
247 logger.log(Level.SEVERE,
"Error closing connection.", ex);
262 logger.log(Level.INFO,
"Deactivating Servies Health Monitor");
279 healthMonitorOutputTimer =
new ScheduledThreadPoolExecutor(1,
new ThreadFactoryBuilder().setNameFormat(
"health_monitor_timer").build());
298 static synchronized void startUpIfEnabled() throws HealthMonitorException {
308 static synchronized void shutdown() throws HealthMonitorException {
320 static synchronized void setEnabled(
boolean enabled)
throws HealthMonitorException {
338 getInstance().deactivateMonitorLocally();
368 if (
isEnabled.get() && (metric !=
null)) {
371 getInstance().addTimingMetric(metric);
372 }
catch (HealthMonitorException ex) {
374 logger.log(Level.SEVERE,
"Error adding timing metric", ex);
391 if (
isEnabled.get() && (metric !=
null)) {
394 metric.normalize(normalization);
395 getInstance().addTimingMetric(metric);
396 }
catch (HealthMonitorException ex) {
398 logger.log(Level.SEVERE,
"Error adding timing metric", ex);
413 synchronized (
this) {
434 UserData userInfo =
new UserData(eventType);
435 synchronized (
this) {
453 List<Image> images = skCase.getImages();
457 long normalization = images.size();
458 if (images.isEmpty()) {
460 }
else if (images.size() == 1) {
462 }
else if (images.size() < 10) {
468 HealthMonitor.submitNormalizedTimingMetric(metric, normalization);
471 }
catch (TskCoreException ex) {
472 throw new HealthMonitorException(
"Error running getImages()", ex);
492 Map<String, TimingInfo> timingMapCopy;
493 List<UserData> userDataCopy;
497 synchronized (
this) {
512 if (timingMapCopy.keySet().isEmpty() && userDataCopy.isEmpty()) {
516 logger.log(Level.INFO,
"Writing health monitor metrics to database");
521 throw new HealthMonitorException(
"Error getting database lock");
526 throw new HealthMonitorException(
"Error getting database connection");
530 String addTimingInfoSql =
"INSERT INTO timing_data (name, host, timestamp, count, average, max, min) VALUES (?, ?, ?, ?, ?, ?, ?)";
531 String addUserInfoSql =
"INSERT INTO user_data (host, username, timestamp, event_type, is_examiner, case_name) VALUES (?, ?, ?, ?, ?, ?)";
532 try (PreparedStatement timingStatement = conn.prepareStatement(addTimingInfoSql);
533 PreparedStatement userStatement = conn.prepareStatement(addUserInfoSql)) {
535 for (String name : timingMapCopy.keySet()) {
538 timingStatement.setString(1, name);
539 timingStatement.setString(2,
hostName);
540 timingStatement.setLong(3, System.currentTimeMillis());
541 timingStatement.setLong(4, info.getCount());
542 timingStatement.setDouble(5, info.getAverage());
543 timingStatement.setDouble(6, info.getMax());
544 timingStatement.setDouble(7, info.getMin());
546 timingStatement.execute();
549 for (UserData userInfo : userDataCopy) {
550 userStatement.setString(1,
hostName);
551 userStatement.setString(2,
username);
552 userStatement.setLong(3, userInfo.getTimestamp());
553 userStatement.setInt(4, userInfo.getEventType().getEventValue());
554 userStatement.setBoolean(5, userInfo.isExaminerNode());
555 userStatement.setString(6, userInfo.getCaseName());
556 userStatement.execute();
559 }
catch (SQLException ex) {
560 throw new HealthMonitorException(
"Error saving metric data to database", ex);
564 }
catch (SQLException ex) {
565 logger.log(Level.SEVERE,
"Error closing Connection.", ex);
569 throw new HealthMonitorException(
"Error releasing database lock", ex);
585 Class.forName(
"org.postgresql.Driver");
587 try (Connection connection = DriverManager.getConnection(
"jdbc:postgresql://" + db.getHost() +
":" + db.getPort() +
"/postgres", db.getUserName(), db.getPassword());
588 Statement statement = connection.createStatement();) {
589 String createCommand =
"SELECT 1 AS result FROM pg_database WHERE datname='" +
DATABASE_NAME +
"'";
590 rs = statement.executeQuery(createCommand);
600 throw new HealthMonitorException(
"Failed check for health monitor database", ex);
614 Class.forName(
"org.postgresql.Driver");
615 try (Connection connection = DriverManager.getConnection(
"jdbc:postgresql://" + db.getHost() +
":" + db.getPort() +
"/postgres", db.getUserName(), db.getPassword());
616 Statement statement = connection.createStatement();) {
617 String createCommand =
"CREATE DATABASE \"" +
DATABASE_NAME +
"\" OWNER \"" + db.getUserName() +
"\"";
618 statement.execute(createCommand);
622 throw new HealthMonitorException(
"Failed to delete health monitor database", ex);
639 StringBuilder connectionURL =
new StringBuilder();
640 connectionURL.append(
"jdbc:postgresql://");
641 connectionURL.append(db.getHost());
642 connectionURL.append(
":");
643 connectionURL.append(db.getPort());
644 connectionURL.append(
"/");
656 throw new HealthMonitorException(
"Error loading database configuration", ex);
667 synchronized (
this) {
673 }
catch (SQLException ex) {
674 throw new HealthMonitorException(
"Failed to close existing database connections.", ex);
685 private Connection
connect() throws HealthMonitorException {
686 synchronized (
this) {
694 }
catch (SQLException ex) {
695 throw new HealthMonitorException(
"Error getting connection from connection pool.", ex);
710 throw new HealthMonitorException(
"Error getting database connection");
712 ResultSet resultSet =
null;
714 try (Statement statement = conn.createStatement()) {
715 resultSet = statement.executeQuery(
"SELECT value FROM db_info WHERE name='SCHEMA_VERSION'");
716 return resultSet.next();
717 }
catch (SQLException ex) {
721 if (resultSet !=
null) {
724 }
catch (SQLException ex) {
725 logger.log(Level.SEVERE,
"Error closing result set", ex);
730 }
catch (SQLException ex) {
731 logger.log(Level.SEVERE,
"Error closing Connection.", ex);
742 static boolean monitorIsEnabled() {
752 synchronized void updateFromGlobalEnabledStatus() throws HealthMonitorException {
754 boolean previouslyEnabled = monitorIsEnabled();
757 if (!UserPreferences.getIsMultiUserModeEnabled()) {
760 if (previouslyEnabled) {
771 if (previouslyEnabled) {
781 CaseDbConnectionInfo currentSettings = UserPreferences.getDatabaseConnectionInfo();
788 }
catch (UserPreferencesException ex) {
789 throw new HealthMonitorException(
"Error reading database connection info", ex);
794 if (currentlyEnabled != previouslyEnabled) {
795 if (!currentlyEnabled) {
815 try (Connection conn =
connect();
816 Statement statement = conn.createStatement();
817 ResultSet resultSet = statement.executeQuery(
"SELECT value FROM db_info WHERE name='MONITOR_ENABLED'")) {
819 if (resultSet.next()) {
820 return (resultSet.getBoolean(
"value"));
822 throw new HealthMonitorException(
"No enabled status found in database");
823 }
catch (SQLException ex) {
824 throw new HealthMonitorException(
"Error initializing database", ex);
835 try (Connection conn =
connect();
836 Statement statement = conn.createStatement();) {
837 statement.execute(
"UPDATE db_info SET value='" + status +
"' WHERE name='MONITOR_ENABLED'");
838 }
catch (SQLException ex) {
839 throw new HealthMonitorException(
"Error setting enabled status", ex);
850 private CaseDbSchemaVersionNumber
getVersion() throws HealthMonitorException {
853 throw new HealthMonitorException(
"Error getting database connection");
855 ResultSet resultSet =
null;
857 try (Statement statement = conn.createStatement()) {
858 int minorVersion = 0;
859 int majorVersion = 0;
860 resultSet = statement.executeQuery(
"SELECT value FROM db_info WHERE name='SCHEMA_MINOR_VERSION'");
861 if (resultSet.next()) {
862 String minorVersionStr = resultSet.getString(
"value");
864 minorVersion = Integer.parseInt(minorVersionStr);
865 }
catch (NumberFormatException ex) {
866 throw new HealthMonitorException(
"Bad value for schema minor version (" + minorVersionStr +
") - database is corrupt");
870 resultSet = statement.executeQuery(
"SELECT value FROM db_info WHERE name='SCHEMA_VERSION'");
871 if (resultSet.next()) {
872 String majorVersionStr = resultSet.getString(
"value");
874 majorVersion = Integer.parseInt(majorVersionStr);
875 }
catch (NumberFormatException ex) {
876 throw new HealthMonitorException(
"Bad value for schema version (" + majorVersionStr +
") - database is corrupt");
880 return new CaseDbSchemaVersionNumber(majorVersion, minorVersion);
881 }
catch (SQLException ex) {
882 throw new HealthMonitorException(
"Error initializing database", ex);
884 if (resultSet !=
null) {
887 }
catch (SQLException ex) {
888 logger.log(Level.SEVERE,
"Error closing result set", ex);
893 }
catch (SQLException ex) {
894 logger.log(Level.SEVERE,
"Error closing Connection.", ex);
907 throw new HealthMonitorException(
"Error getting database connection");
910 try (Statement statement = conn.createStatement()) {
911 conn.setAutoCommit(
false);
913 statement.execute(
"CREATE TABLE IF NOT EXISTS timing_data ("
914 +
"id SERIAL PRIMARY KEY,"
915 +
"name text NOT NULL,"
916 +
"host text NOT NULL,"
917 +
"timestamp bigint NOT NULL,"
918 +
"count bigint NOT NULL,"
919 +
"average double precision NOT NULL,"
920 +
"max double precision NOT NULL,"
921 +
"min double precision NOT NULL"
924 statement.execute(
"CREATE TABLE IF NOT EXISTS db_info ("
925 +
"id SERIAL PRIMARY KEY NOT NULL,"
926 +
"name text NOT NULL,"
927 +
"value text NOT NULL"
930 statement.execute(
"CREATE TABLE IF NOT EXISTS user_data ("
931 +
"id SERIAL PRIMARY KEY,"
932 +
"host text NOT NULL,"
933 +
"timestamp bigint NOT NULL,"
934 +
"event_type int NOT NULL,"
935 +
"is_examiner BOOLEAN NOT NULL,"
936 +
"case_name text NOT NULL,"
940 statement.execute(
"INSERT INTO db_info (name, value) VALUES ('SCHEMA_VERSION', '" +
CURRENT_DB_SCHEMA_VERSION.getMajor() +
"')");
941 statement.execute(
"INSERT INTO db_info (name, value) VALUES ('SCHEMA_MINOR_VERSION', '" +
CURRENT_DB_SCHEMA_VERSION.getMinor() +
"')");
942 statement.execute(
"INSERT INTO db_info (name, value) VALUES ('MONITOR_ENABLED', 'true')");
945 }
catch (SQLException ex) {
948 }
catch (SQLException ex2) {
949 logger.log(Level.SEVERE,
"Rollback error");
951 throw new HealthMonitorException(
"Error initializing database", ex);
955 }
catch (SQLException ex) {
956 logger.log(Level.SEVERE,
"Error closing connection.", ex);
965 static final class PeriodicHealthMonitorTask
implements Runnable {
981 getInstance().updateFromGlobalEnabledStatus();
982 if (monitorIsEnabled()) {
983 getInstance().gatherTimerBasedMetrics();
984 getInstance().writeCurrentStateToDatabase();
986 }
catch (HealthMonitorException ex) {
987 logger.log(Level.SEVERE,
"Error recording health monitor metrics", ex);
994 switch (
Case.
Events.valueOf(evt.getPropertyName())) {
997 if ((
null == evt.getNewValue()) && (evt.getOldValue() instanceof
Case)) {
1001 }
else if ((
null == evt.getOldValue()) && (evt.getNewValue() instanceof
Case)) {
1014 void populateDatabaseWithSampleData(
int nDays,
int nNodes,
boolean createVerificationData)
throws HealthMonitorException {
1017 throw new HealthMonitorException(
"Can't populate database - monitor not enabled");
1023 throw new HealthMonitorException(
"Error getting database lock");
1026 String[] metricNames = {
"Disk Reads: Hash calculation",
"Database: getImages query",
"Solr: Index chunk",
"Solr: Connectivity check",
1027 "Central Repository: Notable artifact query",
"Central Repository: Bulk insert"};
1029 Random rand =
new Random();
1031 long maxTimestamp = System.currentTimeMillis();
1032 long millisPerHour = 1000 * 60 * 60;
1033 long minTimestamp = maxTimestamp - (nDays * (millisPerHour * 24));
1035 Connection conn =
null;
1039 throw new HealthMonitorException(
"Error getting database connection");
1042 try (Statement statement = conn.createStatement()) {
1044 statement.execute(
"DELETE FROM timing_data");
1045 }
catch (SQLException ex) {
1046 logger.log(Level.SEVERE,
"Error clearing timing data", ex);
1051 String addTimingInfoSql =
"INSERT INTO timing_data (name, host, timestamp, count, average, max, min) VALUES (?, ?, ?, ?, ?, ?, ?)";
1052 try (PreparedStatement statement = conn.prepareStatement(addTimingInfoSql)) {
1054 for (String metricName : metricNames) {
1056 long baseIndex = rand.nextInt(900) + 100;
1057 int multiplier = rand.nextInt(5);
1058 long minIndexTimeNanos;
1059 switch (multiplier) {
1061 minIndexTimeNanos = baseIndex;
1064 minIndexTimeNanos = baseIndex * 1000;
1067 minIndexTimeNanos = baseIndex * 1000 * 1000;
1071 long maxIndexTimeOverMin = minIndexTimeNanos * 3;
1073 for (
int node = 0; node < nNodes; node++) {
1075 String host =
"testHost" + node;
1078 double maxCount = nDays * 24 + 1;
1081 for (
long timestamp = minTimestamp + rand.nextInt(1000 * 60 * 55); timestamp < maxTimestamp; timestamp += millisPerHour) {
1088 double slowNodeMultiplier = 1.0;
1089 if ((maxCount - count) <= 3 * 24) {
1090 slowNodeMultiplier += (3 - (maxCount - count) / 24) * 0.33;
1093 if (!createVerificationData) {
1096 int outlierVal = rand.nextInt(30);
1097 long randVal = rand.nextLong();
1101 if (outlierVal < 2) {
1102 aveTime = minIndexTimeNanos + maxIndexTimeOverMin + randVal % maxIndexTimeOverMin;
1103 }
else if (outlierVal == 2) {
1104 aveTime = (minIndexTimeNanos / 2) + randVal % (minIndexTimeNanos / 2);
1105 }
else if (outlierVal < 17) {
1106 aveTime = minIndexTimeNanos + randVal % (maxIndexTimeOverMin / 2);
1108 aveTime = minIndexTimeNanos + randVal % maxIndexTimeOverMin;
1112 aveTime = aveTime * slowNodeMultiplier;
1118 Calendar thisDate =
new GregorianCalendar();
1119 thisDate.setTimeInMillis(timestamp);
1120 int day = thisDate.get(Calendar.DAY_OF_MONTH);
1121 aveTime = day * 1000000;
1124 statement.setString(1, metricName);
1125 statement.setString(2, host);
1126 statement.setLong(3, timestamp);
1127 statement.setLong(4, 0);
1128 statement.setDouble(5, aveTime / 1000000);
1129 statement.setDouble(6, 0);
1130 statement.setDouble(7, 0);
1132 statement.execute();
1136 }
catch (SQLException ex) {
1137 throw new HealthMonitorException(
"Error saving metric data to database", ex);
1144 }
catch (SQLException ex) {
1145 logger.log(Level.SEVERE,
"Error closing Connection.", ex);
1149 }
catch (CoordinationService.CoordinationServiceException ex) {
1150 throw new HealthMonitorException(
"Error releasing database lock", ex);
1164 Map<String, List<DatabaseTimingResult>> getTimingMetricsFromDatabase(
long timeRange)
throws HealthMonitorException {
1170 throw new HealthMonitorException(
"Health Monitor is not enabled");
1174 long minimumTimestamp = System.currentTimeMillis() - timeRange;
1178 throw new HealthMonitorException(
"Error getting database lock");
1183 throw new HealthMonitorException(
"Error getting database connection");
1186 Map<String, List<DatabaseTimingResult>> resultMap =
new HashMap<>();
1188 try (Statement statement = conn.createStatement();
1189 ResultSet resultSet = statement.executeQuery(
"SELECT * FROM timing_data WHERE timestamp > " + minimumTimestamp)) {
1191 while (resultSet.next()) {
1192 String name = resultSet.getString(
"name");
1193 DatabaseTimingResult timingResult =
new DatabaseTimingResult(resultSet);
1195 if (resultMap.containsKey(name)) {
1196 resultMap.get(name).add(timingResult);
1198 List<DatabaseTimingResult> resultList =
new ArrayList<>();
1199 resultList.add(timingResult);
1200 resultMap.put(name, resultList);
1204 }
catch (SQLException ex) {
1205 throw new HealthMonitorException(
"Error reading timing metrics from database", ex);
1209 }
catch (SQLException ex) {
1210 logger.log(Level.SEVERE,
"Error closing Connection.", ex);
1213 }
catch (CoordinationService.CoordinationServiceException ex) {
1214 throw new HealthMonitorException(
"Error getting database lock", ex);
1227 List<UserData> getUserMetricsFromDatabase(
long timeRange)
throws HealthMonitorException {
1233 throw new HealthMonitorException(
"Health Monitor is not enabled");
1237 long minimumTimestamp = System.currentTimeMillis() - timeRange;
1241 throw new HealthMonitorException(
"Error getting database lock");
1244 List<UserData> resultList =
new ArrayList<>();
1246 try (Connection conn =
connect();
1247 Statement statement = conn.createStatement();
1248 ResultSet resultSet = statement.executeQuery(
"SELECT * FROM user_data WHERE timestamp > " + minimumTimestamp)) {
1250 while (resultSet.next()) {
1251 resultList.add(
new UserData(resultSet));
1254 }
catch (SQLException ex) {
1255 throw new HealthMonitorException(
"Error reading user metrics from database", ex);
1257 }
catch (CoordinationService.CoordinationServiceException ex) {
1258 throw new HealthMonitorException(
"Error getting database lock", ex);
1277 throw new HealthMonitorException(
"Error acquiring database lock");
1279 throw new HealthMonitorException(
"Error acquiring database lock", ex);
1299 throw new HealthMonitorException(
"Error acquiring database lock");
1301 throw new HealthMonitorException(
"Error acquiring database lock");
1340 if (v.value ==
value) {
1344 throw new HealthMonitorException(
"Can not create UserEvent from unknown value " +
value);
1366 return (!this.equals(
LOG_OFF));
1374 static class UserData
implements Comparable<UserData> {
1376 private final UserEvent eventType;
1377 private long timestamp;
1378 private final boolean isExaminer;
1379 private final String hostname;
1381 private String caseName;
1389 private UserData(UserEvent eventType) {
1390 this.eventType = eventType;
1391 this.timestamp = System.currentTimeMillis();
1399 }
catch (NoCurrentCaseException ex) {
1413 UserData(ResultSet resultSet)
throws SQLException, HealthMonitorException {
1414 this.timestamp = resultSet.getLong(
"timestamp");
1415 this.hostname = resultSet.getString(
"host");
1416 this.eventType = UserEvent.valueOf(resultSet.getInt(
"event_type"));
1417 this.isExaminer = resultSet.getBoolean(
"is_examiner");
1418 this.caseName = resultSet.getString(
"case_name");
1419 this.username = resultSet.getString(
"username");
1420 if (this.username ==
null) {
1433 static UserData createDummyUserData(
long timestamp) {
1434 UserData userData =
new UserData(UserEvent.CASE_CLOSE);
1435 userData.timestamp = timestamp;
1444 long getTimestamp() {
1453 String getHostname() {
1462 UserEvent getEventType() {
1471 boolean isExaminerNode() {
1480 String getCaseName() {
1489 String getUserName() {
1494 public int compareTo(UserData otherData) {
1495 return Long.compare(getTimestamp(), otherData.getTimestamp());
1506 private class TimingInfo {
1513 TimingInfo(
TimingMetric metric)
throws HealthMonitorException {
1515 sum = metric.getDuration();
1516 max = metric.getDuration();
1517 min = metric.getDuration();
1530 void addMetric(
TimingMetric metric)
throws HealthMonitorException {
1534 sum += metric.getDuration();
1537 if (
max < metric.getDuration()) {
1538 max = metric.getDuration();
1542 if (
min > metric.getDuration()) {
1543 min = metric.getDuration();
1552 double getAverage() {
1588 static class DatabaseTimingResult {
1590 private final long timestamp;
1591 private final String hostname;
1592 private final long count;
1593 private final double average;
1594 private final double max;
1595 private final double min;
1597 DatabaseTimingResult(ResultSet resultSet)
throws SQLException {
1598 this.timestamp = resultSet.getLong(
"timestamp");
1599 this.hostname = resultSet.getString(
"host");
1600 this.count = resultSet.getLong(
"count");
1601 this.average = resultSet.getDouble(
"average");
1602 this.max = resultSet.getDouble(
"max");
1603 this.min = resultSet.getDouble(
"min");
1611 long getTimestamp() {
1620 double getAverage() {
1656 String getHostName() {