package com.linkedin.thirdeye.client.timeseries; import com.linkedin.thirdeye.anomaly.utils.AnomalyUtils; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.joda.time.DateTime; import com.google.common.collect.Range; import com.linkedin.thirdeye.api.TimeGranularity; import com.linkedin.thirdeye.client.MetricExpression; import com.linkedin.thirdeye.client.MetricFunction; import com.linkedin.thirdeye.client.ThirdEyeClient; import com.linkedin.thirdeye.client.ThirdEyeRequest; import com.linkedin.thirdeye.client.ThirdEyeRequest.ThirdEyeRequestBuilder; import com.linkedin.thirdeye.client.ThirdEyeResponse; import com.linkedin.thirdeye.client.TimeRangeUtils; import com.linkedin.thirdeye.client.cache.QueryCache; import com.linkedin.thirdeye.client.timeseries.TimeSeriesRow.TimeSeriesMetric; import com.linkedin.thirdeye.dashboard.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TimeSeriesHandler { private static final Logger LOG = LoggerFactory.getLogger(TimeSeriesHandler.class); private final QueryCache queryCache; private final boolean doRollUp; // roll up small metrics to OTHER dimension private ExecutorService executorService; public TimeSeriesHandler(QueryCache queryCache) { this.queryCache = queryCache; this.doRollUp = true; } public TimeSeriesHandler(QueryCache queryCache, boolean doRollUp) { this.queryCache = queryCache; this.doRollUp = doRollUp; } public TimeSeriesResponse handle(TimeSeriesRequest timeSeriesRequest) throws Exception { List<Range<DateTime>> timeranges = new ArrayList<>(); TimeGranularity aggregationTimeGranularity = timeSeriesRequest.getAggregationTimeGranularity(); // time ranges DateTime start = timeSeriesRequest.getStart(); DateTime end = timeSeriesRequest.getEnd(); if (timeSeriesRequest.isEndDateInclusive()) { // ThirdEyeRequest is exclusive endpoint, so increment by one bucket end = end.plus(aggregationTimeGranularity.toMillis()); } timeranges = TimeRangeUtils.computeTimeRanges(aggregationTimeGranularity, start, end); // create request ThirdEyeRequest request = createThirdEyeRequest("timeseries", timeSeriesRequest, start, end); Future<ThirdEyeResponse> responseFuture = queryCache.getQueryResultAsync(request); // 5 minutes timeout ThirdEyeResponse response = responseFuture.get(5, TimeUnit.MINUTES); TimeSeriesResponseParser timeSeriesResponseParser = new TimeSeriesResponseParser(response, timeranges, timeSeriesRequest.getAggregationTimeGranularity(), timeSeriesRequest.getGroupByDimensions(), doRollUp); List<TimeSeriesRow> rows = timeSeriesResponseParser.parseResponse(); // compute the derived metrics computeDerivedMetrics(timeSeriesRequest, rows); return new TimeSeriesResponse(rows); } private void computeDerivedMetrics(TimeSeriesRequest timeSeriesRequest, List<TimeSeriesRow> rows) throws Exception { // compute list of derived expressions List<MetricFunction> metricFunctionsFromExpressions = Utils.computeMetricFunctionsFromExpressions(timeSeriesRequest.getMetricExpressions()); Set<String> metricNameSet = new HashSet<>(); for (MetricFunction function : metricFunctionsFromExpressions) { metricNameSet.add(function.getMetricName()); } List<MetricExpression> derivedMetricExpressions = new ArrayList<>(); for (MetricExpression expression : timeSeriesRequest.getMetricExpressions()) { if (!metricNameSet.contains(expression.getExpressionName())) { derivedMetricExpressions.add(expression); } } // add metric expressions if (derivedMetricExpressions.size() > 0) { Map<String, Double> valueContext = new HashMap<>(); for (TimeSeriesRow row : rows) { valueContext.clear(); List<TimeSeriesMetric> metrics = row.getMetrics(); // baseline value for (TimeSeriesMetric metric : metrics) { valueContext.put(metric.getMetricName(), metric.getValue()); } for (MetricExpression expression : derivedMetricExpressions) { String derivedMetricExpression = expression.getExpression(); double derivedMetricValue = MetricExpression.evaluateExpression(derivedMetricExpression, valueContext); if (Double.isInfinite(derivedMetricValue) || Double.isNaN(derivedMetricValue)) { derivedMetricValue = 0; } row.getMetrics().add( new TimeSeriesMetric(expression.getExpressionName(), derivedMetricValue)); } } } } /** * An asynchrous method for handling the time series request. This method initializes executor service (if necessary) * and invokes the synchronous method -- handle() -- in the backend. After invoking this method, users could invoke * shutdownAsyncHandler() to shutdown the executor service if it is no longer needed. * * @param timeSeriesRequest the request to retrieve time series. * @return a future object of time series response for the give request. Returns null if it fails to handle the * request. */ public Future<TimeSeriesResponse> asyncHandle(final TimeSeriesRequest timeSeriesRequest) { // For optimizing concurrency performance by reducing the access to the synchronized method if (executorService == null) { startAsyncHandler(); } Future<TimeSeriesResponse> responseFuture = executorService.submit(new Callable<TimeSeriesResponse>() { public TimeSeriesResponse call () { try { return TimeSeriesHandler.this.handle(timeSeriesRequest); } catch (Exception e) { LOG.warn("Failed to retrieve time series of the request: {}", timeSeriesRequest); } return null; } }); return responseFuture; } /** * Initializes executor service if it is null. This method is thread-safe. */ private synchronized void startAsyncHandler() { if (executorService == null) { executorService = Executors.newFixedThreadPool(10); } } /** * Shutdown the executor service of this TimeSeriesHandler safely. */ public void shutdownAsyncHandler() { AnomalyUtils.safelyShutdownExecutionService(executorService, this.getClass()); } private static ThirdEyeRequest createThirdEyeRequest(String requestReference, TimeSeriesRequest timeSeriesRequest, DateTime start, DateTime end) { ThirdEyeRequestBuilder requestBuilder = ThirdEyeRequest.newBuilder(); requestBuilder.setStartTimeInclusive(start); requestBuilder.setEndTimeExclusive(end); requestBuilder.setFilterSet(timeSeriesRequest.getFilterSet()); requestBuilder.addGroupBy(timeSeriesRequest.getGroupByDimensions()); requestBuilder.setGroupByTimeGranularity(timeSeriesRequest.getAggregationTimeGranularity()); List<MetricExpression> metricExpressions = timeSeriesRequest.getMetricExpressions(); List<MetricFunction> metricFunctionsFromExpressions = Utils.computeMetricFunctionsFromExpressions(metricExpressions); requestBuilder.setMetricFunctions(metricFunctionsFromExpressions); return requestBuilder.build(requestReference); } public ThirdEyeClient getClient() { return queryCache.getClient(); } }