package com.linkedin.thirdeye.client.timeseries; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.apache.commons.collections.map.MultiKeyMap; import com.google.common.collect.HashMultimap; import com.google.common.collect.SetMultimap; import com.linkedin.thirdeye.api.DimensionKey; import com.linkedin.thirdeye.api.MetricSchema; import com.linkedin.thirdeye.api.MetricTimeSeries; import com.linkedin.thirdeye.api.MetricType; import com.linkedin.thirdeye.client.timeseries.TimeSeriesRow.TimeSeriesMetric; /** * Util class to support the older ThirdEyeClient API for time series responses. See * {@link #toMap(TimeSeriesResponse, List)} */ public class TimeSeriesResponseConverter { private static final TimeSeriesResponseConverter instance = new TimeSeriesResponseConverter(); private TimeSeriesResponseConverter() { } public static TimeSeriesResponseConverter getInstance() { return instance; } /** * Convert the response to a Map<DimensionKey, MetricTimeSeries>. DimensionKey is generated based * off of schemaDimensions, while the MetricTimeSeries objects are generated based on the rows * within the response input. The metrics returned in the MetricTimeSeries instances correspond to * the metric names as opposed to the full metric function (eg __COUNT instead of SUM(__COUNT)) */ public static Map<DimensionKey, MetricTimeSeries> toMap(TimeSeriesResponse response, List<String> schemaDimensions) { DimensionKeyGenerator dimensionKeyGenerator = new DimensionKeyGenerator(schemaDimensions); List<String> metrics = new ArrayList<>(response.getMetrics()); Set<String> metricSet = new HashSet<>(metrics); List<MetricType> types = Collections.nCopies(metrics.size(), MetricType.DOUBLE); MetricSchema metricSchema = new MetricSchema(metrics, types); SetMultimap<DimensionKey, TimeSeriesRow> dimensionKeyToRows = HashMultimap.create(); // group the rows by their dimension key for (int i = 0; i < response.getNumRows(); i++) { TimeSeriesRow row = response.getRow(i); DimensionKey dimensionKey = dimensionKeyGenerator.get(row.getDimensionNames(), row.getDimensionValues()); dimensionKeyToRows.put(dimensionKey, row); } Map<DimensionKey, MetricTimeSeries> result = new HashMap<>(); for (Entry<DimensionKey, Collection<TimeSeriesRow>> entry : dimensionKeyToRows.asMap() .entrySet()) { DimensionKey key = entry.getKey(); MetricTimeSeries metricTimeSeries = new MetricTimeSeries(metricSchema); result.put(key, metricTimeSeries); for (TimeSeriesRow timeSeriesRow : entry.getValue()) { long timestamp = timeSeriesRow.getStart(); for (TimeSeriesMetric metric : timeSeriesRow.getMetrics()) { String metricName = metric.getMetricName(); // Only add the row metric if it's listed in the response object. The row metric may // contain additional info, eg the raw metrics required for calculating derived ones. if (metricSet.contains(metricName)) { Double value = metric.getValue(); metricTimeSeries.increment(timestamp, metricName, value); } } } } return result; } private static class DimensionKeyGenerator { private final String[] baseKey; private final DimensionKey baseDimensionKey; private final Map<String, Integer> dimensionIndexMap = new HashMap<>(); private final MultiKeyMap dimensionKeyCache = new MultiKeyMap(); DimensionKeyGenerator(List<String> dimensions) { this.baseKey = new String[dimensions.size()]; Arrays.fill(this.baseKey, "*"); this.baseDimensionKey = new DimensionKey(baseKey); for (int i = 0; i < dimensions.size(); i++) { String dimension = dimensions.get(i); dimensionIndexMap.put(dimension, i); } } DimensionKey get(List<String> dimensionNames, List<String> dimensionValues) { // returns base key if the input is not grouped properly if (dimensionNames == null) { return baseDimensionKey; } for (String dimensionName : dimensionNames) { if (!dimensionIndexMap.containsKey(dimensionName)) { return baseDimensionKey; } } if (!dimensionKeyCache.containsKey(dimensionNames, dimensionValues)) { String[] key = Arrays.copyOf(this.baseKey, this.baseKey.length); for (int idx = 0; idx < dimensionNames.size(); ++idx) { int i = this.dimensionIndexMap.get(dimensionNames.get(idx)); key[i] = dimensionValues.get(idx); } DimensionKey dimensionKey = new DimensionKey(key); dimensionKeyCache.put(dimensionNames, dimensionValues, dimensionKey); return dimensionKey; } else { return (DimensionKey) dimensionKeyCache.get(dimensionNames, dimensionValues); } } } }