/*
* RHQ Management Platform
* Copyright (C) 2005-2013 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
package org.rhq.coregui.client.inventory.resource.detail.monitoring.table;
import static org.rhq.core.domain.measurement.DataType.COMPLEX;
import static org.rhq.core.domain.measurement.DataType.MEASUREMENT;
import static org.rhq.coregui.client.inventory.resource.detail.monitoring.table.MetricsGridFieldName.ALERT_COUNT;
import static org.rhq.coregui.client.inventory.resource.detail.monitoring.table.MetricsGridFieldName.AVG_VALUE;
import static org.rhq.coregui.client.inventory.resource.detail.monitoring.table.MetricsGridFieldName.LIVE_VALUE;
import static org.rhq.coregui.client.inventory.resource.detail.monitoring.table.MetricsGridFieldName.MAX_VALUE;
import static org.rhq.coregui.client.inventory.resource.detail.monitoring.table.MetricsGridFieldName.METRIC_DEF_ID;
import static org.rhq.coregui.client.inventory.resource.detail.monitoring.table.MetricsGridFieldName.METRIC_LABEL;
import static org.rhq.coregui.client.inventory.resource.detail.monitoring.table.MetricsGridFieldName.METRIC_NAME;
import static org.rhq.coregui.client.inventory.resource.detail.monitoring.table.MetricsGridFieldName.METRIC_SCHEDULE_ID;
import static org.rhq.coregui.client.inventory.resource.detail.monitoring.table.MetricsGridFieldName.METRIC_UNITS;
import static org.rhq.coregui.client.inventory.resource.detail.monitoring.table.MetricsGridFieldName.MIN_VALUE;
import static org.rhq.coregui.client.inventory.resource.detail.monitoring.table.MetricsGridFieldName.RESOURCE_ID;
import static org.rhq.coregui.client.inventory.resource.detail.monitoring.table.MetricsGridFieldName.SPARKLINE;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.InvocationException;
import com.smartgwt.client.data.DSRequest;
import com.smartgwt.client.data.DSResponse;
import com.smartgwt.client.data.Record;
import com.smartgwt.client.widgets.grid.CellFormatter;
import com.smartgwt.client.widgets.grid.ListGridField;
import com.smartgwt.client.widgets.grid.ListGridRecord;
import org.rhq.core.domain.criteria.Criteria;
import org.rhq.core.domain.measurement.DataType;
import org.rhq.core.domain.measurement.MeasurementData;
import org.rhq.core.domain.measurement.MeasurementDefinition;
import org.rhq.core.domain.measurement.MeasurementSchedule;
import org.rhq.core.domain.measurement.MeasurementUnits;
import org.rhq.core.domain.measurement.composite.MeasurementDataNumericHighLowComposite;
import org.rhq.core.domain.measurement.ui.MetricDisplaySummary;
import org.rhq.core.domain.measurement.ui.MetricDisplayValue;
import org.rhq.core.domain.resource.Resource;
import org.rhq.coregui.client.CoreGUI;
import org.rhq.coregui.client.gwt.GWTServiceLookup;
import org.rhq.coregui.client.inventory.common.graph.CustomDateRangeState;
import org.rhq.coregui.client.util.BrowserUtility;
import org.rhq.coregui.client.util.Log;
import org.rhq.coregui.client.util.MeasurementConverterClient;
import org.rhq.coregui.client.util.MeasurementUtility;
import org.rhq.coregui.client.util.RPCDataSource;
import org.rhq.coregui.client.util.async.Command;
import org.rhq.coregui.client.util.async.CountDownLatch;
/**
* A simple data source to read in metric data summaries for a resource.
* This doesn't support paging - everything is returned in one query. Since
* the number of metrics per resource is relatively small (never more than tens of them),
* we just load them all in at once.
*
* @author John Mazzitelli
* @author Mike Thompson
*/
public class MetricsViewDataSource extends RPCDataSource<MetricDisplaySummary, Criteria> {
private static final int NUMBER_OF_METRIC_POINTS = 60;
private final Resource resource;
private List<MetricDisplaySummary> metricDisplaySummaries;
private List<List<MeasurementDataNumericHighLowComposite>> metricsDataList;
private Set<MeasurementData> liveMeasurementDataSet;
private int[] definitionArrayIds;
private int[] enabledScheduleIds;
private int[] enabledScheduleDefinitionIds;
private HashMap<Integer, MeasurementUnits> scheduleToMeasurementUnitMap = new HashMap<Integer, MeasurementUnits>();
public MetricsViewDataSource(Resource resource) {
this.resource = resource;
}
/**
* The view that contains the list grid which will display this datasource's data will call this
* method to get the field information which is used to control the display of the data.
*
* @return list grid fields used to display the datasource data
*/
public ArrayList<ListGridField> getListGridFields() {
ArrayList<ListGridField> fields = new ArrayList<ListGridField>(7);
ListGridField sparklineField = new ListGridField(SPARKLINE.getValue(), MSG.chart_metrics_sparkline_header());
sparklineField.setCellFormatter(new CellFormatter() {
@Override
public String format(Object value, ListGridRecord record, int rowNum, int colNum) {
if (value == null) {
return "";
}
String contents = "<span id='sparkline_" + resource.getId() + "-"
+ record.getAttributeAsInt(METRIC_DEF_ID.getValue()) + "' class='dynamicsparkline' width='70' "
+ "values='" + record.getAttribute(SPARKLINE.getValue()) + "'></span>";
return contents;
}
});
sparklineField.setWidth(80);
fields.add(sparklineField);
ListGridField nameField = new ListGridField(METRIC_LABEL.getValue(), METRIC_LABEL.getLabel());
nameField.setWidth("30%");
fields.add(nameField);
ListGridField minField = new ListGridField(MIN_VALUE.getValue(), MIN_VALUE.getLabel());
minField.setWidth("15%");
fields.add(minField);
ListGridField maxField = new ListGridField(MAX_VALUE.getValue(), MAX_VALUE.getLabel());
maxField.setWidth("15%");
fields.add(maxField);
ListGridField avgField = new ListGridField(AVG_VALUE.getValue(), AVG_VALUE.getLabel());
avgField.setWidth("15%");
fields.add(avgField);
ListGridField liveField = new ListGridField(LIVE_VALUE.getValue(), LIVE_VALUE.getLabel());
liveField.setWidth("15%");
fields.add(liveField);
ListGridField alertsField = new ListGridField(ALERT_COUNT.getValue(), ALERT_COUNT.getLabel());
alertsField.setWidth("10%");
fields.add(alertsField);
return fields;
}
@Override
public MetricDisplaySummary copyValues(Record from) {
// we should never need this method - we only go in one direction
// if we ever need this, just have copyValues store an "object" attribute whose value is "from"
// which this method then just reads out. Since we don't need this now, save memory by not
// keeping the MetricDisplayValue around
return null;
}
@Override
public ListGridRecord copyValues(MetricDisplaySummary from) {
MeasurementUtility.formatSimpleMetrics(from);
ListGridRecord record = new ListGridRecord();
record.setAttribute(SPARKLINE.getValue(), getCsvMetricsForSparkline(from.getDefinitionId()));
record.setAttribute(METRIC_LABEL.getValue(), from.getLabel());
record.setAttribute(ALERT_COUNT.getValue(), String.valueOf(from.getAlertCount()));
record.setAttribute(MIN_VALUE.getValue(), getMetricStringValue(from.getMinMetric()));
record.setAttribute(MAX_VALUE.getValue(), getMetricStringValue(from.getMaxMetric()));
record.setAttribute(AVG_VALUE.getValue(), getMetricStringValue(from.getAvgMetric()));
record.setAttribute(LIVE_VALUE.getValue(), buildLiveValue(from));
record.setAttribute(METRIC_DEF_ID.getValue(), from.getDefinitionId());
record.setAttribute(METRIC_SCHEDULE_ID.getValue(), from.getScheduleId());
record.setAttribute(METRIC_UNITS.getValue(), from.getUnits());
record.setAttribute(METRIC_NAME.getValue(), from.getMetricName());
record.setAttribute(RESOURCE_ID.getValue(), resource.getId());
return record;
}
private String buildLiveValue(MetricDisplaySummary from) {
StringBuilder sb = new StringBuilder();
for (MeasurementData measurementData : liveMeasurementDataSet) {
if (from.getScheduleId() == measurementData.getScheduleId()) {
double doubleValue;
if (measurementData.getValue() instanceof Number) {
doubleValue = ((Number) measurementData.getValue()).doubleValue();
} else {
doubleValue = Double.parseDouble(measurementData.getValue().toString());
}
String liveValue = MeasurementConverterClient.format(doubleValue,
MeasurementUnits.valueOf(from.getUnits()), true, 0, 1);
sb.append(liveValue);
break;
}
}
return sb.toString();
}
private String getCsvMetricsForSparkline(int definitionId) {
StringBuilder sb = new StringBuilder();
List<MeasurementDataNumericHighLowComposite> selectedMetricsList = getMeasurementsForMeasurementDefId(definitionId);
for (MeasurementDataNumericHighLowComposite measurementData : selectedMetricsList) {
if (!Double.isNaN(measurementData.getValue())) {
sb.append((int) measurementData.getValue());
sb.append(",");
}
}
if (sb.toString().endsWith(",")) {
sb.setLength(sb.length() - 1);
}
// handle the case where we have just installed the server so not much history
// and our date range is set such that only one value returns which the
// sparkline graph will not plot anything, so we need at least 2 values
if (!sb.toString().contains(",")) {
// append another value just so we have 2 values and it will graph
return "0," + sb.toString();
}
return sb.toString();
}
private List<MeasurementDataNumericHighLowComposite> getMeasurementsForMeasurementDefId(int definitionId) {
int selectedIndex = 0;
// find the ordinal position as specified when querying the metrics
for (int i = 0; i < definitionArrayIds.length; i++) {
if (definitionArrayIds[i] == definitionId) {
selectedIndex = i;
break;
}
}
return metricsDataList.get(selectedIndex);
}
protected String getMetricStringValue(MetricDisplayValue value) {
return (value != null) ? value.toString() : "";
}
@Override
protected Criteria getFetchCriteria(DSRequest request) {
// NOTE: we don't use criterias for this datasource, just return null
return null;
}
@Override
protected void executeFetch(final DSRequest request, final DSResponse response, final Criteria unused) {
GWTServiceLookup.getMeasurementScheduleService().findSchedulesForResourceAndType(resource.getId(),
DataType.MEASUREMENT, null, true, new AsyncCallback<ArrayList<MeasurementSchedule>>() {
@Override
public void onSuccess(ArrayList<MeasurementSchedule> measurementSchedules) {
enabledScheduleIds = new int[measurementSchedules.size()];
enabledScheduleDefinitionIds = new int[measurementSchedules.size()];
int i = 0;
for (MeasurementSchedule measurementSchedule : measurementSchedules) {
enabledScheduleIds[i] = measurementSchedule.getId();
enabledScheduleDefinitionIds[i++] = measurementSchedule.getDefinition().getId();
}
// This latch is the last thing that gets executed after we have executed the
// 2 queries in Parallel
final CountDownLatch countDownLatch = CountDownLatch.create(2, new Command() {
@Override
public void execute() {
// we needed the ResourceMetrics query and Metric Display Summary
// to finish before we can query the live metrics and populate the
// result response
queryLiveMetrics(request, response);
}
});
Long start = CustomDateRangeState.getInstance().getStartTime();
Long end = CustomDateRangeState.getInstance().getEndTime();
queryResourceMetrics(resource, start, end, countDownLatch);
queryMetricDisplaySummaries(enabledScheduleIds, start, end, countDownLatch);
}
@Override
public void onFailure(Throwable caught) {
CoreGUI.getErrorHandler().handleError("Cannot load schedules", caught);
}
});
}
private void queryLiveMetrics(final DSRequest request, final DSResponse response) {
if (enabledScheduleDefinitionIds.length == 0) {
prepareUI(null, request, response);
return; // no enabled metrics, let's save 1 round trip
}
// actually go out and ask the agents for the data
GWTServiceLookup.getMeasurementDataService(60000).findLiveData(resource.getId(), enabledScheduleDefinitionIds,
new AsyncCallback<Set<MeasurementData>>() {
@Override
public void onSuccess(Set<MeasurementData> result) {
prepareUI(result, request, response);
}
/**
* Called when an asynchronous call fails to complete normally. {@link IncompatibleRemoteServiceException}s, {@link
* InvocationException}s, or checked exceptions thrown by the service method are examples of the type of failures that
* can be passed to this method.
* <p/>
* <p> If <code>caught</code> is an instance of an {@link IncompatibleRemoteServiceException} the application should
* try to get into a state where a browser refresh can be safely done. </p>
*
* @param caught failure encountered while executing a remote procedure call
*/
@Override
public void onFailure(Throwable caught) {
CoreGUI.getErrorHandler().handleError("Cannot load metrics", caught);
}
});
}
/**
* Just a helper method to keep code DRY
*/
private void prepareUI(Set<MeasurementData> result, DSRequest request, DSResponse response) {
liveMeasurementDataSet = null == result ? Collections.<MeasurementData>emptySet() : result;
response.setData(buildRecords(metricDisplaySummaries));
processResponse(request.getRequestId(), response);
new Timer() {
@Override
public void run() {
BrowserUtility.graphSparkLines();
}
}.schedule(150);
}
private void queryMetricDisplaySummaries(int[] scheduleIds, Long startTime, Long endTime,
final CountDownLatch countDownLatch) {
if (enabledScheduleIds.length == 0) {
setMetricDisplaySummaries(Collections.<MetricDisplaySummary>emptyList());
countDownLatch.countDown();
return; // no enabled metrics, let's save 1 round trip
}
GWTServiceLookup.getMeasurementChartsService().getMetricDisplaySummariesForResource(resource.getId(),
scheduleIds, startTime, endTime, new AsyncCallback<ArrayList<MetricDisplaySummary>>() {
@Override
public void onSuccess(ArrayList<MetricDisplaySummary> metricDisplaySummaries) {
setMetricDisplaySummaries(metricDisplaySummaries);
countDownLatch.countDown();
}
@Override
public void onFailure(Throwable caught) {
CoreGUI.getErrorHandler().handleError("Cannot load metrics", caught);
countDownLatch.countDown();
}
}
);
}
private void setMetricDisplaySummaries(List<MetricDisplaySummary> metricDisplaySummaries) {
this.metricDisplaySummaries = metricDisplaySummaries;
}
private void queryResourceMetrics(final Resource resource, final Long startTime, final Long endTime,
final CountDownLatch countDownLatch) {
List<MeasurementDefinition> definitions = getMetricDefinitions(resource);
if (definitions.size() == 0) {
countDownLatch.countDown();
return;
}
// create a mapping of schedules ids to MeasurementUnits
for (MeasurementDefinition definition : definitions) {
if (null != definition.getSchedules()) {
for (MeasurementSchedule schedule : definition.getSchedules()) {
scheduleToMeasurementUnitMap.put(schedule.getId(), definition.getUnits());
}
}
}
//bundle definition ids for asynch call.
definitionArrayIds = new int[definitions.size()];
//sort the charting data ex. Free Memory, Free Swap Space,..System Load
Collections.sort(definitions, new Comparator<MeasurementDefinition>() {
@Override
public int compare(MeasurementDefinition o1, MeasurementDefinition o2) {
return o1.getDisplayName().compareTo(o2.getDisplayName());
}
});
//organize definitionArrayIds for ordered request on server.
int index = 0;
for (MeasurementDefinition definitionToDisplay : definitions) {
definitionArrayIds[index++] = definitionToDisplay.getId();
}
GWTServiceLookup.getMeasurementDataService().findDataForResource(resource.getId(), definitionArrayIds,
startTime, endTime, NUMBER_OF_METRIC_POINTS,
new AsyncCallback<List<List<MeasurementDataNumericHighLowComposite>>>() {
@Override
public void onFailure(Throwable caught) {
Log.warn("Error retrieving recent metrics charting data for resource [" + resource.getId() + "]:"
+ caught.getMessage());
countDownLatch.countDown();
}
@Override
public void onSuccess(List<List<MeasurementDataNumericHighLowComposite>> measurementDataList) {
if (null != measurementDataList && !measurementDataList.isEmpty()) {
metricsDataList = measurementDataList;
}
countDownLatch.countDown();
}
});
}
private List<MeasurementDefinition> getMetricDefinitions(Resource resource) {
List<MeasurementDefinition> definitions = new ArrayList<MeasurementDefinition>();
for (MeasurementDefinition measurementDefinition : resource.getResourceType().getMetricDefinitions()) {
if (measurementDefinition.getDataType() == MEASUREMENT || measurementDefinition.getDataType() == COMPLEX) {
definitions.add(measurementDefinition);
}
}
return definitions;
}
}