diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/ServicesHealthMonitor.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/ServicesHealthMonitor.java index ebed5d5f61..82533086aa 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/ServicesHealthMonitor.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/ServicesHealthMonitor.java @@ -27,6 +27,8 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.Map; import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -53,7 +55,7 @@ public class ServicesHealthMonitor { private final static String DATABASE_NAME = "ServicesHealthMonitor"; private final static String MODULE_NAME = "ServicesHealthMonitor"; private final static String IS_ENABLED_KEY = "is_enabled"; - private final static long DATABASE_WRITE_INTERVAL = 60; // Minutes + private final static long DATABASE_WRITE_INTERVAL = 1; // Minutes TODO - put back to an hour public static final CaseDbSchemaVersionNumber CURRENT_DB_SCHEMA_VERSION = new CaseDbSchemaVersionNumber(1, 0); @@ -111,6 +113,9 @@ public class ServicesHealthMonitor { logger.log(Level.INFO, "Activating Servies Health Monitor"); + // Make sure there are no left over connections to an old database + shutdownConnections(); + if (!UserPreferences.getIsMultiUserModeEnabled()) { throw new HealthMonitorException("Multi user mode is not enabled - can not activate services health monitor"); } @@ -523,7 +528,7 @@ public class ServicesHealthMonitor { ResultSet resultSet = null; try (Statement statement = conn.createStatement()) { - int minorVersion = 0; + int minorVersion = 0; int majorVersion = 0; resultSet = statement.executeQuery("SELECT value FROM db_info WHERE name='SCHEMA_MINOR_VERSION'"); if (resultSet.next()) { @@ -636,6 +641,76 @@ public class ServicesHealthMonitor { } } + /** + * Get all timing metrics currently stored in the database. This also converts + * the times to milliseconds (from nanoseconds). + * @return A map with metric name mapped to a list of data + * @throws HealthMonitorException + */ + Map> getTimingMetricsFromDatabase() throws HealthMonitorException { + + // Make sure the monitor is enabled. It could theoretically get disabled after this + // check but it doesn't seem worth holding a lock to ensure that it doesn't since that + // may slow down ingest. + if(! isEnabled.get()) { + throw new HealthMonitorException("Health Monitor is not enabled"); + } + + CoordinationService.Lock lock = getSharedDbLock(); + if(lock == null) { + throw new HealthMonitorException("Error getting database lock"); + } + + try{ + Connection conn = connect(); + if(conn == null) { + throw new HealthMonitorException("Error getting database connection"); + } + ResultSet resultSet = null; + + Map> resultMap = new HashMap<>(); + + try (Statement statement = conn.createStatement()) { + + resultSet = statement.executeQuery("SELECT * FROM timing_data"); + while (resultSet.next()) { + String name = resultSet.getString("name"); + DatabaseTimingResult timingResult = new DatabaseTimingResult(resultSet); + + if(resultMap.containsKey(name)) { + resultMap.get(name).add(timingResult); + } else { + List resultList = new ArrayList<>(); + resultList.add(timingResult); + resultMap.put(name, resultList); + } + } + return resultMap; + } catch (SQLException ex) { + throw new HealthMonitorException("Error reading timing metrics from database", ex); + } finally { + if(resultSet != null) { + try { + resultSet.close(); + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Error closing result set", ex); + } + } + try { + conn.close(); + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Error closing Connection.", ex); + } + } + } finally { + try { + lock.release(); + } catch (CoordinationService.CoordinationServiceException ex) { + throw new HealthMonitorException("Error releasing database lock", ex); + } + } + } + /** * Get an exclusive lock for the health monitor database. * Acquire this before creating, initializing, or updating the database schema. @@ -753,4 +828,73 @@ public class ServicesHealthMonitor { return count; } } + + /** + * Class for retrieving timing metrics from the database to display to the user. + * All times will be in milliseconds. + */ + static class DatabaseTimingResult { + private long timestamp; // Time the metric was recorded + private long count; // Number of metrics collected + private double average; // Average of the durations collected (milliseconds) + private double max; // Maximum value found (milliseconds) + private double min; // Minimum value found (milliseconds) + + // TODO - maybe delete this + DatabaseTimingResult(long timestamp, long count, double average, double max, double min) { + this.timestamp = timestamp; + this.count = count; + this.average = average; + this.max = max; + this.min = min; + } + + DatabaseTimingResult(ResultSet resultSet) throws SQLException { + this.timestamp = resultSet.getLong("timestamp"); + this.count = resultSet.getLong("count"); + this.average = resultSet.getLong("average") / 1000000; + this.max = resultSet.getLong("max") / 1000000; + this.min = resultSet.getLong("min") / 1000000; + } + + /** + * Get the timestamp for when the metric was recorded + * @return + */ + long getTimestamp() { + return timestamp; + } + + /** + * Get the average duration + * @return average duration (milliseconds) + */ + double getAverage() { + return average; + } + + /** + * Get the maximum duration + * @return maximum duration (milliseconds) + */ + double getMax() { + return max; + } + + /** + * Get the minimum duration + * @return minimum duration (milliseconds) + */ + double getMin() { + return min; + } + + /** + * Get the total number of metrics collected + * @return number of metrics collected + */ + long getCount() { + return count; + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/TimingMetricGraphPanel.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/TimingMetricGraphPanel.java new file mode 100644 index 0000000000..955ffde780 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/TimingMetricGraphPanel.java @@ -0,0 +1,187 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.healthmonitor; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.RenderingHints; +import java.awt.Stroke; +import java.util.ArrayList; +import java.util.List; +import javax.swing.JPanel; +import org.sleuthkit.autopsy.healthmonitor.ServicesHealthMonitor.DatabaseTimingResult; + +/** + * + */ +class TimingMetricGraphPanel extends JPanel { + + private int width = 800; + private int heigth = 400; + private int padding = 25; + private int labelPadding = 25; + private Color lineColor = new Color(44, 102, 230, 180); + private Color pointColor = new Color(100, 100, 100, 180); + private Color gridColor = new Color(200, 200, 200, 200); + private static final Stroke GRAPH_STROKE = new BasicStroke(2f); + private int pointWidth = 4; + private int numberYDivisions = 10; + private List timingResults; + + TimingMetricGraphPanel(List timingResults) { + this.timingResults = timingResults; + } + + private double getMaxScore() { + // Find the highest of the max values + double maxScore = Double.MIN_VALUE; + for (DatabaseTimingResult score : timingResults) { + maxScore = Math.max(maxScore, score.getMax()); + } + return maxScore; + } + + private double getMinScore() { + // Find the highest of the max values + double minScore = Double.MAX_VALUE; + for (DatabaseTimingResult score : timingResults) { + minScore = Math.min(minScore, score.getMin()); + } + return minScore; + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + double maxScore = getMaxScore(); + double minScore = getMinScore(); + + + double xScale = ((double) getWidth() - (2 * padding) - labelPadding) / (timingResults.size() - 1); // TODO: make this based on timestamps + double yScale = ((double) getHeight() - 2 * padding - labelPadding) / (maxScore - minScore); + + //List graphPoints = new ArrayList<>(); + List averageGraphPoints = new ArrayList<>(); + List maxGraphPoints = new ArrayList<>(); + List minGraphPoints = new ArrayList<>(); + for (int i = 0; i < timingResults.size(); i++) { + int x1 = (int) (i * xScale + padding + labelPadding); + int yAve = (int) ((maxScore - timingResults.get(i).getAverage()) * yScale + padding); + int yMax = (int) ((maxScore - timingResults.get(i).getMax()) * yScale + padding); + int yMin = (int) ((maxScore - timingResults.get(i).getMin()) * yScale + padding); + averageGraphPoints.add(new Point(x1, yAve)); + maxGraphPoints.add(new Point(x1, yMax)); + minGraphPoints.add(new Point(x1, yMin)); + } + + // draw white background + g2.setColor(Color.WHITE); + g2.fillRect(padding + labelPadding, padding, getWidth() - (2 * padding) - labelPadding, getHeight() - 2 * padding - labelPadding); + g2.setColor(Color.BLACK); + + // create hatch marks and grid lines for y axis. + for (int i = 0; i < numberYDivisions + 1; i++) { + int x0 = padding + labelPadding; + int x1 = pointWidth + padding + labelPadding; + int y0 = getHeight() - ((i * (getHeight() - padding * 2 - labelPadding)) / numberYDivisions + padding + labelPadding); + int y1 = y0; + if (timingResults.size() > 0) { + g2.setColor(gridColor); + g2.drawLine(padding + labelPadding + 1 + pointWidth, y0, getWidth() - padding, y1); + g2.setColor(Color.BLACK); + String yLabel = ((int) ((getMinScore() + (maxScore - minScore) * ((i * 1.0) / numberYDivisions)) * 100)) / 100.0 + ""; + FontMetrics metrics = g2.getFontMetrics(); + int labelWidth = metrics.stringWidth(yLabel); + g2.drawString(yLabel, x0 - labelWidth - 5, y0 + (metrics.getHeight() / 2) - 3); + } + g2.drawLine(x0, y0, x1, y1); + } + + // and for x axis + for (int i = 0; i < timingResults.size(); i++) { + if (timingResults.size() > 1) { + int x0 = i * (getWidth() - padding * 2 - labelPadding) / (timingResults.size() - 1) + padding + labelPadding; + int x1 = x0; + int y0 = getHeight() - padding - labelPadding; + int y1 = y0 - pointWidth; + if ((i % ((int) ((timingResults.size() / 20.0)) + 1)) == 0) { + g2.setColor(gridColor); + g2.drawLine(x0, getHeight() - padding - labelPadding - 1 - pointWidth, x1, padding); + g2.setColor(Color.BLACK); + String xLabel = i + ""; + FontMetrics metrics = g2.getFontMetrics(); + int labelWidth = metrics.stringWidth(xLabel); + g2.drawString(xLabel, x0 - labelWidth / 2, y0 + metrics.getHeight() + 3); + } + g2.drawLine(x0, y0, x1, y1); + } + } + + // create x and y axes + g2.drawLine(padding + labelPadding, getHeight() - padding - labelPadding, padding + labelPadding, padding); + g2.drawLine(padding + labelPadding, getHeight() - padding - labelPadding, getWidth() - padding, getHeight() - padding - labelPadding); + + Stroke oldStroke = g2.getStroke(); + g2.setColor(lineColor); + g2.setStroke(GRAPH_STROKE); + for (int i = 0; i < averageGraphPoints.size() - 1; i++) { + int x1 = averageGraphPoints.get(i).x; + int y1 = averageGraphPoints.get(i).y; + int x2 = averageGraphPoints.get(i + 1).x; + int y2 = averageGraphPoints.get(i + 1).y; + g2.drawLine(x1, y1, x2, y2); + } + + g2.setColor(new Color(10, 10, 200)); + for (int i = 0; i < maxGraphPoints.size() - 1; i++) { + int x1 = maxGraphPoints.get(i).x; + int y1 = maxGraphPoints.get(i).y; + int x2 = maxGraphPoints.get(i + 1).x; + int y2 = maxGraphPoints.get(i + 1).y; + g2.drawLine(x1, y1, x2, y2); + } + + g2.setColor(new Color(10, 200, 20)); + for (int i = 0; i < minGraphPoints.size() - 1; i++) { + int x1 = minGraphPoints.get(i).x; + int y1 = minGraphPoints.get(i).y; + int x2 = minGraphPoints.get(i + 1).x; + int y2 = minGraphPoints.get(i + 1).y; + g2.drawLine(x1, y1, x2, y2); + } + + g2.setStroke(oldStroke); + g2.setColor(pointColor); + /*for (int i = 0; i < graphPoints.size(); i++) { + int x = graphPoints.get(i).x - pointWidth / 2; + int y = graphPoints.get(i).y - pointWidth / 2; + int ovalW = pointWidth; + int ovalH = pointWidth; + g2.fillOval(x, y, ovalW, ovalH); + }*/ + } + +}