/**
* Copyright (c) Codice Foundation
* <p>
* This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or any later version.
* <p>
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details. A copy of the GNU Lesser General Public License
* is distributed along with this program and can be found at
* <http://www.gnu.org/licenses/lgpl.html>.
*/
package ddf.metrics.reporting.internal.rrd4j;
import java.awt.Color;
import java.awt.Rectangle;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
import org.apache.commons.math.stat.descriptive.SummaryStatistics;
import org.apache.poi.hslf.model.Picture;
import org.apache.poi.hslf.model.Slide;
import org.apache.poi.hslf.model.TextBox;
import org.apache.poi.hslf.model.TextRun;
import org.apache.poi.hslf.usermodel.SlideShow;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.codice.ddf.platform.util.XMLUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.MutableDateTime;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.rrd4j.ConsolFun;
import org.rrd4j.DsType;
import org.rrd4j.core.Datasource;
import org.rrd4j.core.FetchData;
import org.rrd4j.core.FetchRequest;
import org.rrd4j.core.RrdDb;
import org.rrd4j.graph.RrdGraph;
import org.rrd4j.graph.RrdGraphDef;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import ddf.metrics.reporting.internal.MetricsGraphException;
import ddf.metrics.reporting.internal.MetricsRetriever;
/**
* Retrieves metrics historical data from an RRD file and formats that data in a variety of formats
* over a specified time range.
* <p>
* The supported formats include:
* <ul>
* <li>a PNG graph of the data (returned to the client as a byte array)</li>
* <li>a CSV file which can be displayed in Microsoft Excel, OpenOffice Calc, etc.</li>
* <li>a stream in XLS-format, which can be displayed in Microsoft Excel or OpenOffice Calc</li>
* <li>a stream in PPT-format, which can be displayed in Microsoft PowerPoint or OpenOffice Impress</li>
* <li>as XML (no schema provided)</li>
* <li>a JSON-formatted string</li>
* </ul>
* <p>
* Aggregate reports, which include the data for all metrics, over a specified time range are also
* supported in XLS (Excel) and PPT (PowerPoint) format. For example, if there are 10 metrics that
* are having data collected, then an aggregate report in XLS would be a spreadsheet with a separate
* worksheet for each of the 10 metrics. Similarly, this aggregate report in PPT format would
* consist of a slide per metric, where each slide contains the metric's graph and total count (if
* applicable).
*
* @author rodgersh
* @author ddf.isgs@lmco.com
*/
public class RrdMetricsRetriever implements MetricsRetriever {
private static final transient Logger LOGGER =
LoggerFactory.getLogger(RrdMetricsRetriever.class);
private static final double DEFAULT_METRICS_MAX_THRESHOLD = 4000000000.0;
private static final int RRD_STEP = 60;
public static final String SUMMARY_TIMESTAMP = "dd-MM-yy HHmm";
public static final int EXCEL_MAX_COLUMNS = 256;
/**
* Used for formatting long timestamps into more readable calendar dates/times.
*/
private static final String MONTHS[] =
{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
/**
* Max threshold for a metric's sample value. Used to filter out spike data that typically has a
* value of 4.2E+09 or higher.
*/
private double metricsMaxThreshold;
public enum SUMMARY_INTERVALS {
minute, hour, day, week, month;
}
public RrdMetricsRetriever() {
this(DEFAULT_METRICS_MAX_THRESHOLD);
}
public RrdMetricsRetriever(double metricsMaxThreshold) {
LOGGER.trace("Setting metricsMaxThreshold = {}", metricsMaxThreshold);
this.metricsMaxThreshold = metricsMaxThreshold;
}
/**
* Formats timestamp (in seconds since Unix epoch) into human-readable format of MMM DD YYYY
* hh:mm:ss.
* <p>
* Example: Apr 10 2013 09:14:43
*
* @param timestamp time in seconds since Unix epoch of Jan 1, 1970 12:00:00
* @return formatted date/time string of the form MMM DD YYYY hh:mm:ss
*/
static String getCalendarTime(long timestamp) {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(TimeUnit.SECONDS.toMillis(timestamp));
String calTime =
MONTHS[calendar.get(Calendar.MONTH)] + " " + calendar.get(Calendar.DATE) + " "
+ calendar.get(Calendar.YEAR) + " ";
calTime += addLeadingZero(calendar.get(Calendar.HOUR_OF_DAY)) + ":";
calTime += addLeadingZero(calendar.get(Calendar.MINUTE)) + ":";
calTime += addLeadingZero(calendar.get(Calendar.SECOND));
return calTime;
}
static String addLeadingZero(int value) {
if (value < 10) {
return "0" + String.valueOf(value);
}
return String.valueOf(value);
}
/**
* Convert string, if it is in camelCase, to individual words with each word starting with a
* capital letter
*/
public static String convertCamelCase(String input) {
String[] parts = StringUtils.splitByCharacterTypeCamelCase(input);
String convertedStr = StringUtils.join(parts, " ");
convertedStr = WordUtils.capitalizeFully(convertedStr)
.trim();
return convertedStr;
}
@Override
public byte[] createGraph(String metricName, String rrdFilename, long startTime, long endTime)
throws IOException, MetricsGraphException {
// Create default label and title for graph
String displayableMetricName = convertCamelCase(metricName);
String verticalAxisLabel = displayableMetricName;
String title = displayableMetricName + " for " + getCalendarTime(startTime) + " to "
+ getCalendarTime(endTime);
return createGraph(metricName, rrdFilename, startTime, endTime, verticalAxisLabel, title);
}
@Override
public byte[] createGraph(String metricName, String rrdFilename, long startTime, long endTime,
String verticalAxisLabel, String title) throws IOException, MetricsGraphException {
// Create RRD DB in read-only mode for the specified RRD file
RrdDb rrdDb = new RrdDb(rrdFilename, true);
// Extract the data source (should always only be one data source per RRD file - otherwise
// we have a problem)
if (rrdDb.getDsCount() != 1) {
throw new MetricsGraphException(
"Only one data source per RRD file is supported - RRD file " + rrdFilename
+ " has " + rrdDb.getDsCount() + " data sources.");
}
// Define attributes of the graph to be created for this metric
RrdGraphDef graphDef = new RrdGraphDef();
graphDef.setTimeSpan(startTime, endTime);
graphDef.setImageFormat("PNG");
graphDef.setShowSignature(false);
graphDef.setStep(RRD_STEP);
graphDef.setVerticalLabel(verticalAxisLabel);
graphDef.setHeight(500);
graphDef.setWidth(1000);
graphDef.setTitle(title);
// Since we have verified only one datasource in RRD file/RRDb, then know
// that we can index by zero safely and get the metric's data
Datasource dataSource = rrdDb.getDatasource(0);
DsType dataSourceType = dataSource.getType();
// Determine the type of Data Source for this RRD file
// (Need to know this because COUNTER and DERIVE data is averaged across samples and the
// vertical axis of the
// generated graph by default will show data per rrdStep interval)
if (dataSourceType == DsType.COUNTER || dataSourceType == DsType.DERIVE) {
if (LOGGER.isTraceEnabled()) {
dumpData(ConsolFun.TOTAL,
"TOTAL",
rrdDb,
dataSourceType.name(),
startTime,
endTime);
}
// If we ever needed to adjust the metric's data collected by RRD by the archive step
// (which is the rrdStep * archiveSampleCount) this is how to do it.
// FetchRequest fetchRequest = rrdDb.createFetchRequest(ConsolFun.AVERAGE, startTime,
// endTime);
// Archive archive = rrdDb.findMatchingArchive(fetchRequest);
// long archiveStep = archive.getArcStep();
// LOGGER.debug("archiveStep = " + archiveStep);
long rrdStep = rrdDb.getRrdDef()
.getStep();
LOGGER.debug("rrdStep = {}", rrdStep);
// Still TBD if we want to graph the AVERAGE data on the same graph
// graphDef.comment(metricName + " ");
// graphDef.datasource("myAverage", rrdFilename, "data", ConsolFun.AVERAGE);
// graphDef.datasource("realAverage", "myAverage," + rrdStep + ",*");
// graphDef.line("realAverage", Color.GREEN, "Average", 2);
// Multiplied by the rrdStep to "undo" the automatic averaging that RRD does
// when it collects TOTAL data - we want the actual totals for the step, not
// the average of the totals.
graphDef.datasource("myTotal", rrdFilename, dataSource.getName(), ConsolFun.TOTAL);
graphDef.datasource("realTotal", "myTotal," + rrdStep + ",*");
// If real total exceeds the threshold value used to constrain/filter spike data out,
// then set total to UNKNOWN, which means this sample will not be graphed. This prevents
// spike data that is typically 4.2E+09 (graphed as 4.3G) from corrupting the RRD graph.
graphDef.datasource("constrainedTotal",
"realTotal," + metricsMaxThreshold + ",GT,UNKN,realTotal,IF");
graphDef.line("constrainedTotal", Color.BLUE, convertCamelCase(metricName), 2);
// Add some spacing between the graph and the summary stats shown beneath the graph
graphDef.comment("\\s");
graphDef.comment("\\s");
graphDef.comment("\\c");
// Average, Min, and Max over all of the TOTAL data - displayed at bottom of the graph
graphDef.gprint("constrainedTotal", ConsolFun.AVERAGE, "Average = %.3f%s");
graphDef.gprint("constrainedTotal", ConsolFun.MIN, "Min = %.3f%s");
graphDef.gprint("constrainedTotal", ConsolFun.MAX, "Max = %.3f%s");
} else if (dataSourceType == DsType.GAUGE) {
if (LOGGER.isTraceEnabled()) {
dumpData(ConsolFun.AVERAGE,
"AVERAGE",
rrdDb,
dataSourceType.name(),
startTime,
endTime);
}
graphDef.datasource("myAverage", rrdFilename, dataSource.getName(), ConsolFun.AVERAGE);
graphDef.line("myAverage", Color.RED, convertCamelCase(metricName), 2);
// Add some spacing between the graph and the summary stats shown beneath the graph
graphDef.comment("\\s");
graphDef.comment("\\s");
graphDef.comment("\\c");
// Average, Min, and Max over all of the AVERAGE data - displayed at bottom of the graph
graphDef.gprint("myAverage", ConsolFun.AVERAGE, "Average = %.3f%s");
graphDef.gprint("myAverage", ConsolFun.MIN, "Min = %.3f%s");
graphDef.gprint("myAverage", ConsolFun.MAX, "Max = %.3f%s");
} else {
rrdDb.close();
throw new MetricsGraphException(
"Unsupported data source type " + dataSourceType.name() + " in RRD file "
+ rrdFilename
+ ", only DERIVE, COUNTER and GAUGE data source types supported.");
}
rrdDb.close();
// Use "-" as filename so that RRD creates the graph only in memory (no file is
// created, hence no file locking problems due to race conditions between multiple clients)
graphDef.setFilename("-");
RrdGraph graph = new RrdGraph(graphDef);
return graph.getRrdGraphInfo()
.getBytes();
}
@Override
public String createCsvData(String rrdFilename, long startTime, long endTime)
throws IOException, MetricsGraphException {
LOGGER.trace("ENTERING: createCsvData");
MetricData metricData = getMetricData(rrdFilename, startTime, endTime);
StringBuffer csv = new StringBuffer("");
csv.append("Timestamp,Value\n");
List<Long> timestamps = metricData.getTimestamps();
List<Double> values = metricData.getValues();
for (int i = 0; i < timestamps.size(); i++) {
String timestamp = getCalendarTime(timestamps.get(i));
csv.append(timestamp + "," + new Double(values.get(i)) + "\n");
}
LOGGER.trace("csv = {}", csv.toString());
LOGGER.trace("EXITING: createCsvData");
return csv.toString();
}
@Override
public String createXmlData(String metricName, String rrdFilename, long startTime, long endTime)
throws IOException, MetricsGraphException {
LOGGER.trace("ENTERING: createXmlData");
MetricData metricData = getMetricData(rrdFilename, startTime, endTime);
String displayableMetricName = convertCamelCase(metricName);
String title = displayableMetricName + " for " + getCalendarTime(startTime) + " to "
+ getCalendarTime(endTime);
String xmlString = null;
try {
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
// root elements
Document doc = docBuilder.newDocument();
Element rootElement = doc.createElement(metricName);
doc.appendChild(rootElement);
Element titleElement = doc.createElement("title");
titleElement.appendChild(doc.createTextNode(title));
rootElement.appendChild(titleElement);
Element dataElement = doc.createElement("data");
rootElement.appendChild(dataElement);
List<Long> timestamps = metricData.getTimestamps();
List<Double> values = metricData.getValues();
for (int i = 0; i < timestamps.size(); i++) {
Element sampleElement = doc.createElement("sample");
dataElement.appendChild(sampleElement);
String timestamp = getCalendarTime(timestamps.get(i));
Element timestampElement = doc.createElement("timestamp");
timestampElement.appendChild(doc.createTextNode(timestamp));
sampleElement.appendChild(timestampElement);
Element valueElement = doc.createElement("value");
valueElement.appendChild(doc.createTextNode(String.valueOf(values.get(i))));
sampleElement.appendChild(valueElement);
}
if (metricData.hasTotalCount()) {
Element totalCountElement = doc.createElement("totalCount");
totalCountElement.appendChild(doc.createTextNode(Long.toString(metricData.getTotalCount())));
dataElement.appendChild(totalCountElement);
}
// Write the content into xml stringwriter
xmlString = XMLUtils.prettyFormat(doc);
} catch (ParserConfigurationException pce) {
LOGGER.debug("Parsing error while creating xml data", pce);
}
LOGGER.trace("xml = {}", xmlString);
LOGGER.trace("EXITING: createXmlData");
return xmlString;
}
@Override
public OutputStream createXlsData(String metricName, String rrdFilename, long startTime,
long endTime) throws IOException, MetricsGraphException {
LOGGER.trace("ENTERING: createXlsData");
Workbook wb = new HSSFWorkbook();
createSheet(wb, metricName, rrdFilename, startTime, endTime);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
wb.write(bos);
bos.close();
LOGGER.trace("EXITING: createXlsData");
return bos;
}
@Override
public OutputStream createPptData(String metricName, String rrdFilename, long startTime,
long endTime) throws IOException, MetricsGraphException {
LOGGER.trace("ENTERING: createPptData");
SlideShow ppt = new SlideShow();
byte[] graph = createGraph(metricName, rrdFilename, startTime, endTime);
MetricData metricData = getMetricData(rrdFilename, startTime, endTime);
createSlide(ppt, metricName, graph, metricData);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ppt.write(bos);
bos.close();
LOGGER.trace("EXITING: createPptData");
return bos;
}
@Override
public OutputStream createPptReport(List<String> metricNames, String metricsDir, long startTime,
long endTime) throws IOException, MetricsGraphException {
LOGGER.trace("ENTERING: createPptReport");
SlideShow ppt = new SlideShow();
Collections.sort(metricNames);
for (String metricName : metricNames) {
String rrdFilename = getRrdFilename(metricsDir, metricName);
byte[] graph = createGraph(metricName, rrdFilename, startTime, endTime);
MetricData metricData = getMetricData(rrdFilename, startTime, endTime);
createSlide(ppt, metricName, graph, metricData);
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ppt.write(bos);
bos.close();
LOGGER.trace("EXITING: createPptReport");
return bos;
}
@Override
public String createJsonData(String metricName, String rrdFilename, long startTime,
long endTime) throws IOException, MetricsGraphException {
LOGGER.trace("ENTERING: createJsonData");
JSONObject obj = new JSONObject();
String displayableMetricName = convertCamelCase(metricName);
MetricData metricData = getMetricData(rrdFilename, startTime, endTime);
String title = displayableMetricName + " for " + getCalendarTime(startTime) + " to "
+ getCalendarTime(endTime);
obj.put("title", title);
List<Long> timestamps = metricData.getTimestamps();
List<Double> values = metricData.getValues();
JSONArray samples = new JSONArray();
for (int i = 0; i < timestamps.size(); i++) {
String timestamp = getCalendarTime(timestamps.get(i));
JSONObject sample = new JSONObject();
sample.put("timestamp", timestamp);
sample.put("value", new Double(values.get(i)));
samples.add(sample);
}
obj.put("data", samples);
if (metricData.hasTotalCount()) {
obj.put("totalCount", metricData.getTotalCount());
}
JsonWriter writer = new JsonWriter();
obj.writeJSONString(writer);
String jsonText = writer.toString();
LOGGER.trace("jsonText = {}", jsonText);
LOGGER.trace("EXITING: createJsonData");
return jsonText;
}
@Override
public OutputStream createXlsReport(List<String> metricNames, String metricsDir, long startTime,
long endTime, String summaryInterval) throws IOException, MetricsGraphException {
LOGGER.trace("ENTERING: createXlsReport");
Workbook wb = new HSSFWorkbook();
Collections.sort(metricNames);
if (StringUtils.isNotEmpty(summaryInterval)) {
createSummary(wb,
metricNames,
metricsDir,
startTime,
endTime,
SUMMARY_INTERVALS.valueOf(summaryInterval));
} else {
for (int i = 0; i < metricNames.size(); i++) {
String metricName = metricNames.get(i);
String rrdFilename = getRrdFilename(metricsDir, metricName);
String displayName = i + metricName;
createSheet(wb, displayName, rrdFilename, startTime, endTime);
}
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
wb.write(bos);
bos.close();
LOGGER.trace("EXITING: createXlsReport");
return bos;
}
private void createSummary(Workbook wb, List<String> metricNames, String metricsDir,
long startTime, long endTime, SUMMARY_INTERVALS summaryInterval)
throws IOException, MetricsGraphException {
// convert seconds to milliseconds
startTime = TimeUnit.SECONDS.toMillis(startTime);
endTime = TimeUnit.SECONDS.toMillis(endTime);
DateTime reportStart = new DateTime(startTime, DateTimeZone.UTC);
DateTime reportEnd = new DateTime(endTime, DateTimeZone.UTC);
Sheet sheet = wb.createSheet();
wb.setSheetName(0,
reportStart.toString(SUMMARY_TIMESTAMP) + " to " + reportEnd.toString(
SUMMARY_TIMESTAMP));
Row headingRow = sheet.createRow(0);
int columnMax = 1;
for (String metricName : metricNames) {
MutableDateTime chunkStart = new MutableDateTime(reportStart);
MutableDateTime chunkEnd = new MutableDateTime(chunkStart);
Row row = sheet.createRow(metricNames.indexOf(metricName) + 1);
int columnCounter = 1;
Boolean isSum = null;
while (reportEnd.compareTo(chunkEnd) > 0 && columnCounter < EXCEL_MAX_COLUMNS) {
increment(chunkEnd, summaryInterval);
if (chunkEnd.isAfter(reportEnd)) {
chunkEnd.setMillis(reportEnd);
}
// offset range by one millisecond so rrd will calculate granularity correctly
chunkEnd.addMillis(-1);
MetricData metricData = getMetricData(getRrdFilename(metricsDir, metricName),
TimeUnit.MILLISECONDS.toSeconds(chunkStart.getMillis()),
TimeUnit.MILLISECONDS.toSeconds(chunkEnd.getMillis()));
isSum = metricData.hasTotalCount();
chunkEnd.addMillis(1);
if (headingRow.getCell(columnCounter) == null) {
Cell headingRowCell = headingRow.createCell(columnCounter);
headingRowCell.getCellStyle()
.setWrapText(true);
headingRowCell.setCellValue(getTimestamp(chunkStart,
chunkEnd,
columnCounter,
summaryInterval));
}
Cell sumOrAvg = row.createCell(columnCounter);
if (isSum) {
sumOrAvg.setCellValue((double) metricData.getTotalCount());
} else {
sumOrAvg.setCellValue(cumulativeRunningAverage(metricData.getValues()));
}
chunkStart.setMillis(chunkEnd);
columnCounter++;
}
columnMax = columnCounter;
if (isSum != null) {
row.createCell(0)
.setCellValue(convertCamelCase(metricName) + " (" + (isSum ? "sum" : "avg")
+ ")");
}
}
for (int i = 0; i < columnMax; i++) {
sheet.autoSizeColumn(i);
}
}
private double cumulativeRunningAverage(List<Double> values) {
if (values.size() == 0) {
return 0;
}
SummaryStatistics summaryStatistics = new SummaryStatistics();
for (Double value : values) {
summaryStatistics.addValue(value);
}
return summaryStatistics.getMean();
}
private String getTimestamp(MutableDateTime chunkStart, MutableDateTime chunkEnd,
int columnCounter, SUMMARY_INTERVALS summaryInterval) {
StringBuilder title = new StringBuilder();
title.append(StringUtils.capitalize(summaryInterval.toString()))
.append(" ")
.append(columnCounter)
.append("\n");
String timestamp = SUMMARY_TIMESTAMP;
switch (summaryInterval) {
case minute:
case hour:
break;
case day:
case week:
timestamp = "dd-MM-y";
break;
case month:
timestamp = "MM-y";
break;
}
title.append(chunkStart.toDateTime(DateTimeZone.getDefault())
.toString(timestamp))
.append(" to ")
.append(chunkEnd.toDateTime(DateTimeZone.getDefault())
.toString(timestamp));
return title.toString();
}
private String getRrdFilename(String metricsDir, String metricName) {
return metricsDir + metricName + ".rrd";
}
private void increment(MutableDateTime chunkStart, SUMMARY_INTERVALS summaryInterval) {
switch (summaryInterval) {
case minute:
chunkStart.addMinutes(1);
break;
case hour:
chunkStart.addHours(1);
break;
case day:
chunkStart.addDays(1);
break;
case week:
chunkStart.addWeeks(1);
break;
case month:
chunkStart.addMonths(1);
break;
}
}
/**
* Creates an Excel worksheet containing the metric's data (timestamps and values) for the
* specified time range. This worksheet is titled with the trhe metric's name and added to the
* specified Workbook.
*
* @param wb the workbook to add this worksheet to
* @param metricName the name of the metric whose data is being rendered in this worksheet
* @param rrdFilename the name of the RRD file to retrieve the metric's data from
* @param startTime start time, in seconds since Unix epoch, to fetch metric's data
* @param endTime end time, in seconds since Unix epoch, to fetch metric's data
* @throws IOException
* @throws MetricsGraphException
*/
private void createSheet(Workbook wb, String metricName, String rrdFilename, long startTime,
long endTime) throws IOException, MetricsGraphException {
LOGGER.trace("ENTERING: createSheet");
MetricData metricData = getMetricData(rrdFilename, startTime, endTime);
String displayableMetricName = convertCamelCase(metricName);
String title = displayableMetricName + " for " + getCalendarTime(startTime) + " to "
+ getCalendarTime(endTime);
Sheet sheet = wb.createSheet(displayableMetricName);
Font headerFont = wb.createFont();
headerFont.setBoldweight(Font.BOLDWEIGHT_BOLD);
CellStyle columnHeadingsStyle = wb.createCellStyle();
columnHeadingsStyle.setFont(headerFont);
CellStyle bannerStyle = wb.createCellStyle();
bannerStyle.setFont(headerFont);
bannerStyle.setFillForegroundColor(HSSFColor.PALE_BLUE.index);
bannerStyle.setFillPattern(CellStyle.SOLID_FOREGROUND);
int rowCount = 0;
Row row = sheet.createRow((short) rowCount);
Cell cell = row.createCell(0);
cell.setCellValue(title);
cell.setCellStyle(bannerStyle);
rowCount++;
// Blank row for spacing/readability
row = sheet.createRow((short) rowCount);
cell = row.createCell(0);
cell.setCellValue("");
rowCount++;
row = sheet.createRow((short) rowCount);
cell = row.createCell(0);
cell.setCellValue("Timestamp");
cell.setCellStyle(columnHeadingsStyle);
cell = row.createCell(1);
cell.setCellValue("Value");
cell.setCellStyle(columnHeadingsStyle);
rowCount++;
List<Long> timestamps = metricData.getTimestamps();
List<Double> values = metricData.getValues();
for (int i = 0; i < timestamps.size(); i++) {
String timestamp = getCalendarTime(timestamps.get(i));
row = sheet.createRow((short) rowCount);
row.createCell(0)
.setCellValue(timestamp);
row.createCell(1)
.setCellValue(values.get(i));
rowCount++;
}
if (metricData.hasTotalCount()) {
// Blank row for spacing/readability
row = sheet.createRow((short) rowCount);
cell = row.createCell(0);
cell.setCellValue("");
rowCount++;
row = sheet.createRow((short) rowCount);
cell = row.createCell(0);
cell.setCellValue("Total Count: ");
cell.setCellStyle(columnHeadingsStyle);
row.createCell(1)
.setCellValue(metricData.getTotalCount());
}
sheet.autoSizeColumn(0);
sheet.autoSizeColumn(1);
LOGGER.trace("EXITING: createSheet");
}
/**
* Retrieves the RRD stored data for the specified metric over the specified time range.
*
* @param rrdFilename the name of the RRD file containing the metric's data
* @param startTime start time, in seconds since Unix epoch, to fetch metric's data
* @param endTime end time, in seconds since Unix epoch, to fetch metric's data
* @return domain object containing the metric's sampled data, which consists of the timestamps
* and their associated values, and the total count of the sampled data
* @throws IOException
* @throws MetricsGraphException
*/
public MetricData getMetricData(String rrdFilename, long startTime, long endTime)
throws IOException, MetricsGraphException {
LOGGER.trace("ENTERING: getMetricData");
// Create RRD DB in read-only mode for the specified RRD file
RrdDb rrdDb = new RrdDb(rrdFilename, true);
// Extract the data source (should always only be one data source per RRD file - otherwise
// we have a problem)
if (rrdDb.getDsCount() != 1) {
throw new MetricsGraphException(
"Only one data source per RRD file is supported - RRD file " + rrdFilename
+ " has " + rrdDb.getDsCount() + " data sources.");
}
// The step (sample) interval that determines how often RRD collects the metric's data
long rrdStep = rrdDb.getRrdDef()
.getStep();
// Retrieve the RRD file's data source type to determine how (later)
// to store the metric's data for presentation.
DsType dataSourceType = rrdDb.getDatasource(0)
.getType();
// Fetch the metric's data from the RRD file for the specified time range
FetchRequest fetchRequest = rrdDb.createFetchRequest(ConsolFun.TOTAL, startTime, endTime);
FetchData fetchData = fetchRequest.fetchData();
long[] timestamps = fetchData.getTimestamps();
double[] values = fetchData.getValues(0);
// Done retrieving data from the RRD database - close it, otherwise no one else will
// be able to access it later.
rrdDb.close();
// The lists of the metric's timestamps and their associated values that have non-"NaN"
// values
List<Long> validTimestamps = new ArrayList<Long>();
List<Double> validValues = new ArrayList<Double>();
long totalCount = 0;
MetricData metricData = new MetricData();
if (dataSourceType == DsType.COUNTER || dataSourceType == DsType.DERIVE) {
// Counters are for constantly incrementing data, hence they can
// have a summation of their totals
metricData.setHasTotalCount(true);
for (int i = 0; i < timestamps.length; i++) {
long timestamp = timestamps[i];
// Filter out the RRD values that have not yet been sampled (they will
// have been set to NaN as a placeholder when the RRD file was created)
if (timestamp >= startTime && timestamp <= endTime && !Double.toString(values[i])
.equals("NaN")) {
// RRD averages the collected samples over the step interval.
// To "undo" this averaging and get the actual count, need to
// multiply the sampled data value by the RRD step interval.
double nonAveragedValue = (double) (values[i] * rrdStep);
validTimestamps.add(timestamp);
validValues.add(nonAveragedValue);
totalCount += (long) nonAveragedValue;
}
}
} else if (dataSourceType == DsType.GAUGE) {
// Gauges are for data that waxes and wanes, hence no total count
metricData.setHasTotalCount(false);
for (int i = 0; i < timestamps.length; i++) {
long timestamp = timestamps[i];
// Filter out the RRD values that have not yet been sampled (they will
// have been set to NaN as a placeholder when the RRD file was created)
if (timestamp >= startTime && timestamp <= endTime && !Double.toString(values[i])
.equals("NaN")) {
validTimestamps.add(timestamp);
validValues.add(values[i]);
}
}
}
metricData.setTimestamps(validTimestamps);
metricData.setValues(validValues);
metricData.setTotalCount(totalCount);
LOGGER.trace("EXITING: getMetricData");
return metricData;
}
/**
* Adds a slide containing the metric's graph to the PowerPoint slide deck. The title is usually
* the metric's name and is usually in camelCase format. This will be converted to individual,
* capitalized words to the slide's title. The metric's data is used to determine the total
* count across all of the metric's data, which is displayed at the bottom of the slide, under
* the graph.
*
* @param ppt the PowerPoint slide deck to add this slide to
* @param title the title for this slide
* @param graph the metric's graph to be added to this slide
* @param metricData the metric's data
* @throws IOException
* @throws MetricsGraphException
*/
private void createSlide(SlideShow ppt, String title, byte[] graph, MetricData metricData)
throws IOException, MetricsGraphException {
LOGGER.trace("ENTERING: createSlide");
if (LOGGER.isDebugEnabled()) {
java.awt.Dimension pgsize = ppt.getPageSize();
int pgx = pgsize.width; // slide width (720)
int pgy = pgsize.height; // slide height (540)
LOGGER.debug("ppt page width = {}", pgx);
LOGGER.debug("ppt page height = {}", pgy);
}
// Convert title, if it is in camelCase, to individual words with each word
// starting with a capital letter
String slideTitle = convertCamelCase(title);
Slide slide = ppt.createSlide();
// Add the title to the slide
TextBox titleTextBox = slide.addTitle();
TextRun textRun = titleTextBox.getTextRun();
textRun.getRichTextRuns()[0].setFontSize(32);
titleTextBox.setText(slideTitle);
titleTextBox.setHorizontalAlignment(TextBox.AlignCenter);
// Add the metric's graph to the slide
int idx = ppt.addPicture(graph, Picture.PNG);
Picture pict = new Picture(idx);
// set graph's position and size in the slide
// (Be sure to maintain aspect ratio for the image when specifying the
// width and height. Refer to width and height values used in createGraph())
pict.setAnchor(new Rectangle(20, 100, 650, 325));
slide.addShape(pict);
// If metric has a total count, add it under the graph on the slide
if (metricData.hasTotalCount()) {
TextBox totalCountTextBox = new TextBox();
textRun = totalCountTextBox.getTextRun();
textRun.getRichTextRuns()[0].setFontSize(14);
totalCountTextBox.setText("Total Count: " + metricData.getTotalCount());
totalCountTextBox.setHorizontalAlignment(TextBox.AlignLeft);
// x,y values determined relative to x,y of graph's anchor position
// and the height of the graph
totalCountTextBox.setAnchor(new Rectangle(20, 450, 250, 80));
slide.addShape(totalCountTextBox);
}
LOGGER.trace("EXITING: createSlide");
}
public void setMetricsMaxThreshold(double metricsMaxThreshold) {
LOGGER.trace("Setting metricsMaxThreshold = {}", metricsMaxThreshold);
this.metricsMaxThreshold = metricsMaxThreshold;
}
private void dumpData(ConsolFun consolFun, String dataType, RrdDb rrdDb, String dsType,
long startTime, long endTime) {
String rrdFilename = rrdDb.getPath();
LOGGER.trace("*********** START Dump of RRD file: [{}] ***************", rrdFilename);
LOGGER.trace("metricsMaxThreshold = {}", metricsMaxThreshold);
FetchRequest fetchRequest = rrdDb.createFetchRequest(consolFun, startTime, endTime);
try {
FetchData fetchData = fetchRequest.fetchData();
LOGGER.trace("************ {}: {} **************", dsType, dataType);
int rrdStep = RRD_STEP; // in seconds
long[] timestamps = fetchData.getTimestamps();
double[] values = fetchData.getValues(0);
double[] adjustedValues = new double[values.length];
for (int i = 0; i < values.length; i++) {
double adjustedValue = values[i] * rrdStep;
adjustedValues[i] = adjustedValue;
LOGGER.trace(
getCalendarTime(timestamps[i]) + ": " + values[i] + " (adjusted value = "
+ adjustedValue + ", floor = " + Math.floor(adjustedValue)
+ ", round = " + Math.round(adjustedValue) + ")");
}
LOGGER.trace("adjustedValues.length = {}", adjustedValues.length);
for (int i = 0; i < adjustedValues.length; i++) {
if (adjustedValues[i] > metricsMaxThreshold) {
LOGGER.trace("Value [{}] is an OUTLIER", adjustedValues[i]);
}
}
} catch (IOException e) {
}
LOGGER.trace("*********** END Dump of RRD file: [{}] ***************", rrdFilename);
}
}