package com.linkedin.thirdeye.anomalydetection.function; import com.linkedin.pinot.pql.parsers.utils.Pair; import com.linkedin.thirdeye.anomalydetection.AnomalyDetectionUtils; import com.linkedin.thirdeye.anomalydetection.context.AnomalyDetectionContext; import com.linkedin.thirdeye.anomalydetection.context.TimeSeries; import com.linkedin.thirdeye.anomalydetection.context.TimeSeriesKey; import com.linkedin.thirdeye.api.DimensionMap; import com.linkedin.thirdeye.api.MetricTimeSeries; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.TimeUnit; import org.joda.time.DateTime; import org.joda.time.Interval; public class BackwardAnomalyFunctionUtils { public static List<Pair<Long, Long>> toBackwardCompatibleDataRanges( List<Interval> timeSeriesIntervals) { List<Pair<Long, Long>> dataRanges = new ArrayList<>(); for (Interval interval : timeSeriesIntervals) { Pair<Long, Long> dataRange = new Pair<>(interval.getStartMillis(), interval.getEndMillis()); dataRanges.add(dataRange); } return dataRanges; } /** * Splits a MetricTimeSeries to current (observed) time series and baselines. The list of time * series is sorted by the start time of their interval in the reversed natural order. Therefore, * the current time series is located at the beginning of the returned list. * * @param metricTimeSeries the metric time series that contains current and baseline time series. * @param metricName the metric name to retrieve the value from the given metric time series. * @param timeSeriesIntervals the intervals of the split time series. * @return a list of time series, which are split from the metric time series. */ public static List<TimeSeries> splitSetsOfTimeSeries(MetricTimeSeries metricTimeSeries, String metricName, List<Interval> timeSeriesIntervals) { List<TimeSeries> timeSeriesList = new ArrayList<>(timeSeriesIntervals.size()); for (Interval interval : timeSeriesIntervals) { TimeSeries timeSeries = new TimeSeries(); timeSeries.setTimeSeriesInterval(interval); timeSeriesList.add(timeSeries); } // Sort time series by their start time in reversed natural order, i.e., the latest time series // is arranged in the front of the list Collections.sort(timeSeriesList, Collections.reverseOrder(new TimeSeriesStartTimeComparator())); // If a timestamp is contained in the interval of a time series, then the timestamp and its // value is inserted into that time series. If the intervals of time series have overlap, then // the timestamp and its value could be inserted to multiple time series. for (long timestamp : metricTimeSeries.getTimeWindowSet()) { for (TimeSeries timeSeries : timeSeriesList) { if (timeSeries.getTimeSeriesInterval().contains(timestamp)) { double value = metricTimeSeries.get(timestamp, metricName).doubleValue(); timeSeries.set(timestamp, value); } } } return timeSeriesList; } /** * Returns an anomaly detection context from the given information. * * @param anomalyFunction the anomaly function for anomaly detection. * @param timeSeries the given time series. * @param metric the metric name of the given time series. * @param exploredDimensions the dimension map of the given time series. * @param windowStart the start of the interval of the time series. * @param windowEnd the end of the interval of the time series. * * @return an anomaly detection context from the given information. */ public static AnomalyDetectionContext buildAnomalyDetectionContext( AnomalyDetectionFunction anomalyFunction, MetricTimeSeries timeSeries, String metric, DimensionMap exploredDimensions, int bucketSize, TimeUnit bucketUnit, DateTime windowStart, DateTime windowEnd) { // Create the anomaly detection context for the new modularized anomaly function AnomalyDetectionContext anomalyDetectionContext = new AnomalyDetectionContext(); anomalyDetectionContext .setBucketSizeInMS(AnomalyDetectionUtils.getBucketInMillis(bucketSize, bucketUnit)); anomalyDetectionContext.setAnomalyDetectionFunction(anomalyFunction); // Construct TimeSeriesKey TimeSeriesKey timeSeriesKey = new TimeSeriesKey(); timeSeriesKey.setDimensionMap(exploredDimensions); timeSeriesKey.setMetricName(metric); anomalyDetectionContext.setTimeSeriesKey(timeSeriesKey); // Split time series to observed time series and baselines for each metric for (String metricName : anomalyFunction.getSpec().getMetrics()) { List<Interval> intervals = anomalyFunction.getTimeSeriesIntervals(windowStart.getMillis(), windowEnd.getMillis()); List<TimeSeries> timeSeriesList = BackwardAnomalyFunctionUtils.splitSetsOfTimeSeries(timeSeries, metricName, intervals); anomalyDetectionContext.setCurrent(metricName, timeSeriesList.get(0)); timeSeriesList.remove(0); anomalyDetectionContext.setBaselines(metricName, timeSeriesList); } return anomalyDetectionContext; } /** * Compares Time Series by the start time of their interval. */ private static class TimeSeriesStartTimeComparator implements Comparator<TimeSeries> { @Override public int compare(TimeSeries ts1, TimeSeries ts2) { long startTime1 = ts1.getTimeSeriesInterval().getStartMillis(); long startTime2 = ts2.getTimeSeriesInterval().getStartMillis(); return (int) (startTime1 - startTime2); } } }