From 37fa7fb33c49cfbb9d2620ee59e350e23af8d2fc Mon Sep 17 00:00:00 2001 From: Brian Brazil Date: Mon, 18 Jan 2021 13:57:02 +0000 Subject: [PATCH] Add support for Info metrics Signed-off-by: Brian Brazil --- .../prometheus/client/CollectorRegistry.java | 4 + .../main/java/io/prometheus/client/Info.java | 177 ++++++++++++++++++ .../io/prometheus/client/EnumerationTest.java | 2 +- .../java/io/prometheus/client/InfoTest.java | 96 ++++++++++ .../client/exporter/common/TextFormat.java | 6 + .../common/TextFormatOpenMetricsTest.java | 12 ++ .../exporter/common/TextFormatTest.java | 10 + 7 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 simpleclient/src/main/java/io/prometheus/client/Info.java create mode 100644 simpleclient/src/test/java/io/prometheus/client/InfoTest.java diff --git a/simpleclient/src/main/java/io/prometheus/client/CollectorRegistry.java b/simpleclient/src/main/java/io/prometheus/client/CollectorRegistry.java index 590beeba5..55d33287d 100644 --- a/simpleclient/src/main/java/io/prometheus/client/CollectorRegistry.java +++ b/simpleclient/src/main/java/io/prometheus/client/CollectorRegistry.java @@ -129,6 +129,10 @@ private List collectorNames(Collector m) { names.add(family.name + "_bucket"); names.add(family.name); break; + case INFO: + names.add(family.name + "_info"); + names.add(family.name); + break; default: names.add(family.name); } diff --git a/simpleclient/src/main/java/io/prometheus/client/Info.java b/simpleclient/src/main/java/io/prometheus/client/Info.java new file mode 100644 index 000000000..e878ae386 --- /dev/null +++ b/simpleclient/src/main/java/io/prometheus/client/Info.java @@ -0,0 +1,177 @@ +package io.prometheus.client; + +import io.prometheus.client.CKMSQuantiles.Quantile; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +/** + * Info metric, key-value pairs. + * + * Examples of Info include, build information, version information, and potential target metadata, + * The first provided state will be the default. + * + *

+ * Example enumeration: + *

+ * {@code
+ *   class YourClass {
+ *     static final Info buildInfo = Info.build()
+ *         .name("your_build_info").help("Build information.")
+ *         .register();
+ *
+ *     void func() {
+ *          // Your code here.
+ *         buildInfo.info("branch", "HEAD", "version", "1.2.3", "revision", "e0704b");
+ *     }
+ *   }
+ * }
+ * 
+ */ +public class Info extends SimpleCollector implements Counter.Describable { + + Info(Builder b) { + super(b); + } + + public static class Builder extends SimpleCollector.Builder { + @Override + public Info create() { + if (!unit.isEmpty()) { + throw new IllegalStateException("Info metrics cannot have a unit."); + } + return new Info(this); + } + } + + /** + * Return a Builder to allow configuration of a new Info. Ensures required fields are provided. + * + * @param name The name of the metric + * @param help The help string of the metric + */ + public static Builder build(String name, String help) { + return new Builder().name(name).help(help); + } + + /** + * Return a Builder to allow configuration of a new Info. + */ + public static Builder build() { + return new Builder(); + } + + @Override + protected Child newChild() { + return new Child(labelNames); + } + + + /** + * The value of a single Info. + *

+ * Warning: References to a Child become invalid after using + * {@link SimpleCollector#remove} or {@link SimpleCollector#clear}. + */ + public static class Child { + + private Map value = Collections.emptyMap(); + private List labelNames; + + private Child(List labelNames) { + this.labelNames = labelNames; + } + + /** + * Set the info. + */ + public void info(Map v) { + for (String key : v.keySet()) { + checkMetricLabelName(key); + } + for (String label : labelNames) { + if (v.containsKey(label)) { + throw new IllegalArgumentException("Info and its value cannot have the same label name."); + } + } + this.value = v; + } + /** + * Set the info. + * + * @param v labels as pairs of key values + */ + public void info(String... v) { + if (v.length % 2 != 0) { + throw new IllegalArgumentException("An even number of arguments must be passed"); + } + Map m = new TreeMap(); + for (int i = 0; i < v.length; i+=2) { + m.put(v[i], v[i+1]); + } + info(m); + } + + /** + * Get the info. + */ + public Map get() { + return value; + } + } + + // Convenience methods. + /** + * Set the info on the info with no labels. + */ + public void info(String... v) { + noLabelsChild.info(v); + } + + /** + * Set the info on the info with no labels. + * + * @param v labels as pairs of key values + */ + public void info(Map v) { + noLabelsChild.info(v); + } + + /** + * Get the the info. + */ + public Map get() { + return noLabelsChild.get(); + } + + @Override + public List collect() { + List samples = new ArrayList(); + for(Map.Entry, Child> c: children.entrySet()) { + Map v = c.getValue().get(); + List names = new ArrayList(labelNames); + List values = new ArrayList(c.getKey()); + for(Map.Entry l: v.entrySet()) { + names.add(l.getKey()); + values.add(l.getValue()); + } + samples.add(new MetricFamilySamples.Sample(fullname + "_info", names, values, 1.0)); + } + + return familySamplesList(Type.INFO, samples); + } + + @Override + public List describe() { + return Collections.singletonList( + new MetricFamilySamples(fullname, Type.INFO, help, Collections.emptyList())); + } + +} diff --git a/simpleclient/src/test/java/io/prometheus/client/EnumerationTest.java b/simpleclient/src/test/java/io/prometheus/client/EnumerationTest.java index 158036360..4c8fd893a 100644 --- a/simpleclient/src/test/java/io/prometheus/client/EnumerationTest.java +++ b/simpleclient/src/test/java/io/prometheus/client/EnumerationTest.java @@ -65,7 +65,7 @@ public void testLabels() { assertEquals(1.0, getLabeledState("b", "bar"), .001); } - public enum myEnum { + enum myEnum { FOO, BAR, } diff --git a/simpleclient/src/test/java/io/prometheus/client/InfoTest.java b/simpleclient/src/test/java/io/prometheus/client/InfoTest.java new file mode 100644 index 000000000..02b1dbbe8 --- /dev/null +++ b/simpleclient/src/test/java/io/prometheus/client/InfoTest.java @@ -0,0 +1,96 @@ +package io.prometheus.client; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + + +public class InfoTest { + + CollectorRegistry registry; + Info noLabels, labels; + + @Before + public void setUp() { + registry = new CollectorRegistry(); + noLabels = Info.build().name("nolabels").help("help").register(registry); + labels = Info.build().name("labels").help("help").labelNames("l").register(registry); + } + + private Double getInfo(String metric, String... labels) { + String[] names = new String[labels.length / 2]; + String[] values = new String[labels.length / 2]; + for (int i = 0; i < labels.length; i+=2) { + names[i/2] = labels[i]; + values[i/2] = labels[i+1]; + } + return registry.getSampleValue(metric + "_info", names, values); + } + + @Test + public void testInfo() { + assertEquals(null, getInfo("nolabels", "foo", "bar")); + noLabels.info("foo", "bar"); + assertEquals(1.0, getInfo("nolabels", "foo", "bar"), .001); + noLabels.info("foo", "bar", "baz", "meh"); + assertEquals(null, getInfo("nolabels", "foo", "bar")); + assertEquals(1.0, getInfo("nolabels", "baz", "meh", "foo", "bar"), .001); + } + + @Test + public void testDefaultValue() { + assertEquals(1.0, getInfo("nolabels"), .001); + } + + @Test + public void testLabels() { + assertEquals(null, getInfo("labels", "l", "a", "foo", "bar")); + assertEquals(null, getInfo("labels", "l", "b", "baz", "meh")); + labels.labels("a").info("foo", "bar"); + assertEquals(1.0, getInfo("labels", "l", "a", "foo", "bar"), .001); + assertEquals(null, getInfo("labels", "l", "b", "baz", "meh")); + labels.labels("b").info("baz", "meh"); + assertEquals(1.0, getInfo("labels", "l", "a", "foo", "bar"), .001); + assertEquals(1.0, getInfo("labels", "l", "b", "baz", "meh"), .001); + + assertEquals(null, getInfo("nolabels", "l", "a")); + assertEquals(null, getInfo("nolabels", "l", "b")); + } + + @Test(expected=IllegalArgumentException.class) + public void testDuplicateNameLabelThrows() { + Info i = Info.build().name("labels").help("help").labelNames("l").create(); + i.labels("a").info("l", "bar"); + } + + @Test(expected=IllegalArgumentException.class) + public void testOddInfoThrows() { + Info i = Info.build().name("labels").help("help").create(); + i.info("odd"); + } + + @Test(expected=IllegalStateException.class) + public void testUnitThrows() { + Info.build().unit("seconds").name("nolabels").help("help").create(); + } + + @Test + public void testCollect() { + labels.labels("a").info("foo", "bar", "baz", "meh"); + List mfs = labels.collect(); + + ArrayList samples = new ArrayList(); + samples.add(new Collector.MetricFamilySamples.Sample("labels_info", asList("l", "baz", "foo"), asList("a", "meh", "bar"), 1.0)); + Collector.MetricFamilySamples mfsFixture = new Collector.MetricFamilySamples("labels", Collector.Type.INFO, "help", samples); + + assertEquals(1, mfs.size()); + assertEquals(mfsFixture, mfs.get(0)); + } +} diff --git a/simpleclient_common/src/main/java/io/prometheus/client/exporter/common/TextFormat.java b/simpleclient_common/src/main/java/io/prometheus/client/exporter/common/TextFormat.java index a16a57632..7e4aaa4a2 100644 --- a/simpleclient_common/src/main/java/io/prometheus/client/exporter/common/TextFormat.java +++ b/simpleclient_common/src/main/java/io/prometheus/client/exporter/common/TextFormat.java @@ -69,6 +69,9 @@ public static void write004(Writer writer, Enumeration