/* * Copyright 2014-2017 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.netflix.spectator.agent; import java.beans.Introspector; import java.util.ArrayDeque; import java.util.Deque; import java.util.Map; import java.util.function.DoubleBinaryOperator; /** * Helper for handling basic expressions uses as part of the mapping config. */ final class MappingExpr { private MappingExpr() { } /** * Substitute named variables in the pattern string with the corresponding * values in the variables map. * * @param pattern * Pattern string with placeholders, name surounded by curly braces, e.g.: * {@code {variable name}}. * @param vars * Map of variable substitutions that are available. * @return * String with values substituted in. If no matching key is found for a * placeholder, then it will not be modified and left in place. */ static String substitute(String pattern, Map<String, String> vars) { String value = pattern; for (Map.Entry<String, String> entry : vars.entrySet()) { String v = Introspector.decapitalize(entry.getValue()); value = value.replace("{" + entry.getKey() + "}", v); } return value; } /** * Evaluate a simple stack expression for the value. * * @param expr * Basic stack expression that supports placeholders, numeric constants, * and basic binary operations (:add, :sub, :mul, :div). * @param vars * Map of variable substitutions that are available. * @return * Double value for the expression. If the expression cannot be evaluated * properly, then null will be returned. */ @SuppressWarnings("PMD") static Double eval(String expr, Map<String, ? extends Number> vars) { Deque<Double> stack = new ArrayDeque<>(); String[] parts = expr.split("[,\\s]+"); for (String part : parts) { switch (part) { case ":add": binaryOp(stack, (a, b) -> a + b); break; case ":sub": binaryOp(stack, (a, b) -> a - b); break; case ":mul": binaryOp(stack, (a, b) -> a * b); break; case ":div": binaryOp(stack, (a, b) -> a / b); break; case ":if-changed": ifChanged(stack); break; default: if (part.startsWith("{") && part.endsWith("}")) { Number v = vars.get(part.substring(1, part.length() - 1)); if (v == null) v = Double.NaN; stack.addFirst(v.doubleValue()); } else { stack.addFirst(Double.parseDouble(part)); } break; } } return stack.removeFirst(); } private static void binaryOp(Deque<Double> stack, DoubleBinaryOperator op) { double b = stack.removeFirst(); double a = stack.removeFirst(); stack.addFirst(op.applyAsDouble(a, b)); } /** * Helper to zero out a value if there is not a change. For a stack with {@code num v1 v2}, * if {@code v1 == v2}, then push 0.0 otherwise push {@code num}. * * For some values placed in JMX they are not regularly updated in all circumstances and * reporting the same value for each polling iteration gives the false impression of activity * when there is none. A common example is timers with the metrics library where the reservoir * is not rescaled during a fetch. * * https://github.com/dropwizard/metrics/issues/1030 * * This operator can be used in conjunction with the previous variables to zero out the * misleading snapshots based on the count. For example: * * <pre> * {50thPercentile},{Count},{previous:Count},:if-changed * </pre> */ private static void ifChanged(Deque<Double> stack) { double v2 = stack.removeFirst(); double v1 = stack.removeFirst(); double num = stack.removeFirst(); stack.addFirst((Double.compare(v1, v2) == 0) ? 0.0 : num); } }