package com.linkedin.thirdeye.detector.function;
import com.linkedin.pinot.pql.parsers.utils.Pair;
import com.linkedin.thirdeye.anomaly.views.AnomalyTimelinesView;
import com.linkedin.thirdeye.api.DimensionMap;
import com.linkedin.thirdeye.api.MetricTimeSeries;
import com.linkedin.thirdeye.dashboard.views.TimeBucket;
import com.linkedin.thirdeye.datalayer.dto.MergedAnomalyResultDTO;
import com.linkedin.thirdeye.datalayer.dto.RawAnomalyResultDTO;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* See params for property configuration.
* <p/>
* min - lower threshold limit for average (inclusive). Will trigger alert if datapoint < min
* (strictly less than)
* <p/>
* max - upper threshold limit for average (inclusive). Will trigger alert if datapoint > max
* (strictly greater than)
*/
public class RatioOutlierFunction extends BaseAnomalyFunction {
public static final String DEFAULT_MESSAGE_TEMPLATE = "change : %.2f %%, currentVal : %.2f, min : %.2f, max : %.2f";
public static final String MIN_VAL = "min";
public static final String MAX_VAL = "max";
final static Logger LOG = LoggerFactory.getLogger(RatioOutlierFunction.class);
public String[] getPropertyKeys() {
return new String [] {MIN_VAL, MAX_VAL};
}
@Override
public List<Pair<Long, Long>> getDataRangeIntervals(Long monitoringWindowStartTime,
Long monitoringWindowEndTime) {
List<Pair<Long, Long>> startEndTimeIntervals = new ArrayList<>();
startEndTimeIntervals.add(new Pair<>(monitoringWindowStartTime, monitoringWindowEndTime));
return startEndTimeIntervals;
}
@Override
public List<RawAnomalyResultDTO> analyze(DimensionMap exploredDimensions,
MetricTimeSeries timeSeries, DateTime windowStart, DateTime windowEnd,
List<MergedAnomalyResultDTO> knownAnomalies) throws Exception {
List<RawAnomalyResultDTO> anomalyResults = new ArrayList<>();
// Parse function properties
Properties props = getProperties();
// Get min / max props
Double min = null;
if (props.containsKey(MIN_VAL)) {
min = Double.valueOf(props.getProperty(MIN_VAL));
}
Double max = null;
if (props.containsKey(MAX_VAL)) {
max = Double.valueOf(props.getProperty(MAX_VAL));
}
// Metric
String topicMetric = getSpec().getTopicMetric();
// This function only detects anomalies on one metric, i.e., metrics[0]
assert (getSpec().getMetrics().size() == 2);
LOG.info("Testing ratios {} for outliers", getSpec().getMetrics());
// Compute the bucket size, so we can iterate in those steps
long bucketMillis = TimeUnit.MILLISECONDS.convert(getSpec().getBucketSize(), getSpec().getBucketUnit());
long numBuckets = (windowEnd.getMillis() - windowStart.getMillis()) / bucketMillis;
Map<String, Double> averages = new HashMap<String, Double>();
for (String m : getSpec().getMetrics()) {
// Compute the weight of this time series (average across whole)
double averageValue = 0;
for (Long time : timeSeries.getTimeWindowSet()) {
averageValue += timeSeries.get(time, m).doubleValue();
}
// avg value of this time series
averageValue /= numBuckets;
averages.put(m, averageValue);
}
String m_a = getSpec().getMetrics().get(0);
String m_b = getSpec().getMetrics().get(1);
for (Long timeBucket : timeSeries.getTimeWindowSet()) {
double value_a = timeSeries.get(timeBucket, m_a).doubleValue();
double value_b = timeSeries.get(timeBucket, m_b).doubleValue();
if (value_b == 0.0d) continue;
double ratio = value_a / value_b;
double deviationFromThreshold = getDeviationFromThreshold(ratio, min, max);
LOG.info("{}={}, {}={}, ratio={}, min={}, max={}, deviation={}", m_a, value_a, m_b, value_b, ratio, min, max, deviationFromThreshold);
if (deviationFromThreshold != 0.0) {
RawAnomalyResultDTO anomalyResult = new RawAnomalyResultDTO();
anomalyResult.setProperties(getSpec().getProperties());
anomalyResult.setStartTime(timeBucket);
anomalyResult.setEndTime(timeBucket + bucketMillis); // point-in-time
anomalyResult.setDimensions(exploredDimensions);
anomalyResult.setScore(ratio);
anomalyResult.setWeight(Math.abs(deviationFromThreshold)); // higher change, higher the severity
String message = String.format(DEFAULT_MESSAGE_TEMPLATE, deviationFromThreshold, ratio, min, max);
anomalyResult.setMessage(message);
anomalyResults.add(anomalyResult);
}
}
return anomalyResults;
}
@Override
public AnomalyTimelinesView getTimeSeriesView(MetricTimeSeries timeSeries, long bucketMillis,
String metric, long viewWindowStartTime, long viewWindowEndTime,
List<MergedAnomalyResultDTO> knownAnomalies) {
double min = 0.0d;
try {
// Parse function properties
Properties props = getProperties();
// Get min / max props
if (props.containsKey(MIN_VAL)) {
min = Double.valueOf(props.getProperty(MIN_VAL));
}
} catch(IOException e) {
LOG.warn("Error extracting min value, using 0.0 instead");
}
String m_a = getSpec().getMetrics().get(0);
String m_b = getSpec().getMetrics().get(1);
AnomalyTimelinesView view = new AnomalyTimelinesView();
int bucketCount = (int) ((viewWindowEndTime - viewWindowStartTime) / bucketMillis);
for (int i = 0; i < bucketCount; ++i) {
long currentBucketMillis = viewWindowStartTime + i * bucketMillis;
long baselineBucketMillis = currentBucketMillis - TimeUnit.DAYS.toMillis(7);
TimeBucket timebucket =
new TimeBucket(currentBucketMillis, currentBucketMillis + bucketMillis, baselineBucketMillis,
baselineBucketMillis + bucketMillis);
view.addTimeBuckets(timebucket);
double value_a = timeSeries.get(currentBucketMillis, m_a).doubleValue();
double value_b = timeSeries.get(currentBucketMillis, m_b).doubleValue();
if (value_b != 0.0d) {
double ratio = value_a / value_b;
view.addCurrentValues(ratio);
} else {
view.addCurrentValues(Double.NaN);
}
view.addBaselineValues(min);
}
return view;
}
@Override
public void updateMergedAnomalyInfo(MergedAnomalyResultDTO anomalyToUpdated, MetricTimeSeries timeSeries,
DateTime windowStart, DateTime windowEnd, List<MergedAnomalyResultDTO> knownAnomalies)
throws Exception {
// TODO: implement
}
private double getDeviationFromThreshold(double currentValue, Double min, Double max) {
if ((min != null && currentValue < min && min != 0d)) {
return calculateChange(currentValue, min);
} else if (max != null && currentValue > max && max != 0d) {
return calculateChange(currentValue, max);
}
return 0;
}
static class MetricPair {
final String a;
final String b;
public MetricPair(String a, String b) {
this.a = a;
this.b = b;
}
}
}