Autopsy  4.7.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
EnterpriseHealthMonitor.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 
56 
64 public final class EnterpriseHealthMonitor implements PropertyChangeListener {
65 
66  private final static Logger logger = Logger.getLogger(EnterpriseHealthMonitor.class.getName());
67  private final static String DATABASE_NAME = "EnterpriseHealthMonitor";
68  private final static long DATABASE_WRITE_INTERVAL = 60; // Minutes
69  public static final CaseDbSchemaVersionNumber CURRENT_DB_SCHEMA_VERSION
70  = new CaseDbSchemaVersionNumber(1, 1);
71 
72  private static final AtomicBoolean isEnabled = new AtomicBoolean(false);
74 
75  private ScheduledThreadPoolExecutor healthMonitorOutputTimer;
76  private final Map<String, TimingInfo> timingInfoMap;
77  private final List<UserData> userInfoList;
78  private static final int CONN_POOL_SIZE = 10;
79  private BasicDataSource connectionPool = null;
80  private CaseDbConnectionInfo connectionSettingsInUse = null;
81  private String hostName;
82 
83  private EnterpriseHealthMonitor() throws HealthMonitorException {
84 
85  // Create the map to collect timing metrics. The map will exist regardless
86  // of whether the monitor is enabled.
87  timingInfoMap = new HashMap<>();
88 
89  // Create the list to hold user information. The list will exist regardless
90  // of whether the monitor is enabled.
91  userInfoList = new ArrayList<>();
92 
93  // Get the host name
94  try {
95  hostName = java.net.InetAddress.getLocalHost().getHostName();
96  } catch (java.net.UnknownHostException ex) {
97  // Continue on, but log the error and generate a UUID to use for this session
98  hostName = UUID.randomUUID().toString();
99  logger.log(Level.SEVERE, "Unable to look up host name - falling back to UUID " + hostName, ex);
100  }
101 
102  // Read from the database to determine if the module is enabled
103  updateFromGlobalEnabledStatus();
104 
105  // Start the timer for database checks and writes
106  startTimer();
107  }
108 
114  synchronized static EnterpriseHealthMonitor getInstance() throws HealthMonitorException {
115  if (instance == null) {
116  instance = new EnterpriseHealthMonitor();
118  }
119  return instance;
120  }
121 
128  private synchronized void activateMonitorLocally() throws HealthMonitorException {
129 
130  logger.log(Level.INFO, "Activating Servies Health Monitor");
131 
132  // Make sure there are no left over connections to an old database
134 
136  throw new HealthMonitorException("Multi user mode is not enabled - can not activate health monitor");
137  }
138 
139  // Set up database (if needed)
141  if(lock == null) {
142  throw new HealthMonitorException("Error getting database lock");
143  }
144 
145  // Check if the database exists
146  if (! databaseExists()) {
147 
148  // If not, create a new one
149  createDatabase();
150  }
151 
152  if( ! databaseIsInitialized()) {
154  }
155 
156  if( ! CURRENT_DB_SCHEMA_VERSION.equals(getVersion())) {
158  }
159 
161  throw new HealthMonitorException("Error releasing database lock", ex);
162  }
163 
164  // Clear out any old data
165  timingInfoMap.clear();
166  userInfoList.clear();
167  }
168 
172  private void upgradeDatabaseSchema() throws HealthMonitorException {
173 
174  logger.log(Level.INFO, "Upgrading Health Monitor database");
175  CaseDbSchemaVersionNumber currentSchema = getVersion();
176 
177  Connection conn = connect();
178  if(conn == null) {
179  throw new HealthMonitorException("Error getting database connection");
180  }
181 
182  try (Statement statement = conn.createStatement()) {
183  conn.setAutoCommit(false);
184 
185  // Upgrade from 1.0 to 1.1
186  // Changes: user_data table added
187  if(currentSchema.compareTo(new CaseDbSchemaVersionNumber(1,1)) < 0) {
188 
189  // Add the user_data table
190  statement.execute("CREATE TABLE IF NOT EXISTS user_data ("+
191  "id SERIAL PRIMARY KEY," +
192  "host text NOT NULL," +
193  "timestamp bigint NOT NULL," +
194  "event_type int NOT NULL," +
195  "is_examiner boolean NOT NULL," +
196  "case_name text NOT NULL" +
197  ")");
198  }
199 
200  // Update the schema version
201  statement.execute("UPDATE db_info SET value='" + CURRENT_DB_SCHEMA_VERSION.getMajor() + "' WHERE name='SCHEMA_VERSION'");
202  statement.execute("UPDATE db_info SET value='" + CURRENT_DB_SCHEMA_VERSION.getMinor() + "' WHERE name='SCHEMA_MINOR_VERSION'");
203 
204  conn.commit();
205  logger.log(Level.INFO, "Health Monitor database upgraded to version {0}", CURRENT_DB_SCHEMA_VERSION.toString());
206  } catch (SQLException ex) {
207  try {
208  conn.rollback();
209  } catch (SQLException ex2) {
210  logger.log(Level.SEVERE, "Rollback error");
211  }
212  throw new HealthMonitorException("Error upgrading database", ex);
213  } finally {
214  try {
215  conn.close();
216  } catch (SQLException ex) {
217  logger.log(Level.SEVERE, "Error closing connection.", ex);
218  }
219  }
220  }
221 
229  private synchronized void deactivateMonitorLocally() throws HealthMonitorException {
230 
231  logger.log(Level.INFO, "Deactivating Servies Health Monitor");
232 
233  // Clear out the collected data
234  timingInfoMap.clear();
235 
236  // Shut down the connection pool
238  }
239 
243  private synchronized void startTimer() {
244  // Make sure the previous executor (if it exists) has been stopped
245  stopTimer();
246 
247  healthMonitorOutputTimer = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat("health_monitor_timer").build());
248  healthMonitorOutputTimer.scheduleWithFixedDelay(new PeriodicHealthMonitorTask(), DATABASE_WRITE_INTERVAL, DATABASE_WRITE_INTERVAL, TimeUnit.MINUTES);
249  }
250 
254  private synchronized void stopTimer() {
255  if(healthMonitorOutputTimer != null) {
256  ThreadUtils.shutDownTaskExecutor(healthMonitorOutputTimer);
257  }
258  }
259 
264  static synchronized void startUpIfEnabled() throws HealthMonitorException {
265  getInstance().addUserEvent(UserEvent.LOG_ON);
266  }
267 
273  static synchronized void shutdown() throws HealthMonitorException {
274  getInstance().addUserEvent(UserEvent.LOG_OFF);
275  recordMetrics();
276  }
277 
283  static synchronized void setEnabled(boolean enabled) throws HealthMonitorException {
284  if(enabled == isEnabled.get()) {
285  // The setting has not changed, so do nothing
286  return;
287  }
288 
289  if(enabled) {
290  getInstance().activateMonitorLocally();
291 
292  // If activateMonitor fails, we won't update this
293  getInstance().setGlobalEnabledStatusInDB(true);
294  isEnabled.set(true);
295  } else {
296  if(isEnabled.get()) {
297  // If we were enabled before, set the global state to disabled
298  getInstance().setGlobalEnabledStatusInDB(false);
299  }
300  isEnabled.set(false);
301  getInstance().deactivateMonitorLocally();
302  }
303  }
304 
315  public static TimingMetric getTimingMetric(String name) {
316  if(isEnabled.get()) {
317  return new TimingMetric(name);
318  }
319  return null;
320  }
321 
329  public static void submitTimingMetric(TimingMetric metric) {
330  if(isEnabled.get() && (metric != null)) {
331  metric.stopTiming();
332  try {
333  getInstance().addTimingMetric(metric);
334  } catch (HealthMonitorException ex) {
335  // We don't want calling methods to have to check for exceptions, so just log it
336  logger.log(Level.SEVERE, "Error adding timing metric", ex);
337  }
338  }
339  }
340 
350  public static void submitNormalizedTimingMetric(TimingMetric metric, long normalization) {
351  if(isEnabled.get() && (metric != null)) {
352  metric.stopTiming();
353  try {
354  metric.normalize(normalization);
355  getInstance().addTimingMetric(metric);
356  } catch (HealthMonitorException ex) {
357  // We don't want calling methods to have to check for exceptions, so just log it
358  logger.log(Level.SEVERE, "Error adding timing metric", ex);
359  }
360  }
361  }
362 
367  private void addTimingMetric(TimingMetric metric) throws HealthMonitorException {
368 
369  // Do as little as possible within the synchronized block to minimize
370  // blocking with multiple threads.
371  synchronized(this) {
372  // There's a small check-then-act situation here where isEnabled
373  // may have changed before reaching this code. This is fine -
374  // the map still exists and any extra data added after the monitor
375  // is disabled will be deleted if the monitor is re-enabled. This
376  // seems preferable to doing another check on isEnabled within
377  // the synchronized block.
378  if(timingInfoMap.containsKey(metric.getName())) {
379  timingInfoMap.get(metric.getName()).addMetric(metric);
380  } else {
381  timingInfoMap.put(metric.getName(), new TimingInfo(metric));
382  }
383  }
384  }
385 
390  private void addUserEvent(UserEvent eventType) {
391  UserData userInfo = new UserData(eventType);
392  synchronized(this) {
393  userInfoList.add(userInfo);
394  }
395  }
396 
406  private void performDatabaseQuery() throws HealthMonitorException {
407  try {
408  SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase();
409  TimingMetric metric = EnterpriseHealthMonitor.getTimingMetric("Database: getImages query");
410  List<Image> images = skCase.getImages();
411 
412  // Through testing we found that this normalization gives us fairly
413  // consistent results for different numbers of data sources.
414  long normalization = images.size();
415  if (images.isEmpty()) {
416  normalization += 2;
417  } else if (images.size() == 1){
418  normalization += 3;
419  } else if (images.size() < 10) {
420  normalization += 5;
421  } else {
422  normalization += 7;
423  }
424 
426  } catch (NoCurrentCaseException ex) {
427  // If there's no case open, we just can't do the metrics.
428  } catch (TskCoreException ex) {
429  throw new HealthMonitorException("Error running getImages()", ex);
430  }
431  }
432 
437  private void gatherTimerBasedMetrics() throws HealthMonitorException {
439  }
440 
445  private void writeCurrentStateToDatabase() throws HealthMonitorException {
446 
447  Map<String, TimingInfo> timingMapCopy;
448  List<UserData> userDataCopy;
449 
450  // Do as little as possible within the synchronized block since it will
451  // block threads attempting to record metrics.
452  synchronized(this) {
453  if(! isEnabled.get()) {
454  return;
455  }
456 
457  // Make a shallow copy of the timing map. The map should be small - one entry
458  // per metric name.
459  timingMapCopy = new HashMap<>(timingInfoMap);
460  timingInfoMap.clear();
461 
462  userDataCopy = new ArrayList<>(userInfoList);
463  userInfoList.clear();
464  }
465 
466  // Check if there's anything to report
467  if(timingMapCopy.keySet().isEmpty() && userDataCopy.isEmpty()) {
468  return;
469  }
470 
471  logger.log(Level.INFO, "Writing health monitor metrics to database");
472 
473  // Write to the database
474  try (CoordinationService.Lock lock = getSharedDbLock()) {
475  if(lock == null) {
476  throw new HealthMonitorException("Error getting database lock");
477  }
478 
479  Connection conn = connect();
480  if(conn == null) {
481  throw new HealthMonitorException("Error getting database connection");
482  }
483 
484  // Add metrics to the database
485  String addTimingInfoSql = "INSERT INTO timing_data (name, host, timestamp, count, average, max, min) VALUES (?, ?, ?, ?, ?, ?, ?)";
486  String addUserInfoSql = "INSERT INTO user_data (host, timestamp, event_type, is_examiner, case_name) VALUES (?, ?, ?, ?, ?)";
487  try (PreparedStatement timingStatement = conn.prepareStatement(addTimingInfoSql);
488  PreparedStatement userStatement = conn.prepareStatement(addUserInfoSql)) {
489 
490  for(String name:timingMapCopy.keySet()) {
491  TimingInfo info = timingMapCopy.get(name);
492 
493  timingStatement.setString(1, name);
494  timingStatement.setString(2, hostName);
495  timingStatement.setLong(3, System.currentTimeMillis());
496  timingStatement.setLong(4, info.getCount());
497  timingStatement.setDouble(5, info.getAverage());
498  timingStatement.setDouble(6, info.getMax());
499  timingStatement.setDouble(7, info.getMin());
500 
501  timingStatement.execute();
502  }
503 
504  for(UserData userInfo:userDataCopy) {
505  userStatement.setString(1, hostName);
506  userStatement.setLong(2, userInfo.getTimestamp());
507  userStatement.setInt(3, userInfo.getEventType().getEventValue());
508  userStatement.setBoolean(4, userInfo.isExaminerNode());
509  userStatement.setString(5, userInfo.getCaseName());
510  userStatement.execute();
511  }
512 
513  } catch (SQLException ex) {
514  throw new HealthMonitorException("Error saving metric data to database", ex);
515  } finally {
516  try {
517  conn.close();
518  } catch (SQLException ex) {
519  logger.log(Level.SEVERE, "Error closing Connection.", ex);
520  }
521  }
523  throw new HealthMonitorException("Error releasing database lock", ex);
524  }
525  }
526 
533  private boolean databaseExists() throws HealthMonitorException {
534  try {
535  // Use the same database settings as the case
536  CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo();
537  Class.forName("org.postgresql.Driver"); //NON-NLS
538  ResultSet rs = null;
539  try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS
540  Statement statement = connection.createStatement();) {
541  String createCommand = "SELECT 1 AS result FROM pg_database WHERE datname='" + DATABASE_NAME + "'";
542  rs = statement.executeQuery(createCommand);
543  if(rs.next()) {
544  return true;
545  }
546  } finally {
547  if(rs != null) {
548  rs.close();
549  }
550  }
551  } catch (UserPreferencesException | ClassNotFoundException | SQLException ex) {
552  throw new HealthMonitorException("Failed check for health monitor database", ex);
553  }
554  return false;
555  }
556 
561  private void createDatabase() throws HealthMonitorException {
562  try {
563  // Use the same database settings as the case
564  CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo();
565  Class.forName("org.postgresql.Driver"); //NON-NLS
566  try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS
567  Statement statement = connection.createStatement();) {
568  String createCommand = "CREATE DATABASE \"" + DATABASE_NAME + "\" OWNER \"" + db.getUserName() + "\""; //NON-NLS
569  statement.execute(createCommand);
570  }
571  logger.log(Level.INFO, "Created new health monitor database " + DATABASE_NAME);
572  } catch (UserPreferencesException | ClassNotFoundException | SQLException ex) {
573  throw new HealthMonitorException("Failed to delete health monitor database", ex);
574  }
575  }
576 
581  private void setupConnectionPool() throws HealthMonitorException {
582  try {
583  CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo();
584  connectionSettingsInUse = db;
585 
586  connectionPool = new BasicDataSource();
587  connectionPool.setDriverClassName("org.postgresql.Driver");
588 
589  StringBuilder connectionURL = new StringBuilder();
590  connectionURL.append("jdbc:postgresql://");
591  connectionURL.append(db.getHost());
592  connectionURL.append(":");
593  connectionURL.append(db.getPort());
594  connectionURL.append("/");
595  connectionURL.append(DATABASE_NAME);
596 
597  connectionPool.setUrl(connectionURL.toString());
598  connectionPool.setUsername(db.getUserName());
599  connectionPool.setPassword(db.getPassword());
600 
601  // tweak pool configuration
602  connectionPool.setInitialSize(3); // start with 3 connections
603  connectionPool.setMaxIdle(CONN_POOL_SIZE); // max of 10 idle connections
604  connectionPool.setValidationQuery("SELECT version()");
605  } catch (UserPreferencesException ex) {
606  throw new HealthMonitorException("Error loading database configuration", ex);
607  }
608  }
609 
614  private void shutdownConnections() throws HealthMonitorException {
615  try {
616  synchronized(this) {
617  if(connectionPool != null){
618  connectionPool.close();
619  connectionPool = null; // force it to be re-created on next connect()
620  }
621  }
622  } catch (SQLException ex) {
623  throw new HealthMonitorException("Failed to close existing database connections.", ex); // NON-NLS
624  }
625  }
626 
633  private Connection connect() throws HealthMonitorException {
634  synchronized (this) {
635  if (connectionPool == null) {
637  }
638  }
639 
640  try {
641  return connectionPool.getConnection();
642  } catch (SQLException ex) {
643  throw new HealthMonitorException("Error getting connection from connection pool.", ex); // NON-NLS
644  }
645  }
646 
653  private boolean databaseIsInitialized() throws HealthMonitorException {
654  Connection conn = connect();
655  if(conn == null) {
656  throw new HealthMonitorException("Error getting database connection");
657  }
658  ResultSet resultSet = null;
659 
660  try (Statement statement = conn.createStatement()) {
661  resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='SCHEMA_VERSION'");
662  return resultSet.next();
663  } catch (SQLException ex) {
664  // This likely just means that the db_info table does not exist
665  return false;
666  } finally {
667  if(resultSet != null) {
668  try {
669  resultSet.close();
670  } catch (SQLException ex) {
671  logger.log(Level.SEVERE, "Error closing result set", ex);
672  }
673  }
674  try {
675  conn.close();
676  } catch (SQLException ex) {
677  logger.log(Level.SEVERE, "Error closing Connection.", ex);
678  }
679  }
680  }
681 
687  static boolean monitorIsEnabled() {
688  return isEnabled.get();
689  }
690 
696  synchronized void updateFromGlobalEnabledStatus() throws HealthMonitorException {
697 
698  boolean previouslyEnabled = monitorIsEnabled();
699 
700  // We can't even check the database if multi user settings aren't enabled.
701  if (!UserPreferences.getIsMultiUserModeEnabled()) {
702  isEnabled.set(false);
703 
704  if(previouslyEnabled) {
706  }
707  return;
708  }
709 
710  // If the health monitor database doesn't exist or if it is not initialized,
711  // then monitoring isn't enabled
712  if ((! databaseExists()) || (! databaseIsInitialized())) {
713  isEnabled.set(false);
714 
715  if(previouslyEnabled) {
717  }
718  return;
719  }
720 
721  // If we're currently enabled, check whether the multiuser settings have changed.
722  // If they have, force a reset on the connection pool.
723  if(previouslyEnabled && (connectionSettingsInUse != null)) {
724  try {
725  CaseDbConnectionInfo currentSettings = UserPreferences.getDatabaseConnectionInfo();
726  if(! (connectionSettingsInUse.getUserName().equals(currentSettings.getUserName())
727  && connectionSettingsInUse.getPassword().equals(currentSettings.getPassword())
728  && connectionSettingsInUse.getPort().equals(currentSettings.getPort())
729  && connectionSettingsInUse.getHost().equals(currentSettings.getHost()) )) {
731  }
732  } catch (UserPreferencesException ex) {
733  throw new HealthMonitorException("Error reading database connection info", ex);
734  }
735  }
736 
737  boolean currentlyEnabled = getGlobalEnabledStatusFromDB();
738  if( currentlyEnabled != previouslyEnabled) {
739  if( ! currentlyEnabled ) {
740  isEnabled.set(false);
742  } else {
743  isEnabled.set(true);
745  }
746  }
747  }
748 
755  private boolean getGlobalEnabledStatusFromDB() throws HealthMonitorException {
756 
757  try (Connection conn = connect();
758  Statement statement = conn.createStatement();
759  ResultSet resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='MONITOR_ENABLED'")) {
760 
761  if (resultSet.next()) {
762  return(resultSet.getBoolean("value"));
763  }
764  throw new HealthMonitorException("No enabled status found in database");
765  } catch (SQLException ex) {
766  throw new HealthMonitorException("Error initializing database", ex);
767  }
768  }
769 
774  private void setGlobalEnabledStatusInDB(boolean status) throws HealthMonitorException {
775 
776  try (Connection conn = connect();
777  Statement statement = conn.createStatement();) {
778  statement.execute("UPDATE db_info SET value='" + status + "' WHERE name='MONITOR_ENABLED'");
779  } catch (SQLException ex) {
780  throw new HealthMonitorException("Error setting enabled status", ex);
781  }
782  }
783 
789  private CaseDbSchemaVersionNumber getVersion() throws HealthMonitorException {
790  Connection conn = connect();
791  if(conn == null) {
792  throw new HealthMonitorException("Error getting database connection");
793  }
794  ResultSet resultSet = null;
795 
796  try (Statement statement = conn.createStatement()) {
797  int minorVersion = 0;
798  int majorVersion = 0;
799  resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='SCHEMA_MINOR_VERSION'");
800  if (resultSet.next()) {
801  String minorVersionStr = resultSet.getString("value");
802  try {
803  minorVersion = Integer.parseInt(minorVersionStr);
804  } catch (NumberFormatException ex) {
805  throw new HealthMonitorException("Bad value for schema minor version (" + minorVersionStr + ") - database is corrupt");
806  }
807  }
808 
809  resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='SCHEMA_VERSION'");
810  if (resultSet.next()) {
811  String majorVersionStr = resultSet.getString("value");
812  try {
813  majorVersion = Integer.parseInt(majorVersionStr);
814  } catch (NumberFormatException ex) {
815  throw new HealthMonitorException("Bad value for schema version (" + majorVersionStr + ") - database is corrupt");
816  }
817  }
818 
819  return new CaseDbSchemaVersionNumber(majorVersion, minorVersion);
820  } catch (SQLException ex) {
821  throw new HealthMonitorException("Error initializing database", ex);
822  } finally {
823  if(resultSet != null) {
824  try {
825  resultSet.close();
826  } catch (SQLException ex) {
827  logger.log(Level.SEVERE, "Error closing result set", ex);
828  }
829  }
830  try {
831  conn.close();
832  } catch (SQLException ex) {
833  logger.log(Level.SEVERE, "Error closing Connection.", ex);
834  }
835  }
836  }
837 
842  private void initializeDatabaseSchema() throws HealthMonitorException {
843  Connection conn = connect();
844  if(conn == null) {
845  throw new HealthMonitorException("Error getting database connection");
846  }
847 
848  try (Statement statement = conn.createStatement()) {
849  conn.setAutoCommit(false);
850 
851  statement.execute("CREATE TABLE IF NOT EXISTS timing_data (" +
852  "id SERIAL PRIMARY KEY," +
853  "name text NOT NULL," +
854  "host text NOT NULL," +
855  "timestamp bigint NOT NULL," +
856  "count bigint NOT NULL," +
857  "average double precision NOT NULL," +
858  "max double precision NOT NULL," +
859  "min double precision NOT NULL" +
860  ")");
861 
862  statement.execute("CREATE TABLE IF NOT EXISTS db_info (" +
863  "id SERIAL PRIMARY KEY NOT NULL," +
864  "name text NOT NULL," +
865  "value text NOT NULL" +
866  ")");
867 
868  statement.execute("CREATE TABLE IF NOT EXISTS user_data ("+
869  "id SERIAL PRIMARY KEY," +
870  "host text NOT NULL," +
871  "timestamp bigint NOT NULL," +
872  "event_type int NOT NULL," +
873  "is_examiner BOOLEAN NOT NULL," +
874  "case_name text NOT NULL" +
875  ")");
876 
877 
878  statement.execute("INSERT INTO db_info (name, value) VALUES ('SCHEMA_VERSION', '" + CURRENT_DB_SCHEMA_VERSION.getMajor() + "')");
879  statement.execute("INSERT INTO db_info (name, value) VALUES ('SCHEMA_MINOR_VERSION', '" + CURRENT_DB_SCHEMA_VERSION.getMinor() + "')");
880  statement.execute("INSERT INTO db_info (name, value) VALUES ('MONITOR_ENABLED', 'true')");
881 
882  conn.commit();
883  } catch (SQLException ex) {
884  try {
885  conn.rollback();
886  } catch (SQLException ex2) {
887  logger.log(Level.SEVERE, "Rollback error");
888  }
889  throw new HealthMonitorException("Error initializing database", ex);
890  } finally {
891  try {
892  conn.close();
893  } catch (SQLException ex) {
894  logger.log(Level.SEVERE, "Error closing connection.", ex);
895  }
896  }
897  }
898 
903  static final class PeriodicHealthMonitorTask implements Runnable {
904 
905  @Override
906  public void run() {
907  recordMetrics();
908  }
909  }
910 
918  private static void recordMetrics() {
919  try {
920  getInstance().updateFromGlobalEnabledStatus();
921  if(monitorIsEnabled()) {
922  getInstance().gatherTimerBasedMetrics();
923  getInstance().writeCurrentStateToDatabase();
924  }
925  } catch (HealthMonitorException ex) {
926  logger.log(Level.SEVERE, "Error performing periodic task", ex); //NON-NLS
927  }
928  }
929 
930  @Override
931  public void propertyChange(PropertyChangeEvent evt) {
932 
933  switch (Case.Events.valueOf(evt.getPropertyName())) {
934 
935  case CURRENT_CASE:
936  if ((null == evt.getNewValue()) && (evt.getOldValue() instanceof Case)) {
937  // Case is closing
938  addUserEvent(UserEvent.CASE_CLOSE);
939 
940  } else if((null == evt.getOldValue()) && (evt.getNewValue() instanceof Case)) {
941  // Case is opening
942  addUserEvent(UserEvent.CASE_OPEN);
943  }
944  break;
945  }
946  }
947 
953  void populateDatabaseWithSampleData(int nDays, int nNodes, boolean createVerificationData) throws HealthMonitorException {
954 
955  if(! isEnabled.get()) {
956  throw new HealthMonitorException("Can't populate database - monitor not enabled");
957  }
958 
959  // Get the database lock
960  CoordinationService.Lock lock = getSharedDbLock();
961  if(lock == null) {
962  throw new HealthMonitorException("Error getting database lock");
963  }
964 
965  String[] metricNames = {"Disk Reads: Hash calculation", "Database: getImages query", "Solr: Index chunk", "Solr: Connectivity check",
966  "Correlation Engine: Notable artifact query", "Correlation Engine: Bulk insert"}; // NON-NLS
967 
968  Random rand = new Random();
969 
970  long maxTimestamp = System.currentTimeMillis();
971  long millisPerHour = 1000 * 60 * 60;
972  long minTimestamp = maxTimestamp - (nDays * (millisPerHour * 24));
973 
974  Connection conn = null;
975  try {
976  conn = connect();
977  if(conn == null) {
978  throw new HealthMonitorException("Error getting database connection");
979  }
980 
981  try (Statement statement = conn.createStatement()) {
982 
983  statement.execute("DELETE FROM timing_data"); // NON-NLS
984  } catch (SQLException ex) {
985  logger.log(Level.SEVERE, "Error clearing timing data", ex);
986  return;
987  }
988 
989 
990 
991  // Add timing metrics to the database
992  String addTimingInfoSql = "INSERT INTO timing_data (name, host, timestamp, count, average, max, min) VALUES (?, ?, ?, ?, ?, ?, ?)";
993  try (PreparedStatement statement = conn.prepareStatement(addTimingInfoSql)) {
994 
995  for(String metricName:metricNames) {
996 
997  long baseIndex = rand.nextInt(900) + 100;
998  int multiplier = rand.nextInt(5);
999  long minIndexTimeNanos;
1000  switch(multiplier) {
1001  case 0:
1002  minIndexTimeNanos = baseIndex;
1003  break;
1004  case 1:
1005  minIndexTimeNanos = baseIndex * 1000;
1006  break;
1007  default:
1008  minIndexTimeNanos = baseIndex * 1000 * 1000;
1009  break;
1010  }
1011 
1012  long maxIndexTimeOverMin = minIndexTimeNanos * 3;
1013 
1014  for(int node = 0;node < nNodes; node++) {
1015 
1016  String host = "testHost" + node; // NON-NLS
1017 
1018  double count = 0;
1019  double maxCount = nDays * 24 + 1;
1020 
1021  // Record data every hour, with a small amount of randomness about when it starts
1022  for(long timestamp = minTimestamp + rand.nextInt(1000 * 60 * 55);timestamp < maxTimestamp;timestamp += millisPerHour) {
1023 
1024  double aveTime;
1025 
1026  // This creates data that increases in the last couple of days of the simulated
1027  // collection
1028  count++;
1029  double slowNodeMultiplier = 1.0;
1030  if((maxCount - count) <= 3 * 24) {
1031  slowNodeMultiplier += (3 - (maxCount - count) / 24) * 0.33;
1032  }
1033 
1034  if( ! createVerificationData ) {
1035  // Try to make a reasonable sample data set, with most points in a small range
1036  // but some higher and lower
1037  int outlierVal = rand.nextInt(30);
1038  long randVal = rand.nextLong();
1039  if(randVal < 0) {
1040  randVal *= -1;
1041  }
1042  if(outlierVal < 2){
1043  aveTime = minIndexTimeNanos + maxIndexTimeOverMin + randVal % maxIndexTimeOverMin;
1044  } else if(outlierVal == 2){
1045  aveTime = (minIndexTimeNanos / 2) + randVal % (minIndexTimeNanos / 2);
1046  } else if(outlierVal < 17) {
1047  aveTime = minIndexTimeNanos + randVal % (maxIndexTimeOverMin / 2);
1048  } else {
1049  aveTime = minIndexTimeNanos + randVal % maxIndexTimeOverMin;
1050  }
1051 
1052  if(node == 1) {
1053  aveTime = aveTime * slowNodeMultiplier;
1054  }
1055  } else {
1056  // Create a data set strictly for testing that the display is working
1057  // correctly. The average time will equal the day of the month from
1058  // the timestamp (in milliseconds)
1059  Calendar thisDate = new GregorianCalendar();
1060  thisDate.setTimeInMillis(timestamp);
1061  int day = thisDate.get(Calendar.DAY_OF_MONTH);
1062  aveTime = day * 1000000;
1063  }
1064 
1065 
1066  statement.setString(1, metricName);
1067  statement.setString(2, host);
1068  statement.setLong(3, timestamp);
1069  statement.setLong(4, 0);
1070  statement.setDouble(5, aveTime / 1000000);
1071  statement.setDouble(6, 0);
1072  statement.setDouble(7, 0);
1073 
1074  statement.execute();
1075  }
1076  }
1077  }
1078  } catch (SQLException ex) {
1079  throw new HealthMonitorException("Error saving metric data to database", ex);
1080  }
1081  } finally {
1082  try {
1083  if(conn != null) {
1084  conn.close();
1085  }
1086  } catch (SQLException ex) {
1087  logger.log(Level.SEVERE, "Error closing Connection.", ex);
1088  }
1089  try {
1090  lock.release();
1091  } catch (CoordinationService.CoordinationServiceException ex) {
1092  throw new HealthMonitorException("Error releasing database lock", ex);
1093  }
1094  }
1095  }
1096 
1103  Map<String, List<DatabaseTimingResult>> getTimingMetricsFromDatabase(long timeRange) throws HealthMonitorException {
1104 
1105  // Make sure the monitor is enabled. It could theoretically get disabled after this
1106  // check but it doesn't seem worth holding a lock to ensure that it doesn't since that
1107  // may slow down ingest.
1108  if(! isEnabled.get()) {
1109  throw new HealthMonitorException("Health Monitor is not enabled");
1110  }
1111 
1112  // Calculate the smallest timestamp we should return
1113  long minimumTimestamp = System.currentTimeMillis() - timeRange;
1114 
1115  try (CoordinationService.Lock lock = getSharedDbLock()) {
1116  if(lock == null) {
1117  throw new HealthMonitorException("Error getting database lock");
1118  }
1119 
1120  Connection conn = connect();
1121  if(conn == null) {
1122  throw new HealthMonitorException("Error getting database connection");
1123  }
1124 
1125  Map<String, List<DatabaseTimingResult>> resultMap = new HashMap<>();
1126 
1127  try (Statement statement = conn.createStatement();
1128  ResultSet resultSet = statement.executeQuery("SELECT * FROM timing_data WHERE timestamp > " + minimumTimestamp)) {
1129 
1130  while (resultSet.next()) {
1131  String name = resultSet.getString("name");
1132  DatabaseTimingResult timingResult = new DatabaseTimingResult(resultSet);
1133 
1134  if(resultMap.containsKey(name)) {
1135  resultMap.get(name).add(timingResult);
1136  } else {
1137  List<DatabaseTimingResult> resultList = new ArrayList<>();
1138  resultList.add(timingResult);
1139  resultMap.put(name, resultList);
1140  }
1141  }
1142  return resultMap;
1143  } catch (SQLException ex) {
1144  throw new HealthMonitorException("Error reading timing metrics from database", ex);
1145  } finally {
1146  try {
1147  conn.close();
1148  } catch (SQLException ex) {
1149  logger.log(Level.SEVERE, "Error closing Connection.", ex);
1150  }
1151  }
1152  } catch (CoordinationService.CoordinationServiceException ex) {
1153  throw new HealthMonitorException("Error getting database lock", ex);
1154  }
1155  }
1156 
1163  List<UserData> getUserMetricsFromDatabase(long timeRange) throws HealthMonitorException {
1164 
1165  // Make sure the monitor is enabled. It could theoretically get disabled after this
1166  // check but it doesn't seem worth holding a lock to ensure that it doesn't since that
1167  // may slow down ingest.
1168  if(! isEnabled.get()) {
1169  throw new HealthMonitorException("Health Monitor is not enabled");
1170  }
1171 
1172  // Calculate the smallest timestamp we should return
1173  long minimumTimestamp = System.currentTimeMillis() - timeRange;
1174 
1175  try (CoordinationService.Lock lock = getSharedDbLock()) {
1176  if(lock == null) {
1177  throw new HealthMonitorException("Error getting database lock");
1178  }
1179 
1180  List<UserData> resultList = new ArrayList<>();
1181 
1182  try (Connection conn = connect();
1183  Statement statement = conn.createStatement();
1184  ResultSet resultSet = statement.executeQuery("SELECT * FROM user_data WHERE timestamp > " + minimumTimestamp)) {
1185 
1186  while (resultSet.next()) {
1187  resultList.add(new UserData(resultSet));
1188  }
1189  return resultList;
1190  } catch (SQLException ex) {
1191  throw new HealthMonitorException("Error reading user metrics from database", ex);
1192  }
1193  } catch (CoordinationService.CoordinationServiceException ex) {
1194  throw new HealthMonitorException("Error getting database lock", ex);
1195  }
1196  }
1197 
1204  private CoordinationService.Lock getExclusiveDbLock() throws HealthMonitorException{
1205  try {
1207 
1208  if(lock != null){
1209  return lock;
1210  }
1211  throw new HealthMonitorException("Error acquiring database lock");
1212  } catch (InterruptedException | CoordinationService.CoordinationServiceException ex){
1213  throw new HealthMonitorException("Error acquiring database lock", ex);
1214  }
1215  }
1216 
1223  private CoordinationService.Lock getSharedDbLock() throws HealthMonitorException{
1224  try {
1225  String databaseNodeName = DATABASE_NAME;
1227 
1228  if(lock != null){
1229  return lock;
1230  }
1231  throw new HealthMonitorException("Error acquiring database lock");
1232  } catch (InterruptedException | CoordinationService.CoordinationServiceException ex){
1233  throw new HealthMonitorException("Error acquiring database lock");
1234  }
1235  }
1236 
1240  enum UserEvent {
1241  LOG_ON(0),
1242  LOG_OFF(1),
1243  CASE_OPEN(2),
1244  CASE_CLOSE(3);
1245 
1246  int value;
1247 
1248  UserEvent(int value) {
1249  this.value = value;
1250  }
1251 
1256  int getEventValue() {
1257  return value;
1258  }
1259 
1266  static UserEvent valueOf(int value) throws HealthMonitorException {
1267  for (UserEvent v : UserEvent.values()) {
1268  if (v.value == value) {
1269  return v;
1270  }
1271  }
1272  throw new HealthMonitorException("Can not create UserEvent from unknown value " + value);
1273  }
1274 
1280  boolean caseIsOpen() {
1281  return(this.equals(CASE_OPEN));
1282  }
1283 
1289  boolean userIsLoggedIn() {
1290  // LOG_ON, CASE_OPEN, and CASE_CLOSED events all imply that the user
1291  // is logged in
1292  return( ! this.equals(LOG_OFF));
1293  }
1294  }
1295 
1301  static class UserData {
1302  private final UserEvent eventType;
1303  private long timestamp;
1304  private final boolean isExaminer;
1305  private final String hostname;
1306  private String caseName;
1307 
1313  private UserData(UserEvent eventType) {
1314  this.eventType = eventType;
1315  this.timestamp = System.currentTimeMillis();
1316  this.isExaminer = (UserPreferences.SelectedMode.STANDALONE == UserPreferences.getMode());
1317  this.hostname = "";
1318 
1319  // If there's a case open, record the name
1320  try {
1321  this.caseName = Case.getCurrentCaseThrows().getDisplayName();
1322  } catch (NoCurrentCaseException ex) {
1323  // It's not an error if there's no case open
1324  this.caseName = "";
1325  }
1326  }
1327 
1334  UserData(ResultSet resultSet) throws SQLException, HealthMonitorException {
1335  this.timestamp = resultSet.getLong("timestamp");
1336  this.hostname = resultSet.getString("host");
1337  this.eventType = UserEvent.valueOf(resultSet.getInt("event_type"));
1338  this.isExaminer = resultSet.getBoolean("is_examiner");
1339  this.caseName = resultSet.getString("case_name");
1340  }
1341 
1348  static UserData createDummyUserData(long timestamp) {
1349  UserData userData = new UserData(UserEvent.CASE_CLOSE);
1350  userData.timestamp = timestamp;
1351  return userData;
1352  }
1353 
1358  long getTimestamp() {
1359  return timestamp;
1360  }
1361 
1366  String getHostname() {
1367  return hostname;
1368  }
1369 
1374  UserEvent getEventType() {
1375  return eventType;
1376  }
1377 
1382  boolean isExaminerNode() {
1383  return isExaminer;
1384  }
1385 
1390  String getCaseName() {
1391  return caseName;
1392  }
1393  }
1394 
1403  private class TimingInfo {
1404  private long count; // Number of metrics collected
1405  private double sum; // Sum of the durations collected (nanoseconds)
1406  private double max; // Maximum value found (nanoseconds)
1407  private double min; // Minimum value found (nanoseconds)
1408 
1409  TimingInfo(TimingMetric metric) throws HealthMonitorException {
1410  count = 1;
1411  sum = metric.getDuration();
1412  max = metric.getDuration();
1413  min = metric.getDuration();
1414  }
1415 
1423  void addMetric(TimingMetric metric) throws HealthMonitorException {
1424 
1425  // Keep track of needed info to calculate the average
1426  count++;
1427  sum += metric.getDuration();
1428 
1429  // Check if this is the longest duration seen
1430  if(max < metric.getDuration()) {
1431  max = metric.getDuration();
1432  }
1433 
1434  // Check if this is the lowest duration seen
1435  if(min > metric.getDuration()) {
1436  min = metric.getDuration();
1437  }
1438  }
1439 
1444  double getAverage() {
1445  return sum / count;
1446  }
1447 
1452  double getMax() {
1453  return max;
1454  }
1455 
1460  double getMin() {
1461  return min;
1462  }
1463 
1468  long getCount() {
1469  return count;
1470  }
1471  }
1472 
1477  static class DatabaseTimingResult {
1478  private final long timestamp; // Time the metric was recorded
1479  private final String hostname; // Host that recorded the metric
1480  private final long count; // Number of metrics collected
1481  private final double average; // Average of the durations collected (milliseconds)
1482  private final double max; // Maximum value found (milliseconds)
1483  private final double min; // Minimum value found (milliseconds)
1484 
1485  DatabaseTimingResult(ResultSet resultSet) throws SQLException {
1486  this.timestamp = resultSet.getLong("timestamp");
1487  this.hostname = resultSet.getString("host");
1488  this.count = resultSet.getLong("count");
1489  this.average = resultSet.getDouble("average");
1490  this.max = resultSet.getDouble("max");
1491  this.min = resultSet.getDouble("min");
1492  }
1493 
1498  long getTimestamp() {
1499  return timestamp;
1500  }
1501 
1506  double getAverage() {
1507  return average;
1508  }
1509 
1514  double getMax() {
1515  return max;
1516  }
1517 
1522  double getMin() {
1523  return min;
1524  }
1525 
1530  long getCount() {
1531  return count;
1532  }
1533 
1538  String getHostName() {
1539  return hostname;
1540  }
1541  }
1542 }
static CaseDbConnectionInfo getDatabaseConnectionInfo()
static void shutDownTaskExecutor(ExecutorService executor)
static void submitNormalizedTimingMetric(TimingMetric metric, long normalization)
Lock tryGetExclusiveLock(CategoryNode category, String nodePath, int timeOut, TimeUnit timeUnit)
static void addPropertyChangeListener(PropertyChangeListener listener)
Definition: Case.java:383
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
Lock tryGetSharedLock(CategoryNode category, String nodePath, int timeOut, TimeUnit timeUnit)

Copyright © 2012-2016 Basis Technology. Generated on: Mon Jun 18 2018
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.