Autopsy 4.22.1
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 */
19package org.sleuthkit.autopsy.healthmonitor;
20
21import com.google.common.util.concurrent.ThreadFactoryBuilder;
22import java.beans.PropertyChangeEvent;
23import java.beans.PropertyChangeListener;
24import java.sql.Connection;
25import java.sql.DriverManager;
26import java.sql.PreparedStatement;
27import java.sql.ResultSet;
28import java.sql.SQLException;
29import java.sql.Statement;
30import java.util.Map;
31import java.util.HashMap;
32import java.util.List;
33import java.util.ArrayList;
34import java.util.Calendar;
35import java.util.GregorianCalendar;
36import java.util.UUID;
37import java.util.concurrent.ScheduledThreadPoolExecutor;
38import java.util.concurrent.TimeUnit;
39import java.util.concurrent.atomic.AtomicBoolean;
40import java.util.logging.Level;
41import java.util.Random;
42import org.apache.commons.dbcp2.BasicDataSource;
43import org.sleuthkit.autopsy.casemodule.Case;
44import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
45import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
46import org.sleuthkit.autopsy.core.UserPreferences;
47import org.sleuthkit.autopsy.core.UserPreferencesException;
48import org.sleuthkit.autopsy.coreutils.Logger;
49import org.sleuthkit.autopsy.coreutils.ThreadUtils;
50import org.sleuthkit.datamodel.CaseDbConnectionInfo;
51import org.sleuthkit.datamodel.CaseDbSchemaVersionNumber;
52import org.sleuthkit.datamodel.Image;
53import org.sleuthkit.datamodel.SleuthkitCase;
54import org.sleuthkit.datamodel.TskCoreException;
55
63public 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
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) {
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);
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
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();
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() {
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
1000
1001 } else if ((null == evt.getOldValue()) && (evt.getNewValue() instanceof Case)) {
1002 // Case is opening
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
1313
1315
1317 this.value = value;
1318 }
1319
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();
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 void addPropertyChangeListener(PropertyChangeListener listener)
Definition Case.java:675
Lock tryGetExclusiveLock(CategoryNode category, String nodePath, int timeOut, TimeUnit timeUnit)
Lock tryGetSharedLock(CategoryNode category, String nodePath, int timeOut, TimeUnit timeUnit)
static CaseDbConnectionInfo getDatabaseConnectionInfo()
synchronized static Logger getLogger(String name)
Definition Logger.java:124
static void shutDownTaskExecutor(ExecutorService executor)
final Map< String, TimingInfo > timingInfoMap
static final CaseDbSchemaVersionNumber CURRENT_DB_SCHEMA_VERSION
static TimingMetric getTimingMetric(String name)
static void submitTimingMetric(TimingMetric metric)
static void submitNormalizedTimingMetric(TimingMetric metric, long normalization)

Copyright © 2012-2024 Sleuth Kit Labs. Generated on:
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.