/*************************************************************************
* Copyright 2009-2013 Eucalyptus Systems, Inc.
*
* 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 3 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, see http://www.gnu.org/licenses/.
*
* Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
************************************************************************/
package com.eucalyptus.cluster.callback.reporting;
import java.util.ArrayList;
import java.util.Date;
import java.util.Map;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
import javax.persistence.EntityTransaction;
import com.eucalyptus.cloudwatch.common.internal.domain.metricdata.Units;
import com.eucalyptus.cloudwatch.common.msgs.Dimension;
import com.eucalyptus.cloudwatch.common.msgs.Dimensions;
import com.eucalyptus.cloudwatch.common.msgs.MetricDatum;
import com.eucalyptus.cloudwatch.common.msgs.StatisticSet;
import com.eucalyptus.entities.TransactionResource;
import com.google.common.collect.ImmutableMap;
import org.apache.log4j.Logger;
import org.hibernate.Criteria;
import org.hibernate.criterion.Restrictions;
import com.eucalyptus.cluster.callback.reporting.DefaultAbsoluteMetricConverter.AbsoluteMetricCache;
import com.eucalyptus.entities.Entities;
import com.eucalyptus.records.Logs;
public class AbsoluteMetricHelper {
static final double TOLERANCE = 0.0000001; // arbitrary to check double "equality"
static final Map<String, String> EBS_ABSOLUTE_METRICS =
new ImmutableMap.Builder<String, String>()
.put("VolumeReadOpsAbsolute", "VolumeReadOps")
.put("VolumeWriteOpsAbsolute", "VolumeWriteOps")
.put("VolumeReadBytesAbsolute", "VolumeReadBytes")
.put("VolumeWriteBytesAbsolute", "VolumeWriteBytes")
.put("VolumeConsumedReadWriteOpsAbsolute", "VolumeConsumedReadWriteOps")
.put("VolumeTotalReadTimeAbsolute", "VolumeTotalReadTime")
.put("VolumeTotalWriteTimeAbsolute", "VolumeTotalWriteTime")
.put("VolumeTotalReadWriteTimeAbsolute", "VolumeTotalReadWriteTime")
.build();
static final Map<String, String> EC2_ABSOLUTE_METRICS =
new ImmutableMap.Builder<String, String>()
.put("DiskReadOpsAbsolute", "DiskReadOps")
.put("DiskWriteOpsAbsolute", "DiskWriteOps")
.put("DiskReadBytesAbsolute", "DiskReadBytes")
.put("DiskWriteBytesAbsolute", "DiskWriteBytes")
.put("NetworkInAbsolute", "NetworkIn")
.put("NetworkOutAbsolute", "NetworkOut")
.put("NetworkPacketsInAbsolute", "NetworkPacketsIn")
.put("NetworkPacketsOutAbsolute", "NetworkPacketsOut")
.build();
static final long MAX_DIFFERENCE_DURATION_MS = TimeUnit.MINUTES.toMillis(15L); // 15 minutes, also max reporting time
static final String AWS_EBS_NAMESPACE = "AWS/EBS";
static final String VOLUME_ID_DIM_NAME = "VolumeId";
static final String AWS_EC2_NAMESPACE = "AWS/EC2";
static final String INSTANCE_ID_DIM_NAME = "InstanceId";
static final String VOLUME_READ_OPS_METRIC_NAME = "VolumeReadOps";
static final String VOLUME_QUEUE_LENGTH_METRIC_NAME = "VolumeQueueLength";
static final String VOLUME_TOTAL_READ_WRITE_TIME_METRIC_NAME = "VolumeTotalReadWriteTime";
static final String VOLUME_QUEUE_LENGTH_PLACEHOLDER_METRIC_NAME = "VolumeQueueLengthPlaceHolder";
static final String VOLUME_QUEUE_LENGTH_PLACEHOLDER_ABSOLUTE_METRIC_NAME = "VolumeQueueLengthPlaceHolderAbsolute";
static final String CPU_UTILIZATION_MS_ABSOLUTE_METRIC_NAME = "CPUUtilizationMSAbsolute";
static final String CPU_UTILIZATION_METRIC_NAME = "CPUUtilization";
private static final Logger LOG = Logger.getLogger(AbsoluteMetricHelper.class);
static void convertVolumeTotalReadWriteTimeToVolumeIdleTime(final MetricDatum datum) {
// we convert this to VolumeIdleTime = Period Length - VolumeTotalReadWriteTime on the period (though won't be negative)
datum.setMetricName("VolumeIdleTime");
double totalReadWriteTime = datum.getStatisticValues().getSum(); // value is in seconds
double totalPeriodTime = 60.0 * datum.getStatisticValues().getSampleCount();
double totalIdleTime = totalPeriodTime - totalReadWriteTime;
if (totalIdleTime < 0) totalIdleTime = 0; // if we have read and written more than in the period, don't go negative
datum.getStatisticValues().setSum(totalIdleTime);
double averageIdleTime = totalIdleTime / datum.getStatisticValues().getSampleCount();
datum.getStatisticValues().setMaximum(averageIdleTime);
datum.getStatisticValues().setMinimum(averageIdleTime);
}
static AbsoluteMetricQueueItem createVolumeThroughputMetric(String accountId, String nameSpace, MetricDatum datum) {
// add volume throughput percentage. (The guess is that there will be a set of volume metrics.
// Attach it to one so it will be sent as many times as the others.
// add one
MetricDatum vtpDatum = new MetricDatum();
vtpDatum.setMetricName("VolumeThroughputPercentage");
vtpDatum.setTimestamp(datum.getTimestamp());
vtpDatum.setUnit(Units.Percent.toString());
// should be 100% but weigh it the same
if (datum.getValue() != null) {
vtpDatum.setValue(100.0); // Any time we have a volume data, this value is 100%
} else if (datum.getStatisticValues() != null) {
StatisticSet statisticSet = new StatisticSet();
statisticSet.setMaximum(100.0);
statisticSet.setMinimum(100.0);
statisticSet.setSum(100.0 * datum.getStatisticValues().getSampleCount());
statisticSet.setSampleCount(datum.getStatisticValues().getSampleCount());
vtpDatum.setStatisticValues(statisticSet);
}
// use the same dimensions as current metric
Dimensions vtpDimensions = new Dimensions();
ArrayList<Dimension> vtpDimensionsMember = new ArrayList<Dimension>();
if ( datum.getDimensions( ) != null ) for ( final Dimension dimension: datum.getDimensions( ).getMember( ) ) {
Dimension vtpDimension = new Dimension();
vtpDimension.setName(dimension.getName());
vtpDimension.setValue(dimension.getValue());
vtpDimensionsMember.add(vtpDimension);
}
vtpDimensions.setMember(vtpDimensionsMember);
vtpDatum.setDimensions(vtpDimensions);
AbsoluteMetricQueueItem vtpQueueItem = new AbsoluteMetricQueueItem();
vtpQueueItem.setAccountId(accountId);
vtpQueueItem.setNamespace(nameSpace);
vtpQueueItem.setMetricDatum(vtpDatum);
return vtpQueueItem;
}
public static class MetricDifferenceInfo {
private Double valueDifference;
private Long elapsedTimeInMillis;
public Double getValueDifference() {
return valueDifference;
}
public Long getElapsedTimeInMillis() {
return elapsedTimeInMillis;
}
public MetricDifferenceInfo(Double valueDifference, Long elapsedTimeInMillis) {
this.valueDifference = valueDifference;
this.elapsedTimeInMillis = elapsedTimeInMillis;
}
}
public static MetricDifferenceInfo calculateDifferenceSinceLastEvent(AbsoluteMetricCache cache, String namespace, String metricName, String dimensionName, String dimensionValue, Date newTimestamp, Double newMetricValue) {
LOG.trace("namespace="+namespace+",metricName="+metricName+",dimensionName="+dimensionName+",dimensionValue="+dimensionValue+",newTimestamp="+newTimestamp+",newMetricValue="+newMetricValue);
MetricDifferenceInfo returnValue = null;
AbsoluteMetricHistory lastEntity = cache.lookup(namespace, metricName, dimensionName, dimensionValue);
if (lastEntity == null) {
// first data point, add it and return null (nothing to diff against)
LOG.trace("First entry");
lastEntity = new AbsoluteMetricHistory();
lastEntity.setNamespace(namespace);
lastEntity.setMetricName(metricName);
lastEntity.setDimensionName(dimensionName);
lastEntity.setDimensionValue(dimensionValue);
lastEntity.setTimestamp(newTimestamp);
lastEntity.setLastMetricValue(newMetricValue);
Entities.persist(lastEntity);
cache.put(namespace, metricName, dimensionName, dimensionValue, lastEntity);
returnValue = null;
} else {
long elapsedTimeInMillis = newTimestamp.getTime() - lastEntity.getTimestamp().getTime();
LOG.trace("lastTimestamp="+lastEntity.getTimestamp());
double valueDifference = newMetricValue - lastEntity.getLastMetricValue();
if (elapsedTimeInMillis < 0) {
LOG.trace("earlier point, kicking out");
// a negative value of elapsedTimeInMillis means this data point is not useful
return null;
} else if (elapsedTimeInMillis == 0) {
if (Math.abs(valueDifference) > TOLERANCE) {
LOG.warn("Getting different values " + newMetricValue + " and " + lastEntity.getLastMetricValue() + " for absolute metric " + metricName + " at the same timestamp " + newTimestamp + ", keeping the second value.");
}
return null; // not a useful data point either
} else if (elapsedTimeInMillis > MAX_DIFFERENCE_DURATION_MS) {
// Too much time has passed, a useful data point, but we will not report the 'difference'. We will reset.
LOG.trace("too much time has passed, (" + elapsedTimeInMillis + " ms), starting over");
lastEntity.setTimestamp(newTimestamp);
lastEntity.setLastMetricValue(newMetricValue);
returnValue = null;
} else if (elapsedTimeInMillis > 0) {
lastEntity.setTimestamp(newTimestamp);
lastEntity.setLastMetricValue(newMetricValue);
if (valueDifference < -TOLERANCE) { // value has gone "down" (or down more than the TOLERANCE)
// if the value difference is negative (i.e. has gone down, the assumption is that the NC has restarted, and the new
// value started from some time in the past. Best thing to do here is either assume it is a first point again, or
// assume the previous point had a 0 value. Not sure which is the better choice, but for now, we will make it a "first"
// point again
returnValue = null;
} else { // truncate differences within AbsoluteMetricCommon.TOLERANCE to zero
if (Math.abs(valueDifference) < TOLERANCE) {
valueDifference = 0.0;
}
returnValue = new MetricDifferenceInfo(valueDifference, elapsedTimeInMillis);
}
}
if (returnValue != null) {
LOG.trace("new values=valueDifference="+valueDifference+",elapsedTimeInMillis="+elapsedTimeInMillis);
} else {
LOG.trace("sending null value out");
}
}
return returnValue;
}
public static MetricDifferenceInfo calculateDifferenceSinceLastEvent(String namespace, String metricName, String dimensionName, String dimensionValue, Date newTimestamp, Double newMetricValue) {
LOG.trace("namespace="+namespace+",metricName="+metricName+",dimensionName="+dimensionName+",dimensionValue="+dimensionValue+",newTimestamp="+newTimestamp+",newMetricValue="+newMetricValue);
MetricDifferenceInfo returnValue = null;
try (final TransactionResource db = Entities.transactionFor(AbsoluteMetricHistory.class)) {
Criteria criteria = Entities.createCriteria(AbsoluteMetricHistory.class)
.add( Restrictions.eq( "namespace", namespace ) )
.add( Restrictions.eq( "metricName", metricName ) )
.add( Restrictions.eq( "dimensionName", dimensionName ) )
.add( Restrictions.eq( "dimensionValue", dimensionValue ) );
AbsoluteMetricHistory lastEntity = (AbsoluteMetricHistory) criteria.uniqueResult();
if (lastEntity == null) {
// first data point, add it and return null (nothing to diff against)
LOG.trace("First entry");
lastEntity = new AbsoluteMetricHistory();
lastEntity.setNamespace(namespace);
lastEntity.setMetricName(metricName);
lastEntity.setDimensionName(dimensionName);
lastEntity.setDimensionValue(dimensionValue);
lastEntity.setTimestamp(newTimestamp);
lastEntity.setLastMetricValue(newMetricValue);
Entities.persist(lastEntity);
returnValue = null;
} else {
long elapsedTimeInMillis = newTimestamp.getTime() - lastEntity.getTimestamp().getTime();
LOG.trace("lastTimestamp="+lastEntity.getTimestamp());
double valueDifference = newMetricValue - lastEntity.getLastMetricValue();
if (elapsedTimeInMillis < 0) {
LOG.trace("earlier point, kicking out");
// a negative value of elapsedTimeInMillis means this data point is not useful
returnValue = null;
} else if (elapsedTimeInMillis == 0) {
if (Math.abs(valueDifference) > TOLERANCE) {
LOG.warn("Getting different values " + newMetricValue + " and " + lastEntity.getLastMetricValue() + " for absolute metric " + metricName + " at the same timestamp " + newTimestamp + ", keeping the second value.");
}
returnValue = null; // not a useful data point either
} else if (elapsedTimeInMillis > MAX_DIFFERENCE_DURATION_MS) {
// Too much time has passed, a useful data point, but we will not report the 'difference'. We will reset.
LOG.trace("too much time has passed, (" + elapsedTimeInMillis + " ms), starting over");
lastEntity.setTimestamp(newTimestamp);
lastEntity.setLastMetricValue(newMetricValue);
returnValue = null;
} else if (elapsedTimeInMillis > 0) {
lastEntity.setTimestamp(newTimestamp);
lastEntity.setLastMetricValue(newMetricValue);
if (valueDifference < -TOLERANCE) { // value has gone "down" (or down more than the TOLERANCE)
// if the value difference is negative (i.e. has gone down, the assumption is that the NC has restarted, and the new
// value started from some time in the past. Best thing to do here is either assume it is a first point again, or
// assume the previous point had a 0 value. Not sure which is the better choice, but for now, we will make it a "first"
// point again
returnValue = null;
} else { // truncate differences within AbsoluteMetricCommon.TOLERANCE to zero
if (Math.abs(valueDifference) < TOLERANCE) {
valueDifference = 0.0;
}
returnValue = new MetricDifferenceInfo(valueDifference, elapsedTimeInMillis);
}
}
if (returnValue != null) {
LOG.trace("new values=valueDifference="+valueDifference+",elapsedTimeInMillis="+elapsedTimeInMillis);
} else {
LOG.trace("sending null value out");
}
}
db.commit();
}
return returnValue;
}
/**
* Delete all absolute metric history before a certain date
* @param before the date to delete before (inclusive)
*/
public static void deleteAbsoluteMetricHistory(Date before) {
try (final TransactionResource db = Entities.transactionFor(AbsoluteMetricHistory.class)) {
Map<String, Date> criteria = new HashMap<String, Date>();
criteria.put("before", before);
Entities.deleteAllMatching(AbsoluteMetricHistory.class, "WHERE timestamp < :before", criteria);
db.commit();
}
}
}