Autopsy  4.19.3
Graphical digital forensics platform for The Sleuth Kit and other tools.
HealthMonitor.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2018 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.healthmonitor;
20 
21 import com.google.common.util.concurrent.ThreadFactoryBuilder;
22 import java.beans.PropertyChangeEvent;
23 import java.beans.PropertyChangeListener;
24 import java.sql.Connection;
25 import java.sql.DriverManager;
26 import java.sql.PreparedStatement;
27 import java.sql.ResultSet;
28 import java.sql.SQLException;
29 import java.sql.Statement;
30 import java.util.Map;
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.ArrayList;
34 import java.util.Calendar;
35 import java.util.GregorianCalendar;
36 import java.util.UUID;
37 import java.util.concurrent.ScheduledThreadPoolExecutor;
38 import java.util.concurrent.TimeUnit;
39 import java.util.concurrent.atomic.AtomicBoolean;
40 import java.util.logging.Level;
41 import java.util.Random;
42 import org.apache.commons.dbcp2.BasicDataSource;
50 import org.sleuthkit.datamodel.CaseDbConnectionInfo;
51 import org.sleuthkit.datamodel.CaseDbSchemaVersionNumber;
52 import org.sleuthkit.datamodel.Image;
53 import org.sleuthkit.datamodel.SleuthkitCase;
54 import org.sleuthkit.datamodel.TskCoreException;
55 
63 public final class HealthMonitor implements PropertyChangeListener {
64 
65  private final static Logger logger = Logger.getLogger(HealthMonitor.class.getName());
66  private final static String DATABASE_NAME = "HealthMonitor";
67  private final static long DATABASE_WRITE_INTERVAL = 60; // Minutes
68  private final static CaseDbSchemaVersionNumber CURRENT_DB_SCHEMA_VERSION = new CaseDbSchemaVersionNumber(1, 2);
69 
70  private final static AtomicBoolean isEnabled = new AtomicBoolean(false);
71  private static HealthMonitor instance;
72 
73  private ScheduledThreadPoolExecutor healthMonitorOutputTimer;
74  private final Map<String, TimingInfo> timingInfoMap;
75  private final List<UserData> userInfoList;
76  private final static int CONN_POOL_SIZE = 10;
77  private BasicDataSource connectionPool = null;
78  private CaseDbConnectionInfo connectionSettingsInUse = null;
79  private String hostName;
80  private final String username;
81 
82  private HealthMonitor() throws HealthMonitorException {
83 
84  // Create the map to collect timing metrics. The map will exist regardless
85  // of whether the monitor is enabled.
86  timingInfoMap = new HashMap<>();
87 
88  // Create the list to hold user information. The list will exist regardless
89  // of whether the monitor is enabled.
90  userInfoList = new ArrayList<>();
91 
92  // Get the host name
93  try {
94  hostName = java.net.InetAddress.getLocalHost().getHostName();
95  } catch (java.net.UnknownHostException ex) {
96  // Continue on, but log the error and generate a UUID to use for this session
97  hostName = UUID.randomUUID().toString();
98  logger.log(Level.SEVERE, "Unable to look up host name - falling back to UUID " + hostName, ex);
99  }
100 
101  // Get the user name
102  username = System.getProperty("user.name");
103 
104  // Read from the database to determine if the module is enabled
105  updateFromGlobalEnabledStatus();
106 
107  // Start the timer for database checks and writes
108  startTimer();
109  }
110 
118  synchronized static HealthMonitor getInstance() throws HealthMonitorException {
119  if (instance == null) {
120  instance = new HealthMonitor();
122  }
123  return instance;
124  }
125 
133  private synchronized void activateMonitorLocally() throws HealthMonitorException {
134 
135  logger.log(Level.INFO, "Activating Servies Health Monitor");
136 
137  // Make sure there are no left over connections to an old database
139 
141  throw new HealthMonitorException("Multi user mode is not enabled - can not activate health monitor");
142  }
143 
144  // Set up database (if needed)
146  if (lock == null) {
147  throw new HealthMonitorException("Error getting database lock");
148  }
149 
150  // Check if the database exists
151  if (!databaseExists()) {
152 
153  // If not, create a new one
154  createDatabase();
155  }
156 
157  if (!databaseIsInitialized()) {
159  }
160 
161  if (getVersion().compareTo(CURRENT_DB_SCHEMA_VERSION) < 0) {
163  }
164 
166  throw new HealthMonitorException("Error releasing database lock", ex);
167  }
168 
169  // Clear out any old data
170  timingInfoMap.clear();
171  userInfoList.clear();
172  }
173 
177  private void upgradeDatabaseSchema() throws HealthMonitorException {
178 
179  logger.log(Level.INFO, "Upgrading Health Monitor database");
180  CaseDbSchemaVersionNumber currentSchema = getVersion();
181 
182  Connection conn = connect();
183  if (conn == null) {
184  throw new HealthMonitorException("Error getting database connection");
185  }
186  ResultSet resultSet = null;
187 
188  try (Statement statement = conn.createStatement()) {
189  conn.setAutoCommit(false);
190 
191  // NOTE: Due to a bug in the upgrade code, earlier versions of Autopsy will erroneously
192  // run the upgrade if the database is a higher version than it expects. Therefore all
193  // table changes must account for the possiblility of running multiple times.
194 
195  // Upgrade from 1.0 to 1.1
196  // Changes: user_data table added
197  if (currentSchema.compareTo(new CaseDbSchemaVersionNumber(1, 1)) < 0) {
198 
199  // Add the user_data table
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"
207  + ")");
208  }
209 
210  // Upgrade from 1.1 to 1.2
211  // Changes: username added to user_data table
212  if (currentSchema.compareTo(new CaseDbSchemaVersionNumber(1, 2)) < 0) {
213 
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()) {
218  // Add the user_data table
219  statement.execute("ALTER TABLE user_data ADD COLUMN username text");
220  }
221  }
222 
223  // Update the schema version
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'");
226 
227  conn.commit();
228  logger.log(Level.INFO, "Health Monitor database upgraded to version {0}", CURRENT_DB_SCHEMA_VERSION.toString());
229  } catch (SQLException ex) {
230  try {
231  conn.rollback();
232  } catch (SQLException ex2) {
233  logger.log(Level.SEVERE, "Rollback error");
234  }
235  throw new HealthMonitorException("Error upgrading database", ex);
236  } finally {
237  if (resultSet != null) {
238  try {
239  resultSet.close();
240  } catch (SQLException ex2) {
241  logger.log(Level.SEVERE, "Error closing result set");
242  }
243  }
244  try {
245  conn.close();
246  } catch (SQLException ex) {
247  logger.log(Level.SEVERE, "Error closing connection.", ex);
248  }
249  }
250  }
251 
260  private synchronized void deactivateMonitorLocally() throws HealthMonitorException {
261 
262  logger.log(Level.INFO, "Deactivating Servies Health Monitor");
263 
264  // Clear out the collected data
265  timingInfoMap.clear();
266 
267  // Shut down the connection pool
269  }
270 
275  private synchronized void startTimer() {
276  // Make sure the previous executor (if it exists) has been stopped
277  stopTimer();
278 
279  healthMonitorOutputTimer = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat("health_monitor_timer").build());
280  healthMonitorOutputTimer.scheduleWithFixedDelay(new PeriodicHealthMonitorTask(), DATABASE_WRITE_INTERVAL, DATABASE_WRITE_INTERVAL, TimeUnit.MINUTES);
281  }
282 
286  private synchronized void stopTimer() {
287  if (healthMonitorOutputTimer != null) {
288  ThreadUtils.shutDownTaskExecutor(healthMonitorOutputTimer);
289  }
290  }
291 
298  static synchronized void startUpIfEnabled() throws HealthMonitorException {
299  getInstance().addUserEvent(UserEvent.LOG_ON);
300  }
301 
308  static synchronized void shutdown() throws HealthMonitorException {
309  getInstance().addUserEvent(UserEvent.LOG_OFF);
310  recordMetrics();
311  }
312 
320  static synchronized void setEnabled(boolean enabled) throws HealthMonitorException {
321  if (enabled == isEnabled.get()) {
322  // The setting has not changed, so do nothing
323  return;
324  }
325 
326  if (enabled) {
327  getInstance().activateMonitorLocally();
328 
329  // If activateMonitor fails, we won't update this
330  getInstance().setGlobalEnabledStatusInDB(true);
331  isEnabled.set(true);
332  } else {
333  if (isEnabled.get()) {
334  // If we were enabled before, set the global state to disabled
335  getInstance().setGlobalEnabledStatusInDB(false);
336  }
337  isEnabled.set(false);
338  getInstance().deactivateMonitorLocally();
339  }
340  }
341 
353  public static TimingMetric getTimingMetric(String name) {
354  if (isEnabled.get()) {
355  return new TimingMetric(name);
356  }
357  return null;
358  }
359 
367  public static void submitTimingMetric(TimingMetric metric) {
368  if (isEnabled.get() && (metric != null)) {
369  metric.stopTiming();
370  try {
371  getInstance().addTimingMetric(metric);
372  } catch (HealthMonitorException ex) {
373  // We don't want calling methods to have to check for exceptions, so just log it
374  logger.log(Level.SEVERE, "Error adding timing metric", ex);
375  }
376  }
377  }
378 
390  public static void submitNormalizedTimingMetric(TimingMetric metric, long normalization) {
391  if (isEnabled.get() && (metric != null)) {
392  metric.stopTiming();
393  try {
394  metric.normalize(normalization);
395  getInstance().addTimingMetric(metric);
396  } catch (HealthMonitorException ex) {
397  // We don't want calling methods to have to check for exceptions, so just log it
398  logger.log(Level.SEVERE, "Error adding timing metric", ex);
399  }
400  }
401  }
402 
409  private void addTimingMetric(TimingMetric metric) throws HealthMonitorException {
410 
411  // Do as little as possible within the synchronized block to minimize
412  // blocking with multiple threads.
413  synchronized (this) {
414  // There's a small check-then-act situation here where isEnabled
415  // may have changed before reaching this code. This is fine -
416  // the map still exists and any extra data added after the monitor
417  // is disabled will be deleted if the monitor is re-enabled. This
418  // seems preferable to doing another check on isEnabled within
419  // the synchronized block.
420  if (timingInfoMap.containsKey(metric.getName())) {
421  timingInfoMap.get(metric.getName()).addMetric(metric);
422  } else {
423  timingInfoMap.put(metric.getName(), new TimingInfo(metric));
424  }
425  }
426  }
427 
433  private void addUserEvent(UserEvent eventType) {
434  UserData userInfo = new UserData(eventType);
435  synchronized (this) {
436  userInfoList.add(userInfo);
437  }
438  }
439 
449  private void performDatabaseQuery() throws HealthMonitorException {
450  try {
451  SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase();
452  TimingMetric metric = HealthMonitor.getTimingMetric("Database: getImages query");
453  List<Image> images = skCase.getImages();
454 
455  // Through testing we found that this normalization gives us fairly
456  // consistent results for different numbers of data sources.
457  long normalization = images.size();
458  if (images.isEmpty()) {
459  normalization += 2;
460  } else if (images.size() == 1) {
461  normalization += 3;
462  } else if (images.size() < 10) {
463  normalization += 5;
464  } else {
465  normalization += 7;
466  }
467 
468  HealthMonitor.submitNormalizedTimingMetric(metric, normalization);
469  } catch (NoCurrentCaseException ex) {
470  // If there's no case open, we just can't do the metrics.
471  } catch (TskCoreException ex) {
472  throw new HealthMonitorException("Error running getImages()", ex);
473  }
474  }
475 
481  private void gatherTimerBasedMetrics() throws HealthMonitorException {
483  }
484 
490  private void writeCurrentStateToDatabase() throws HealthMonitorException {
491 
492  Map<String, TimingInfo> timingMapCopy;
493  List<UserData> userDataCopy;
494 
495  // Do as little as possible within the synchronized block since it will
496  // block threads attempting to record metrics.
497  synchronized (this) {
498  if (!isEnabled.get()) {
499  return;
500  }
501 
502  // Make a shallow copy of the timing map. The map should be small - one entry
503  // per metric name.
504  timingMapCopy = new HashMap<>(timingInfoMap);
505  timingInfoMap.clear();
506 
507  userDataCopy = new ArrayList<>(userInfoList);
508  userInfoList.clear();
509  }
510 
511  // Check if there's anything to report
512  if (timingMapCopy.keySet().isEmpty() && userDataCopy.isEmpty()) {
513  return;
514  }
515 
516  logger.log(Level.INFO, "Writing health monitor metrics to database");
517 
518  // Write to the database
519  try (CoordinationService.Lock lock = getSharedDbLock()) {
520  if (lock == null) {
521  throw new HealthMonitorException("Error getting database lock");
522  }
523 
524  Connection conn = connect();
525  if (conn == null) {
526  throw new HealthMonitorException("Error getting database connection");
527  }
528 
529  // Add metrics to the database
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)) {
534 
535  for (String name : timingMapCopy.keySet()) {
536  TimingInfo info = timingMapCopy.get(name);
537 
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());
545 
546  timingStatement.execute();
547  }
548 
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();
557  }
558 
559  } catch (SQLException ex) {
560  throw new HealthMonitorException("Error saving metric data to database", ex);
561  } finally {
562  try {
563  conn.close();
564  } catch (SQLException ex) {
565  logger.log(Level.SEVERE, "Error closing Connection.", ex);
566  }
567  }
569  throw new HealthMonitorException("Error releasing database lock", ex);
570  }
571  }
572 
581  private boolean databaseExists() throws HealthMonitorException {
582  try {
583  // Use the same database settings as the case
584  CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo();
585  Class.forName("org.postgresql.Driver"); //NON-NLS
586  ResultSet rs = null;
587  try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS
588  Statement statement = connection.createStatement();) {
589  String createCommand = "SELECT 1 AS result FROM pg_database WHERE datname='" + DATABASE_NAME + "'";
590  rs = statement.executeQuery(createCommand);
591  if (rs.next()) {
592  return true;
593  }
594  } finally {
595  if (rs != null) {
596  rs.close();
597  }
598  }
599  } catch (UserPreferencesException | ClassNotFoundException | SQLException ex) {
600  throw new HealthMonitorException("Failed check for health monitor database", ex);
601  }
602  return false;
603  }
604 
610  private void createDatabase() throws HealthMonitorException {
611  try {
612  // Use the same database settings as the case
613  CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo();
614  Class.forName("org.postgresql.Driver"); //NON-NLS
615  try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS
616  Statement statement = connection.createStatement();) {
617  String createCommand = "CREATE DATABASE \"" + DATABASE_NAME + "\" OWNER \"" + db.getUserName() + "\""; //NON-NLS
618  statement.execute(createCommand);
619  }
620  logger.log(Level.INFO, "Created new health monitor database " + DATABASE_NAME);
621  } catch (UserPreferencesException | ClassNotFoundException | SQLException ex) {
622  throw new HealthMonitorException("Failed to delete health monitor database", ex);
623  }
624  }
625 
631  private void setupConnectionPool() throws HealthMonitorException {
632  try {
633  CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo();
634  connectionSettingsInUse = db;
635 
636  connectionPool = new BasicDataSource();
637  connectionPool.setDriverClassName("org.postgresql.Driver");
638 
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("/");
645  connectionURL.append(DATABASE_NAME);
646 
647  connectionPool.setUrl(connectionURL.toString());
648  connectionPool.setUsername(db.getUserName());
649  connectionPool.setPassword(db.getPassword());
650 
651  // tweak pool configuration
652  connectionPool.setInitialSize(3); // start with 3 connections
653  connectionPool.setMaxIdle(CONN_POOL_SIZE); // max of 10 idle connections
654  connectionPool.setValidationQuery("SELECT version()");
655  } catch (UserPreferencesException ex) {
656  throw new HealthMonitorException("Error loading database configuration", ex);
657  }
658  }
659 
665  private void shutdownConnections() throws HealthMonitorException {
666  try {
667  synchronized (this) {
668  if (connectionPool != null) {
669  connectionPool.close();
670  connectionPool = null; // force it to be re-created on next connect()
671  }
672  }
673  } catch (SQLException ex) {
674  throw new HealthMonitorException("Failed to close existing database connections.", ex); // NON-NLS
675  }
676  }
677 
685  private Connection connect() throws HealthMonitorException {
686  synchronized (this) {
687  if (connectionPool == null) {
689  }
690  }
691 
692  try {
693  return connectionPool.getConnection();
694  } catch (SQLException ex) {
695  throw new HealthMonitorException("Error getting connection from connection pool.", ex); // NON-NLS
696  }
697  }
698 
707  private boolean databaseIsInitialized() throws HealthMonitorException {
708  Connection conn = connect();
709  if (conn == null) {
710  throw new HealthMonitorException("Error getting database connection");
711  }
712  ResultSet resultSet = null;
713 
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) {
718  // This likely just means that the db_info table does not exist
719  return false;
720  } finally {
721  if (resultSet != null) {
722  try {
723  resultSet.close();
724  } catch (SQLException ex) {
725  logger.log(Level.SEVERE, "Error closing result set", ex);
726  }
727  }
728  try {
729  conn.close();
730  } catch (SQLException ex) {
731  logger.log(Level.SEVERE, "Error closing Connection.", ex);
732  }
733  }
734  }
735 
742  static boolean monitorIsEnabled() {
743  return isEnabled.get();
744  }
745 
752  synchronized void updateFromGlobalEnabledStatus() throws HealthMonitorException {
753 
754  boolean previouslyEnabled = monitorIsEnabled();
755 
756  // We can't even check the database if multi user settings aren't enabled.
757  if (!UserPreferences.getIsMultiUserModeEnabled()) {
758  isEnabled.set(false);
759 
760  if (previouslyEnabled) {
762  }
763  return;
764  }
765 
766  // If the health monitor database doesn't exist or if it is not initialized,
767  // then monitoring isn't enabled
768  if ((!databaseExists()) || (!databaseIsInitialized())) {
769  isEnabled.set(false);
770 
771  if (previouslyEnabled) {
773  }
774  return;
775  }
776 
777  // If we're currently enabled, check whether the multiuser settings have changed.
778  // If they have, force a reset on the connection pool.
779  if (previouslyEnabled && (connectionSettingsInUse != null)) {
780  try {
781  CaseDbConnectionInfo currentSettings = UserPreferences.getDatabaseConnectionInfo();
782  if (!(connectionSettingsInUse.getUserName().equals(currentSettings.getUserName())
783  && connectionSettingsInUse.getPassword().equals(currentSettings.getPassword())
784  && connectionSettingsInUse.getPort().equals(currentSettings.getPort())
785  && connectionSettingsInUse.getHost().equals(currentSettings.getHost()))) {
787  }
788  } catch (UserPreferencesException ex) {
789  throw new HealthMonitorException("Error reading database connection info", ex);
790  }
791  }
792 
793  boolean currentlyEnabled = getGlobalEnabledStatusFromDB();
794  if (currentlyEnabled != previouslyEnabled) {
795  if (!currentlyEnabled) {
796  isEnabled.set(false);
798  } else {
799  isEnabled.set(true);
801  }
802  }
803  }
804 
813  private boolean getGlobalEnabledStatusFromDB() throws HealthMonitorException {
814 
815  try (Connection conn = connect();
816  Statement statement = conn.createStatement();
817  ResultSet resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='MONITOR_ENABLED'")) {
818 
819  if (resultSet.next()) {
820  return (resultSet.getBoolean("value"));
821  }
822  throw new HealthMonitorException("No enabled status found in database");
823  } catch (SQLException ex) {
824  throw new HealthMonitorException("Error initializing database", ex);
825  }
826  }
827 
833  private void setGlobalEnabledStatusInDB(boolean status) throws HealthMonitorException {
834 
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);
840  }
841  }
842 
850  private CaseDbSchemaVersionNumber getVersion() throws HealthMonitorException {
851  Connection conn = connect();
852  if (conn == null) {
853  throw new HealthMonitorException("Error getting database connection");
854  }
855  ResultSet resultSet = null;
856 
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");
863  try {
864  minorVersion = Integer.parseInt(minorVersionStr);
865  } catch (NumberFormatException ex) {
866  throw new HealthMonitorException("Bad value for schema minor version (" + minorVersionStr + ") - database is corrupt");
867  }
868  }
869 
870  resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='SCHEMA_VERSION'");
871  if (resultSet.next()) {
872  String majorVersionStr = resultSet.getString("value");
873  try {
874  majorVersion = Integer.parseInt(majorVersionStr);
875  } catch (NumberFormatException ex) {
876  throw new HealthMonitorException("Bad value for schema version (" + majorVersionStr + ") - database is corrupt");
877  }
878  }
879 
880  return new CaseDbSchemaVersionNumber(majorVersion, minorVersion);
881  } catch (SQLException ex) {
882  throw new HealthMonitorException("Error initializing database", ex);
883  } finally {
884  if (resultSet != null) {
885  try {
886  resultSet.close();
887  } catch (SQLException ex) {
888  logger.log(Level.SEVERE, "Error closing result set", ex);
889  }
890  }
891  try {
892  conn.close();
893  } catch (SQLException ex) {
894  logger.log(Level.SEVERE, "Error closing Connection.", ex);
895  }
896  }
897  }
898 
904  private void initializeDatabaseSchema() throws HealthMonitorException {
905  Connection conn = connect();
906  if (conn == null) {
907  throw new HealthMonitorException("Error getting database connection");
908  }
909 
910  try (Statement statement = conn.createStatement()) {
911  conn.setAutoCommit(false);
912 
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"
922  + ")");
923 
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"
928  + ")");
929 
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,"
937  + "username text"
938  + ")");
939 
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')");
943 
944  conn.commit();
945  } catch (SQLException ex) {
946  try {
947  conn.rollback();
948  } catch (SQLException ex2) {
949  logger.log(Level.SEVERE, "Rollback error");
950  }
951  throw new HealthMonitorException("Error initializing database", ex);
952  } finally {
953  try {
954  conn.close();
955  } catch (SQLException ex) {
956  logger.log(Level.SEVERE, "Error closing connection.", ex);
957  }
958  }
959  }
960 
965  static final class PeriodicHealthMonitorTask implements Runnable {
966 
967  @Override
968  public void run() {
969  recordMetrics();
970  }
971  }
972 
979  private static void recordMetrics() {
980  try {
981  getInstance().updateFromGlobalEnabledStatus();
982  if (monitorIsEnabled()) {
983  getInstance().gatherTimerBasedMetrics();
984  getInstance().writeCurrentStateToDatabase();
985  }
986  } catch (HealthMonitorException ex) {
987  logger.log(Level.SEVERE, "Error recording health monitor metrics", ex); //NON-NLS
988  }
989  }
990 
991  @Override
992  public void propertyChange(PropertyChangeEvent evt) {
993 
994  switch (Case.Events.valueOf(evt.getPropertyName())) {
995 
996  case CURRENT_CASE:
997  if ((null == evt.getNewValue()) && (evt.getOldValue() instanceof Case)) {
998  // Case is closing
999  addUserEvent(UserEvent.CASE_CLOSE);
1000 
1001  } else if ((null == evt.getOldValue()) && (evt.getNewValue() instanceof Case)) {
1002  // Case is opening
1003  addUserEvent(UserEvent.CASE_OPEN);
1004  }
1005  break;
1006  }
1007  }
1008 
1014  void populateDatabaseWithSampleData(int nDays, int nNodes, boolean createVerificationData) throws HealthMonitorException {
1015 
1016  if (!isEnabled.get()) {
1017  throw new HealthMonitorException("Can't populate database - monitor not enabled");
1018  }
1019 
1020  // Get the database lock
1021  CoordinationService.Lock lock = getSharedDbLock();
1022  if (lock == null) {
1023  throw new HealthMonitorException("Error getting database lock");
1024  }
1025 
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"}; // NON-NLS
1028 
1029  Random rand = new Random();
1030 
1031  long maxTimestamp = System.currentTimeMillis();
1032  long millisPerHour = 1000 * 60 * 60;
1033  long minTimestamp = maxTimestamp - (nDays * (millisPerHour * 24));
1034 
1035  Connection conn = null;
1036  try {
1037  conn = connect();
1038  if (conn == null) {
1039  throw new HealthMonitorException("Error getting database connection");
1040  }
1041 
1042  try (Statement statement = conn.createStatement()) {
1043 
1044  statement.execute("DELETE FROM timing_data"); // NON-NLS
1045  } catch (SQLException ex) {
1046  logger.log(Level.SEVERE, "Error clearing timing data", ex);
1047  return;
1048  }
1049 
1050  // Add timing metrics to the database
1051  String addTimingInfoSql = "INSERT INTO timing_data (name, host, timestamp, count, average, max, min) VALUES (?, ?, ?, ?, ?, ?, ?)";
1052  try (PreparedStatement statement = conn.prepareStatement(addTimingInfoSql)) {
1053 
1054  for (String metricName : metricNames) {
1055 
1056  long baseIndex = rand.nextInt(900) + 100;
1057  int multiplier = rand.nextInt(5);
1058  long minIndexTimeNanos;
1059  switch (multiplier) {
1060  case 0:
1061  minIndexTimeNanos = baseIndex;
1062  break;
1063  case 1:
1064  minIndexTimeNanos = baseIndex * 1000;
1065  break;
1066  default:
1067  minIndexTimeNanos = baseIndex * 1000 * 1000;
1068  break;
1069  }
1070 
1071  long maxIndexTimeOverMin = minIndexTimeNanos * 3;
1072 
1073  for (int node = 0; node < nNodes; node++) {
1074 
1075  String host = "testHost" + node; // NON-NLS
1076 
1077  double count = 0;
1078  double maxCount = nDays * 24 + 1;
1079 
1080  // Record data every hour, with a small amount of randomness about when it starts
1081  for (long timestamp = minTimestamp + rand.nextInt(1000 * 60 * 55); timestamp < maxTimestamp; timestamp += millisPerHour) {
1082 
1083  double aveTime;
1084 
1085  // This creates data that increases in the last couple of days of the simulated
1086  // collection
1087  count++;
1088  double slowNodeMultiplier = 1.0;
1089  if ((maxCount - count) <= 3 * 24) {
1090  slowNodeMultiplier += (3 - (maxCount - count) / 24) * 0.33;
1091  }
1092 
1093  if (!createVerificationData) {
1094  // Try to make a reasonable sample data set, with most points in a small range
1095  // but some higher and lower
1096  int outlierVal = rand.nextInt(30);
1097  long randVal = rand.nextLong();
1098  if (randVal < 0) {
1099  randVal *= -1;
1100  }
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);
1107  } else {
1108  aveTime = minIndexTimeNanos + randVal % maxIndexTimeOverMin;
1109  }
1110 
1111  if (node == 1) {
1112  aveTime = aveTime * slowNodeMultiplier;
1113  }
1114  } else {
1115  // Create a data set strictly for testing that the display is working
1116  // correctly. The average time will equal the day of the month from
1117  // the timestamp (in milliseconds)
1118  Calendar thisDate = new GregorianCalendar();
1119  thisDate.setTimeInMillis(timestamp);
1120  int day = thisDate.get(Calendar.DAY_OF_MONTH);
1121  aveTime = day * 1000000;
1122  }
1123 
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);
1131 
1132  statement.execute();
1133  }
1134  }
1135  }
1136  } catch (SQLException ex) {
1137  throw new HealthMonitorException("Error saving metric data to database", ex);
1138  }
1139  } finally {
1140  try {
1141  if (conn != null) {
1142  conn.close();
1143  }
1144  } catch (SQLException ex) {
1145  logger.log(Level.SEVERE, "Error closing Connection.", ex);
1146  }
1147  try {
1148  lock.release();
1149  } catch (CoordinationService.CoordinationServiceException ex) {
1150  throw new HealthMonitorException("Error releasing database lock", ex);
1151  }
1152  }
1153  }
1154 
1164  Map<String, List<DatabaseTimingResult>> getTimingMetricsFromDatabase(long timeRange) throws HealthMonitorException {
1165 
1166  // Make sure the monitor is enabled. It could theoretically get disabled after this
1167  // check but it doesn't seem worth holding a lock to ensure that it doesn't since that
1168  // may slow down ingest.
1169  if (!isEnabled.get()) {
1170  throw new HealthMonitorException("Health Monitor is not enabled");
1171  }
1172 
1173  // Calculate the smallest timestamp we should return
1174  long minimumTimestamp = System.currentTimeMillis() - timeRange;
1175 
1176  try (CoordinationService.Lock lock = getSharedDbLock()) {
1177  if (lock == null) {
1178  throw new HealthMonitorException("Error getting database lock");
1179  }
1180 
1181  Connection conn = connect();
1182  if (conn == null) {
1183  throw new HealthMonitorException("Error getting database connection");
1184  }
1185 
1186  Map<String, List<DatabaseTimingResult>> resultMap = new HashMap<>();
1187 
1188  try (Statement statement = conn.createStatement();
1189  ResultSet resultSet = statement.executeQuery("SELECT * FROM timing_data WHERE timestamp > " + minimumTimestamp)) {
1190 
1191  while (resultSet.next()) {
1192  String name = resultSet.getString("name");
1193  DatabaseTimingResult timingResult = new DatabaseTimingResult(resultSet);
1194 
1195  if (resultMap.containsKey(name)) {
1196  resultMap.get(name).add(timingResult);
1197  } else {
1198  List<DatabaseTimingResult> resultList = new ArrayList<>();
1199  resultList.add(timingResult);
1200  resultMap.put(name, resultList);
1201  }
1202  }
1203  return resultMap;
1204  } catch (SQLException ex) {
1205  throw new HealthMonitorException("Error reading timing metrics from database", ex);
1206  } finally {
1207  try {
1208  conn.close();
1209  } catch (SQLException ex) {
1210  logger.log(Level.SEVERE, "Error closing Connection.", ex);
1211  }
1212  }
1213  } catch (CoordinationService.CoordinationServiceException ex) {
1214  throw new HealthMonitorException("Error getting database lock", ex);
1215  }
1216  }
1217 
1227  List<UserData> getUserMetricsFromDatabase(long timeRange) throws HealthMonitorException {
1228 
1229  // Make sure the monitor is enabled. It could theoretically get disabled after this
1230  // check but it doesn't seem worth holding a lock to ensure that it doesn't since that
1231  // may slow down ingest.
1232  if (!isEnabled.get()) {
1233  throw new HealthMonitorException("Health Monitor is not enabled");
1234  }
1235 
1236  // Calculate the smallest timestamp we should return
1237  long minimumTimestamp = System.currentTimeMillis() - timeRange;
1238 
1239  try (CoordinationService.Lock lock = getSharedDbLock()) {
1240  if (lock == null) {
1241  throw new HealthMonitorException("Error getting database lock");
1242  }
1243 
1244  List<UserData> resultList = new ArrayList<>();
1245 
1246  try (Connection conn = connect();
1247  Statement statement = conn.createStatement();
1248  ResultSet resultSet = statement.executeQuery("SELECT * FROM user_data WHERE timestamp > " + minimumTimestamp)) {
1249 
1250  while (resultSet.next()) {
1251  resultList.add(new UserData(resultSet));
1252  }
1253  return resultList;
1254  } catch (SQLException ex) {
1255  throw new HealthMonitorException("Error reading user metrics from database", ex);
1256  }
1257  } catch (CoordinationService.CoordinationServiceException ex) {
1258  throw new HealthMonitorException("Error getting database lock", ex);
1259  }
1260  }
1261 
1270  private CoordinationService.Lock getExclusiveDbLock() throws HealthMonitorException {
1271  try {
1273 
1274  if (lock != null) {
1275  return lock;
1276  }
1277  throw new HealthMonitorException("Error acquiring database lock");
1278  } catch (InterruptedException | CoordinationService.CoordinationServiceException ex) {
1279  throw new HealthMonitorException("Error acquiring database lock", ex);
1280  }
1281  }
1282 
1291  private CoordinationService.Lock getSharedDbLock() throws HealthMonitorException {
1292  try {
1293  String databaseNodeName = DATABASE_NAME;
1295 
1296  if (lock != null) {
1297  return lock;
1298  }
1299  throw new HealthMonitorException("Error acquiring database lock");
1300  } catch (InterruptedException | CoordinationService.CoordinationServiceException ex) {
1301  throw new HealthMonitorException("Error acquiring database lock");
1302  }
1303  }
1304 
1308  enum UserEvent {
1309  LOG_ON(0),
1310  LOG_OFF(1),
1311  CASE_OPEN(2),
1312  CASE_CLOSE(3);
1313 
1314  int value;
1315 
1316  UserEvent(int value) {
1317  this.value = value;
1318  }
1319 
1325  int getEventValue() {
1326  return value;
1327  }
1328 
1338  static UserEvent valueOf(int value) throws HealthMonitorException {
1339  for (UserEvent v : UserEvent.values()) {
1340  if (v.value == value) {
1341  return v;
1342  }
1343  }
1344  throw new HealthMonitorException("Can not create UserEvent from unknown value " + value);
1345  }
1346 
1353  boolean caseIsOpen() {
1354  return (this.equals(CASE_OPEN));
1355  }
1356 
1363  boolean userIsLoggedIn() {
1364  // LOG_ON, CASE_OPEN, and CASE_CLOSED events all imply that the user
1365  // is logged in
1366  return (!this.equals(LOG_OFF));
1367  }
1368  }
1369 
1374  static class UserData implements Comparable<UserData> {
1375 
1376  private final UserEvent eventType;
1377  private long timestamp;
1378  private final boolean isExaminer;
1379  private final String hostname;
1380  private String username;
1381  private String caseName;
1382 
1389  private UserData(UserEvent eventType) {
1390  this.eventType = eventType;
1391  this.timestamp = System.currentTimeMillis();
1392  this.isExaminer = (UserPreferences.SelectedMode.STANDALONE == UserPreferences.getMode());
1393  this.hostname = "";
1394  this.username = "";
1395 
1396  // If there's a case open, record the name
1397  try {
1398  this.caseName = Case.getCurrentCaseThrows().getDisplayName();
1399  } catch (NoCurrentCaseException ex) {
1400  // It's not an error if there's no case open
1401  this.caseName = "";
1402  }
1403  }
1404 
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) {
1421  this.username = "";
1422  }
1423  }
1424 
1433  static UserData createDummyUserData(long timestamp) {
1434  UserData userData = new UserData(UserEvent.CASE_CLOSE);
1435  userData.timestamp = timestamp;
1436  return userData;
1437  }
1438 
1444  long getTimestamp() {
1445  return timestamp;
1446  }
1447 
1453  String getHostname() {
1454  return hostname;
1455  }
1456 
1462  UserEvent getEventType() {
1463  return eventType;
1464  }
1465 
1471  boolean isExaminerNode() {
1472  return isExaminer;
1473  }
1474 
1480  String getCaseName() {
1481  return caseName;
1482  }
1483 
1489  String getUserName() {
1490  return username;
1491  }
1492 
1493  @Override
1494  public int compareTo(UserData otherData) {
1495  return Long.compare(getTimestamp(), otherData.getTimestamp());
1496  }
1497  }
1498 
1506  private class TimingInfo {
1507 
1508  private long count; // Number of metrics collected
1509  private double sum; // Sum of the durations collected (nanoseconds)
1510  private double max; // Maximum value found (nanoseconds)
1511  private double min; // Minimum value found (nanoseconds)
1512 
1513  TimingInfo(TimingMetric metric) throws HealthMonitorException {
1514  count = 1;
1515  sum = metric.getDuration();
1516  max = metric.getDuration();
1517  min = metric.getDuration();
1518  }
1519 
1530  void addMetric(TimingMetric metric) throws HealthMonitorException {
1531 
1532  // Keep track of needed info to calculate the average
1533  count++;
1534  sum += metric.getDuration();
1535 
1536  // Check if this is the longest duration seen
1537  if (max < metric.getDuration()) {
1538  max = metric.getDuration();
1539  }
1540 
1541  // Check if this is the lowest duration seen
1542  if (min > metric.getDuration()) {
1543  min = metric.getDuration();
1544  }
1545  }
1546 
1552  double getAverage() {
1553  return sum / count;
1554  }
1555 
1561  double getMax() {
1562  return max;
1563  }
1564 
1570  double getMin() {
1571  return min;
1572  }
1573 
1579  long getCount() {
1580  return count;
1581  }
1582  }
1583 
1588  static class DatabaseTimingResult {
1589 
1590  private final long timestamp; // Time the metric was recorded
1591  private final String hostname; // Host that recorded the metric
1592  private final long count; // Number of metrics collected
1593  private final double average; // Average of the durations collected (milliseconds)
1594  private final double max; // Maximum value found (milliseconds)
1595  private final double min; // Minimum value found (milliseconds)
1596 
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");
1604  }
1605 
1611  long getTimestamp() {
1612  return timestamp;
1613  }
1614 
1620  double getAverage() {
1621  return average;
1622  }
1623 
1629  double getMax() {
1630  return max;
1631  }
1632 
1638  double getMin() {
1639  return min;
1640  }
1641 
1647  long getCount() {
1648  return count;
1649  }
1650 
1656  String getHostName() {
1657  return hostname;
1658  }
1659  }
1660 }
static final CaseDbSchemaVersionNumber CURRENT_DB_SCHEMA_VERSION
static CaseDbConnectionInfo getDatabaseConnectionInfo()
final Map< String, TimingInfo > timingInfoMap
static TimingMetric getTimingMetric(String name)
static void shutDownTaskExecutor(ExecutorService executor)
ScheduledThreadPoolExecutor healthMonitorOutputTimer
Lock tryGetExclusiveLock(CategoryNode category, String nodePath, int timeOut, TimeUnit timeUnit)
static void addPropertyChangeListener(PropertyChangeListener listener)
Definition: Case.java:674
static void submitTimingMetric(TimingMetric metric)
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
Lock tryGetSharedLock(CategoryNode category, String nodePath, int timeOut, TimeUnit timeUnit)
static void submitNormalizedTimingMetric(TimingMetric metric, long normalization)

Copyright © 2012-2022 Basis Technology. Generated on: Tue Jun 27 2023
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.