package com.linkedin.thirdeye.anomaly.merge; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.linkedin.thirdeye.datalayer.dto.MergedAnomalyResultDTO; import com.linkedin.thirdeye.datalayer.dto.RawAnomalyResultDTO; /** * Given list of {@link RawAnomalyResultDTO} and merge parameters, this utility performs time based merge */ public abstract class AnomalyTimeBasedSummarizer { private final static Logger LOG = LoggerFactory.getLogger(AnomalyTimeBasedSummarizer.class); private AnomalyTimeBasedSummarizer() { } /** * @param anomalies : list of raw anomalies to be merged with last mergedAnomaly * @param mergeDuration : length of a merged anomaly * @param sequentialAllowedGap : allowed gap between two raw anomalies in order to merge * * @return */ public static List<MergedAnomalyResultDTO> mergeAnomalies(List<RawAnomalyResultDTO> anomalies, long mergeDuration, long sequentialAllowedGap) { return mergeAnomalies(null, anomalies, mergeDuration, sequentialAllowedGap); } /** * @param mergedAnomaly : last merged anomaly * @param anomalies : list of raw anomalies to be merged with last mergedAnomaly * @param maxMergedDurationMillis : length of a merged anomaly * @param sequentialAllowedGap : allowed gap between two raw anomalies in order to merge * @return */ public static List<MergedAnomalyResultDTO> mergeAnomalies(MergedAnomalyResultDTO mergedAnomaly, List<RawAnomalyResultDTO> anomalies, long maxMergedDurationMillis, long sequentialAllowedGap) { // sort anomalies in natural order of start time Collections.sort(anomalies, new Comparator<RawAnomalyResultDTO>() { @Override public int compare(RawAnomalyResultDTO o1, RawAnomalyResultDTO o2) { return (int) ((o1.getStartTime() - o2.getStartTime()) / 1000); } }); boolean applySequentialGapBasedSplit = false; boolean applyMaxDurationBasedSplit = false; if (maxMergedDurationMillis > 0) { applyMaxDurationBasedSplit = true; } if (sequentialAllowedGap > 0) { applySequentialGapBasedSplit = true; } List<MergedAnomalyResultDTO> mergedAnomalies = new ArrayList<>(); for (int i = 0; i < anomalies.size(); i++) { RawAnomalyResultDTO currentResult = anomalies.get(i); if (mergedAnomaly == null || currentResult.getEndTime() < mergedAnomaly.getStartTime()) { mergedAnomaly = new MergedAnomalyResultDTO(); populateMergedResult(mergedAnomaly, currentResult); } else { // compare current with merged and decide whether to merge the current result or create a new one if (applySequentialGapBasedSplit && (currentResult.getStartTime() - mergedAnomaly.getEndTime()) > sequentialAllowedGap) { // Split here // add previous merged result mergedAnomalies.add(mergedAnomaly); //set current raw result mergedAnomaly = new MergedAnomalyResultDTO(); populateMergedResult(mergedAnomaly, currentResult); } else { // add the current raw result into mergedResult if (currentResult.getStartTime() < mergedAnomaly.getStartTime()) { mergedAnomaly.setStartTime(currentResult.getStartTime()); } if (currentResult.getEndTime() > mergedAnomaly.getEndTime()) { mergedAnomaly.setEndTime(currentResult.getEndTime()); } if (!mergedAnomaly.getAnomalyResults().contains(currentResult)) { mergedAnomaly.getAnomalyResults().add(currentResult); currentResult.setMerged(true); } } } // till this point merged result contains current raw result if (applyMaxDurationBasedSplit // check if Max Duration for merged has passed, if so, create new one && mergedAnomaly.getEndTime() - mergedAnomaly.getStartTime() >= maxMergedDurationMillis) { // check if next anomaly has same start time as current one, that should be merged with current one too if (i < (anomalies.size() - 1) && anomalies.get(i + 1).getStartTime() .equals(currentResult.getStartTime())) { // no need to split as we want to include the next raw anomaly into the current one } else { // Split here mergedAnomalies.add(mergedAnomaly); mergedAnomaly = null; } } if (i == (anomalies.size() - 1) && mergedAnomaly != null) { mergedAnomalies.add(mergedAnomaly); } } LOG.info("merging [{}] raw anomalies", anomalies.size()); return mergedAnomalies; } private static void populateMergedResult(MergedAnomalyResultDTO mergedAnomaly, RawAnomalyResultDTO currentResult) { if (!mergedAnomaly.getAnomalyResults().contains(currentResult)) { mergedAnomaly.getAnomalyResults().add(currentResult); currentResult.setMerged(true); } // only set collection, keep metric, dimensions and function null mergedAnomaly.setCollection(currentResult.getCollection()); mergedAnomaly.setMetric(currentResult.getMetric()); mergedAnomaly.setStartTime(currentResult.getStartTime()); mergedAnomaly.setEndTime(currentResult.getEndTime()); mergedAnomaly.setCreatedTime(System.currentTimeMillis()); } }