package com.linkedin.thirdeye.detector.metric.transfer;
import com.linkedin.thirdeye.api.MetricTimeSeries;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.commons.collections.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MetricTransfer {
private static final Logger LOG = LoggerFactory.getLogger(MetricTransfer.class);
private static final boolean DEBUG = true;
public static final String SEASONAL_SIZE = "seasonalSize";
public static final String SEASONAL_UNIT = "seasonalUnit";
public static final String BASELINE_SEASONAL_PERIOD = "baselineSeasonalPeriod";
public static final String DEFAULT_SEASONAL_SIZE = "7";
public static final String DEFAULT_SEASONAL_UNIT = "DAYS";
public static final String DEFAULT_BASELINE_SEASONAL_PERIOD = "3";
/**
* Use the scaling factor to normalize the input time series
*
* @param metricsToModify the data MetricTimeSeries to be modified by scaling factor
* @param windowStartTime the timestamp when the data belongs to current monitoring window
* @param scalingFactorList list of scaling factors
* @param metricName the metricName withing the timeseries to be modified
* @param properties the properties for scaling the values
*/
public static void rescaleMetric(MetricTimeSeries metricsToModify, long windowStartTime,
List<ScalingFactor> scalingFactorList, String metricName, Properties properties) {
if (CollectionUtils.isEmpty(scalingFactorList)) {
return; // no transformation if there is no scaling factor in
}
List<ScaledValue> scaledValues = null;
if (DEBUG) {
scaledValues = new ArrayList<>();
}
int seasonalSize = Integer.parseInt(properties.getProperty(SEASONAL_SIZE, DEFAULT_SEASONAL_SIZE));
TimeUnit seasonalUnit = TimeUnit.valueOf(properties.getProperty(SEASONAL_UNIT, DEFAULT_SEASONAL_UNIT));
long seasonalMillis = seasonalUnit.toMillis(seasonalSize);
int baselineSeasonalPeriod = Integer.parseInt(properties.getProperty(BASELINE_SEASONAL_PERIOD, DEFAULT_BASELINE_SEASONAL_PERIOD));
// The scaling model works as follows:
// 1. If the time range of scaling factor overlaps with current time series, then the scaling
// factor is applied to all corresponding baseline time series.
// 2. If the time range of scaling factor overlaps with a baseline time series, then baseline
// value that overlaps with the scaling factor is set to 0. Note that in current
// implementation of ThirdEye value 0 means missing data. Thus, the baseline value will be
// removed from baseline calculation.
//
// The model is implemented as follows:
// 1. Check if a timestamp located in any time range of scaling factor.
// 2. If it is, then check if it is a current value or baseline value.
// 3a. If it is a baseline value, then set its value to 0.
// 3b. If it is a current value, then apply the scaling factor to its corresponding baseline
// values.
Set<Long> timeWindowSet = metricsToModify.getTimeWindowSet();
for (long ts : timeWindowSet) {
for (ScalingFactor sf: scalingFactorList) {
if (sf.isInTimeWindow(ts)) {
if (ts < windowStartTime) { // the timestamp belongs to a baseline time series
if (DEBUG) {
double originalValue = metricsToModify.get(ts, metricName).doubleValue();
scaledValues.add(new ScaledValue(ts, originalValue, 0.0));
}
metricsToModify.set(ts, metricName, 0.0); // zero will be removed in analyze function
} else { // the timestamp belongs to the current time series
for (int i = 1; i <= baselineSeasonalPeriod; ++i) {
long baseTs = ts - i * seasonalMillis;
if (timeWindowSet.contains(baseTs)) {
double originalValue = metricsToModify.get(baseTs, metricName).doubleValue();
double scaledValue = originalValue * sf.getScalingFactor();
metricsToModify.set(baseTs, metricName, scaledValue);
if (DEBUG) {
scaledValues.add(new ScaledValue(baseTs, originalValue, scaledValue));
}
}
}
}
}
}
}
if (DEBUG) {
if (CollectionUtils.isNotEmpty(scaledValues)) {
Collections.sort(scaledValues);
StringBuilder sb = new StringBuilder();
String separator = "";
for (ScaledValue scaledValue : scaledValues) {
sb.append(separator).append(scaledValue.toString());
separator = ", ";
}
LOG.info("Transformed values: {}", sb.toString());
}
}
}
/**
* This class is used to store debugging information, which is used to show the status of the
* transformed time series.
*/
private static class ScaledValue implements Comparable<ScaledValue> {
long timestamp;
double originalValue;
double scaledValue;
public ScaledValue(long timestamp, double originalValue, double scaledValue) {
this.timestamp = timestamp;
this.originalValue = originalValue;
this.scaledValue = scaledValue;
}
/**
* Used to sort ScaledValue by the natural order of their timestamp
*/
@Override
public int compareTo(ScaledValue o) {
return Long.compare(timestamp, o.timestamp);
}
@Override
public String toString() {
return "ScaledValue{" + "time=" + timestamp + ", Value: " + originalValue
+ "->" + scaledValue + ", scale=" + scaledValue / originalValue + '}';
}
}
}