package com.linkedin.thirdeye.client; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import com.linkedin.thirdeye.constant.MetricAggFunction; import com.linkedin.thirdeye.datalayer.dto.DatasetConfigDTO; import com.linkedin.thirdeye.datalayer.dto.MetricConfigDTO; import com.linkedin.thirdeye.datalayer.pojo.MetricConfigBean; import com.linkedin.thirdeye.util.ThirdEyeUtils; import parsii.eval.Expression; import parsii.eval.Parser; import parsii.eval.Scope; import parsii.eval.Variable; import parsii.tokenizer.ParseException; /** * This class maintains the metric name, the metric expression composed of metric ids, and the aggregation function * The dataset is required here, because we need to know which dataset to query in cases of count(*) and select max(time) * For other cases, it can be derived from the metric id in the expression */ public class MetricExpression { private static String COUNT_METRIC = "__COUNT"; private static String COUNT_METRIC_ESCAPED = "A__COUNT"; private String expressionName; private String expression; private MetricAggFunction aggFunction = MetricAggFunction.SUM; private String dataset; public MetricExpression(String expression, String dataset) { this(expression.replaceAll("[\\s]+", ""), expression, dataset); } public MetricExpression(String expressionName, String expression, String dataset) { this(expressionName, expression, MetricAggFunction.SUM, dataset); } public MetricExpression(String expressionName, String expression, MetricAggFunction aggFunction, String dataset) { this.expressionName = expressionName; this.expression = expression; this.aggFunction = aggFunction; this.dataset = dataset; } public String getExpressionName() { return expressionName; } public String getExpression() { return expression; } public String getDataset() { return dataset; } @Override public String toString() { return expression; } public List<MetricFunction> computeMetricFunctions() { try { Scope scope = Scope.create(); Set<String> metricTokens = new TreeSet<>(); // can be either metric names or ids ! :-/ // expression parser errors out on variables starting with _ // we're replacing the __COUNT default metric, with an escaped string // after evaluating, we replace the escaped string back with the original String modifiedExpressions = expression.replace(COUNT_METRIC, COUNT_METRIC_ESCAPED); Parser.parse(modifiedExpressions, scope); metricTokens = scope.getLocalNames(); ArrayList<MetricFunction> metricFunctions = new ArrayList<>(); for (String metricToken : metricTokens) { Long metricId = null; MetricConfigDTO metricConfig = null; String metricDataset = dataset; DatasetConfigDTO datasetConfig = ThirdEyeUtils.getDatasetConfigFromName(metricDataset); if (metricToken.equals(COUNT_METRIC_ESCAPED)) { metricToken = COUNT_METRIC; } else { metricId = Long.valueOf(metricToken.replace(MetricConfigBean.DERIVED_METRIC_ID_PREFIX, "")); metricConfig = ThirdEyeUtils.getMetricConfigFromId(metricId); if (metricConfig != null) { metricDataset = metricConfig.getDataset(); } } metricFunctions.add( new MetricFunction(aggFunction, metricToken, metricId, metricDataset, metricConfig, datasetConfig)); } return metricFunctions; } catch (ParseException e) { throw new RuntimeException("Exception parsing expressionString:" + expression, e); } } public static double evaluateExpression(MetricExpression expression, Map<String, Double> context) throws Exception { return evaluateExpression(expression.getExpression(), context); } public static double evaluateExpression(String expressionString, Map<String, Double> context) throws Exception { Scope scope = Scope.create(); expressionString = expressionString.replace(COUNT_METRIC, COUNT_METRIC_ESCAPED); Map<String, Double> metricValueContext = context; if (context.containsKey(COUNT_METRIC)) { metricValueContext = new HashMap<>(context); metricValueContext.put(COUNT_METRIC_ESCAPED, context.get(COUNT_METRIC)); } Expression expression = Parser.parse(expressionString, scope); for (String metricName : metricValueContext.keySet()) { Variable variable = scope.create(metricName); if (!metricValueContext.containsKey(metricName)) { throw new Exception( "No value set for metric:" + metricName + " in the context:" + metricValueContext); } variable.setValue(metricValueContext.get(metricName)); } return expression.evaluate(); } public static void main(String[] args) throws Exception { String expressionString = "(successCount)/(__COUNT)"; MetricExpression expression = new MetricExpression("Approval_Rate", expressionString); Map<String, Double> metricValueContext = new HashMap<>(); metricValueContext.put("__COUNT", 0d); metricValueContext.put("successCount", 0d); double result = MetricExpression.evaluateExpression(expressionString, metricValueContext); System.out.println(Double.isInfinite(result)); } }