Autopsy  4.14.0
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, 1);
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 
81  private HealthMonitor() throws HealthMonitorException {
82 
83  // Create the map to collect timing metrics. The map will exist regardless
84  // of whether the monitor is enabled.
85  timingInfoMap = new HashMap<>();
86 
87  // Create the list to hold user information. The list will exist regardless
88  // of whether the monitor is enabled.
89  userInfoList = new ArrayList<>();
90 
91  // Get the host name
92  try {
93  hostName = java.net.InetAddress.getLocalHost().getHostName();
94  } catch (java.net.UnknownHostException ex) {
95  // Continue on, but log the error and generate a UUID to use for this session
96  hostName = UUID.randomUUID().toString();
97  logger.log(Level.SEVERE, "Unable to look up host name - falling back to UUID " + hostName, ex);
98  }
99 
100  // Read from the database to determine if the module is enabled
101  updateFromGlobalEnabledStatus();
102 
103  // Start the timer for database checks and writes
104  startTimer();
105  }
106 
114  synchronized static HealthMonitor getInstance() throws HealthMonitorException {
115  if (instance == null) {
116  instance = new HealthMonitor();
118  }
119  return instance;
120  }
121 
129  private synchronized void activateMonitorLocally() throws HealthMonitorException {
130 
131  logger.log(Level.INFO, "Activating Servies Health Monitor");
132 
133  // Make sure there are no left over connections to an old database
135 
137  throw new HealthMonitorException("Multi user mode is not enabled - can not activate health monitor");
138  }
139 
140  // Set up database (if needed)
142  if (lock == null) {
143  throw new HealthMonitorException("Error getting database lock");
144  }
145 
146  // Check if the database exists
147  if (!databaseExists()) {
148 
149  // If not, create a new one
150  createDatabase();
151  }
152 
153  if (!databaseIsInitialized()) {
155  }
156 
157  if (!CURRENT_DB_SCHEMA_VERSION.equals(getVersion())) {
159  }
160 
162  throw new HealthMonitorException("Error releasing database lock", ex);
163  }
164 
165  // Clear out any old data
166  timingInfoMap.clear();
167  userInfoList.clear();
168  }
169 
173  private void upgradeDatabaseSchema() throws HealthMonitorException {
174 
175  logger.log(Level.INFO, "Upgrading Health Monitor database");
176  CaseDbSchemaVersionNumber currentSchema = getVersion();
177 
178  Connection conn = connect();
179  if (conn == null) {
180  throw new HealthMonitorException("Error getting database connection");
181  }
182 
183  try (Statement statement = conn.createStatement()) {
184  conn.setAutoCommit(false);
185 
186  // Upgrade from 1.0 to 1.1
187  // Changes: user_data table added
188  if (currentSchema.compareTo(new CaseDbSchemaVersionNumber(1, 1)) < 0) {
189 
190  // Add the user_data table
191  statement.execute("CREATE TABLE IF NOT EXISTS user_data ("
192  + "id SERIAL PRIMARY KEY,"
193  + "host text NOT NULL,"
194  + "timestamp bigint NOT NULL,"
195  + "event_type int NOT NULL,"
196  + "is_examiner boolean NOT NULL,"
197  + "case_name text NOT NULL"
198  + ")");
199  }
200 
201  // Update the schema version
202  statement.execute("UPDATE db_info SET value='" + CURRENT_DB_SCHEMA_VERSION.getMajor() + "' WHERE name='SCHEMA_VERSION'");
203  statement.execute("UPDATE db_info SET value='" + CURRENT_DB_SCHEMA_VERSION.getMinor() + "' WHERE name='SCHEMA_MINOR_VERSION'");
204 
205  conn.commit();
206  logger.log(Level.INFO, "Health Monitor database upgraded to version {0}", CURRENT_DB_SCHEMA_VERSION.toString());
207  } catch (SQLException ex) {
208  try {
209  conn.rollback();
210  } catch (SQLException ex2) {
211  logger.log(Level.SEVERE, "Rollback error");
212  }
213  throw new HealthMonitorException("Error upgrading database", ex);
214  } finally {
215  try {
216  conn.close();
217  } catch (SQLException ex) {
218  logger.log(Level.SEVERE, "Error closing connection.", ex);
219  }
220  }
221  }
222 
231  private synchronized void deactivateMonitorLocally() throws HealthMonitorException {
232 
233  logger.log(Level.INFO, "Deactivating Servies Health Monitor");
234 
235  // Clear out the collected data
236  timingInfoMap.clear();
237 
238  // Shut down the connection pool
240  }
241 
246  private synchronized void startTimer() {
247  // Make sure the previous executor (if it exists) has been stopped
248  stopTimer();
249 
250  healthMonitorOutputTimer = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat("health_monitor_timer").build());
251  healthMonitorOutputTimer.scheduleWithFixedDelay(new PeriodicHealthMonitorTask(), DATABASE_WRITE_INTERVAL, DATABASE_WRITE_INTERVAL, TimeUnit.MINUTES);
252  }
253 
257  private synchronized void stopTimer() {
258  if (healthMonitorOutputTimer != null) {
259  ThreadUtils.shutDownTaskExecutor(healthMonitorOutputTimer);
260  }
261  }
262 
269  static synchronized void startUpIfEnabled() throws HealthMonitorException {
270  getInstance().addUserEvent(UserEvent.LOG_ON);
271  }
272 
279  static synchronized void shutdown() throws HealthMonitorException {
280  getInstance().addUserEvent(UserEvent.LOG_OFF);
281  recordMetrics();
282  }
283 
291  static synchronized void setEnabled(boolean enabled) throws HealthMonitorException {
292  if (enabled == isEnabled.get()) {
293  // The setting has not changed, so do nothing
294  return;
295  }
296 
297  if (enabled) {
298  getInstance().activateMonitorLocally();
299 
300  // If activateMonitor fails, we won't update this
301  getInstance().setGlobalEnabledStatusInDB(true);
302  isEnabled.set(true);
303  } else {
304  if (isEnabled.get()) {
305  // If we were enabled before, set the global state to disabled
306  getInstance().setGlobalEnabledStatusInDB(false);
307  }
308  isEnabled.set(false);
309  getInstance().deactivateMonitorLocally();
310  }
311  }
312 
324  public static TimingMetric getTimingMetric(String name) {
325  if (isEnabled.get()) {
326  return new TimingMetric(name);
327  }
328  return null;
329  }
330 
338  public static void submitTimingMetric(TimingMetric metric) {
339  if (isEnabled.get() && (metric != null)) {
340  metric.stopTiming();
341  try {
342  getInstance().addTimingMetric(metric);
343  } catch (HealthMonitorException ex) {
344  // We don't want calling methods to have to check for exceptions, so just log it
345  logger.log(Level.SEVERE, "Error adding timing metric", ex);
346  }
347  }
348  }
349 
361  public static void submitNormalizedTimingMetric(TimingMetric metric, long normalization) {
362  if (isEnabled.get() && (metric != null)) {
363  metric.stopTiming();
364  try {
365  metric.normalize(normalization);
366  getInstance().addTimingMetric(metric);
367  } catch (HealthMonitorException ex) {
368  // We don't want calling methods to have to check for exceptions, so just log it
369  logger.log(Level.SEVERE, "Error adding timing metric", ex);
370  }
371  }
372  }
373 
380  private void addTimingMetric(TimingMetric metric) throws HealthMonitorException {
381 
382  // Do as little as possible within the synchronized block to minimize
383  // blocking with multiple threads.
384  synchronized (this) {
385  // There's a small check-then-act situation here where isEnabled
386  // may have changed before reaching this code. This is fine -
387  // the map still exists and any extra data added after the monitor
388  // is disabled will be deleted if the monitor is re-enabled. This
389  // seems preferable to doing another check on isEnabled within
390  // the synchronized block.
391  if (timingInfoMap.containsKey(metric.getName())) {
392  timingInfoMap.get(metric.getName()).addMetric(metric);
393  } else {
394  timingInfoMap.put(metric.getName(), new TimingInfo(metric));
395  }
396  }
397  }
398 
404  private void addUserEvent(UserEvent eventType) {
405  UserData userInfo = new UserData(eventType);
406  synchronized (this) {
407  userInfoList.add(userInfo);
408  }
409  }
410 
420  private void performDatabaseQuery() throws HealthMonitorException {
421  try {
422  SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase();
423  TimingMetric metric = HealthMonitor.getTimingMetric("Database: getImages query");
424  List<Image> images = skCase.getImages();
425 
426  // Through testing we found that this normalization gives us fairly
427  // consistent results for different numbers of data sources.
428  long normalization = images.size();
429  if (images.isEmpty()) {
430  normalization += 2;
431  } else if (images.size() == 1) {
432  normalization += 3;
433  } else if (images.size() < 10) {
434  normalization += 5;
435  } else {
436  normalization += 7;
437  }
438 
439  HealthMonitor.submitNormalizedTimingMetric(metric, normalization);
440  } catch (NoCurrentCaseException ex) {
441  // If there's no case open, we just can't do the metrics.
442  } catch (TskCoreException ex) {
443  throw new HealthMonitorException("Error running getImages()", ex);
444  }
445  }
446 
452  private void gatherTimerBasedMetrics() throws HealthMonitorException {
454  }
455 
461  private void writeCurrentStateToDatabase() throws HealthMonitorException {
462 
463  Map<String, TimingInfo> timingMapCopy;
464  List<UserData> userDataCopy;
465 
466  // Do as little as possible within the synchronized block since it will
467  // block threads attempting to record metrics.
468  synchronized (this) {
469  if (!isEnabled.get()) {
470  return;
471  }
472 
473  // Make a shallow copy of the timing map. The map should be small - one entry
474  // per metric name.
475  timingMapCopy = new HashMap<>(timingInfoMap);
476  timingInfoMap.clear();
477 
478  userDataCopy = new ArrayList<>(userInfoList);
479  userInfoList.clear();
480  }
481 
482  // Check if there's anything to report
483  if (timingMapCopy.keySet().isEmpty() && userDataCopy.isEmpty()) {
484  return;
485  }
486 
487  logger.log(Level.INFO, "Writing health monitor metrics to database");
488 
489  // Write to the database
490  try (CoordinationService.Lock lock = getSharedDbLock()) {
491  if (lock == null) {
492  throw new HealthMonitorException("Error getting database lock");
493  }
494 
495  Connection conn = connect();
496  if (conn == null) {
497  throw new HealthMonitorException("Error getting database connection");
498  }
499 
500  // Add metrics to the database
501  String addTimingInfoSql = "INSERT INTO timing_data (name, host, timestamp, count, average, max, min) VALUES (?, ?, ?, ?, ?, ?, ?)";
502  String addUserInfoSql = "INSERT INTO user_data (host, timestamp, event_type, is_examiner, case_name) VALUES (?, ?, ?, ?, ?)";
503  try (PreparedStatement timingStatement = conn.prepareStatement(addTimingInfoSql);
504  PreparedStatement userStatement = conn.prepareStatement(addUserInfoSql)) {
505 
506  for (String name : timingMapCopy.keySet()) {
507  TimingInfo info = timingMapCopy.get(name);
508 
509  timingStatement.setString(1, name);
510  timingStatement.setString(2, hostName);
511  timingStatement.setLong(3, System.currentTimeMillis());
512  timingStatement.setLong(4, info.getCount());
513  timingStatement.setDouble(5, info.getAverage());
514  timingStatement.setDouble(6, info.getMax());
515  timingStatement.setDouble(7, info.getMin());
516 
517  timingStatement.execute();
518  }
519 
520  for (UserData userInfo : userDataCopy) {
521  userStatement.setString(1, hostName);
522  userStatement.setLong(2, userInfo.getTimestamp());
523  userStatement.setInt(3, userInfo.getEventType().getEventValue());
524  userStatement.setBoolean(4, userInfo.isExaminerNode());
525  userStatement.setString(5, userInfo.getCaseName());
526  userStatement.execute();
527  }
528 
529  } catch (SQLException ex) {
530  throw new HealthMonitorException("Error saving metric data to database", ex);
531  } finally {
532  try {
533  conn.close();
534  } catch (SQLException ex) {
535  logger.log(Level.SEVERE, "Error closing Connection.", ex);
536  }
537  }
539  throw new HealthMonitorException("Error releasing database lock", ex);
540  }
541  }
542 
551  private boolean databaseExists() throws HealthMonitorException {
552  try {
553  // Use the same database settings as the case
554  CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo();
555  Class.forName("org.postgresql.Driver"); //NON-NLS
556  ResultSet rs = null;
557  try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS
558  Statement statement = connection.createStatement();) {
559  String createCommand = "SELECT 1 AS result FROM pg_database WHERE datname='" + DATABASE_NAME + "'";
560  rs = statement.executeQuery(createCommand);
561  if (rs.next()) {
562  return true;
563  }
564  } finally {
565  if (rs != null) {
566  rs.close();
567  }
568  }
569  } catch (UserPreferencesException | ClassNotFoundException | SQLException ex) {
570  throw new HealthMonitorException("Failed check for health monitor database", ex);
571  }
572  return false;
573  }
574 
580  private void createDatabase() throws HealthMonitorException {
581  try {
582  // Use the same database settings as the case
583  CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo();
584  Class.forName("org.postgresql.Driver"); //NON-NLS
585  try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS
586  Statement statement = connection.createStatement();) {
587  String createCommand = "CREATE DATABASE \"" + DATABASE_NAME + "\" OWNER \"" + db.getUserName() + "\""; //NON-NLS
588  statement.execute(createCommand);
589  }
590  logger.log(Level.INFO, "Created new health monitor database " + DATABASE_NAME);
591  } catch (UserPreferencesException | ClassNotFoundException | SQLException ex) {
592  throw new HealthMonitorException("Failed to delete health monitor database", ex);
593  }
594  }
595 
601  private void setupConnectionPool() throws HealthMonitorException {
602  try {
603  CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo();
604  connectionSettingsInUse = db;
605 
606  connectionPool = new BasicDataSource();
607  connectionPool.setDriverClassName("org.postgresql.Driver");
608 
609  StringBuilder connectionURL = new StringBuilder();
610  connectionURL.append("jdbc:postgresql://");
611  connectionURL.append(db.getHost());
612  connectionURL.append(":");
613  connectionURL.append(db.getPort());
614  connectionURL.append("/");
615  connectionURL.append(DATABASE_NAME);
616 
617  connectionPool.setUrl(connectionURL.toString());
618  connectionPool.setUsername(db.getUserName());
619  connectionPool.setPassword(db.getPassword());
620 
621  // tweak pool configuration
622  connectionPool.setInitialSize(3); // start with 3 connections
623  connectionPool.setMaxIdle(CONN_POOL_SIZE); // max of 10 idle connections
624  connectionPool.setValidationQuery("SELECT version()");
625  } catch (UserPreferencesException ex) {
626  throw new HealthMonitorException("Error loading database configuration", ex);
627  }
628  }
629 
635  private void shutdownConnections() throws HealthMonitorException {
636  try {
637  synchronized (this) {
638  if (connectionPool != null) {
639  connectionPool.close();
640  connectionPool = null; // force it to be re-created on next connect()
641  }
642  }
643  } catch (SQLException ex) {
644  throw new HealthMonitorException("Failed to close existing database connections.", ex); // NON-NLS
645  }
646  }
647 
655  private Connection connect() throws HealthMonitorException {
656  synchronized (this) {
657  if (connectionPool == null) {
659  }
660  }
661 
662  try {
663  return connectionPool.getConnection();
664  } catch (SQLException ex) {
665  throw new HealthMonitorException("Error getting connection from connection pool.", ex); // NON-NLS
666  }
667  }
668 
677  private boolean databaseIsInitialized() throws HealthMonitorException {
678  Connection conn = connect();
679  if (conn == null) {
680  throw new HealthMonitorException("Error getting database connection");
681  }
682  ResultSet resultSet = null;
683 
684  try (Statement statement = conn.createStatement()) {
685  resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='SCHEMA_VERSION'");
686  return resultSet.next();
687  } catch (SQLException ex) {
688  // This likely just means that the db_info table does not exist
689  return false;
690  } finally {
691  if (resultSet != null) {
692  try {
693  resultSet.close();
694  } catch (SQLException ex) {
695  logger.log(Level.SEVERE, "Error closing result set", ex);
696  }
697  }
698  try {
699  conn.close();
700  } catch (SQLException ex) {
701  logger.log(Level.SEVERE, "Error closing Connection.", ex);
702  }
703  }
704  }
705 
712  static boolean monitorIsEnabled() {
713  return isEnabled.get();
714  }
715 
722  synchronized void updateFromGlobalEnabledStatus() throws HealthMonitorException {
723 
724  boolean previouslyEnabled = monitorIsEnabled();
725 
726  // We can't even check the database if multi user settings aren't enabled.
727  if (!UserPreferences.getIsMultiUserModeEnabled()) {
728  isEnabled.set(false);
729 
730  if (previouslyEnabled) {
732  }
733  return;
734  }
735 
736  // If the health monitor database doesn't exist or if it is not initialized,
737  // then monitoring isn't enabled
738  if ((!databaseExists()) || (!databaseIsInitialized())) {
739  isEnabled.set(false);
740 
741  if (previouslyEnabled) {
743  }
744  return;
745  }
746 
747  // If we're currently enabled, check whether the multiuser settings have changed.
748  // If they have, force a reset on the connection pool.
749  if (previouslyEnabled && (connectionSettingsInUse != null)) {
750  try {
751  CaseDbConnectionInfo currentSettings = UserPreferences.getDatabaseConnectionInfo();
752  if (!(connectionSettingsInUse.getUserName().equals(currentSettings.getUserName())
753  && connectionSettingsInUse.getPassword().equals(currentSettings.getPassword())
754  && connectionSettingsInUse.getPort().equals(currentSettings.getPort())
755  && connectionSettingsInUse.getHost().equals(currentSettings.getHost()))) {
757  }
758  } catch (UserPreferencesException ex) {
759  throw new HealthMonitorException("Error reading database connection info", ex);
760  }
761  }
762 
763  boolean currentlyEnabled = getGlobalEnabledStatusFromDB();
764  if (currentlyEnabled != previouslyEnabled) {
765  if (!currentlyEnabled) {
766  isEnabled.set(false);
768  } else {
769  isEnabled.set(true);
771  }
772  }
773  }
774 
783  private boolean getGlobalEnabledStatusFromDB() throws HealthMonitorException {
784 
785  try (Connection conn = connect();
786  Statement statement = conn.createStatement();
787  ResultSet resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='MONITOR_ENABLED'")) {
788 
789  if (resultSet.next()) {
790  return (resultSet.getBoolean("value"));
791  }
792  throw new HealthMonitorException("No enabled status found in database");
793  } catch (SQLException ex) {
794  throw new HealthMonitorException("Error initializing database", ex);
795  }
796  }
797 
803  private void setGlobalEnabledStatusInDB(boolean status) throws HealthMonitorException {
804 
805  try (Connection conn = connect();
806  Statement statement = conn.createStatement();) {
807  statement.execute("UPDATE db_info SET value='" + status + "' WHERE name='MONITOR_ENABLED'");
808  } catch (SQLException ex) {
809  throw new HealthMonitorException("Error setting enabled status", ex);
810  }
811  }
812 
820  private CaseDbSchemaVersionNumber getVersion() throws HealthMonitorException {
821  Connection conn = connect();
822  if (conn == null) {
823  throw new HealthMonitorException("Error getting database connection");
824  }
825  ResultSet resultSet = null;
826 
827  try (Statement statement = conn.createStatement()) {
828  int minorVersion = 0;
829  int majorVersion = 0;
830  resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='SCHEMA_MINOR_VERSION'");
831  if (resultSet.next()) {
832  String minorVersionStr = resultSet.getString("value");
833  try {
834  minorVersion = Integer.parseInt(minorVersionStr);
835  } catch (NumberFormatException ex) {
836  throw new HealthMonitorException("Bad value for schema minor version (" + minorVersionStr + ") - database is corrupt");
837  }
838  }
839 
840  resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='SCHEMA_VERSION'");
841  if (resultSet.next()) {
842  String majorVersionStr = resultSet.getString("value");
843  try {
844  majorVersion = Integer.parseInt(majorVersionStr);
845  } catch (NumberFormatException ex) {
846  throw new HealthMonitorException("Bad value for schema version (" + majorVersionStr + ") - database is corrupt");
847  }
848  }
849 
850  return new CaseDbSchemaVersionNumber(majorVersion, minorVersion);
851  } catch (SQLException ex) {
852  throw new HealthMonitorException("Error initializing database", ex);
853  } finally {
854  if (resultSet != null) {
855  try {
856  resultSet.close();
857  } catch (SQLException ex) {
858  logger.log(Level.SEVERE, "Error closing result set", ex);
859  }
860  }
861  try {
862  conn.close();
863  } catch (SQLException ex) {
864  logger.log(Level.SEVERE, "Error closing Connection.", ex);
865  }
866  }
867  }
868 
874  private void initializeDatabaseSchema() throws HealthMonitorException {
875  Connection conn = connect();
876  if (conn == null) {
877  throw new HealthMonitorException("Error getting database connection");
878  }
879 
880  try (Statement statement = conn.createStatement()) {
881  conn.setAutoCommit(false);
882 
883  statement.execute("CREATE TABLE IF NOT EXISTS timing_data ("
884  + "id SERIAL PRIMARY KEY,"
885  + "name text NOT NULL,"
886  + "host text NOT NULL,"
887  + "timestamp bigint NOT NULL,"
888  + "count bigint NOT NULL,"
889  + "average double precision NOT NULL,"
890  + "max double precision NOT NULL,"
891  + "min double precision NOT NULL"
892  + ")");
893 
894  statement.execute("CREATE TABLE IF NOT EXISTS db_info ("
895  + "id SERIAL PRIMARY KEY NOT NULL,"
896  + "name text NOT NULL,"
897  + "value text NOT NULL"
898  + ")");
899 
900  statement.execute("CREATE TABLE IF NOT EXISTS user_data ("
901  + "id SERIAL PRIMARY KEY,"
902  + "host text NOT NULL,"
903  + "timestamp bigint NOT NULL,"
904  + "event_type int NOT NULL,"
905  + "is_examiner BOOLEAN NOT NULL,"
906  + "case_name text NOT NULL"
907  + ")");
908 
909  statement.execute("INSERT INTO db_info (name, value) VALUES ('SCHEMA_VERSION', '" + CURRENT_DB_SCHEMA_VERSION.getMajor() + "')");
910  statement.execute("INSERT INTO db_info (name, value) VALUES ('SCHEMA_MINOR_VERSION', '" + CURRENT_DB_SCHEMA_VERSION.getMinor() + "')");
911  statement.execute("INSERT INTO db_info (name, value) VALUES ('MONITOR_ENABLED', 'true')");
912 
913  conn.commit();
914  } catch (SQLException ex) {
915  try {
916  conn.rollback();
917  } catch (SQLException ex2) {
918  logger.log(Level.SEVERE, "Rollback error");
919  }
920  throw new HealthMonitorException("Error initializing database", ex);
921  } finally {
922  try {
923  conn.close();
924  } catch (SQLException ex) {
925  logger.log(Level.SEVERE, "Error closing connection.", ex);
926  }
927  }
928  }
929 
934  static final class PeriodicHealthMonitorTask implements Runnable {
935 
936  @Override
937  public void run() {
938  recordMetrics();
939  }
940  }
941 
948  private static void recordMetrics() {
949  try {
950  getInstance().updateFromGlobalEnabledStatus();
951  if (monitorIsEnabled()) {
952  getInstance().gatherTimerBasedMetrics();
953  getInstance().writeCurrentStateToDatabase();
954  }
955  } catch (HealthMonitorException ex) {
956  logger.log(Level.SEVERE, "Error performing periodic task", ex); //NON-NLS
957  }
958  }
959 
960  @Override
961  public void propertyChange(PropertyChangeEvent evt) {
962 
963  switch (Case.Events.valueOf(evt.getPropertyName())) {
964 
965  case CURRENT_CASE:
966  if ((null == evt.getNewValue()) && (evt.getOldValue() instanceof Case)) {
967  // Case is closing
968  addUserEvent(UserEvent.CASE_CLOSE);
969 
970  } else if ((null == evt.getOldValue()) && (evt.getNewValue() instanceof Case)) {
971  // Case is opening
972  addUserEvent(UserEvent.CASE_OPEN);
973  }
974  break;
975  }
976  }
977 
983  void populateDatabaseWithSampleData(int nDays, int nNodes, boolean createVerificationData) throws HealthMonitorException {
984 
985  if (!isEnabled.get()) {
986  throw new HealthMonitorException("Can't populate database - monitor not enabled");
987  }
988 
989  // Get the database lock
990  CoordinationService.Lock lock = getSharedDbLock();
991  if (lock == null) {
992  throw new HealthMonitorException("Error getting database lock");
993  }
994 
995  String[] metricNames = {"Disk Reads: Hash calculation", "Database: getImages query", "Solr: Index chunk", "Solr: Connectivity check",
996  "Correlation Engine: Notable artifact query", "Correlation Engine: Bulk insert"}; // NON-NLS
997 
998  Random rand = new Random();
999 
1000  long maxTimestamp = System.currentTimeMillis();
1001  long millisPerHour = 1000 * 60 * 60;
1002  long minTimestamp = maxTimestamp - (nDays * (millisPerHour * 24));
1003 
1004  Connection conn = null;
1005  try {
1006  conn = connect();
1007  if (conn == null) {
1008  throw new HealthMonitorException("Error getting database connection");
1009  }
1010 
1011  try (Statement statement = conn.createStatement()) {
1012 
1013  statement.execute("DELETE FROM timing_data"); // NON-NLS
1014  } catch (SQLException ex) {
1015  logger.log(Level.SEVERE, "Error clearing timing data", ex);
1016  return;
1017  }
1018 
1019  // Add timing metrics to the database
1020  String addTimingInfoSql = "INSERT INTO timing_data (name, host, timestamp, count, average, max, min) VALUES (?, ?, ?, ?, ?, ?, ?)";
1021  try (PreparedStatement statement = conn.prepareStatement(addTimingInfoSql)) {
1022 
1023  for (String metricName : metricNames) {
1024 
1025  long baseIndex = rand.nextInt(900) + 100;
1026  int multiplier = rand.nextInt(5);
1027  long minIndexTimeNanos;
1028  switch (multiplier) {
1029  case 0:
1030  minIndexTimeNanos = baseIndex;
1031  break;
1032  case 1:
1033  minIndexTimeNanos = baseIndex * 1000;
1034  break;
1035  default:
1036  minIndexTimeNanos = baseIndex * 1000 * 1000;
1037  break;
1038  }
1039 
1040  long maxIndexTimeOverMin = minIndexTimeNanos * 3;
1041 
1042  for (int node = 0; node < nNodes; node++) {
1043 
1044  String host = "testHost" + node; // NON-NLS
1045 
1046  double count = 0;
1047  double maxCount = nDays * 24 + 1;
1048 
1049  // Record data every hour, with a small amount of randomness about when it starts
1050  for (long timestamp = minTimestamp + rand.nextInt(1000 * 60 * 55); timestamp < maxTimestamp; timestamp += millisPerHour) {
1051 
1052  double aveTime;
1053 
1054  // This creates data that increases in the last couple of days of the simulated
1055  // collection
1056  count++;
1057  double slowNodeMultiplier = 1.0;
1058  if ((maxCount - count) <= 3 * 24) {
1059  slowNodeMultiplier += (3 - (maxCount - count) / 24) * 0.33;
1060  }
1061 
1062  if (!createVerificationData) {
1063  // Try to make a reasonable sample data set, with most points in a small range
1064  // but some higher and lower
1065  int outlierVal = rand.nextInt(30);
1066  long randVal = rand.nextLong();
1067  if (randVal < 0) {
1068  randVal *= -1;
1069  }
1070  if (outlierVal < 2) {
1071  aveTime = minIndexTimeNanos + maxIndexTimeOverMin + randVal % maxIndexTimeOverMin;
1072  } else if (outlierVal == 2) {
1073  aveTime = (minIndexTimeNanos / 2) + randVal % (minIndexTimeNanos / 2);
1074  } else if (outlierVal < 17) {
1075  aveTime = minIndexTimeNanos + randVal % (maxIndexTimeOverMin / 2);
1076  } else {
1077  aveTime = minIndexTimeNanos + randVal % maxIndexTimeOverMin;
1078  }
1079 
1080  if (node == 1) {
1081  aveTime = aveTime * slowNodeMultiplier;
1082  }
1083  } else {
1084  // Create a data set strictly for testing that the display is working
1085  // correctly. The average time will equal the day of the month from
1086  // the timestamp (in milliseconds)
1087  Calendar thisDate = new GregorianCalendar();
1088  thisDate.setTimeInMillis(timestamp);
1089  int day = thisDate.get(Calendar.DAY_OF_MONTH);
1090  aveTime = day * 1000000;
1091  }
1092 
1093  statement.setString(1, metricName);
1094  statement.setString(2, host);
1095  statement.setLong(3, timestamp);
1096  statement.setLong(4, 0);
1097  statement.setDouble(5, aveTime / 1000000);
1098  statement.setDouble(6, 0);
1099  statement.setDouble(7, 0);
1100 
1101  statement.execute();
1102  }
1103  }
1104  }
1105  } catch (SQLException ex) {
1106  throw new HealthMonitorException("Error saving metric data to database", ex);
1107  }
1108  } finally {
1109  try {
1110  if (conn != null) {
1111  conn.close();
1112  }
1113  } catch (SQLException ex) {
1114  logger.log(Level.SEVERE, "Error closing Connection.", ex);
1115  }
1116  try {
1117  lock.release();
1118  } catch (CoordinationService.CoordinationServiceException ex) {
1119  throw new HealthMonitorException("Error releasing database lock", ex);
1120  }
1121  }
1122  }
1123 
1133  Map<String, List<DatabaseTimingResult>> getTimingMetricsFromDatabase(long timeRange) throws HealthMonitorException {
1134 
1135  // Make sure the monitor is enabled. It could theoretically get disabled after this
1136  // check but it doesn't seem worth holding a lock to ensure that it doesn't since that
1137  // may slow down ingest.
1138  if (!isEnabled.get()) {
1139  throw new HealthMonitorException("Health Monitor is not enabled");
1140  }
1141 
1142  // Calculate the smallest timestamp we should return
1143  long minimumTimestamp = System.currentTimeMillis() - timeRange;
1144 
1145  try (CoordinationService.Lock lock = getSharedDbLock()) {
1146  if (lock == null) {
1147  throw new HealthMonitorException("Error getting database lock");
1148  }
1149 
1150  Connection conn = connect();
1151  if (conn == null) {
1152  throw new HealthMonitorException("Error getting database connection");
1153  }
1154 
1155  Map<String, List<DatabaseTimingResult>> resultMap = new HashMap<>();
1156 
1157  try (Statement statement = conn.createStatement();
1158  ResultSet resultSet = statement.executeQuery("SELECT * FROM timing_data WHERE timestamp > " + minimumTimestamp)) {
1159 
1160  while (resultSet.next()) {
1161  String name = resultSet.getString("name");
1162  DatabaseTimingResult timingResult = new DatabaseTimingResult(resultSet);
1163 
1164  if (resultMap.containsKey(name)) {
1165  resultMap.get(name).add(timingResult);
1166  } else {
1167  List<DatabaseTimingResult> resultList = new ArrayList<>();
1168  resultList.add(timingResult);
1169  resultMap.put(name, resultList);
1170  }
1171  }
1172  return resultMap;
1173  } catch (SQLException ex) {
1174  throw new HealthMonitorException("Error reading timing metrics from database", ex);
1175  } finally {
1176  try {
1177  conn.close();
1178  } catch (SQLException ex) {
1179  logger.log(Level.SEVERE, "Error closing Connection.", ex);
1180  }
1181  }
1182  } catch (CoordinationService.CoordinationServiceException ex) {
1183  throw new HealthMonitorException("Error getting database lock", ex);
1184  }
1185  }
1186 
1196  List<UserData> getUserMetricsFromDatabase(long timeRange) throws HealthMonitorException {
1197 
1198  // Make sure the monitor is enabled. It could theoretically get disabled after this
1199  // check but it doesn't seem worth holding a lock to ensure that it doesn't since that
1200  // may slow down ingest.
1201  if (!isEnabled.get()) {
1202  throw new HealthMonitorException("Health Monitor is not enabled");
1203  }
1204 
1205  // Calculate the smallest timestamp we should return
1206  long minimumTimestamp = System.currentTimeMillis() - timeRange;
1207 
1208  try (CoordinationService.Lock lock = getSharedDbLock()) {
1209  if (lock == null) {
1210  throw new HealthMonitorException("Error getting database lock");
1211  }
1212 
1213  List<UserData> resultList = new ArrayList<>();
1214 
1215  try (Connection conn = connect();
1216  Statement statement = conn.createStatement();
1217  ResultSet resultSet = statement.executeQuery("SELECT * FROM user_data WHERE timestamp > " + minimumTimestamp)) {
1218 
1219  while (resultSet.next()) {
1220  resultList.add(new UserData(resultSet));
1221  }
1222  return resultList;
1223  } catch (SQLException ex) {
1224  throw new HealthMonitorException("Error reading user metrics from database", ex);
1225  }
1226  } catch (CoordinationService.CoordinationServiceException ex) {
1227  throw new HealthMonitorException("Error getting database lock", ex);
1228  }
1229  }
1230 
1239  private CoordinationService.Lock getExclusiveDbLock() throws HealthMonitorException {
1240  try {
1242 
1243  if (lock != null) {
1244  return lock;
1245  }
1246  throw new HealthMonitorException("Error acquiring database lock");
1247  } catch (InterruptedException | CoordinationService.CoordinationServiceException ex) {
1248  throw new HealthMonitorException("Error acquiring database lock", ex);
1249  }
1250  }
1251 
1260  private CoordinationService.Lock getSharedDbLock() throws HealthMonitorException {
1261  try {
1262  String databaseNodeName = DATABASE_NAME;
1264 
1265  if (lock != null) {
1266  return lock;
1267  }
1268  throw new HealthMonitorException("Error acquiring database lock");
1269  } catch (InterruptedException | CoordinationService.CoordinationServiceException ex) {
1270  throw new HealthMonitorException("Error acquiring database lock");
1271  }
1272  }
1273 
1277  enum UserEvent {
1278  LOG_ON(0),
1279  LOG_OFF(1),
1280  CASE_OPEN(2),
1281  CASE_CLOSE(3);
1282 
1283  int value;
1284 
1285  UserEvent(int value) {
1286  this.value = value;
1287  }
1288 
1294  int getEventValue() {
1295  return value;
1296  }
1297 
1307  static UserEvent valueOf(int value) throws HealthMonitorException {
1308  for (UserEvent v : UserEvent.values()) {
1309  if (v.value == value) {
1310  return v;
1311  }
1312  }
1313  throw new HealthMonitorException("Can not create UserEvent from unknown value " + value);
1314  }
1315 
1322  boolean caseIsOpen() {
1323  return (this.equals(CASE_OPEN));
1324  }
1325 
1332  boolean userIsLoggedIn() {
1333  // LOG_ON, CASE_OPEN, and CASE_CLOSED events all imply that the user
1334  // is logged in
1335  return (!this.equals(LOG_OFF));
1336  }
1337  }
1338 
1343  static class UserData {
1344 
1345  private final UserEvent eventType;
1346  private long timestamp;
1347  private final boolean isExaminer;
1348  private final String hostname;
1349  private String caseName;
1350 
1357  private UserData(UserEvent eventType) {
1358  this.eventType = eventType;
1359  this.timestamp = System.currentTimeMillis();
1360  this.isExaminer = (UserPreferences.SelectedMode.STANDALONE == UserPreferences.getMode());
1361  this.hostname = "";
1362 
1363  // If there's a case open, record the name
1364  try {
1365  this.caseName = Case.getCurrentCaseThrows().getDisplayName();
1366  } catch (NoCurrentCaseException ex) {
1367  // It's not an error if there's no case open
1368  this.caseName = "";
1369  }
1370  }
1371 
1380  UserData(ResultSet resultSet) throws SQLException, HealthMonitorException {
1381  this.timestamp = resultSet.getLong("timestamp");
1382  this.hostname = resultSet.getString("host");
1383  this.eventType = UserEvent.valueOf(resultSet.getInt("event_type"));
1384  this.isExaminer = resultSet.getBoolean("is_examiner");
1385  this.caseName = resultSet.getString("case_name");
1386  }
1387 
1396  static UserData createDummyUserData(long timestamp) {
1397  UserData userData = new UserData(UserEvent.CASE_CLOSE);
1398  userData.timestamp = timestamp;
1399  return userData;
1400  }
1401 
1407  long getTimestamp() {
1408  return timestamp;
1409  }
1410 
1416  String getHostname() {
1417  return hostname;
1418  }
1419 
1425  UserEvent getEventType() {
1426  return eventType;
1427  }
1428 
1434  boolean isExaminerNode() {
1435  return isExaminer;
1436  }
1437 
1443  String getCaseName() {
1444  return caseName;
1445  }
1446  }
1447 
1455  private class TimingInfo {
1456 
1457  private long count; // Number of metrics collected
1458  private double sum; // Sum of the durations collected (nanoseconds)
1459  private double max; // Maximum value found (nanoseconds)
1460  private double min; // Minimum value found (nanoseconds)
1461 
1462  TimingInfo(TimingMetric metric) throws HealthMonitorException {
1463  count = 1;
1464  sum = metric.getDuration();
1465  max = metric.getDuration();
1466  min = metric.getDuration();
1467  }
1468 
1479  void addMetric(TimingMetric metric) throws HealthMonitorException {
1480 
1481  // Keep track of needed info to calculate the average
1482  count++;
1483  sum += metric.getDuration();
1484 
1485  // Check if this is the longest duration seen
1486  if (max < metric.getDuration()) {
1487  max = metric.getDuration();
1488  }
1489 
1490  // Check if this is the lowest duration seen
1491  if (min > metric.getDuration()) {
1492  min = metric.getDuration();
1493  }
1494  }
1495 
1501  double getAverage() {
1502  return sum / count;
1503  }
1504 
1510  double getMax() {
1511  return max;
1512  }
1513 
1519  double getMin() {
1520  return min;
1521  }
1522 
1528  long getCount() {
1529  return count;
1530  }
1531  }
1532 
1537  static class DatabaseTimingResult {
1538 
1539  private final long timestamp; // Time the metric was recorded
1540  private final String hostname; // Host that recorded the metric
1541  private final long count; // Number of metrics collected
1542  private final double average; // Average of the durations collected (milliseconds)
1543  private final double max; // Maximum value found (milliseconds)
1544  private final double min; // Minimum value found (milliseconds)
1545 
1546  DatabaseTimingResult(ResultSet resultSet) throws SQLException {
1547  this.timestamp = resultSet.getLong("timestamp");
1548  this.hostname = resultSet.getString("host");
1549  this.count = resultSet.getLong("count");
1550  this.average = resultSet.getDouble("average");
1551  this.max = resultSet.getDouble("max");
1552  this.min = resultSet.getDouble("min");
1553  }
1554 
1560  long getTimestamp() {
1561  return timestamp;
1562  }
1563 
1569  double getAverage() {
1570  return average;
1571  }
1572 
1578  double getMax() {
1579  return max;
1580  }
1581 
1587  double getMin() {
1588  return min;
1589  }
1590 
1596  long getCount() {
1597  return count;
1598  }
1599 
1605  String getHostName() {
1606  return hostname;
1607  }
1608  }
1609 }
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:449
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-2020 Basis Technology. Generated on: Wed Apr 8 2020
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.