/*
* Copyright 2016 Google, 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.controllers.filter;
import com.netflix.spectator.api.Measurement;
import com.netflix.spectator.api.Tag;
import java.io.IOException;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Map;
/**
* A general filter specified using a prototype "JSON" document.
*
* The json document generally follows the structure of the response,
* however in addition to containing a "metrics" section of what is desired,
* it also contains an "excludes" section saying what is not desired.
*
* Each of the section contains regular expression rather than literals
* where the regular expressions are matched against the actual names (or values).
* Thus the excludes section can be used to restrict more general matching
* expressions.
*
* Each measurement is evaluated against all the entries in the filter until one
* is found that would cause it to be accepted and not excluded.
*/
public class PrototypeMeasurementFilter implements Predicate<Measurement> {
/**
* Filters based on Spectator Id tag names and/or values.
*/
public static class TagFilterPattern {
/**
* Construct from regex patterns.
*/
public TagFilterPattern(Pattern key, Pattern value) {
this.key = key;
this.value = value;
}
/**
* Construct from the prototype specification.
*/
public TagFilterPattern(PrototypeMeasurementFilterSpecification.TagFilterSpecification spec) {
if (spec == null) return;
String keySpec = spec.getKey();
String valueSpec = spec.getValue();
if (keySpec != null && !keySpec.isEmpty() && !keySpec.equals(".*")) {
key = Pattern.compile(keySpec);
}
if (valueSpec != null && !valueSpec.isEmpty() && !valueSpec.equals(".*")) {
value = Pattern.compile(valueSpec);
}
}
@Override
public int hashCode() {
return Objects.hash(key.pattern(), value.pattern());
}
@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof TagFilterPattern)) return false;
TagFilterPattern other = (TagFilterPattern) obj;
return key.pattern().equals(other.key.pattern()) && value.pattern().equals(other.value.pattern());
}
/**
* Implements the MeasurementFilter interface.
*/
@SuppressWarnings("PMD.JUnit4TestShouldUseTestAnnotation")
public boolean test(Tag tag) {
if ((key != null) && !key.matcher(tag.key()).matches()) return false;
return ((value == null) || value.matcher(tag.value()).matches());
}
@Override
public String toString() {
return String.format("%s=%s", key.pattern(), value.pattern());
}
/**
* Pattern for matching the Spectator tag key.
*/
private Pattern key;
/**
* Pattern for matching the Spectator tag value.
*/
private Pattern value;
};
/**
* Filters on measurement values.
*
* A value includes a set of tags.
* This filter does not currently include the actual measurement value, only sets of tags.
*/
public static class ValueFilterPattern {
/**
* Constructs a filter from a specification.
*/
public ValueFilterPattern(PrototypeMeasurementFilterSpecification.ValueFilterSpecification spec) {
if (spec == null) return;
for (PrototypeMeasurementFilterSpecification.TagFilterSpecification tag : spec.getTags()) {
this.tags.add(new TagFilterPattern(tag));
}
}
@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof ValueFilterPattern)) return false;
ValueFilterPattern other = (ValueFilterPattern) obj;
return tags.equals(other.tags);
}
@Override
public int hashCode() {
return tags.hashCode();
}
/**
* Determins if a particular TagFilter is satisfied among the value's tag set.
*/
static boolean patternInList(TagFilterPattern tagPattern,
Iterable<Tag> sourceTags) {
for (Tag candidateTag : sourceTags) {
if (tagPattern.test(candidateTag)) {
return true;
}
}
return false;
}
/**
* Implements the MeasurementFilter interface.
*/
@SuppressWarnings("PMD.JUnit4TestShouldUseTestAnnotation")
public boolean test(Iterable<Tag> sourceTags) {
for (TagFilterPattern tagPattern : this.tags) {
if (!patternInList(tagPattern, sourceTags)) {
return false;
}
}
return true;
}
/**
* The list of tag filters that must be satisfied for the value to be satisfied.
*/
public List<TagFilterPattern> getTags() {
return tags;
}
private final List<TagFilterPattern> tags = new ArrayList<TagFilterPattern>();
};
/**
* Filters a meter.
*/
public static class MeterFilterPattern {
/**
* Constructs from a specification.
*
* The nameRegex specifies the name of the Spectator meter itself.
*/
public MeterFilterPattern(
String nameRegex,
PrototypeMeasurementFilterSpecification.MeterFilterSpecification spec) {
namePattern = Pattern.compile(nameRegex);
if (spec == null) return;
if (spec.getValues().isEmpty()) {
values.add(new ValueFilterPattern(PrototypeMeasurementFilterSpecification.ValueFilterSpecification.ALL));
}
for (PrototypeMeasurementFilterSpecification.ValueFilterSpecification value : spec.getValues()) {
values.add(new ValueFilterPattern(value));
}
}
@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof MeterFilterPattern)) return false;
MeterFilterPattern other = (MeterFilterPattern) obj;
return namePattern.equals(other.namePattern) && values.equals(other.values);
}
@Override
public int hashCode() {
return Objects.hash(namePattern, values);
}
/**
* Filters the name of the meter.
*/
private final Pattern namePattern;
/**
* A list of value filters acts as a disjunction.
* Any of the values can be satisifed to satisfy the Meter.
*/
public List<ValueFilterPattern> getValues() {
return values;
}
private final List<ValueFilterPattern> values = new ArrayList<ValueFilterPattern>();
};
/**
* A collection of Include patterns and Exclude patterns for filtering.
*/
public static class IncludeExcludePatterns {
/**
* The value patterns that must be satisifed to include.
*/
public List<ValueFilterPattern> getInclude() {
return include;
}
private List<ValueFilterPattern> include = new ArrayList<ValueFilterPattern>();
/**
* The value patterns that cannot be satisifed to include.
* This is meant to refine the include list from being too generous.
*/
public List<ValueFilterPattern> getExclude() {
return exclude;
}
private List<ValueFilterPattern> exclude = new ArrayList<ValueFilterPattern>();
/**
* Default constructor.
*/
public IncludeExcludePatterns() {
// empty.
}
/**
* Constructor.
*/
public IncludeExcludePatterns(List<ValueFilterPattern> include,
List<ValueFilterPattern> exclude) {
this.include.addAll(include);
this.exclude.addAll(exclude);
}
@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof IncludeExcludePatterns)) {
return false;
}
IncludeExcludePatterns other = (IncludeExcludePatterns) obj;
return include.equals(other.include) && exclude.equals(other.exclude);
}
@Override
public int hashCode() {
return Objects.hash(include, exclude);
}
/**
* Implements the MeasurementFilter interface.
*/
@SuppressWarnings("PMD.JUnit4TestShouldUseTestAnnotation")
public boolean test(Measurement measurement) {
boolean ok = include.isEmpty();
for (ValueFilterPattern pattern : include) {
if (pattern.test(measurement.id().tags())) {
ok = true;
break;
}
}
if (ok) {
for (ValueFilterPattern pattern : exclude) {
if (pattern.test(measurement.id().tags())) {
return false;
}
}
}
return ok;
}
};
/**
* Constructor.
*/
public PrototypeMeasurementFilter(PrototypeMeasurementFilterSpecification specification) {
for (Map.Entry<String, PrototypeMeasurementFilterSpecification.MeterFilterSpecification> entry
: specification.getInclude().entrySet()) {
includePatterns.add(new MeterFilterPattern(entry.getKey(), entry.getValue()));
}
for (Map.Entry<String, PrototypeMeasurementFilterSpecification.MeterFilterSpecification> entry
: specification.getExclude().entrySet()) {
excludePatterns.add(new MeterFilterPattern(entry.getKey(), entry.getValue()));
}
}
/**
* Implements the MeasurementFilter interface.
*/
@SuppressWarnings("PMD.JUnit4TestShouldUseTestAnnotation")
public boolean test(Measurement measurement) {
IncludeExcludePatterns patterns = metricToPatterns(measurement.id().name());
return patterns != null && patterns.test(measurement);
}
/**
* Find the IncludeExcludePatterns for filtering a given metric.
*
* The result is the union of all the individual pattern entries
* where their specified metric name patterns matches the actual metric name.
*/
public IncludeExcludePatterns metricToPatterns(String metric) {
IncludeExcludePatterns foundPatterns = metricNameToPatterns.get(metric);
if (foundPatterns != null) {
return foundPatterns;
}
// Since the keys in the prototype can be regular expressions,
// need to look at all of them and can potentially match multiple,
// each having a different set of rules.
foundPatterns = new IncludeExcludePatterns();
for (MeterFilterPattern meterPattern : includePatterns) {
if (meterPattern.namePattern.matcher(metric).matches()) {
foundPatterns.include.addAll(meterPattern.values);
}
}
for (MeterFilterPattern meterPattern : excludePatterns) {
if (meterPattern.namePattern.matcher(metric).matches()) {
foundPatterns.exclude.addAll(meterPattern.values);
}
}
metricNameToPatterns.put(metric, foundPatterns);
return foundPatterns;
}
/**
* Factory method building a filter from a specification file.
*/
public static PrototypeMeasurementFilter loadFromPath(String path) throws IOException {
PrototypeMeasurementFilterSpecification spec =
PrototypeMeasurementFilterSpecification.loadFromPath(path);
return new PrototypeMeasurementFilter(spec);
}
/**
* All the meter filter patterns that can be satisfied.
*/
private final List<MeterFilterPattern> includePatterns = new ArrayList<MeterFilterPattern>();
/**
* All the meter filter patterns that cannot be satisfied.
*/
private final List<MeterFilterPattern> excludePatterns = new ArrayList<MeterFilterPattern>();
/**
* A cache of previously computed includeExcludePatterns.
* Since the patterns are static and meters heavily reused,
* we'll cache previous results for the next time we apply the filter.
*/
private final Map<String, IncludeExcludePatterns> metricNameToPatterns
= new HashMap<String, IncludeExcludePatterns>();
};