package com.wavefront.agent.preprocessor;
import static com.wavefront.agent.preprocessor.PreprocessorUtil.*;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.wavefront.common.TaggedMetricName;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Counter;
import com.yammer.metrics.core.MetricName;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.*;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import org.apache.commons.codec.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.yaml.snakeyaml.Yaml;
/**
* Parses preprocessor rules (organized by listening port)
*
* Created by Vasily on 9/15/16.
*/
public class PreprocessorConfigManager {
private static final Logger logger =
Logger.getLogger(PreprocessorConfigManager.class.getCanonicalName());
private static final Counter configReloads =
Metrics.newCounter(new MetricName("preprocessor", "", "config-reloads.successful"));
private static final Counter failedConfigReloads =
Metrics.newCounter(new MetricName("preprocessor", "", "config-reloads.failed"));
private static final String GLOBAL_PORT_KEY = "global";
// rule keywords
private static final String RULE = "rule";
private static final String ACTION = "action";
private static final String SCOPE = "scope";
private static final String SEARCH = "search";
private static final String REPLACE = "replace";
private static final String MATCH = "match";
private static final String TAG = "tag";
private static final String KEY = "key";
private static final String NEWTAG = "newtag";
private static final String NEWKEY = "newkey";
private static final String VALUE = "value";
private static final String SOURCE = "source";
private static final String INPUT = "input";
private static final String ITERATIONS = "iterations";
private static final String REPLACE_SOURCE = "replaceSource";
private static final String REPLACE_INPUT = "replaceInput";
private static final String ACTION_SUBTYPE = "actionSubtype";
private static final String MAX_LENGTH = "maxLength";
private static final String FIRST_MATCH_ONLY = "firstMatchOnly";
private static final String ALLOW = "allow";
private static final String IF = "if";
public static final String NAMES = "names";
public static final String FUNC = "function";
public static final String OPTS = "opts";
private static final Set ALLOWED_RULE_ARGUMENTS = ImmutableSet.of(RULE, ACTION);
private final Supplier timeSupplier;
private final Map systemPreprocessors = new HashMap<>();
@VisibleForTesting public Map userPreprocessors;
private Map preprocessors = null;
private volatile long systemPreprocessorsTs = Long.MIN_VALUE;
private volatile long userPreprocessorsTs;
private volatile long lastBuild = Long.MIN_VALUE;
private String lastProcessedRules = "";
@VisibleForTesting int totalInvalidRules = 0;
@VisibleForTesting int totalValidRules = 0;
private final Map lockMetricsFilter = new WeakHashMap<>();
public PreprocessorConfigManager() {
this(System::currentTimeMillis);
}
/** @param timeSupplier Supplier for current time (in millis). */
@VisibleForTesting
PreprocessorConfigManager(@Nonnull Supplier timeSupplier) {
this.timeSupplier = timeSupplier;
userPreprocessorsTs = timeSupplier.get();
userPreprocessors = Collections.emptyMap();
}
/**
* Schedules periodic checks for config file modification timestamp and performs hot-reload
*
* @param fileName Path name of the file to be monitored.
* @param fileCheckIntervalMillis Timestamp check interval.
*/
public void setUpConfigFileMonitoring(String fileName, int fileCheckIntervalMillis) {
new Timer("Timer-preprocessor-configmanager")
.schedule(
new TimerTask() {
@Override
public void run() {
loadFileIfModified(fileName);
}
},
fileCheckIntervalMillis,
fileCheckIntervalMillis);
}
public ReportableEntityPreprocessor getSystemPreprocessor(String key) {
systemPreprocessorsTs = timeSupplier.get();
return systemPreprocessors.computeIfAbsent(key, x -> new ReportableEntityPreprocessor());
}
public Supplier get(String handle) {
return () -> getPreprocessor(handle);
}
private ReportableEntityPreprocessor getPreprocessor(String key) {
if ((lastBuild < userPreprocessorsTs || lastBuild < systemPreprocessorsTs)
&& userPreprocessors != null) {
synchronized (this) {
if ((lastBuild < userPreprocessorsTs || lastBuild < systemPreprocessorsTs)
&& userPreprocessors != null) {
this.preprocessors =
Stream.of(this.systemPreprocessors, this.userPreprocessors)
.flatMap(x -> x.entrySet().stream())
.collect(
Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
ReportableEntityPreprocessor::merge));
this.lastBuild = timeSupplier.get();
}
}
}
return this.preprocessors.computeIfAbsent(key, x -> new ReportableEntityPreprocessor());
}
private void requireArguments(@Nonnull Map rule, String... arguments) {
if (rule.isEmpty()) throw new IllegalArgumentException("Rule is empty");
for (String argument : arguments) {
if (rule.get(argument) == null
|| ((rule.get(argument) instanceof String)
&& ((String) rule.get(argument)).replaceAll("[^a-z0-9_-]", "").isEmpty()))
throw new IllegalArgumentException("'" + argument + "' is missing or empty");
}
}
private void allowArguments(@Nonnull Map rule, String... arguments) {
Sets.SetView invalidArguments =
Sets.difference(
rule.keySet(), Sets.union(ALLOWED_RULE_ARGUMENTS, Sets.newHashSet(arguments)));
if (invalidArguments.size() > 0) {
throw new IllegalArgumentException(
"Invalid or not applicable argument(s): " + StringUtils.join(invalidArguments, ","));
}
}
@VisibleForTesting
void loadFileIfModified(String fileName) {
try {
File file = new File(fileName);
long lastModified = file.lastModified();
if (lastModified > userPreprocessorsTs) {
logger.info("File " + file + " has been modified on disk, reloading preprocessor rules");
loadFile(fileName);
configReloads.inc();
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Unable to load preprocessor rules", e);
failedConfigReloads.inc();
}
}
public void processRemoteRules(@Nonnull String rules) {
if (!rules.equals(lastProcessedRules)) {
lastProcessedRules = rules;
logger.info("Preprocessor rules received from remote, processing");
loadFromStream(IOUtils.toInputStream(rules, Charsets.UTF_8));
}
}
public void loadFile(String filename) throws FileNotFoundException {
loadFromStream(new FileInputStream(new File(filename)));
}
@VisibleForTesting
void loadFromStream(InputStream stream) {
totalValidRules = 0;
totalInvalidRules = 0;
Yaml yaml = new Yaml();
Map portMap = new HashMap<>();
lockMetricsFilter.clear();
try {
Map rulesByPort = yaml.load(stream);
if (rulesByPort == null || rulesByPort.isEmpty()) {
logger.warning("Empty preprocessor rule file detected!");
logger.info("Total 0 rules loaded");
synchronized (this) {
this.userPreprocessorsTs = timeSupplier.get();
this.userPreprocessors = Collections.emptyMap();
}
return;
}
for (String strPortKey : rulesByPort.keySet()) {
// Handle comma separated ports and global ports.
// Note: Global ports need to be specified at the end of the file, inorder to be
// applicable to all the explicitly specified ports in preprocessor_rules.yaml file.
List strPortList =
strPortKey.equalsIgnoreCase(GLOBAL_PORT_KEY)
? new ArrayList<>(portMap.keySet())
: Arrays.asList(strPortKey.trim().split("\\s*,\\s*"));
for (String strPort : strPortList) {
portMap.putIfAbsent(strPort, new ReportableEntityPreprocessor());
int validRules = 0;
//noinspection unchecked
List