-
Notifications
You must be signed in to change notification settings - Fork 773
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add label and mapping support in dropwizard metric exporter #98
Closed
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
252 changes: 252 additions & 0 deletions
252
simpleclient_common/src/main/java/io/prometheus/client/exporter/common/MetricMapper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
package io.prometheus.client.exporter.common; | ||
|
||
import org.yaml.snakeyaml.Yaml; | ||
|
||
import java.io.Reader; | ||
import java.util.ArrayList; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.TreeMap; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
|
||
import static java.lang.String.format; | ||
|
||
/** | ||
* Map origin names with a target name, label names and values according a specified configuration. | ||
*/ | ||
public class MetricMapper { | ||
|
||
private static final Pattern snakeCasePattern = Pattern.compile("([a-z0-9])([A-Z])"); | ||
private final boolean lowerCaseOutputNames; | ||
private final boolean lowerCaseOutputLabelNames; | ||
private final Pattern unsafeChars = Pattern.compile("[^a-zA-Z0-9:_]"); | ||
private final Pattern multipleUnderscores = Pattern.compile("__+"); | ||
Map<String, MetricMapping> mappingCache; | ||
private ArrayList<Rule> rules = new ArrayList<Rule>(); | ||
|
||
/** | ||
* @param rules a list of mapping rules to apply. | ||
* @param lowerCaseOutputNames lowercase metric names. | ||
* @param lowerCaseOutpuLabelNames lowercase metric labels names. | ||
*/ | ||
public MetricMapper(ArrayList<Rule> rules, boolean lowerCaseOutputNames, boolean lowerCaseOutpuLabelNames) { | ||
this.rules = rules; | ||
this.mappingCache = new HashMap<String, MetricMapping>(); | ||
this.lowerCaseOutputNames = lowerCaseOutputNames; | ||
this.lowerCaseOutputLabelNames = lowerCaseOutpuLabelNames; | ||
} | ||
|
||
/** | ||
* Replace invalid chars to underscore and replace multiple underscores with one underscore. | ||
* | ||
* @param s a metric name or label name. | ||
* @return a sanitized name. | ||
*/ | ||
private String safeName(String s) { | ||
// Change invalid chars to underscore, and merge underscores. | ||
return multipleUnderscores.matcher(unsafeChars.matcher(s).replaceAll("_")).replaceAll("_"); | ||
} | ||
|
||
/** | ||
* Map a metric to target mapping. | ||
* | ||
* @param metricName | ||
* @return a mapping for the specified metric name. | ||
*/ | ||
public MetricMapping map(String metricName) { | ||
if (!mappingCache.containsKey(metricName)) { | ||
mappingCache.put(metricName, process(metricName)); | ||
} | ||
return mappingCache.get(metricName); | ||
} | ||
|
||
/** | ||
* Process rules for the specified metric name and map it with label names, values and | ||
* help according to the first matched rule. | ||
* | ||
* @param metricName | ||
* @return a MetricMapping associated with a metricName. | ||
*/ | ||
public MetricMapping process(String metricName) { | ||
String targetMetricName; | ||
final String targetHelp; | ||
final String snakeCaseMetricName = snakeCasePattern.matcher(metricName).replaceAll("$1_$2").toLowerCase(); | ||
|
||
for (Rule rule : rules) { | ||
Matcher matcher = null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe move the inside of the loop in a sub-method ? (This will allow you to write a unit test to test only one iteration) |
||
String matchName = (rule.attrNameSnakeCase ? snakeCaseMetricName : metricName); | ||
if (rule.pattern != null) { | ||
matcher = rule.pattern.matcher(matchName); | ||
if (!matcher.matches()) { | ||
continue; | ||
} | ||
} | ||
// Replace matches in help if a help was specified | ||
targetHelp = (rule.help != null) ? matcher.replaceAll(rule.help) : ""; | ||
// Replace matches in metric rule name if specified. Sanitize name | ||
targetMetricName = safeName((rule.name == null) ? metricName : matcher.replaceAll(rule.name)); | ||
if (targetMetricName.isEmpty()) { | ||
throw new IllegalArgumentException("Empty metric name. Original metric name: " + metricName); | ||
} | ||
if (this.lowerCaseOutputNames) { | ||
targetMetricName = targetMetricName.toLowerCase(); | ||
} | ||
ArrayList<String> labelNames = new ArrayList<String>(); | ||
ArrayList<String> labelValues = new ArrayList<String>(); | ||
if (rule.labelNames != null) { | ||
for (int i = 0; i < rule.labelNames.size(); i++) { | ||
final String unsafeLabelName = rule.labelNames.get(i); | ||
final String labelValReplacement = rule.labelValues.get(i); | ||
try { | ||
String labelName = safeName(matcher.replaceAll(unsafeLabelName)); | ||
String labelValue = matcher.replaceAll(labelValReplacement); | ||
if (this.lowerCaseOutputLabelNames) { | ||
labelName = labelName.toLowerCase(); | ||
} | ||
if (!labelName.isEmpty() && !labelValue.isEmpty()) { | ||
labelNames.add(labelName); | ||
labelValues.add(labelValue); | ||
} | ||
} catch (Exception e) { | ||
throw new RuntimeException( | ||
format("Matcher '%s' unable to use: '%s' value: '%s'", matcher, unsafeLabelName, labelValReplacement), e); | ||
} | ||
} | ||
} | ||
return new MetricMapping(targetMetricName, labelNames, labelValues, targetHelp); | ||
} | ||
return MetricMapping.defaultMapping((lowerCaseOutputNames ? metricName.toLowerCase() : metricName)); | ||
} | ||
|
||
|
||
public static MetricMapper load(String yamlConfig) { | ||
return load((Map<String, Object>) new Yaml().load(yamlConfig)); | ||
} | ||
|
||
public static MetricMapper load(Reader reader) { | ||
return load((Map<String, Object>) new Yaml().load(reader)); | ||
} | ||
|
||
public static MetricMapper load() { | ||
return load((Map<String, Object>) null); | ||
} | ||
|
||
public static MetricMapper load(Map<String, Object> config) { | ||
boolean lowercaseOutputName = false; | ||
boolean lowercaseOutputLabelNames = false; | ||
ArrayList<Rule> rules = new ArrayList<Rule>(); | ||
|
||
if (config == null) { //Yaml config empty, set config to empty map. | ||
config = new HashMap<String, Object>(); | ||
} | ||
if (config.containsKey("lowercaseOutputName")) { | ||
lowercaseOutputName = (Boolean) config.get("lowercaseOutputName"); | ||
} | ||
if (config.containsKey("lowercaseOutputLabelNames")) { | ||
lowercaseOutputLabelNames = (Boolean) config.get("lowercaseOutputLabelNames"); | ||
} | ||
|
||
if (config.containsKey("rules")) { | ||
List<Map<String, Object>> configRules = (List<Map<String, Object>>) config.get("rules"); | ||
for (Map<String, Object> ruleObject : configRules) { | ||
Map<String, Object> yamlRule = ruleObject; | ||
Rule rule = new Rule(); | ||
if (yamlRule.containsKey("pattern")) { | ||
rule.pattern = Pattern.compile("^.*" + (String) yamlRule.get("pattern") + ".*$"); | ||
} | ||
if (yamlRule.containsKey("name")) { | ||
rule.name = (String) yamlRule.get("name"); | ||
} | ||
if (yamlRule.containsKey("attrNameSnakeCase")) { | ||
rule.attrNameSnakeCase = (Boolean) yamlRule.get("attrNameSnakeCase"); | ||
} | ||
if (yamlRule.containsKey("help")) { | ||
rule.help = (String) yamlRule.get("help"); | ||
} | ||
if (yamlRule.containsKey("labels")) { | ||
TreeMap labels = new TreeMap((Map<String, Object>) yamlRule.get("labels")); | ||
rule.labelNames = new ArrayList<String>(); | ||
rule.labelValues = new ArrayList<String>(); | ||
for (Map.Entry<String, Object> entry : (Set<Map.Entry<String, Object>>) labels.entrySet()) { | ||
rule.labelNames.add(entry.getKey()); | ||
rule.labelValues.add((String) entry.getValue()); | ||
} | ||
} | ||
|
||
// Validation. | ||
if ((rule.labelNames != null || rule.help != null) && rule.name == null) { | ||
throw new IllegalArgumentException("Must provide name, if help or labels are given: " + yamlRule); | ||
} | ||
if (rule.name != null && rule.pattern == null) { | ||
throw new IllegalArgumentException("Must provide pattern, if name is given: " + yamlRule); | ||
} | ||
rules.add(rule); | ||
} | ||
} else { | ||
// Default to a single default rule. | ||
rules.add(new Rule()); | ||
} | ||
|
||
return new MetricMapper(rules, lowercaseOutputName, lowercaseOutputLabelNames); | ||
} | ||
|
||
/** | ||
* A mapping rule. | ||
*/ | ||
private static class Rule { | ||
Pattern pattern; | ||
String name; | ||
String help; | ||
boolean attrNameSnakeCase; | ||
ArrayList<String> labelNames; | ||
ArrayList<String> labelValues; | ||
} | ||
|
||
/** | ||
* Contains prometheus metric name, label names and values for a source metric name | ||
*/ | ||
public static class MetricMapping { | ||
|
||
private ArrayList<String> labelNames; | ||
private ArrayList<String> labelValues; | ||
private String name; | ||
private String help; | ||
|
||
public MetricMapping(String name, ArrayList<String> labelNames, ArrayList<String> labelValues, String help) { | ||
this.labelNames = labelNames; | ||
this.name = name; | ||
this.labelValues = labelValues; | ||
this.help = help; | ||
} | ||
|
||
public ArrayList<String> getLabelNames() { | ||
return labelNames; | ||
} | ||
|
||
public ArrayList<String> getLabelValues() { | ||
return labelValues; | ||
} | ||
|
||
public String getName() { | ||
return name; | ||
} | ||
|
||
public String getHelp() { | ||
return help; | ||
} | ||
|
||
/** | ||
* Return a default mapping for a metric. | ||
* This mapping contains unmodified metric name, empty label names, values and help. | ||
* | ||
* @param name | ||
* @return the default MetricMapping associated to this metricName. | ||
*/ | ||
public static MetricMapping defaultMapping(String name) { | ||
return new MetricMapping(name, new ArrayList<String>(), new ArrayList<String>(), ""); | ||
} | ||
} | ||
} |
119 changes: 119 additions & 0 deletions
119
simpleclient_common/src/test/java/io/prometheus/client/exporter/common/MetricMapperTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
package io.prometheus.client.exporter.common; | ||
|
||
|
||
import org.junit.Test; | ||
import org.yaml.snakeyaml.error.YAMLException; | ||
|
||
import java.io.StringReader; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
|
||
import static org.junit.Assert.assertEquals; | ||
|
||
public class MetricMapperTest { | ||
|
||
private void assertMapTo(String name, List<String> labelNames, List<String> labelValues, | ||
String help, MetricMapper.MetricMapping mapping) { | ||
assertEquals(mapping.getName(), name); | ||
assertEquals(mapping.getLabelNames(), labelNames); | ||
assertEquals(mapping.getLabelValues(), labelValues); | ||
assertEquals(mapping.getHelp(), help); | ||
} | ||
|
||
@Test | ||
public void testBasicMapping() { | ||
String rules = "---\n" + | ||
"rules:\n" + | ||
" - name: http_requests_errors\n" + | ||
" pattern: ^RequestErrors$\n" + | ||
" - name: foo_bar_bazz\n" + | ||
" pattern: foo_bar_bazz\n" + | ||
" attrNameSnakeCase: true\n" + | ||
" - name: http_requests\n" + | ||
" help: This an help message.\n" + | ||
" pattern: HttpRequest(.*)\n" + | ||
" labels: \n" + | ||
" status: $1\n" + | ||
" foo: bar\n"; | ||
MetricMapper mapper = MetricMapper.load(rules); | ||
assertMapTo("http_requests", Arrays.asList("foo", "status"), Arrays.asList("bar", "Buzzy"), | ||
"This an help message.", mapper.map("HttpRequestBuzzy")); | ||
assertMapTo("http_requests_errors", new ArrayList<String>(), new ArrayList<String>(), "", | ||
mapper.map("RequestErrors")); | ||
assertMapTo("foo_bar_bazz", new ArrayList<String>(), new ArrayList<String>(), "", | ||
mapper.map("FooBarBazz")); | ||
assertMapTo("AnotherNonMatchedRequest", new ArrayList<String>(), new ArrayList<String>(), "", | ||
mapper.map("AnotherNonMatchedRequest")); | ||
|
||
} | ||
|
||
@Test | ||
public void testLowerCaseMapping() { | ||
String rules = "---\n" + | ||
"lowercaseOutputName: true\n" + | ||
"lowercaseOutputLabelNames: true\n" + | ||
"rules:\n" + | ||
" - name: http_requests_errors\n" + | ||
" pattern: ^RequestErrors$\n" + | ||
" - name: http_requests\n" + | ||
" pattern: HttpRequest(.*)\n" + | ||
" labels: \n" + | ||
" Status: $1\n" + | ||
" foo: bar\n"; | ||
MetricMapper mapper = MetricMapper.load(rules); | ||
assertMapTo("http_requests", Arrays.asList("status", "foo"), Arrays.asList("Buzzy", "bar"), | ||
"", mapper.map("HttpRequestBuzzy")); | ||
assertMapTo("http_requests_errors", new ArrayList<String>(), new ArrayList<String>(), "", | ||
mapper.map("RequestErrors")); | ||
assertMapTo("anothernonmatchedrequest", new ArrayList<String>(), new ArrayList<String>(), "", | ||
mapper.map("AnotherNonMatchedRequest")); | ||
} | ||
|
||
@Test(expected = IllegalArgumentException.class) | ||
public void testReplaceToEmptyName() { | ||
String rules = "---\n" + | ||
"rules:\n" + | ||
" - name: $1\n" + | ||
" pattern: ^RequestErrors(.*)$\n"; | ||
MetricMapper mapper = MetricMapper.load(rules); | ||
mapper.map("RequestErrors"); | ||
} | ||
|
||
@Test(expected = YAMLException.class) | ||
public void testLoadInvalidYaml() { | ||
MetricMapper.load("{invali"); | ||
} | ||
|
||
@Test | ||
public void testLoadDefaultMapping() { | ||
MetricMapper mapper = MetricMapper.load(); | ||
assertMapTo("AnotherNonMatchedRequest", new ArrayList<String>(), new ArrayList<String>(), "", | ||
mapper.map("AnotherNonMatchedRequest")); | ||
} | ||
|
||
@Test(expected = IllegalArgumentException.class) | ||
public void testParseInvalidRuleWithNoName() { | ||
String rules = "---\n" + | ||
"rules:\n" + | ||
" - name: foobar\n"; | ||
MetricMapper.load(new StringReader(rules)); | ||
} | ||
|
||
@Test(expected = IllegalArgumentException.class) | ||
public void testParseHelpWithNoName() { | ||
String rules = "---\n" + | ||
"rules:\n" + | ||
" - help: foobar\n"; | ||
MetricMapper.load(new StringReader(rules)); | ||
} | ||
|
||
@Test(expected = IllegalArgumentException.class) | ||
public void testParseLabelNamesWithNoName() { | ||
String rules = "---\n" + | ||
"rules:\n" + | ||
" - labels:\n" + | ||
" foo: bar\n"; | ||
MetricMapper.load(new StringReader(rules)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't add dependencies in here, it's used by the pushgateway and servlet and this may clash with other versions the user is using.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can move the MetricMapper code in the dropwizard package then