From e68daf23336eb5de7856df406eb1d497f51ad3be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20St=C3=A4ber?= Date: Thu, 26 Aug 2021 22:27:29 +0200 Subject: [PATCH] Add a sample name filter for in-/excluding metrics by name or name prefix (#680) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fabian Stäber --- README.md | 12 +- integration_tests/example_application/pom.xml | 2 +- integration_tests/exemplars_otel/pom.xml | 2 +- integration_tests/pom.xml | 1 + .../servlet_jakarta_exporter_webxml/pom.xml | 116 +++++++ .../it/servlet/jakarta/ExampleServlet.java | 27 ++ .../src/main/webapp/WEB-INF/web.xml | 43 +++ .../ServletJakartaExporterWebXmlIT.java | 113 +++++++ .../src/test/resources/Dockerfile | 2 + .../src/test/resources/logback-test.xml | 14 + pom.xml | 4 - .../java/io/prometheus/client/Collector.java | 125 ++++++- .../prometheus/client/CollectorRegistry.java | 95 ++---- .../java/io/prometheus/client/Predicate.java | 8 + .../prometheus/client/SampleNameFilter.java | 242 ++++++++++++++ .../java/io/prometheus/client/Supplier.java | 8 + .../client/CollectorRegistryTest.java | 64 +--- .../client/SampleNameFilterTest.java | 304 ++++++++++++++++++ .../client/hotspot/BufferPoolsExports.java | 83 +++-- .../client/hotspot/ClassLoadingExports.java | 49 ++- .../hotspot/GarbageCollectorExports.java | 32 +- .../client/hotspot/MemoryPoolsExports.java | 294 ++++++++++------- .../client/hotspot/ThreadExports.java | 135 +++++--- .../client/exporter/HTTPServer.java | 221 +++++++++++-- .../exporter/SampleNameFilterSupplier.java | 26 ++ .../client/exporter/TestDaemonFlags.java | 2 +- .../client/exporter/TestHTTPServer.java | 165 ++++++---- .../java/io/prometheus/client/Adapter.java | 20 ++ .../client/exporter/MetricsServlet.java | 26 +- .../common/adapter/ServletConfigAdapter.java | 5 + .../servlet/common/exporter/Exporter.java | 52 ++- .../ServletConfigurationException.java | 4 + .../servlet/common/exporter/ExporterTest.java | 8 +- .../client/servlet/jakarta/Adapter.java | 20 ++ .../jakarta/exporter/MetricsServlet.java | 32 +- 35 files changed, 1898 insertions(+), 458 deletions(-) create mode 100644 integration_tests/servlet_jakarta_exporter_webxml/pom.xml create mode 100644 integration_tests/servlet_jakarta_exporter_webxml/src/main/java/io/prometheus/client/it/servlet/jakarta/ExampleServlet.java create mode 100644 integration_tests/servlet_jakarta_exporter_webxml/src/main/webapp/WEB-INF/web.xml create mode 100644 integration_tests/servlet_jakarta_exporter_webxml/src/test/java/io/prometheus/client/it/servlet/jakarta/ServletJakartaExporterWebXmlIT.java create mode 100644 integration_tests/servlet_jakarta_exporter_webxml/src/test/resources/Dockerfile create mode 100644 integration_tests/servlet_jakarta_exporter_webxml/src/test/resources/logback-test.xml create mode 100644 simpleclient/src/main/java/io/prometheus/client/Predicate.java create mode 100644 simpleclient/src/main/java/io/prometheus/client/SampleNameFilter.java create mode 100644 simpleclient/src/main/java/io/prometheus/client/Supplier.java create mode 100644 simpleclient/src/test/java/io/prometheus/client/SampleNameFilterTest.java create mode 100644 simpleclient_httpserver/src/main/java/io/prometheus/client/exporter/SampleNameFilterSupplier.java create mode 100644 simpleclient_servlet_common/src/main/java/io/prometheus/client/servlet/common/adapter/ServletConfigAdapter.java create mode 100644 simpleclient_servlet_common/src/main/java/io/prometheus/client/servlet/common/exporter/ServletConfigurationException.java diff --git a/README.md b/README.md index f62a6e90a..2aa8044a8 100644 --- a/README.md +++ b/README.md @@ -672,9 +672,14 @@ There are HTTPServer, Servlet, SpringBoot, and Vert.x integrations included in t The simplest of these is the HTTPServer: ```java -HTTPServer server = new HTTPServer(1234); +HTTPServer server = new HTTPServer.Builder() + .withPort(1234) + .build(); ``` +The `HTTPServer.Builder` supports configuration of a `SampleNameFilter` which can be used to +restrict the time series being exported by name. + To add Prometheus exposition to an existing HTTP server using servlets, see the `MetricsServlet`. It also serves as a simple example of how to write a custom endpoint. @@ -689,11 +694,14 @@ server.setHandler(context); context.addServlet(new ServletHolder(new MetricsServlet()), "/metrics"); ``` +Like the HTTPServer, the `MetricsServlet` can be configured with a `SampleNameFilter` which can +be used to restrict the time series being exported by name. See `integration_tests/servlet_jakarta_exporter_webxml/` +for an example how to configure this in `web.xml`. + All HTTP exposition integrations support restricting which time series to return using `?name[]=` URL parameters. Due to implementation limitations, this may have false negatives. - ## Exporting to a Pushgateway The [Pushgateway](https://github.com/prometheus/pushgateway) diff --git a/integration_tests/example_application/pom.xml b/integration_tests/example_application/pom.xml index 7faa6a7de..b8618f9c1 100644 --- a/integration_tests/example_application/pom.xml +++ b/integration_tests/example_application/pom.xml @@ -30,7 +30,7 @@ - ${artifactId} + ${project.artifactId} org.apache.maven.plugins diff --git a/integration_tests/exemplars_otel/pom.xml b/integration_tests/exemplars_otel/pom.xml index eaac05f2f..186b5dbae 100644 --- a/integration_tests/exemplars_otel/pom.xml +++ b/integration_tests/exemplars_otel/pom.xml @@ -54,7 +54,7 @@ - ${artifactId} + ${project.artifactId} org.apache.maven.plugins diff --git a/integration_tests/pom.xml b/integration_tests/pom.xml index fa2609678..9a5d87099 100644 --- a/integration_tests/pom.xml +++ b/integration_tests/pom.xml @@ -26,6 +26,7 @@ exemplars_otel_agent example_application java_versions + servlet_jakarta_exporter_webxml diff --git a/integration_tests/servlet_jakarta_exporter_webxml/pom.xml b/integration_tests/servlet_jakarta_exporter_webxml/pom.xml new file mode 100644 index 000000000..582c5b7da --- /dev/null +++ b/integration_tests/servlet_jakarta_exporter_webxml/pom.xml @@ -0,0 +1,116 @@ + + + 4.0.0 + + + io.prometheus + integration_tests + 0.11.1-SNAPSHOT + + + servlet_jakarta_exporter_webxml + Integration Test - Servlet Jakarta Exporter web.xml + war + + + 1.2.0 + + + + + io.prometheus + simpleclient + ${project.version} + + + io.prometheus + simpleclient_servlet_jakarta + ${project.version} + + + io.prometheus + simpleclient_hotspot + ${project.version} + + + com.squareup.okhttp3 + okhttp + 4.9.1 + + + org.testcontainers + testcontainers + test + + + ch.qos.logback + logback-classic + 1.1.2 + + + jakarta.servlet + jakarta.servlet-api + 5.0.0 + provided + + + + + ${project.artifactId} + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + integration-test + integration-test + + integration-test + + + + verify + verify + + verify + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-dependencies + package + + copy-dependencies + + + + + + maven-war-plugin + 3.3.1 + + + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/integration_tests/servlet_jakarta_exporter_webxml/src/main/java/io/prometheus/client/it/servlet/jakarta/ExampleServlet.java b/integration_tests/servlet_jakarta_exporter_webxml/src/main/java/io/prometheus/client/it/servlet/jakarta/ExampleServlet.java new file mode 100644 index 000000000..e5bc220df --- /dev/null +++ b/integration_tests/servlet_jakarta_exporter_webxml/src/main/java/io/prometheus/client/it/servlet/jakarta/ExampleServlet.java @@ -0,0 +1,27 @@ +package io.prometheus.client.it.servlet.jakarta; + +import io.prometheus.client.hotspot.DefaultExports; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.Writer; + +public class ExampleServlet extends HttpServlet { + + @Override + public void init() { + DefaultExports.initialize(); + } + + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws IOException { + resp.setStatus(200); + resp.setContentType("text/plain"); + Writer writer = new BufferedWriter(resp.getWriter()); + writer.write("Hello, world!\n"); + writer.close(); + } +} diff --git a/integration_tests/servlet_jakarta_exporter_webxml/src/main/webapp/WEB-INF/web.xml b/integration_tests/servlet_jakarta_exporter_webxml/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..4cff3cde6 --- /dev/null +++ b/integration_tests/servlet_jakarta_exporter_webxml/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,43 @@ + + + + + example + io.prometheus.client.it.servlet.jakarta.ExampleServlet + + + + prometheus + io.prometheus.client.servlet.jakarta.filter.MetricsFilter + + metric-name + requests + + + + prometheus-exporter + io.prometheus.client.servlet.jakarta.exporter.MetricsServlet + + name-must-not-start-with + + jvm_threads_deadlocked + jvm_memory_pool + + + + + example + /* + + + prometheus-exporter + /metrics + + + prometheus + example + + \ No newline at end of file diff --git a/integration_tests/servlet_jakarta_exporter_webxml/src/test/java/io/prometheus/client/it/servlet/jakarta/ServletJakartaExporterWebXmlIT.java b/integration_tests/servlet_jakarta_exporter_webxml/src/test/java/io/prometheus/client/it/servlet/jakarta/ServletJakartaExporterWebXmlIT.java new file mode 100644 index 000000000..d560ae532 --- /dev/null +++ b/integration_tests/servlet_jakarta_exporter_webxml/src/test/java/io/prometheus/client/it/servlet/jakarta/ServletJakartaExporterWebXmlIT.java @@ -0,0 +1,113 @@ +package io.prometheus.client.it.servlet.jakarta; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.ImageFromDockerfile; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; + +public class ServletJakartaExporterWebXmlIT { + + private final OkHttpClient client = new OkHttpClient(); + + private static class DockerContainer extends GenericContainer { + DockerContainer() { + super(new ImageFromDockerfile("servlet-jakarta-exporter-webxml") + .withFileFromPath("servlet_jakarta_exporter_webxml.war", Paths.get("target/servlet_jakarta_exporter_webxml.war")) + .withFileFromClasspath("Dockerfile", "Dockerfile")); + } + } + + @Rule + public DockerContainer dockerContainer = new DockerContainer() + .withExposedPorts(8080) + .waitingFor(Wait.forLogMessage(".*oejs.Server:main: Started Server.*", 1)); + + @Test + public void testSampleNameFilter() throws IOException, InterruptedException { + callExampleServlet(); + List metrics = scrapeMetrics(); + assertContains(metrics, "requests_bucket"); + assertContains(metrics, "requests_count"); + assertContains(metrics, "requests_sum"); + assertContains(metrics, "requests_created"); + assertContains(metrics, "requests_status_total"); + assertContains(metrics, "requests_status_created"); + assertContains(metrics, "jvm_gc_collection_seconds_count"); + assertNotContains(metrics, "jvm_threads_deadlocked"); + assertNotContains(metrics, "jvm_memory_pool"); + + List filteredMetrics = scrapeMetricsWithNameFilter("requests_count", "requests_sum"); + assertNotContains(filteredMetrics, "requests_bucket"); + assertContains(filteredMetrics, "requests_count"); + assertContains(filteredMetrics, "requests_sum"); + assertNotContains(filteredMetrics, "requests_created"); + assertNotContains(filteredMetrics, "requests_status_total"); + assertNotContains(filteredMetrics, "requests_status_created"); + assertNotContains(filteredMetrics, "jvm_gc_collection_seconds_count"); + assertNotContains(filteredMetrics, "jvm_threads_deadlocked"); + assertNotContains(filteredMetrics, "jvm_memory_pool"); + } + + private void assertContains(List metrics, String prefix) { + for (String metric : metrics) { + if (metric.startsWith(prefix)) { + return; + } + } + Assert.fail("metric not found: " + prefix); + } + private void assertNotContains(List metrics, String prefix) { + for (String metric : metrics) { + if (metric.startsWith(prefix)) { + Assert.fail("unexpected metric found: " + metric); + } + } + } + private void callExampleServlet() throws IOException { + Request request = new Request.Builder() + .url("http://localhost:" + dockerContainer.getMappedPort(8080) + "/hello") + .build(); + try (Response response = client.newCall(request).execute()) { + Assert.assertEquals("Hello, world!\n", response.body().string()); + } + } + + private List scrapeMetrics() throws IOException { + Request request = new Request.Builder() + .url("http://localhost:" + dockerContainer.getMappedPort(8080) + "/metrics") + .header("Accept", "application/openmetrics-text; version=1.0.0; charset=utf-8") + .build(); + try (Response response = client.newCall(request).execute()) { + return Arrays.asList(response.body().string().split("\\n")); + } + } + + private List scrapeMetricsWithNameFilter(String... names) throws IOException { + StringBuilder param = new StringBuilder(); + boolean first = true; + for (String name : names) { + if (!first) { + param.append("&"); + } + param.append("name[]=").append(name); + first = false; + } + Request request = new Request.Builder() + .url("http://localhost:" + dockerContainer.getMappedPort(8080) + "/metrics?" + param) + .header("Accept", "application/openmetrics-text; version=1.0.0; charset=utf-8") + .build(); + try (Response response = client.newCall(request).execute()) { + return Arrays.asList(response.body().string().split("\\n")); + } + } +} \ No newline at end of file diff --git a/integration_tests/servlet_jakarta_exporter_webxml/src/test/resources/Dockerfile b/integration_tests/servlet_jakarta_exporter_webxml/src/test/resources/Dockerfile new file mode 100644 index 000000000..abd1416db --- /dev/null +++ b/integration_tests/servlet_jakarta_exporter_webxml/src/test/resources/Dockerfile @@ -0,0 +1,2 @@ +FROM jetty:11.0.6 +COPY servlet_jakarta_exporter_webxml.war /var/lib/jetty/webapps/ROOT.war diff --git a/integration_tests/servlet_jakarta_exporter_webxml/src/test/resources/logback-test.xml b/integration_tests/servlet_jakarta_exporter_webxml/src/test/resources/logback-test.xml new file mode 100644 index 000000000..1b7a25aa0 --- /dev/null +++ b/integration_tests/servlet_jakarta_exporter_webxml/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2b4a05bde..08de32541 100644 --- a/pom.xml +++ b/pom.xml @@ -88,10 +88,6 @@ maven-install-plugin 2.4 - - maven-deploy-plugin - 2.7 - maven-resources-plugin 2.6 diff --git a/simpleclient/src/main/java/io/prometheus/client/Collector.java b/simpleclient/src/main/java/io/prometheus/client/Collector.java index fd02a0c1a..3a69500dc 100644 --- a/simpleclient/src/main/java/io/prometheus/client/Collector.java +++ b/simpleclient/src/main/java/io/prometheus/client/Collector.java @@ -17,10 +17,48 @@ * @see Exposition formats. */ public abstract class Collector { + /** - * Return all of the metrics of this Collector. + * Return all metrics of this Collector. */ public abstract List collect(); + + /** + * Like {@link #collect()}, but the result should only contain {@code MetricFamilySamples} where + * {@code sampleNameFilter.test(name)} is {@code true} for at least one Sample name. + *

+ * The default implementation first collects all {@code MetricFamilySamples} and then discards the ones + * where {@code sampleNameFilter.test(name)} returns {@code false} for all names in + * {@link MetricFamilySamples#getNames()}. + * To improve performance, collector implementations should override this method to prevent + * {@code MetricFamilySamples} from being collected if they will be discarded anyways. + * See {@code ThreadExports} for an example. + *

+ * Note that the resulting List may contain {@code MetricFamilySamples} where some Sample names return + * {@code true} for {@code sampleNameFilter.test(name)} but some Sample names return {@code false}. + * This is ok, because before we produce the output format we will call + * {@link MetricFamilySamples#filter(Predicate)} to strip all Samples where {@code sampleNameFilter.test(name)} + * returns {@code false}. + * + * @param sampleNameFilter may be {@code null}, indicating that all metrics should be collected. + */ + public List collect(Predicate sampleNameFilter) { + List all = collect(); + if (sampleNameFilter == null) { + return all; + } + List remaining = new ArrayList(all.size()); + for (MetricFamilySamples mfs : all) { + for (String name : mfs.getNames()) { + if (sampleNameFilter.test(name)) { + remaining.add(mfs); + break; + } + } + } + return remaining; + } + public enum Type { UNKNOWN, // This is untyped in Prometheus text format. COUNTER, @@ -40,7 +78,11 @@ static public class MetricFamilySamples { public final String unit; public final Type type; public final String help; - public final List samples; + public final List samples; // this list is modified when samples are added/removed. + + public MetricFamilySamples(String name, Type type, String help, List samples) { + this(name, "", type, help, samples); + } public MetricFamilySamples(String name, String unit, Type type, String help, List samples) { if (!unit.isEmpty() && !name.endsWith("_" + unit)) { @@ -72,10 +114,83 @@ public MetricFamilySamples(String name, String unit, Type type, String help, Lis this.samples = mungedSamples; } - public MetricFamilySamples(String name, Type type, String help, List samples) { - this(name, "", type, help, samples); + /** + * @param sampleNameFilter may be {@code null} indicating that the result contains the complete list of samples. + * @return A new MetricFamilySamples containing only the Samples matching the {@code sampleNameFilter}, + * or {@code null} if no Sample matches. + */ + public MetricFamilySamples filter(Predicate sampleNameFilter) { + if (sampleNameFilter == null) { + return this; + } + List remainingSamples = new ArrayList(samples.size()); + for (Sample sample : samples) { + if (sampleNameFilter.test(sample.name)) { + remainingSamples.add(sample); + } + } + if (remainingSamples.isEmpty()) { + return null; + } + return new MetricFamilySamples(name, unit, type, help, remainingSamples); } + /** + * List of names that are reserved for Samples in these MetricsFamilySamples. + *

+ * This is used in two places: + *

    + *
  1. To check potential name collisions in {@link CollectorRegistry#register(Collector)}. + *
  2. To check if a collector may contain metrics matching the metric name filter + * in {@link Collector#collect(Predicate)}. + *
+ * Note that {@code getNames()} always includes the name without suffix, even though some + * metrics types (like Counter) will not have a Sample with that name. + * The reason is that the name without suffix is used in the metadata comments ({@code # TYPE}, {@code # UNIT}, + * {@code # HELP}), and as this name must be unique + * we include the name without suffix here as well. + */ + public String[] getNames() { + switch (type) { + case COUNTER: + return new String[]{ + name + "_total", + name + "_created", + name + }; + case SUMMARY: + return new String[]{ + name + "_count", + name + "_sum", + name + "_created", + name + }; + case HISTOGRAM: + return new String[]{ + name + "_count", + name + "_sum", + name + "_bucket", + name + "_created", + name + }; + case GAUGE_HISTOGRAM: + return new String[]{ + name + "_gcount", + name + "_gsum", + name + "_bucket", + name + }; + case INFO: + return new String[]{ + name + "_info", + name + }; + default: + return new String[]{name}; + } + } + + @Override public boolean equals(Object obj) { if (!(obj instanceof MetricFamilySamples)) { @@ -204,7 +319,7 @@ public interface Describable { * Usually custom collectors do not have to implement Describable. If * Describable is not implemented and the CollectorRegistry was created * with auto describe enabled (which is the case for the default registry) - * then {@link collect} will be called at registration time instead of + * then {@link #collect} will be called at registration time instead of * describe. If this could cause problems, either implement a proper * describe, or if that's not practical have describe return an empty * list. diff --git a/simpleclient/src/main/java/io/prometheus/client/CollectorRegistry.java b/simpleclient/src/main/java/io/prometheus/client/CollectorRegistry.java index 0d37cca27..47af5b136 100644 --- a/simpleclient/src/main/java/io/prometheus/client/CollectorRegistry.java +++ b/simpleclient/src/main/java/io/prometheus/client/CollectorRegistry.java @@ -117,38 +117,7 @@ private List collectorNames(Collector m) { List names = new ArrayList(); for (Collector.MetricFamilySamples family : mfs) { - switch (family.type) { - case COUNTER: - names.add(family.name + "_total"); - names.add(family.name + "_created"); - names.add(family.name); - break; - case SUMMARY: - names.add(family.name + "_count"); - names.add(family.name + "_sum"); - names.add(family.name + "_created"); - names.add(family.name); - break; - case HISTOGRAM: - names.add(family.name + "_count"); - names.add(family.name + "_sum"); - names.add(family.name + "_bucket"); - names.add(family.name + "_created"); - names.add(family.name); - break; - case GAUGE_HISTOGRAM: - names.add(family.name + "_gcount"); - names.add(family.name + "_gsum"); - 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); - } + names.addAll(Arrays.asList(family.getNames())); } return names; } @@ -168,7 +137,16 @@ public Enumeration metricFamilySamples() { * histogram, you must include the '_count', '_sum' and '_bucket' names. */ public Enumeration filteredMetricFamilySamples(Set includedNames) { - return new MetricFamilySamplesEnumeration(includedNames); + return new MetricFamilySamplesEnumeration(new SampleNameFilter.Builder().nameMustBeEqualTo(includedNames).build()); + } + + /** + * Enumeration of metrics where {@code sampleNameFilter.test(name)} returns {@code true} for each {@code name} in + * {@link Collector.MetricFamilySamples#getNames()}. + * @param sampleNameFilter may be {@code null}, indicating that the enumeration should contain all metrics. + */ + public Enumeration filteredMetricFamilySamples(Predicate sampleNameFilter) { + return new MetricFamilySamplesEnumeration(sampleNameFilter); } class MetricFamilySamplesEnumeration implements Enumeration { @@ -176,75 +154,56 @@ class MetricFamilySamplesEnumeration implements Enumeration collectorIter; private Iterator metricFamilySamples; private Collector.MetricFamilySamples next; - private Set includedNames; + private final Predicate sampleNameFilter; - MetricFamilySamplesEnumeration(Set includedNames) { - this.includedNames = includedNames; - collectorIter = includedCollectorIterator(includedNames); + MetricFamilySamplesEnumeration(Predicate sampleNameFilter) { + this.sampleNameFilter = sampleNameFilter; + this.collectorIter = filteredCollectorIterator(); findNextElement(); } - private Iterator includedCollectorIterator(Set includedNames) { - if (includedNames.isEmpty()) { + private Iterator filteredCollectorIterator() { + if (sampleNameFilter == null) { return collectors().iterator(); } else { HashSet collectors = new HashSet(); synchronized (namesCollectorsLock) { for (Map.Entry entry : namesToCollectors.entrySet()) { - if (includedNames.contains(entry.getKey())) { + // Note that namesToCollectors contains keys for all combinations of suffixes (_total, _info, etc.). + if (sampleNameFilter.test(entry.getKey())) { collectors.add(entry.getValue()); } } } - return collectors.iterator(); } } MetricFamilySamplesEnumeration() { - this(Collections.emptySet()); + this(null); } private void findNextElement() { next = null; while (metricFamilySamples != null && metricFamilySamples.hasNext()) { - next = filter(metricFamilySamples.next()); + next = metricFamilySamples.next().filter(sampleNameFilter); if (next != null) { return; } } - if (next == null) { - while (collectorIter.hasNext()) { - metricFamilySamples = collectorIter.next().collect().iterator(); - while (metricFamilySamples.hasNext()) { - next = filter(metricFamilySamples.next()); - if (next != null) { - return; - } + while (collectorIter.hasNext()) { + metricFamilySamples = collectorIter.next().collect(sampleNameFilter).iterator(); + while (metricFamilySamples.hasNext()) { + next = metricFamilySamples.next().filter(sampleNameFilter); + if (next != null) { + return; } } } } - private Collector.MetricFamilySamples filter(Collector.MetricFamilySamples next) { - if (includedNames.isEmpty()) { - return next; - } else { - Iterator it = next.samples.iterator(); - while (it.hasNext()) { - if (!includedNames.contains(it.next().name)) { - it.remove(); - } - } - if (next.samples.size() == 0) { - return null; - } - return next; - } - } - public Collector.MetricFamilySamples nextElement() { Collector.MetricFamilySamples current = next; if (current == null) { diff --git a/simpleclient/src/main/java/io/prometheus/client/Predicate.java b/simpleclient/src/main/java/io/prometheus/client/Predicate.java new file mode 100644 index 000000000..6cbd83038 --- /dev/null +++ b/simpleclient/src/main/java/io/prometheus/client/Predicate.java @@ -0,0 +1,8 @@ +package io.prometheus.client; + +/** + * Replacement for Java 8's {@code java.util.function.Predicate} for compatibility with Java versions < 8. + */ +public interface Predicate { + boolean test(T t); +} \ No newline at end of file diff --git a/simpleclient/src/main/java/io/prometheus/client/SampleNameFilter.java b/simpleclient/src/main/java/io/prometheus/client/SampleNameFilter.java new file mode 100644 index 000000000..180dd56c4 --- /dev/null +++ b/simpleclient/src/main/java/io/prometheus/client/SampleNameFilter.java @@ -0,0 +1,242 @@ +package io.prometheus.client; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.StringTokenizer; + +import static java.util.Collections.unmodifiableCollection; + +/** + * Filter samples (i.e. time series) by name. + */ +public class SampleNameFilter implements Predicate { + + /** + * For convenience, a filter that allows all names. + */ + public static final Predicate ALLOW_ALL = new AllowAll(); + + private final Collection nameIsEqualTo; + private final Collection nameIsNotEqualTo; + private final Collection nameStartsWith; + private final Collection nameDoesNotStartWith; + + @Override + public boolean test(String sampleName) { + return matchesNameEqualTo(sampleName) + && !matchesNameNotEqualTo(sampleName) + && matchesNameStartsWith(sampleName) + && !matchesNameDoesNotStartWith(sampleName); + } + + /** + * Replacement for Java 8's {@code Predicate.and()} for compatibility with Java versions < 8. + */ + public Predicate and(final Predicate other) { + if (other == null) { + throw new NullPointerException(); + } + return new Predicate() { + @Override + public boolean test(String s) { + return SampleNameFilter.this.test(s) && other.test(s); + } + }; + } + + private boolean matchesNameEqualTo(String metricName) { + if (nameIsEqualTo.isEmpty()) { + return true; + } + return nameIsEqualTo.contains(metricName); + } + + private boolean matchesNameNotEqualTo(String metricName) { + if (nameIsNotEqualTo.isEmpty()) { + return false; + } + return nameIsNotEqualTo.contains(metricName); + } + + private boolean matchesNameStartsWith(String metricName) { + if (nameStartsWith.isEmpty()) { + return true; + } + for (String prefix : nameStartsWith) { + if (metricName.startsWith(prefix)) { + return true; + } + } + return false; + } + + private boolean matchesNameDoesNotStartWith(String metricName) { + if (nameDoesNotStartWith.isEmpty()) { + return false; + } + for (String prefix : nameDoesNotStartWith) { + if (metricName.startsWith(prefix)) { + return true; + } + } + return false; + } + + public static class Builder { + + private final Collection nameEqualTo = new ArrayList(); + private final Collection nameNotEqualTo = new ArrayList(); + private final Collection nameStartsWith = new ArrayList(); + private final Collection nameDoesNotStartWith = new ArrayList(); + + /** + * @see #nameMustBeEqualTo(Collection) + */ + public Builder nameMustBeEqualTo(String... names) { + return nameMustBeEqualTo(Arrays.asList(names)); + } + + /** + * Only samples with one of the {@code names} will be included. + *

+ * Note that the provided {@code names} will be matched against the sample name (i.e. the time series name) + * and not the metric name. For instance, to retrieve all samples from a histogram, you must include the + * '_count', '_sum' and '_bucket' names. + *

+ * This method should be used by HTTP exporters to implement the {@code ?name[]=} URL parameters. + * + * @param names empty means no restriction. + */ + public Builder nameMustBeEqualTo(Collection names) { + nameEqualTo.addAll(names); + return this; + } + + /** + * @see #nameMustNotBeEqualTo(Collection) + */ + public Builder nameMustNotBeEqualTo(String... names) { + return nameMustNotBeEqualTo(Arrays.asList(names)); + } + + /** + * All samples that are not in {@code names} will be excluded. + *

+ * Note that the provided {@code names} will be matched against the sample name (i.e. the time series name) + * and not the metric name. For instance, to exclude all samples from a histogram, you must exclude the + * '_count', '_sum' and '_bucket' names. + * + * @param names empty means no name will be excluded. + */ + public Builder nameMustNotBeEqualTo(Collection names) { + nameNotEqualTo.addAll(names); + return this; + } + + /** + * @see #nameMustStartWith(Collection) + */ + public Builder nameMustStartWith(String... prefixes) { + return nameMustStartWith(Arrays.asList(prefixes)); + } + + /** + * Only samples whose name starts with one of the {@code prefixes} will be included. + * @param prefixes empty means no restriction. + */ + public Builder nameMustStartWith(Collection prefixes) { + nameStartsWith.addAll(prefixes); + return this; + } + + /** + * @see #nameMustNotStartWith(Collection) + */ + public Builder nameMustNotStartWith(String... prefixes) { + return nameMustNotStartWith(Arrays.asList(prefixes)); + } + + /** + * Samples with names starting with one of the {@code prefixes} will be excluded. + * @param prefixes empty means no time series will be excluded. + */ + public Builder nameMustNotStartWith(Collection prefixes) { + nameDoesNotStartWith.addAll(prefixes); + return this; + } + + public SampleNameFilter build() { + return new SampleNameFilter(nameEqualTo, nameNotEqualTo, nameStartsWith, nameDoesNotStartWith); + } + } + + private SampleNameFilter(Collection nameIsEqualTo, Collection nameIsNotEqualTo, Collection nameStartsWith, Collection nameDoesNotStartWith) { + this.nameIsEqualTo = unmodifiableCollection(nameIsEqualTo); + this.nameIsNotEqualTo = unmodifiableCollection(nameIsNotEqualTo); + this.nameStartsWith = unmodifiableCollection(nameStartsWith); + this.nameDoesNotStartWith = unmodifiableCollection(nameDoesNotStartWith); + } + + private static class AllowAll implements Predicate { + + private AllowAll() { + } + + @Override + public boolean test(String s) { + return true; + } + } + + /** + * Helper method to deserialize a {@code delimiter}-separated list of Strings into a {@code List}. + *

+ * {@code delimiter} is one of {@code , ; \t \n}. + *

+ * This is implemented here so that exporters can provide a consistent configuration format for + * lists of allowed names. + */ + public static List stringToList(String s) { + List result = new ArrayList(); + if (s != null) { + StringTokenizer tokenizer = new StringTokenizer(s, ",; \t\n"); + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + token = token.trim(); + if (token.length() > 0) { + result.add(token); + } + } + } + return result; + } + + /** + * Helper method to compose a filter such that Sample names must + *

    + *
  • match the existing filter
  • + *
  • and be in the list of allowedNames
  • + *
+ * This should be used to implement the {@code names[]} query parameter in HTTP exporters. + * + * @param filter may be null, indicating that the resulting filter should just filter by {@code allowedNames}. + * @param allowedNames may be null or empty, indicating that {@code filter} is returned unmodified. + * @return a filter combining the exising {@code filter} and the {@code allowedNames}, or {@code null} + * if both parameters were {@code null}. + */ + public static Predicate restrictToNamesEqualTo(Predicate filter, Collection allowedNames) { + if (allowedNames != null && !allowedNames.isEmpty()) { + SampleNameFilter allowedNamesFilter = new SampleNameFilter.Builder() + .nameMustBeEqualTo(allowedNames) + .build(); + if (filter == null) { + return allowedNamesFilter; + } else { + return allowedNamesFilter.and(filter); + } + } + return filter; + } +} \ No newline at end of file diff --git a/simpleclient/src/main/java/io/prometheus/client/Supplier.java b/simpleclient/src/main/java/io/prometheus/client/Supplier.java new file mode 100644 index 000000000..3eb766ea0 --- /dev/null +++ b/simpleclient/src/main/java/io/prometheus/client/Supplier.java @@ -0,0 +1,8 @@ +package io.prometheus.client; + +/** + * Replacement for Java 8's {@code java.util.function.Supplier} for compatibility with Java versions < 8. + */ +public interface Supplier { + T get(); +} diff --git a/simpleclient/src/test/java/io/prometheus/client/CollectorRegistryTest.java b/simpleclient/src/test/java/io/prometheus/client/CollectorRegistryTest.java index 4404f247a..b5f56790a 100644 --- a/simpleclient/src/test/java/io/prometheus/client/CollectorRegistryTest.java +++ b/simpleclient/src/test/java/io/prometheus/client/CollectorRegistryTest.java @@ -53,7 +53,7 @@ public void testClear() { assertEquals(0, mfs.size()); } - class EmptyCollector extends Collector { + static class EmptyCollector extends Collector { public List collect() { return new ArrayList(); } @@ -72,29 +72,6 @@ public void testMetricFamilySamples() { assertEquals(new HashSet(Arrays.asList("g", "c", "s")), names); } - @Test - public void testMetricFamilySamples_filterNames() { - Collector g = Gauge.build().name("g").help("h").register(registry); - Collector c = Counter.build().name("c").help("h").register(registry); - Collector s = Summary.build().name("s").help("h").register(registry); - Collector ec = new EmptyCollector().register(registry); - SkippedCollector sr = new SkippedCollector().register(registry); - PartiallyFilterCollector pfr = new PartiallyFilterCollector().register(registry); - HashSet metrics = new HashSet(); - HashSet series = new HashSet(); - for (Collector.MetricFamilySamples metricFamilySamples : Collections.list(registry.filteredMetricFamilySamples( - new HashSet(Arrays.asList("", "s_sum", "c_total", "part_filter_a", "part_filter_c"))))) { - metrics.add(metricFamilySamples.name); - for (Collector.MetricFamilySamples.Sample sample : metricFamilySamples.samples) { - series.add(sample.name); - } - } - - assertEquals(1, sr.collectCallCount); - assertEquals(2, pfr.collectCallCount); - assertEquals(new HashSet(Arrays.asList("s", "c", "part_filter_a", "part_filter_c")), metrics); - assertEquals(new HashSet(Arrays.asList("s_sum", "c_total", "part_filter_a", "part_filter_c")), series); - } @Test public void testEmptyRegistryHasNoMoreElements() { @@ -139,7 +116,7 @@ public void testCanUnAndReregister() { registry.register(h); } - class MyCollector extends Collector { + static class MyCollector extends Collector { public List collect() { List mfs = new ArrayList(); mfs.add(new GaugeMetricFamily("g", "help", 42)); @@ -161,41 +138,4 @@ public void testAutoDescribeThrowsOnReregisteringCustomCollector() { new MyCollector().register(r); new MyCollector().register(r); } - - private static class SkippedCollector extends Collector implements Collector.Describable { - public int collectCallCount = 0; - - @Override - public List collect() { - collectCallCount++; - List mfs = new ArrayList(); - mfs.add(new GaugeMetricFamily("slow_gauge", "help", 123)); - return mfs; - } - - @Override - public List describe() { - return collect(); - } - } - - private static class PartiallyFilterCollector extends Collector implements Collector.Describable { - public int collectCallCount = 0; - - @Override - public List collect() { - collectCallCount++; - List mfs = new ArrayList(); - mfs.add(new GaugeMetricFamily("part_filter_a", "help", 123)); - mfs.add(new GaugeMetricFamily("part_filter_b", "help", 123)); - mfs.add(new GaugeMetricFamily("part_filter_c", "help", 123)); - return mfs; - } - - @Override - public List describe() { - return collect(); - } - } - } diff --git a/simpleclient/src/test/java/io/prometheus/client/SampleNameFilterTest.java b/simpleclient/src/test/java/io/prometheus/client/SampleNameFilterTest.java new file mode 100644 index 000000000..b2c9f157b --- /dev/null +++ b/simpleclient/src/test/java/io/prometheus/client/SampleNameFilterTest.java @@ -0,0 +1,304 @@ +package io.prometheus.client; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertEquals; + +public class SampleNameFilterTest { + + private CollectorRegistry registry; + + @Before + public void setUp() { + registry = new CollectorRegistry(true); + } + + @Test + public void testCounter() { + // It should not make any difference whether a counter is created as "my_counter" or as "my_counter_total". + // We test both ways here and expect the same output. + for (String suffix : new String[] { "", "_total"}) { + registry.clear(); + Counter counter1 = Counter.build() + .name("counter1" + suffix) + .help("test counter 1") + .labelNames("path") + .register(registry); + counter1.labels("/hello").inc(); + counter1.labels("/goodbye").inc(); + Counter counter2 = Counter.build() + .name("counter2" + suffix) + .help("test counter 2") + .register(registry); + counter2.inc(); + + SampleNameFilter filter = new SampleNameFilter.Builder().build(); + List mfsList = Collections.list(registry.filteredMetricFamilySamples(filter)); + assertSamplesInclude(mfsList, "counter1_total", 2); + assertSamplesInclude(mfsList, "counter1_created", 2); + assertSamplesInclude(mfsList, "counter2_total", 1); + assertSamplesInclude(mfsList, "counter2_created", 1); + assertTotalNumberOfSamples(mfsList, 6); + + filter = new SampleNameFilter.Builder().nameMustStartWith("counter1").build(); + mfsList = Collections.list(registry.filteredMetricFamilySamples(filter)); + assertSamplesInclude(mfsList, "counter1_total", 2); + assertSamplesInclude(mfsList, "counter1_created", 2); + assertTotalNumberOfSamples(mfsList, 4); + + filter = new SampleNameFilter.Builder().nameMustNotStartWith("counter1").build(); + mfsList = Collections.list(registry.filteredMetricFamilySamples(filter)); + assertSamplesInclude(mfsList, "counter2_total", 1); + assertSamplesInclude(mfsList, "counter2_created", 1); + assertTotalNumberOfSamples(mfsList, 2); + + filter = new SampleNameFilter.Builder() + .nameMustBeEqualTo("counter2_total") + .nameMustBeEqualTo("counter1_total") + .build(); + mfsList = Collections.list(registry.filteredMetricFamilySamples(filter)); + assertSamplesInclude(mfsList, "counter1_total", 2); + assertSamplesInclude(mfsList, "counter2_total", 1); + assertTotalNumberOfSamples(mfsList, 3); + + filter = new SampleNameFilter.Builder() + .nameMustStartWith("counter1") + .nameMustNotStartWith("counter1_created") + .build(); + mfsList = Collections.list(registry.filteredMetricFamilySamples(filter)); + assertSamplesInclude(mfsList, "counter1_total", 2); + assertTotalNumberOfSamples(mfsList, 2); + + // The following filter would be weird in practice, but let's test this anyways :) + filter = new SampleNameFilter.Builder() + .nameMustBeEqualTo("counter1_created") + .nameMustBeEqualTo("counter2_created") + .nameMustNotStartWith("counter1") + .build(); + mfsList = Collections.list(registry.filteredMetricFamilySamples(filter)); + assertSamplesInclude(mfsList, "counter2_created", 1); + assertTotalNumberOfSamples(mfsList, 1); + + // And finally one that should not match anything + filter = new SampleNameFilter.Builder() + .nameMustBeEqualTo("counter1_total") + .nameMustNotBeEqualTo("counter1_total") + .build(); + mfsList = Collections.list(registry.filteredMetricFamilySamples(filter)); + assertTotalNumberOfSamples(mfsList, 0); + + } + } + + @Test + public void testCustomCollector() { + Collector myCollector = new Collector() { + @Override + public List collect() { + List result = new ArrayList(); + + String name = "temperature_centigrade"; + List labelNames = Collections.singletonList("location"); + GaugeMetricFamily temperatureCentigrade = new GaugeMetricFamily(name, "temperature centigrade", labelNames); + temperatureCentigrade.samples.add(new MetricFamilySamples.Sample(name, labelNames, Collections.singletonList("outside"), 26.0)); + temperatureCentigrade.samples.add(new MetricFamilySamples.Sample(name, labelNames, Collections.singletonList("inside"), 22.0)); + result.add(temperatureCentigrade); + + name = "temperature_fahrenheit"; + GaugeMetricFamily temperatureFahrenheit = new GaugeMetricFamily(name, "temperature fahrenheit", labelNames); + temperatureFahrenheit.samples.add(new MetricFamilySamples.Sample(name, labelNames, Collections.singletonList("outside"), 78.8)); + temperatureFahrenheit.samples.add(new MetricFamilySamples.Sample(name, labelNames, Collections.singletonList("inside"), 71.6)); + result.add(temperatureFahrenheit); + + return result; + } + }; + registry.register(myCollector); + + SampleNameFilter filter = new SampleNameFilter.Builder() + .nameMustStartWith("temperature_centigrade") + .build(); + List mfsList = Collections.list(registry.filteredMetricFamilySamples(filter)); + assertSamplesInclude(mfsList, "temperature_centigrade", 2); + assertTotalNumberOfSamples(mfsList, 2); + + filter = new SampleNameFilter.Builder() + .nameMustNotStartWith("temperature_centigrade") + .build(); + mfsList = Collections.list(registry.filteredMetricFamilySamples(filter)); + assertSamplesInclude(mfsList, "temperature_fahrenheit", 2); + assertTotalNumberOfSamples(mfsList, 2); + + filter = new SampleNameFilter.Builder() + .nameMustNotStartWith("temperature") + .build(); + mfsList = Collections.list(registry.filteredMetricFamilySamples(filter)); + assertTotalNumberOfSamples(mfsList, 0); + + filter = new SampleNameFilter.Builder() + .nameMustBeEqualTo("temperature_centigrade") + .build(); + mfsList = Collections.list(registry.filteredMetricFamilySamples(filter)); + assertSamplesInclude(mfsList, "temperature_centigrade", 2); + assertTotalNumberOfSamples(mfsList, 2); + + filter = new SampleNameFilter.Builder() + .nameMustNotBeEqualTo("temperature_centigrade") + .build(); + mfsList = Collections.list(registry.filteredMetricFamilySamples(filter)); + assertSamplesInclude(mfsList, "temperature_fahrenheit", 2); + assertTotalNumberOfSamples(mfsList, 2); + + filter = new SampleNameFilter.Builder() + .nameMustStartWith("temperature_c") + .build(); + mfsList = Collections.list(registry.filteredMetricFamilySamples(filter)); + assertSamplesInclude(mfsList, "temperature_centigrade", 2); + assertTotalNumberOfSamples(mfsList, 2); + } + + @Test + public void testHistogram() { + Histogram histogram = Histogram.build() + .name("test_histogram") + .help("test histogram") + .create(); + histogram.observe(100); + histogram.observe(200); + registry.register(histogram); + + SampleNameFilter filter = new SampleNameFilter.Builder().build(); + List mfsList = Collections.list(registry.filteredMetricFamilySamples(filter)); + assertSamplesInclude(mfsList, "test_histogram_bucket", 15); + assertSamplesInclude(mfsList, "test_histogram_count", 1); + assertSamplesInclude(mfsList, "test_histogram_sum", 1); + assertSamplesInclude(mfsList, "test_histogram_created", 1); + assertTotalNumberOfSamples(mfsList, 18); + + filter = new SampleNameFilter.Builder() + .nameMustStartWith("test_histogram") + // nameMustStartWith() is for the names[] query parameter in HTTP exporters. + // If histogram_created is missing in the names[] list, it should not be exported + // even though includePrefixes matches histogram_created. + .nameMustBeEqualTo("test_histogram_bucket") + .nameMustBeEqualTo("test_histogram_count") + .nameMustBeEqualTo("test_histogram_sum") + .build(); + mfsList = Collections.list(registry.filteredMetricFamilySamples(filter)); + assertSamplesInclude(mfsList, "test_histogram_bucket", 15); + assertSamplesInclude(mfsList, "test_histogram_count", 1); + assertSamplesInclude(mfsList, "test_histogram_sum", 1); + assertTotalNumberOfSamples(mfsList, 17); + + filter = new SampleNameFilter.Builder() + .nameMustStartWith("test_histogram") + // histogram without buckets + .nameMustNotStartWith("test_histogram_bucket") + .build(); + mfsList = Collections.list(registry.filteredMetricFamilySamples(filter)); + assertSamplesInclude(mfsList, "test_histogram_count", 1); + assertSamplesInclude(mfsList, "test_histogram_sum", 1); + assertSamplesInclude(mfsList, "test_histogram_created", 1); + assertTotalNumberOfSamples(mfsList, 3); + } + + /** + * Before {@link SampleNameFilter} was introduced, the {@link CollectorRegistry#filteredMetricFamilySamples(Set)} + * method could be used to pass included names directly. That method still there for compatibility. + * This is the original test copied over from {@link CollectorRegistryTest}. + */ + @Test + public void testLegacyApi() { + Gauge.build().name("g").help("h").register(registry); + Counter.build().name("c").help("h").register(registry); + Summary.build().name("s").help("h").register(registry); + new EmptyCollector().register(registry); + SkippedCollector sr = new SkippedCollector().register(registry); + PartiallyFilterCollector pfr = new PartiallyFilterCollector().register(registry); + HashSet metrics = new HashSet(); + HashSet series = new HashSet(); + Set includedNames = new HashSet(Arrays.asList("", "s_sum", "c_total", "part_filter_a", "part_filter_c")); + List mfsList = Collections.list(registry.filteredMetricFamilySamples(includedNames)); + for (Collector.MetricFamilySamples metricFamilySamples : mfsList) { + metrics.add(metricFamilySamples.name); + for (Collector.MetricFamilySamples.Sample sample : metricFamilySamples.samples) { + series.add(sample.name); + } + } + assertEquals(1, sr.collectCallCount); + assertEquals(2, pfr.collectCallCount); + assertEquals(new HashSet(Arrays.asList("s", "c", "part_filter_a", "part_filter_c")), metrics); + assertEquals(new HashSet(Arrays.asList("s_sum", "c_total", "part_filter_a", "part_filter_c")), series); + } + + private static class EmptyCollector extends Collector { + public List collect() { + return new ArrayList(); + } + } + + private static class SkippedCollector extends Collector implements Collector.Describable { + public int collectCallCount = 0; + + @Override + public List collect() { + collectCallCount++; + List mfs = new ArrayList(); + mfs.add(new GaugeMetricFamily("slow_gauge", "help", 123)); + return mfs; + } + + @Override + public List describe() { + return collect(); + } + } + + private static class PartiallyFilterCollector extends Collector implements Collector.Describable { + public int collectCallCount = 0; + + @Override + public List collect() { + collectCallCount++; + List mfs = new ArrayList(); + mfs.add(new GaugeMetricFamily("part_filter_a", "help", 123)); + mfs.add(new GaugeMetricFamily("part_filter_b", "help", 123)); + mfs.add(new GaugeMetricFamily("part_filter_c", "help", 123)); + return mfs; + } + + @Override + public List describe() { + return collect(); + } + } + + private void assertSamplesInclude(List mfsList, String name, int times) { + int count = 0; + for (Collector.MetricFamilySamples mfs : mfsList) { + for (Collector.MetricFamilySamples.Sample sample : mfs.samples) { + if (sample.name.equals(name)) { + count++; + } + } + } + Assert.assertEquals("Wrong number of samples for " + name, times, count); + } + + private void assertTotalNumberOfSamples(List mfsList, int n) { + int count = 0; + for (Collector.MetricFamilySamples mfs : mfsList) { + count += mfs.samples.size(); + } + Assert.assertEquals("Wrong total number of samples", n, count); + } +} \ No newline at end of file diff --git a/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/BufferPoolsExports.java b/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/BufferPoolsExports.java index d456b5e7b..36d4290d6 100644 --- a/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/BufferPoolsExports.java +++ b/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/BufferPoolsExports.java @@ -2,6 +2,7 @@ import io.prometheus.client.Collector; import io.prometheus.client.GaugeMetricFamily; +import io.prometheus.client.Predicate; import java.lang.management.ManagementFactory; import java.lang.reflect.InvocationTargetException; @@ -11,6 +12,8 @@ import java.util.List; import java.util.logging.Logger; +import static io.prometheus.client.SampleNameFilter.ALLOW_ALL; + /** * Exports metrics about JVM buffers. * @@ -19,6 +22,10 @@ */ public class BufferPoolsExports extends Collector { + private static final String JVM_BUFFER_POOL_USED_BYTES = "jvm_buffer_pool_used_bytes"; + private static final String JVM_BUFFER_POOL_CAPACITY_BYTES = "jvm_buffer_pool_capacity_bytes"; + private static final String JVM_BUFFER_POOL_USED_BUFFERS = "jvm_buffer_pool_used_buffers"; + private static final Logger LOGGER = Logger.getLogger(BufferPoolsExports.class.getName()); private final List bufferPoolMXBeans = new ArrayList(); @@ -65,37 +72,60 @@ private static List accessBufferPoolMXBeans(final Class bufferPoolMXB @Override public List collect() { + return collect(null); + } + + @Override + public List collect(Predicate nameFilter) { List mfs = new ArrayList(); - GaugeMetricFamily used = new GaugeMetricFamily( - "jvm_buffer_pool_used_bytes", - "Used bytes of a given JVM buffer pool.", - Collections.singletonList("pool")); - mfs.add(used); - GaugeMetricFamily capacity = new GaugeMetricFamily( - "jvm_buffer_pool_capacity_bytes", - "Bytes capacity of a given JVM buffer pool.", - Collections.singletonList("pool")); - mfs.add(capacity); - GaugeMetricFamily buffers = new GaugeMetricFamily( - "jvm_buffer_pool_used_buffers", - "Used buffers of a given JVM buffer pool.", - Collections.singletonList("pool")); - mfs.add(buffers); + if (nameFilter == null) { + nameFilter = ALLOW_ALL; + } + GaugeMetricFamily used = null; + if (nameFilter.test(JVM_BUFFER_POOL_USED_BYTES)) { + used = new GaugeMetricFamily( + JVM_BUFFER_POOL_USED_BYTES, + "Used bytes of a given JVM buffer pool.", + Collections.singletonList("pool")); + mfs.add(used); + } + GaugeMetricFamily capacity = null; + if (nameFilter.test(JVM_BUFFER_POOL_CAPACITY_BYTES)) { + capacity = new GaugeMetricFamily( + JVM_BUFFER_POOL_CAPACITY_BYTES, + "Bytes capacity of a given JVM buffer pool.", + Collections.singletonList("pool")); + mfs.add(capacity); + } + GaugeMetricFamily buffers = null; + if (nameFilter.test(JVM_BUFFER_POOL_USED_BUFFERS)) { + buffers = new GaugeMetricFamily( + JVM_BUFFER_POOL_USED_BUFFERS, + "Used buffers of a given JVM buffer pool.", + Collections.singletonList("pool")); + mfs.add(buffers); + } for (final Object pool : bufferPoolMXBeans) { - used.addMetric( - Collections.singletonList(getName(pool)), - callLongMethond(getMemoryUsed,pool)); - capacity.addMetric( - Collections.singletonList(getName(pool)), - callLongMethond(getTotalCapacity,pool)); - buffers.addMetric( - Collections.singletonList(getName(pool)), - callLongMethond(getCount,pool)); + if (used != null) { + used.addMetric( + Collections.singletonList(getName(pool)), + callLongMethod(getMemoryUsed, pool)); + } + if (capacity != null) { + capacity.addMetric( + Collections.singletonList(getName(pool)), + callLongMethod(getTotalCapacity, pool)); + } + if (buffers != null) { + buffers.addMetric( + Collections.singletonList(getName(pool)), + callLongMethod(getCount, pool)); + } } return mfs; } - private long callLongMethond(final Method method, final Object pool) { + private long callLongMethod(final Method method, final Object pool) { try { return (Long)method.invoke(pool); } catch (IllegalAccessException e) { @@ -116,7 +146,4 @@ private String getName(final Object pool) { } return ""; } - - - } diff --git a/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/ClassLoadingExports.java b/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/ClassLoadingExports.java index feeffe11f..b23b14be5 100644 --- a/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/ClassLoadingExports.java +++ b/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/ClassLoadingExports.java @@ -3,12 +3,15 @@ import io.prometheus.client.Collector; import io.prometheus.client.CounterMetricFamily; import io.prometheus.client.GaugeMetricFamily; +import io.prometheus.client.Predicate; import java.lang.management.ManagementFactory; import java.lang.management.ClassLoadingMXBean; import java.util.ArrayList; import java.util.List; +import static io.prometheus.client.SampleNameFilter.ALLOW_ALL; + /** * Exports metrics about JVM classloading. *

@@ -26,6 +29,11 @@ * */ public class ClassLoadingExports extends Collector { + + private static final String JVM_CLASSES_CURRENTLY_LOADED = "jvm_classes_currently_loaded"; + private static final String JVM_CLASSES_LOADED_TOTAL = "jvm_classes_loaded_total"; + private static final String JVM_CLASSES_UNLOADED_TOTAL = "jvm_classes_unloaded_total"; + private final ClassLoadingMXBean clBean; public ClassLoadingExports() { @@ -36,25 +44,36 @@ public ClassLoadingExports(ClassLoadingMXBean clBean) { this.clBean = clBean; } - void addClassLoadingMetrics(List sampleFamilies) { - sampleFamilies.add(new GaugeMetricFamily( - "jvm_classes_currently_loaded", - "The number of classes that are currently loaded in the JVM", - clBean.getLoadedClassCount())); - sampleFamilies.add(new CounterMetricFamily( - "jvm_classes_loaded_total", - "The total number of classes that have been loaded since the JVM has started execution", - clBean.getTotalLoadedClassCount())); - sampleFamilies.add(new CounterMetricFamily( - "jvm_classes_unloaded_total", - "The total number of classes that have been unloaded since the JVM has started execution", - clBean.getUnloadedClassCount())); + void addClassLoadingMetrics(List sampleFamilies, Predicate nameFilter) { + if (nameFilter.test(JVM_CLASSES_CURRENTLY_LOADED)) { + sampleFamilies.add(new GaugeMetricFamily( + JVM_CLASSES_CURRENTLY_LOADED, + "The number of classes that are currently loaded in the JVM", + clBean.getLoadedClassCount())); + } + if (nameFilter.test(JVM_CLASSES_LOADED_TOTAL)) { + sampleFamilies.add(new CounterMetricFamily( + JVM_CLASSES_LOADED_TOTAL, + "The total number of classes that have been loaded since the JVM has started execution", + clBean.getTotalLoadedClassCount())); + } + if (nameFilter.test(JVM_CLASSES_UNLOADED_TOTAL)) { + sampleFamilies.add(new CounterMetricFamily( + JVM_CLASSES_UNLOADED_TOTAL, + "The total number of classes that have been unloaded since the JVM has started execution", + clBean.getUnloadedClassCount())); + } } - + @Override public List collect() { + return collect(null); + } + + @Override + public List collect(Predicate nameFilter) { List mfs = new ArrayList(); - addClassLoadingMetrics(mfs); + addClassLoadingMetrics(mfs, nameFilter == null ? ALLOW_ALL : nameFilter); return mfs; } } diff --git a/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/GarbageCollectorExports.java b/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/GarbageCollectorExports.java index ad0b1f029..8aa18c3e6 100644 --- a/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/GarbageCollectorExports.java +++ b/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/GarbageCollectorExports.java @@ -1,6 +1,7 @@ package io.prometheus.client.hotspot; import io.prometheus.client.Collector; +import io.prometheus.client.Predicate; import io.prometheus.client.SummaryMetricFamily; import java.lang.management.GarbageCollectorMXBean; @@ -25,6 +26,9 @@ * */ public class GarbageCollectorExports extends Collector { + + private static final String JVM_GC_COLLECTION_SECONDS = "jvm_gc_collection_seconds"; + private final List garbageCollectors; public GarbageCollectorExports() { @@ -35,19 +39,27 @@ public GarbageCollectorExports() { this.garbageCollectors = garbageCollectors; } + @Override public List collect() { - SummaryMetricFamily gcCollection = new SummaryMetricFamily( - "jvm_gc_collection_seconds", - "Time spent in a given JVM garbage collector in seconds.", - Collections.singletonList("gc")); - for (final GarbageCollectorMXBean gc : garbageCollectors) { + return collect(null); + } + + @Override + public List collect(Predicate nameFilter) { + List mfs = new ArrayList(); + if (nameFilter == null || nameFilter.test(JVM_GC_COLLECTION_SECONDS)) { + SummaryMetricFamily gcCollection = new SummaryMetricFamily( + JVM_GC_COLLECTION_SECONDS, + "Time spent in a given JVM garbage collector in seconds.", + Collections.singletonList("gc")); + for (final GarbageCollectorMXBean gc : garbageCollectors) { gcCollection.addMetric( - Collections.singletonList(gc.getName()), - gc.getCollectionCount(), - gc.getCollectionTime() / MILLISECONDS_PER_SECOND); + Collections.singletonList(gc.getName()), + gc.getCollectionCount(), + gc.getCollectionTime() / MILLISECONDS_PER_SECOND); + } + mfs.add(gcCollection); } - List mfs = new ArrayList(); - mfs.add(gcCollection); return mfs; } } diff --git a/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/MemoryPoolsExports.java b/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/MemoryPoolsExports.java index 0b37e72ba..4729fbe70 100644 --- a/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/MemoryPoolsExports.java +++ b/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/MemoryPoolsExports.java @@ -2,6 +2,7 @@ import io.prometheus.client.Collector; import io.prometheus.client.GaugeMetricFamily; +import io.prometheus.client.Predicate; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; @@ -11,6 +12,8 @@ import java.util.Collections; import java.util.List; +import static io.prometheus.client.SampleNameFilter.ALLOW_ALL; + /** * Exports metrics about JVM memory areas. *

@@ -29,6 +32,26 @@ * */ public class MemoryPoolsExports extends Collector { + + private static final String JVM_MEMORY_OBJECTS_PENDING_FINALIZATION = "jvm_memory_objects_pending_finalization"; + private static final String JVM_MEMORY_BYTES_USED = "jvm_memory_bytes_used"; + private static final String JVM_MEMORY_BYTES_COMMITTED = "jvm_memory_bytes_committed"; + private static final String JVM_MEMORY_BYTES_MAX = "jvm_memory_bytes_max"; + private static final String JVM_MEMORY_BYTES_INIT = "jvm_memory_bytes_init"; + + // Note: The Prometheus naming convention is that units belong at the end of the metric name. + // For new metrics like jvm_memory_pool_collection_used_bytes we follow that convention. + // For old metrics like jvm_memory_pool_bytes_used we keep the names as they are to avoid a breaking change. + + private static final String JVM_MEMORY_POOL_BYTES_USED = "jvm_memory_pool_bytes_used"; + private static final String JVM_MEMORY_POOL_BYTES_COMMITTED = "jvm_memory_pool_bytes_committed"; + private static final String JVM_MEMORY_POOL_BYTES_MAX = "jvm_memory_pool_bytes_max"; + private static final String JVM_MEMORY_POOL_BYTES_INIT = "jvm_memory_pool_bytes_init"; + private static final String JVM_MEMORY_POOL_COLLECTION_USED_BYTES = "jvm_memory_pool_collection_used_bytes"; + private static final String JVM_MEMORY_POOL_COLLECTION_COMMITTED_BYTES = "jvm_memory_pool_collection_committed_bytes"; + private static final String JVM_MEMORY_POOL_COLLECTION_MAX_BYTES = "jvm_memory_pool_collection_max_bytes"; + private static final String JVM_MEMORY_POOL_COLLECTION_INIT_BYTES = "jvm_memory_pool_collection_init_bytes"; + private final MemoryMXBean memoryBean; private final List poolBeans; @@ -44,131 +67,174 @@ public MemoryPoolsExports(MemoryMXBean memoryBean, this.poolBeans = poolBeans; } - void addMemoryAreaMetrics(List sampleFamilies) { + void addMemoryAreaMetrics(List sampleFamilies, Predicate nameFilter) { MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage(); MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage(); - GaugeMetricFamily finalizer = new GaugeMetricFamily( - "jvm_memory_objects_pending_finalization", - "The number of objects waiting in the finalizer queue.", - memoryBean.getObjectPendingFinalizationCount()); - sampleFamilies.add(finalizer); - - GaugeMetricFamily used = new GaugeMetricFamily( - "jvm_memory_bytes_used", - "Used bytes of a given JVM memory area.", - Collections.singletonList("area")); - used.addMetric(Collections.singletonList("heap"), heapUsage.getUsed()); - used.addMetric(Collections.singletonList("nonheap"), nonHeapUsage.getUsed()); - sampleFamilies.add(used); - - GaugeMetricFamily committed = new GaugeMetricFamily( - "jvm_memory_bytes_committed", - "Committed (bytes) of a given JVM memory area.", - Collections.singletonList("area")); - committed.addMetric(Collections.singletonList("heap"), heapUsage.getCommitted()); - committed.addMetric(Collections.singletonList("nonheap"), nonHeapUsage.getCommitted()); - sampleFamilies.add(committed); - - GaugeMetricFamily max = new GaugeMetricFamily( - "jvm_memory_bytes_max", - "Max (bytes) of a given JVM memory area.", - Collections.singletonList("area")); - max.addMetric(Collections.singletonList("heap"), heapUsage.getMax()); - max.addMetric(Collections.singletonList("nonheap"), nonHeapUsage.getMax()); - sampleFamilies.add(max); - - GaugeMetricFamily init = new GaugeMetricFamily( - "jvm_memory_bytes_init", - "Initial bytes of a given JVM memory area.", - Collections.singletonList("area")); - init.addMetric(Collections.singletonList("heap"), heapUsage.getInit()); - init.addMetric(Collections.singletonList("nonheap"), nonHeapUsage.getInit()); - sampleFamilies.add(init); + if (nameFilter.test(JVM_MEMORY_OBJECTS_PENDING_FINALIZATION)) { + GaugeMetricFamily finalizer = new GaugeMetricFamily( + JVM_MEMORY_OBJECTS_PENDING_FINALIZATION, + "The number of objects waiting in the finalizer queue.", + memoryBean.getObjectPendingFinalizationCount()); + sampleFamilies.add(finalizer); + } + + if (nameFilter.test(JVM_MEMORY_BYTES_USED)) { + GaugeMetricFamily used = new GaugeMetricFamily( + JVM_MEMORY_BYTES_USED, + "Used bytes of a given JVM memory area.", + Collections.singletonList("area")); + used.addMetric(Collections.singletonList("heap"), heapUsage.getUsed()); + used.addMetric(Collections.singletonList("nonheap"), nonHeapUsage.getUsed()); + sampleFamilies.add(used); + } + + if (nameFilter.test(JVM_MEMORY_POOL_BYTES_COMMITTED)) { + GaugeMetricFamily committed = new GaugeMetricFamily( + JVM_MEMORY_BYTES_COMMITTED, + "Committed (bytes) of a given JVM memory area.", + Collections.singletonList("area")); + committed.addMetric(Collections.singletonList("heap"), heapUsage.getCommitted()); + committed.addMetric(Collections.singletonList("nonheap"), nonHeapUsage.getCommitted()); + sampleFamilies.add(committed); + } + + if (nameFilter.test(JVM_MEMORY_BYTES_MAX)) { + GaugeMetricFamily max = new GaugeMetricFamily( + JVM_MEMORY_BYTES_MAX, + "Max (bytes) of a given JVM memory area.", + Collections.singletonList("area")); + max.addMetric(Collections.singletonList("heap"), heapUsage.getMax()); + max.addMetric(Collections.singletonList("nonheap"), nonHeapUsage.getMax()); + sampleFamilies.add(max); + } + + if (nameFilter.test(JVM_MEMORY_BYTES_INIT)) { + GaugeMetricFamily init = new GaugeMetricFamily( + JVM_MEMORY_BYTES_INIT, + "Initial bytes of a given JVM memory area.", + Collections.singletonList("area")); + init.addMetric(Collections.singletonList("heap"), heapUsage.getInit()); + init.addMetric(Collections.singletonList("nonheap"), nonHeapUsage.getInit()); + sampleFamilies.add(init); + } } - void addMemoryPoolMetrics(List sampleFamilies) { - - // Note: The Prometheus naming convention is that units belong at the end of the metric name. - // For new metrics like jvm_memory_pool_collection_used_bytes we follow that convention. - // For old metrics like jvm_memory_pool_bytes_used we keep the names as they are to avoid a breaking change. - - GaugeMetricFamily used = new GaugeMetricFamily( - "jvm_memory_pool_bytes_used", - "Used bytes of a given JVM memory pool.", - Collections.singletonList("pool")); - sampleFamilies.add(used); - GaugeMetricFamily committed = new GaugeMetricFamily( - "jvm_memory_pool_bytes_committed", - "Committed bytes of a given JVM memory pool.", - Collections.singletonList("pool")); - sampleFamilies.add(committed); - GaugeMetricFamily max = new GaugeMetricFamily( - "jvm_memory_pool_bytes_max", - "Max bytes of a given JVM memory pool.", - Collections.singletonList("pool")); - sampleFamilies.add(max); - GaugeMetricFamily init = new GaugeMetricFamily( - "jvm_memory_pool_bytes_init", - "Initial bytes of a given JVM memory pool.", - Collections.singletonList("pool")); - sampleFamilies.add(init); - GaugeMetricFamily collectionUsed = new GaugeMetricFamily( - "jvm_memory_pool_collection_used_bytes", - "Used bytes after last collection of a given JVM memory pool.", - Collections.singletonList("pool")); - sampleFamilies.add(collectionUsed); - GaugeMetricFamily collectionCommitted = new GaugeMetricFamily( - "jvm_memory_pool_collection_committed_bytes", - "Committed after last collection bytes of a given JVM memory pool.", - Collections.singletonList("pool")); - sampleFamilies.add(collectionCommitted); - GaugeMetricFamily collectionMax = new GaugeMetricFamily( - "jvm_memory_pool_collection_max_bytes", - "Max bytes after last collection of a given JVM memory pool.", - Collections.singletonList("pool")); - sampleFamilies.add(collectionMax); - GaugeMetricFamily collectionInit = new GaugeMetricFamily( - "jvm_memory_pool_collection_init_bytes", - "Initial after last collection bytes of a given JVM memory pool.", - Collections.singletonList("pool")); - sampleFamilies.add(collectionInit); - for (final MemoryPoolMXBean pool : poolBeans) { - MemoryUsage poolUsage = pool.getUsage(); - used.addMetric( - Collections.singletonList(pool.getName()), - poolUsage.getUsed()); - committed.addMetric( - Collections.singletonList(pool.getName()), - poolUsage.getCommitted()); - max.addMetric( - Collections.singletonList(pool.getName()), - poolUsage.getMax()); - init.addMetric( - Collections.singletonList(pool.getName()), - poolUsage.getInit()); - MemoryUsage collectionPoolUsage = pool.getCollectionUsage(); - if (collectionPoolUsage != null) { - collectionUsed.addMetric( - Collections.singletonList(pool.getName()), - collectionPoolUsage.getUsed()); - collectionCommitted.addMetric( - Collections.singletonList(pool.getName()), - collectionPoolUsage.getCommitted()); - collectionMax.addMetric( - Collections.singletonList(pool.getName()), - collectionPoolUsage.getMax()); - collectionInit.addMetric( - Collections.singletonList(pool.getName()), - collectionPoolUsage.getInit()); + void addMemoryPoolMetrics(List sampleFamilies, Predicate nameFilter) { + + boolean anyPoolMetricPassesFilter = false; + + GaugeMetricFamily used = null; + if (nameFilter.test(JVM_MEMORY_POOL_BYTES_USED)) { + used = new GaugeMetricFamily( + JVM_MEMORY_POOL_BYTES_USED, + "Used bytes of a given JVM memory pool.", + Collections.singletonList("pool")); + sampleFamilies.add(used); + anyPoolMetricPassesFilter = true; + } + GaugeMetricFamily committed = null; + if (nameFilter.test(JVM_MEMORY_POOL_BYTES_COMMITTED)) { + committed = new GaugeMetricFamily( + JVM_MEMORY_POOL_BYTES_COMMITTED, + "Committed bytes of a given JVM memory pool.", + Collections.singletonList("pool")); + sampleFamilies.add(committed); + anyPoolMetricPassesFilter = true; + } + GaugeMetricFamily max = null; + if (nameFilter.test(JVM_MEMORY_POOL_BYTES_MAX)) { + max = new GaugeMetricFamily( + JVM_MEMORY_POOL_BYTES_MAX, + "Max bytes of a given JVM memory pool.", + Collections.singletonList("pool")); + sampleFamilies.add(max); + anyPoolMetricPassesFilter = true; + } + GaugeMetricFamily init = null; + if (nameFilter.test(JVM_MEMORY_POOL_BYTES_INIT)) { + init = new GaugeMetricFamily( + JVM_MEMORY_POOL_BYTES_INIT, + "Initial bytes of a given JVM memory pool.", + Collections.singletonList("pool")); + sampleFamilies.add(init); + anyPoolMetricPassesFilter = true; + } + GaugeMetricFamily collectionUsed = null; + if (nameFilter.test(JVM_MEMORY_POOL_COLLECTION_USED_BYTES)) { + collectionUsed = new GaugeMetricFamily( + JVM_MEMORY_POOL_COLLECTION_USED_BYTES, + "Used bytes after last collection of a given JVM memory pool.", + Collections.singletonList("pool")); + sampleFamilies.add(collectionUsed); + anyPoolMetricPassesFilter = true; + } + GaugeMetricFamily collectionCommitted = null; + if (nameFilter.test(JVM_MEMORY_POOL_COLLECTION_COMMITTED_BYTES)) { + collectionCommitted = new GaugeMetricFamily( + JVM_MEMORY_POOL_COLLECTION_COMMITTED_BYTES, + "Committed after last collection bytes of a given JVM memory pool.", + Collections.singletonList("pool")); + sampleFamilies.add(collectionCommitted); + anyPoolMetricPassesFilter = true; + } + GaugeMetricFamily collectionMax = null; + if (nameFilter.test(JVM_MEMORY_POOL_COLLECTION_MAX_BYTES)) { + collectionMax = new GaugeMetricFamily( + JVM_MEMORY_POOL_COLLECTION_MAX_BYTES, + "Max bytes after last collection of a given JVM memory pool.", + Collections.singletonList("pool")); + sampleFamilies.add(collectionMax); + anyPoolMetricPassesFilter = true; + } + GaugeMetricFamily collectionInit = null; + if (nameFilter.test(JVM_MEMORY_POOL_COLLECTION_INIT_BYTES)) { + collectionInit = new GaugeMetricFamily( + JVM_MEMORY_POOL_COLLECTION_INIT_BYTES, + "Initial after last collection bytes of a given JVM memory pool.", + Collections.singletonList("pool")); + sampleFamilies.add(collectionInit); + anyPoolMetricPassesFilter = true; + } + if (anyPoolMetricPassesFilter) { + for (final MemoryPoolMXBean pool : poolBeans) { + MemoryUsage poolUsage = pool.getUsage(); + if (poolUsage != null) { + addPoolMetrics(used, committed, max, init, pool.getName(), poolUsage); + } + MemoryUsage collectionPoolUsage = pool.getCollectionUsage(); + if (collectionPoolUsage != null) { + addPoolMetrics(collectionUsed, collectionCommitted, collectionMax, collectionInit, pool.getName(), collectionPoolUsage); + } } } } + private void addPoolMetrics(GaugeMetricFamily used, GaugeMetricFamily committed, GaugeMetricFamily max, GaugeMetricFamily init, String poolName, MemoryUsage poolUsage) { + if (used != null) { + used.addMetric(Collections.singletonList(poolName), poolUsage.getUsed()); + } + if (committed != null) { + committed.addMetric(Collections.singletonList(poolName), poolUsage.getCommitted()); + } + if (max != null) { + max.addMetric(Collections.singletonList(poolName), poolUsage.getMax()); + } + if (init != null) { + init.addMetric(Collections.singletonList(poolName), poolUsage.getInit()); + } + } + + @Override public List collect() { + return collect(null); + } + + @Override + public List collect(Predicate nameFilter) { List mfs = new ArrayList(); - addMemoryAreaMetrics(mfs); - addMemoryPoolMetrics(mfs); + addMemoryAreaMetrics(mfs, nameFilter == null ? ALLOW_ALL : nameFilter); + addMemoryPoolMetrics(mfs, nameFilter == null ? ALLOW_ALL : nameFilter); return mfs; } } diff --git a/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/ThreadExports.java b/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/ThreadExports.java index 55c90a654..c63db20d7 100644 --- a/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/ThreadExports.java +++ b/simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/ThreadExports.java @@ -3,6 +3,8 @@ import io.prometheus.client.Collector; import io.prometheus.client.CounterMetricFamily; import io.prometheus.client.GaugeMetricFamily; +import io.prometheus.client.SampleNameFilter; +import io.prometheus.client.Predicate; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; @@ -13,6 +15,8 @@ import java.util.List; import java.util.Map; +import static io.prometheus.client.SampleNameFilter.ALLOW_ALL; + /** * Exports metrics about JVM thread areas. *

@@ -31,6 +35,15 @@ * */ public class ThreadExports extends Collector { + + private static final String JVM_THREADS_CURRENT = "jvm_threads_current"; + private static final String JVM_THREADS_DAEMON = "jvm_threads_daemon"; + private static final String JVM_THREADS_PEAK = "jvm_threads_peak"; + private static final String JVM_THREADS_STARTED_TOTAL = "jvm_threads_started_total"; + private static final String JVM_THREADS_DEADLOCKED = "jvm_threads_deadlocked"; + private static final String JVM_THREADS_DEADLOCKED_MONITOR = "jvm_threads_deadlocked_monitor"; + private static final String JVM_THREADS_STATE = "jvm_threads_state"; + private final ThreadMXBean threadBean; public ThreadExports() { @@ -41,56 +54,70 @@ public ThreadExports(ThreadMXBean threadBean) { this.threadBean = threadBean; } - void addThreadMetrics(List sampleFamilies) { - sampleFamilies.add( - new GaugeMetricFamily( - "jvm_threads_current", - "Current thread count of a JVM", - threadBean.getThreadCount())); - - sampleFamilies.add( - new GaugeMetricFamily( - "jvm_threads_daemon", - "Daemon thread count of a JVM", - threadBean.getDaemonThreadCount())); - - sampleFamilies.add( - new GaugeMetricFamily( - "jvm_threads_peak", - "Peak thread count of a JVM", - threadBean.getPeakThreadCount())); - - sampleFamilies.add( - new CounterMetricFamily( - "jvm_threads_started_total", - "Started thread count of a JVM", - threadBean.getTotalStartedThreadCount())); - - sampleFamilies.add( - new GaugeMetricFamily( - "jvm_threads_deadlocked", - "Cycles of JVM-threads that are in deadlock waiting to acquire object monitors or ownable synchronizers", - nullSafeArrayLength(threadBean.findDeadlockedThreads()))); - - sampleFamilies.add( - new GaugeMetricFamily( - "jvm_threads_deadlocked_monitor", - "Cycles of JVM-threads that are in deadlock waiting to acquire object monitors", - nullSafeArrayLength(threadBean.findMonitorDeadlockedThreads()))); - - GaugeMetricFamily threadStateFamily = new GaugeMetricFamily( - "jvm_threads_state", - "Current count of threads by state", - Collections.singletonList("state")); - - Map threadStateCounts = getThreadStateCountMap(); - for (Map.Entry entry : threadStateCounts.entrySet()) { - threadStateFamily.addMetric( - Collections.singletonList(entry.getKey().toString()), - entry.getValue() - ); + void addThreadMetrics(List sampleFamilies, Predicate nameFilter) { + if (nameFilter.test(JVM_THREADS_CURRENT)) { + sampleFamilies.add( + new GaugeMetricFamily( + JVM_THREADS_CURRENT, + "Current thread count of a JVM", + threadBean.getThreadCount())); + } + + if (nameFilter.test(JVM_THREADS_DAEMON)) { + sampleFamilies.add( + new GaugeMetricFamily( + JVM_THREADS_DAEMON, + "Daemon thread count of a JVM", + threadBean.getDaemonThreadCount())); + } + + if (nameFilter.test(JVM_THREADS_PEAK)) { + sampleFamilies.add( + new GaugeMetricFamily( + JVM_THREADS_PEAK, + "Peak thread count of a JVM", + threadBean.getPeakThreadCount())); + } + + if (nameFilter.test(JVM_THREADS_STARTED_TOTAL)) { + sampleFamilies.add( + new CounterMetricFamily( + JVM_THREADS_STARTED_TOTAL, + "Started thread count of a JVM", + threadBean.getTotalStartedThreadCount())); + } + + if (nameFilter.test(JVM_THREADS_DEADLOCKED)) { + sampleFamilies.add( + new GaugeMetricFamily( + JVM_THREADS_DEADLOCKED, + "Cycles of JVM-threads that are in deadlock waiting to acquire object monitors or ownable synchronizers", + nullSafeArrayLength(threadBean.findDeadlockedThreads()))); + } + + if (nameFilter.test(JVM_THREADS_DEADLOCKED_MONITOR)) { + sampleFamilies.add( + new GaugeMetricFamily( + JVM_THREADS_DEADLOCKED_MONITOR, + "Cycles of JVM-threads that are in deadlock waiting to acquire object monitors", + nullSafeArrayLength(threadBean.findMonitorDeadlockedThreads()))); + } + + if (nameFilter.test(JVM_THREADS_STATE)) { + GaugeMetricFamily threadStateFamily = new GaugeMetricFamily( + JVM_THREADS_STATE, + "Current count of threads by state", + Collections.singletonList("state")); + + Map threadStateCounts = getThreadStateCountMap(); + for (Map.Entry entry : threadStateCounts.entrySet()) { + threadStateFamily.addMetric( + Collections.singletonList(entry.getKey().toString()), + entry.getValue() + ); + } + sampleFamilies.add(threadStateFamily); } - sampleFamilies.add(threadStateFamily); } private Map getThreadStateCountMap() { @@ -118,9 +145,15 @@ private static double nullSafeArrayLength(long[] array) { return null == array ? 0 : array.length; } + @Override public List collect() { + return collect(null); + } + + @Override + public List collect(Predicate nameFilter) { List mfs = new ArrayList(); - addThreadMetrics(mfs); + addThreadMetrics(mfs, nameFilter == null ? ALLOW_ALL : nameFilter); return mfs; } -} +} \ No newline at end of file diff --git a/simpleclient_httpserver/src/main/java/io/prometheus/client/exporter/HTTPServer.java b/simpleclient_httpserver/src/main/java/io/prometheus/client/exporter/HTTPServer.java index d6afdcd69..dbc031307 100644 --- a/simpleclient_httpserver/src/main/java/io/prometheus/client/exporter/HTTPServer.java +++ b/simpleclient_httpserver/src/main/java/io/prometheus/client/exporter/HTTPServer.java @@ -1,12 +1,17 @@ package io.prometheus.client.exporter; import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.SampleNameFilter; +import io.prometheus.client.Predicate; +import io.prometheus.client.Supplier; import io.prometheus.client.exporter.common.TextFormat; import java.io.ByteArrayOutputStream; +import java.io.Closeable; import java.io.IOException; import java.io.OutputStreamWriter; import java.net.HttpURLConnection; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URLDecoder; import java.nio.charset.Charset; @@ -35,7 +40,7 @@ * } * * */ -public class HTTPServer { +public class HTTPServer implements Closeable { static { if (!System.getProperties().containsKey("sun.net.httpserver.maxReqTime")) { @@ -61,10 +66,16 @@ protected ByteArrayOutputStream initialValue() public static class HTTPMetricHandler implements HttpHandler { private final CollectorRegistry registry; private final LocalByteArray response = new LocalByteArray(); + private final Supplier> sampleNameFilterSupplier; private final static String HEALTHY_RESPONSE = "Exporter is Healthy."; HTTPMetricHandler(CollectorRegistry registry) { - this.registry = registry; + this(registry, null); + } + + HTTPMetricHandler(CollectorRegistry registry, Supplier> sampleNameFilterSupplier) { + this.registry = registry; + this.sampleNameFilterSupplier = sampleNameFilterSupplier; } @Override @@ -80,8 +91,13 @@ public void handle(HttpExchange t) throws IOException { } else { String contentType = TextFormat.chooseContentType(t.getRequestHeaders().getFirst("Accept")); t.getResponseHeaders().set("Content-Type", contentType); - TextFormat.writeFormat(contentType, osw, - registry.filteredMetricFamilySamples(parseQuery(query))); + Predicate filter = sampleNameFilterSupplier == null ? null : sampleNameFilterSupplier.get(); + filter = SampleNameFilter.restrictToNamesEqualTo(filter, parseQuery(query)); + if (filter == null) { + TextFormat.writeFormat(contentType, osw, registry.metricFamilySamples()); + } else { + TextFormat.writeFormat(contentType, osw, registry.filteredMetricFamilySamples(filter)); + } } osw.close(); @@ -103,7 +119,6 @@ public void handle(HttpExchange t) throws IOException { } t.close(); } - } protected static boolean shouldUseCompression(HttpExchange exchange) { @@ -166,67 +181,216 @@ static ThreadFactory defaultThreadFactory(boolean daemon) { protected final ExecutorService executorService; /** - * Start a HTTP server serving Prometheus metrics from the given registry using the given {@link HttpServer}. + * We keep the original constructors of {@link HTTPServer} for compatibility, but new configuration + * parameters like {@code sampleNameFilter} must be configured using the Builder. + */ + public static class Builder { + + private int port = 0; + private String hostname = null; + private InetAddress inetAddress = null; + private InetSocketAddress inetSocketAddress = null; + private HttpServer httpServer = null; + private CollectorRegistry registry = CollectorRegistry.defaultRegistry; + private boolean daemon = false; + private Predicate sampleNameFilter; + private Supplier> sampleNameFilterSupplier; + + /** + * Port to bind to. Must not be called together with {@link #withInetSocketAddress(InetSocketAddress)} + * or {@link #withHttpServer(HttpServer)}. Default is 0, indicating that a random port will be selected. + */ + public Builder withPort(int port) { + this.port = port; + return this; + } + + /** + * Use this hostname to resolve the IP address to bind to. Must not be called together with + * {@link #withInetAddress(InetAddress)} or {@link #withInetSocketAddress(InetSocketAddress)} + * or {@link #withHttpServer(HttpServer)}. + * Default is empty, indicating that the HTTPServer binds to the wildcard address. + */ + public Builder withHostname(String hostname) { + this.hostname = hostname; + return this; + } + + /** + * Bind to this IP address. Must not be called together with {@link #withHostname(String)} or + * {@link #withInetSocketAddress(InetSocketAddress)} or {@link #withHttpServer(HttpServer)}. + * Default is empty, indicating that the HTTPServer binds to the wildcard address. + */ + public Builder withInetAddress(InetAddress address) { + this.inetAddress = address; + return this; + } + + /** + * Listen on this address. Must not be called together with {@link #withPort(int)}, + * {@link #withHostname(String)}, {@link #withInetAddress(InetAddress)}, or {@link #withHttpServer(HttpServer)}. + */ + public Builder withInetSocketAddress(InetSocketAddress address) { + this.inetSocketAddress = address; + return this; + } + + /** + * Use this httpServer. The {@code httpServer} is expected to already be bound to an address. + * Must not be called together with {@link #withPort(int)}, or {@link #withHostname(String)}, + * or {@link #withInetAddress(InetAddress)}, or {@link #withInetSocketAddress(InetSocketAddress)}. + */ + public Builder withHttpServer(HttpServer httpServer) { + this.httpServer = httpServer; + return this; + } + + /** + * By default, the {@link HTTPServer} uses non-daemon threads. Set this to {@code true} to + * run the {@link HTTPServer} with daemon threads. + */ + public Builder withDaemonThreads(boolean daemon) { + this.daemon = daemon; + return this; + } + + /** + * Optional: Only export time series where {@code sampleNameFilter.test(name)} returns true. + *

+ * Use this if the sampleNameFilter remains the same throughout the lifetime of the HTTPServer. + * If the sampleNameFilter changes during runtime, use {@link #withSampleNameFilterSupplier(Supplier)}. + */ + public Builder withSampleNameFilter(Predicate sampleNameFilter) { + this.sampleNameFilter = sampleNameFilter; + return this; + } + + /** + * Optional: Only export time series where {@code sampleNameFilter.test(name)} returns true. + *

+ * Use this if the sampleNameFilter may change during runtime, like for example if you have a + * hot reload mechanism for your filter config. + * If the sampleNameFilter remains the same throughout the lifetime of the HTTPServer, + * use {@link #withSampleNameFilter(Predicate)} instead. + */ + public Builder withSampleNameFilterSupplier(Supplier> sampleNameFilterSupplier) { + this.sampleNameFilterSupplier = sampleNameFilterSupplier; + return this; + } + + /** + * Optional: Default is {@link CollectorRegistry#defaultRegistry}. + */ + public Builder withRegistry(CollectorRegistry registry) { + this.registry = registry; + return this; + } + + public HTTPServer build() throws IOException { + if (sampleNameFilter != null) { + assertNull(sampleNameFilterSupplier, "cannot configure 'sampleNameFilter' and 'sampleNameFilterSupplier' at the same time"); + sampleNameFilterSupplier = SampleNameFilterSupplier.of(sampleNameFilter); + } + if (httpServer != null) { + assertZero(port, "cannot configure 'httpServer' and 'port' at the same time"); + assertNull(hostname, "cannot configure 'httpServer' and 'hostname' at the same time"); + assertNull(inetAddress, "cannot configure 'httpServer' and 'inetAddress' at the same time"); + assertNull(inetSocketAddress, "cannot configure 'httpServer' and 'inetSocketAddress' at the same time"); + return new HTTPServer(httpServer, registry, daemon, sampleNameFilterSupplier); + } else if (inetSocketAddress != null) { + assertZero(port, "cannot configure 'inetSocketAddress' and 'port' at the same time"); + assertNull(hostname, "cannot configure 'inetSocketAddress' and 'hostname' at the same time"); + assertNull(inetAddress, "cannot configure 'inetSocketAddress' and 'inetAddress' at the same time"); + } else if (inetAddress != null) { + assertNull(hostname, "cannot configure 'inetAddress' and 'hostname' at the same time"); + inetSocketAddress = new InetSocketAddress(inetAddress, port); + } else if (hostname != null) { + inetSocketAddress = new InetSocketAddress(hostname, port); + } else { + inetSocketAddress = new InetSocketAddress(port); + } + return new HTTPServer(HttpServer.create(inetSocketAddress, 3), registry, daemon, sampleNameFilterSupplier); + } + + private void assertNull(Object o, String msg) { + if (o != null) { + throw new IllegalStateException(msg); + } + } + + private void assertZero(int i, String msg) { + if (i != 0) { + throw new IllegalStateException(msg); + } + } + } + + /** + * Start an HTTP server serving Prometheus metrics from the given registry using the given {@link HttpServer}. * The {@code httpServer} is expected to already be bound to an address */ public HTTPServer(HttpServer httpServer, CollectorRegistry registry, boolean daemon) throws IOException { - if (httpServer.getAddress() == null) - throw new IllegalArgumentException("HttpServer hasn't been bound to an address"); - - server = httpServer; - HttpHandler mHandler = new HTTPMetricHandler(registry); - server.createContext("/", mHandler); - server.createContext("/metrics", mHandler); - server.createContext("/-/healthy", mHandler); - executorService = Executors.newFixedThreadPool(5, NamedDaemonThreadFactory.defaultThreadFactory(daemon)); - server.setExecutor(executorService); - start(daemon); + this(httpServer, registry, daemon, null); } /** - * Start a HTTP server serving Prometheus metrics from the given registry. + * Start an HTTP server serving Prometheus metrics from the given registry. */ public HTTPServer(InetSocketAddress addr, CollectorRegistry registry, boolean daemon) throws IOException { this(HttpServer.create(addr, 3), registry, daemon); } /** - * Start a HTTP server serving Prometheus metrics from the given registry using non-daemon threads. + * Start an HTTP server serving Prometheus metrics from the given registry using non-daemon threads. */ public HTTPServer(InetSocketAddress addr, CollectorRegistry registry) throws IOException { this(addr, registry, false); } /** - * Start a HTTP server serving the default Prometheus registry. + * Start an HTTP server serving the default Prometheus registry. */ public HTTPServer(int port, boolean daemon) throws IOException { this(new InetSocketAddress(port), CollectorRegistry.defaultRegistry, daemon); } /** - * Start a HTTP server serving the default Prometheus registry using non-daemon threads. + * Start an HTTP server serving the default Prometheus registry using non-daemon threads. */ public HTTPServer(int port) throws IOException { this(port, false); } /** - * Start a HTTP server serving the default Prometheus registry. + * Start an HTTP server serving the default Prometheus registry. */ public HTTPServer(String host, int port, boolean daemon) throws IOException { this(new InetSocketAddress(host, port), CollectorRegistry.defaultRegistry, daemon); } /** - * Start a HTTP server serving the default Prometheus registry using non-daemon threads. + * Start an HTTP server serving the default Prometheus registry using non-daemon threads. */ public HTTPServer(String host, int port) throws IOException { this(new InetSocketAddress(host, port), CollectorRegistry.defaultRegistry, false); } + private HTTPServer(HttpServer httpServer, CollectorRegistry registry, boolean daemon, Supplier> sampleNameFilterSupplier) { + if (httpServer.getAddress() == null) + throw new IllegalArgumentException("HttpServer hasn't been bound to an address"); + + server = httpServer; + HttpHandler mHandler = new HTTPMetricHandler(registry, sampleNameFilterSupplier); + server.createContext("/", mHandler); + server.createContext("/metrics", mHandler); + server.createContext("/-/healthy", mHandler); + executorService = Executors.newFixedThreadPool(5, NamedDaemonThreadFactory.defaultThreadFactory(daemon)); + server.setExecutor(executorService); + start(daemon); + } + /** - * Start a HTTP server by making sure that its background thread inherit proper daemon flag. + * Start an HTTP server by making sure that its background thread inherit proper daemon flag. */ private void start(boolean daemon) { if (daemon == Thread.currentThread().isDaemon()) { @@ -254,8 +418,17 @@ public void run() { /** * Stop the HTTP server. + * @deprecated renamed to close(), so that the HTTPServer can be used in try-with-resources. */ public void stop() { + close(); + } + + /** + * Stop the HTTPServer. + */ + @Override + public void close() { server.stop(0); executorService.shutdown(); // Free any (parked/idle) threads in pool } diff --git a/simpleclient_httpserver/src/main/java/io/prometheus/client/exporter/SampleNameFilterSupplier.java b/simpleclient_httpserver/src/main/java/io/prometheus/client/exporter/SampleNameFilterSupplier.java new file mode 100644 index 000000000..68094ad81 --- /dev/null +++ b/simpleclient_httpserver/src/main/java/io/prometheus/client/exporter/SampleNameFilterSupplier.java @@ -0,0 +1,26 @@ +package io.prometheus.client.exporter; + +import io.prometheus.client.Predicate; +import io.prometheus.client.Supplier; + +/** + * For convenience, an implementation of a {@code Supplier>} that + * always returns the same sampleNameFilter. + */ +public class SampleNameFilterSupplier implements Supplier> { + + private final Predicate sampleNameFilter; + + public static SampleNameFilterSupplier of(Predicate sampleNameFilter) { + return new SampleNameFilterSupplier(sampleNameFilter); + } + + private SampleNameFilterSupplier(Predicate sampleNameFilter) { + this.sampleNameFilter = sampleNameFilter; + } + + @Override + public Predicate get() { + return sampleNameFilter; + } +} diff --git a/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/TestDaemonFlags.java b/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/TestDaemonFlags.java index 80bca9d90..aaddd5e22 100644 --- a/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/TestDaemonFlags.java +++ b/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/TestDaemonFlags.java @@ -7,7 +7,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Java6Assertions.assertThat; public class TestDaemonFlags { diff --git a/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/TestHTTPServer.java b/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/TestHTTPServer.java index c9f6beed6..af99ccf1f 100644 --- a/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/TestHTTPServer.java +++ b/simpleclient_httpserver/src/test/java/io/prometheus/client/exporter/TestHTTPServer.java @@ -10,50 +10,42 @@ import java.util.Scanner; import java.util.zip.GZIPInputStream; -import org.junit.After; +import io.prometheus.client.SampleNameFilter; import org.junit.Before; import org.junit.Test; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Java6Assertions.assertThat; public class TestHTTPServer { - HTTPServer s; + CollectorRegistry registry; @Before public void init() throws IOException { - CollectorRegistry registry = new CollectorRegistry(); + registry = new CollectorRegistry(); Gauge.build("a", "a help").register(registry); Gauge.build("b", "a help").register(registry); Gauge.build("c", "a help").register(registry); - s = new HTTPServer(new InetSocketAddress(0), registry); - } - - @After - public void cleanup() { - s.stop(); } - - String request(String context, String suffix) throws IOException { + String request(HTTPServer s, String context, String suffix) throws IOException { String url = "http://localhost:" + s.server.getAddress().getPort() + context + suffix; URLConnection connection = new URL(url).openConnection(); connection.setDoOutput(true); connection.connect(); - Scanner s = new Scanner(connection.getInputStream(), "UTF-8").useDelimiter("\\A"); - return s.hasNext() ? s.next() : ""; + Scanner scanner = new Scanner(connection.getInputStream(), "UTF-8").useDelimiter("\\A"); + return scanner.hasNext() ? scanner.next() : ""; } - String request(String suffix) throws IOException { - return request("/metrics", suffix); + String request(HTTPServer s, String suffix) throws IOException { + return request(s, "/metrics", suffix); } - String requestWithCompression(String suffix) throws IOException { - return requestWithCompression("/metrics", suffix); + String requestWithCompression(HTTPServer s, String suffix) throws IOException { + return requestWithCompression(s, "/metrics", suffix); } - String requestWithCompression(String context, String suffix) throws IOException { + String requestWithCompression(HTTPServer s, String context, String suffix) throws IOException { String url = "http://localhost:" + s.server.getAddress().getPort() + context + suffix; URLConnection connection = new URL(url).openConnection(); connection.setDoOutput(true); @@ -61,18 +53,18 @@ String requestWithCompression(String context, String suffix) throws IOException connection.setRequestProperty("Accept-Encoding", "gzip, deflate"); connection.connect(); GZIPInputStream gzs = new GZIPInputStream(connection.getInputStream()); - Scanner s = new Scanner(gzs).useDelimiter("\\A"); - return s.hasNext() ? s.next() : ""; + Scanner scanner = new Scanner(gzs).useDelimiter("\\A"); + return scanner.hasNext() ? scanner.next() : ""; } - String requestWithAccept(String accept) throws IOException { + String requestWithAccept(HTTPServer s, String accept) throws IOException { String url = "http://localhost:" + s.server.getAddress().getPort(); URLConnection connection = new URL(url).openConnection(); connection.setDoOutput(true); connection.setDoInput(true); connection.setRequestProperty("Accept", accept); - Scanner s = new Scanner(connection.getInputStream(), "UTF-8").useDelimiter("\\A"); - return s.hasNext() ? s.next() : ""; + Scanner scanner = new Scanner(connection.getInputStream(), "UTF-8").useDelimiter("\\A"); + return scanner.hasNext() ? scanner.next() : ""; } @Test(expected = IllegalArgumentException.class) @@ -84,67 +76,130 @@ public void testRefuseUsingUnbound() throws IOException { @Test public void testSimpleRequest() throws IOException { - String response = request(""); - assertThat(response).contains("a 0.0"); - assertThat(response).contains("b 0.0"); - assertThat(response).contains("c 0.0"); + HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + try { + String response = request(s, ""); + assertThat(response).contains("a 0.0"); + assertThat(response).contains("b 0.0"); + assertThat(response).contains("c 0.0"); + } finally { + s.close(); + } } @Test public void testBadParams() throws IOException { - String response = request("?x"); - assertThat(response).contains("a 0.0"); - assertThat(response).contains("b 0.0"); - assertThat(response).contains("c 0.0"); + HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + try { + String response = request(s, "?x"); + assertThat(response).contains("a 0.0"); + assertThat(response).contains("b 0.0"); + assertThat(response).contains("c 0.0"); + } finally { + s.close(); + } } @Test public void testSingleName() throws IOException { - String response = request("?name[]=a"); - assertThat(response).contains("a 0.0"); - assertThat(response).doesNotContain("b 0.0"); - assertThat(response).doesNotContain("c 0.0"); + HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + try { + String response = request(s, "?name[]=a"); + assertThat(response).contains("a 0.0"); + assertThat(response).doesNotContain("b 0.0"); + assertThat(response).doesNotContain("c 0.0"); + } finally { + s.close(); + } } @Test public void testMultiName() throws IOException { - String response = request("?name[]=a&name[]=b"); - assertThat(response).contains("a 0.0"); - assertThat(response).contains("b 0.0"); - assertThat(response).doesNotContain("c 0.0"); + HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + try { + String response = request(s, "?name[]=a&name[]=b"); + assertThat(response).contains("a 0.0"); + assertThat(response).contains("b 0.0"); + assertThat(response).doesNotContain("c 0.0"); + } finally { + s.close(); + } + } + + @Test + public void testSampleNameFilter() throws IOException { + HTTPServer s = new HTTPServer.Builder() + .withRegistry(registry) + .withSampleNameFilter(new SampleNameFilter.Builder() + .nameMustNotStartWith("a") + .build()) + .build(); + try { + String response = request(s, "?name[]=a&name[]=b"); + assertThat(response).doesNotContain("a 0.0"); + assertThat(response).contains("b 0.0"); + assertThat(response).doesNotContain("c 0.0"); + } finally { + s.close(); + } } @Test public void testDecoding() throws IOException { - String response = request("?n%61me[]=%61"); - assertThat(response).contains("a 0.0"); - assertThat(response).doesNotContain("b 0.0"); - assertThat(response).doesNotContain("c 0.0"); + HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + try { + String response = request(s, "?n%61me[]=%61"); + assertThat(response).contains("a 0.0"); + assertThat(response).doesNotContain("b 0.0"); + assertThat(response).doesNotContain("c 0.0"); + } finally { + s.close(); + } } @Test public void testGzipCompression() throws IOException { - String response = requestWithCompression(""); - assertThat(response).contains("a 0.0"); - assertThat(response).contains("b 0.0"); - assertThat(response).contains("c 0.0"); + HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + try { + String response = requestWithCompression(s, ""); + assertThat(response).contains("a 0.0"); + assertThat(response).contains("b 0.0"); + assertThat(response).contains("c 0.0"); + } finally { + s.close(); + } } @Test public void testOpenMetrics() throws IOException { - String response = requestWithAccept("application/openmetrics-text; version=0.0.1,text/plain;version=0.0.4;q=0.5,*/*;q=0.1"); - assertThat(response).contains("# EOF"); + HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + try { + String response = requestWithAccept(s, "application/openmetrics-text; version=0.0.1,text/plain;version=0.0.4;q=0.5,*/*;q=0.1"); + assertThat(response).contains("# EOF"); + } finally { + s.close(); + } } @Test public void testHealth() throws IOException { - String response = request("/-/healthy", ""); - assertThat(response).contains("Exporter is Healthy"); + HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + try { + String response = request(s, "/-/healthy", ""); + assertThat(response).contains("Exporter is Healthy"); + } finally { + s.close(); + } } @Test public void testHealthGzipCompression() throws IOException { - String response = requestWithCompression("/-/healthy", ""); - assertThat(response).contains("Exporter is Healthy"); + HTTPServer s = new HTTPServer(new InetSocketAddress(0), registry); + try { + String response = requestWithCompression(s, "/-/healthy", ""); + assertThat(response).contains("Exporter is Healthy"); + } finally { + s.close(); + } } } diff --git a/simpleclient_servlet/src/main/java/io/prometheus/client/Adapter.java b/simpleclient_servlet/src/main/java/io/prometheus/client/Adapter.java index 6c1f493a5..8433fd67d 100644 --- a/simpleclient_servlet/src/main/java/io/prometheus/client/Adapter.java +++ b/simpleclient_servlet/src/main/java/io/prometheus/client/Adapter.java @@ -3,8 +3,10 @@ import io.prometheus.client.servlet.common.adapter.FilterConfigAdapter; import io.prometheus.client.servlet.common.adapter.HttpServletRequestAdapter; import io.prometheus.client.servlet.common.adapter.HttpServletResponseAdapter; +import io.prometheus.client.servlet.common.adapter.ServletConfigAdapter; import javax.servlet.FilterConfig; +import javax.servlet.ServletConfig; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @@ -89,6 +91,20 @@ public String getInitParameter(String name) { } } + private static class ServletConfigAdapterImpl implements ServletConfigAdapter { + + final ServletConfig delegate; + + private ServletConfigAdapterImpl(ServletConfig delegate) { + this.delegate = delegate; + } + + @Override + public String getInitParameter(String name) { + return delegate.getInitParameter(name); + } + } + public static HttpServletRequestAdapter wrap(HttpServletRequest req) { return new HttpServletRequestAdapterImpl(req); } @@ -100,4 +116,8 @@ public static HttpServletResponseAdapter wrap(HttpServletResponse resp) { public static FilterConfigAdapter wrap(FilterConfig filterConfig) { return new FilterConfigAdapterImpl(filterConfig); } + + public static ServletConfigAdapter wrap(ServletConfig servletConfig) { + return new ServletConfigAdapterImpl(servletConfig); + } } \ No newline at end of file diff --git a/simpleclient_servlet/src/main/java/io/prometheus/client/exporter/MetricsServlet.java b/simpleclient_servlet/src/main/java/io/prometheus/client/exporter/MetricsServlet.java index 77560335b..76a6d5cee 100644 --- a/simpleclient_servlet/src/main/java/io/prometheus/client/exporter/MetricsServlet.java +++ b/simpleclient_servlet/src/main/java/io/prometheus/client/exporter/MetricsServlet.java @@ -1,8 +1,13 @@ package io.prometheus.client.exporter; +import io.prometheus.client.Adapter; import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.Predicate; import io.prometheus.client.servlet.common.exporter.Exporter; +import io.prometheus.client.servlet.common.exporter.ServletConfigurationException; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -18,11 +23,28 @@ public class MetricsServlet extends HttpServlet { private final Exporter exporter; public MetricsServlet() { - exporter = new Exporter(); + this(CollectorRegistry.defaultRegistry, null); + } + + public MetricsServlet(Predicate sampleNameFilter) { + this(CollectorRegistry.defaultRegistry, sampleNameFilter); } public MetricsServlet(CollectorRegistry registry) { - exporter = new Exporter(registry); + this(registry, null); + } + + public MetricsServlet(CollectorRegistry registry, Predicate sampleNameFilter) { + exporter = new Exporter(registry, sampleNameFilter); + } + + @Override + public void init(ServletConfig servletConfig) throws ServletException { + try { + exporter.init(Adapter.wrap(servletConfig)); + } catch (ServletConfigurationException e) { + throw new ServletException(e); + } } @Override diff --git a/simpleclient_servlet_common/src/main/java/io/prometheus/client/servlet/common/adapter/ServletConfigAdapter.java b/simpleclient_servlet_common/src/main/java/io/prometheus/client/servlet/common/adapter/ServletConfigAdapter.java new file mode 100644 index 000000000..f63d60a5c --- /dev/null +++ b/simpleclient_servlet_common/src/main/java/io/prometheus/client/servlet/common/adapter/ServletConfigAdapter.java @@ -0,0 +1,5 @@ +package io.prometheus.client.servlet.common.adapter; + +public interface ServletConfigAdapter { + String getInitParameter(String name); +} diff --git a/simpleclient_servlet_common/src/main/java/io/prometheus/client/servlet/common/exporter/Exporter.java b/simpleclient_servlet_common/src/main/java/io/prometheus/client/servlet/common/exporter/Exporter.java index 4797710b0..8a76dd218 100644 --- a/simpleclient_servlet_common/src/main/java/io/prometheus/client/servlet/common/exporter/Exporter.java +++ b/simpleclient_servlet_common/src/main/java/io/prometheus/client/servlet/common/exporter/Exporter.java @@ -1,9 +1,12 @@ package io.prometheus.client.servlet.common.exporter; import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.SampleNameFilter; +import io.prometheus.client.Predicate; import io.prometheus.client.servlet.common.adapter.HttpServletRequestAdapter; import io.prometheus.client.servlet.common.adapter.HttpServletResponseAdapter; import io.prometheus.client.exporter.common.TextFormat; +import io.prometheus.client.servlet.common.adapter.ServletConfigAdapter; import java.io.BufferedWriter; import java.io.IOException; @@ -11,29 +14,53 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; /** * The MetricsServlet class exists to provide a simple way of exposing the metrics values. - * */ public class Exporter { - private CollectorRegistry registry; + public static final String NAME_MUST_BE_EQUAL_TO = "name-must-be-equal-to"; + public static final String NAME_MUST_NOT_BE_EQUAL_TO = "name-must-not-be-equal-to"; + public static final String NAME_MUST_START_WITH = "name-must-start-with"; + public static final String NAME_MUST_NOT_START_WITH = "name-must-not-start-with"; - /** - * Construct a MetricsServlet for the default registry. - */ - public Exporter() { - this(CollectorRegistry.defaultRegistry); - } + private CollectorRegistry registry; + private Predicate sampleNameFilter; /** * Construct a MetricsServlet for the given registry. * @param registry collector registry + * @param sampleNameFilter programmatically set a {@link SampleNameFilter}. + * If there are any filter options configured in {@code ServletConfig}, they will be merged + * so that samples need to pass both filters to be exported. + * sampleNameFilter may be {@code null} indicating that nothing should be filtered. */ - public Exporter(CollectorRegistry registry) { + public Exporter(CollectorRegistry registry, Predicate sampleNameFilter) { this.registry = registry; + this.sampleNameFilter = sampleNameFilter; + } + + public void init(ServletConfigAdapter servletConfig) throws ServletConfigurationException { + List allowedNames = SampleNameFilter.stringToList(servletConfig.getInitParameter(NAME_MUST_BE_EQUAL_TO)); + List excludedNames = SampleNameFilter.stringToList(servletConfig.getInitParameter(NAME_MUST_NOT_BE_EQUAL_TO)); + List allowedPrefixes = SampleNameFilter.stringToList(servletConfig.getInitParameter(NAME_MUST_START_WITH)); + List excludedPrefixes = SampleNameFilter.stringToList(servletConfig.getInitParameter(NAME_MUST_NOT_START_WITH)); + if (!allowedPrefixes.isEmpty() || !excludedPrefixes.isEmpty() || !allowedNames.isEmpty() || !excludedNames.isEmpty()) { + SampleNameFilter filter = new SampleNameFilter.Builder() + .nameMustBeEqualTo(allowedNames) + .nameMustNotBeEqualTo(excludedNames) + .nameMustStartWith(allowedPrefixes) + .nameMustNotStartWith(excludedPrefixes) + .build(); + if (this.sampleNameFilter != null) { + this.sampleNameFilter = filter.and(this.sampleNameFilter); + } else { + this.sampleNameFilter = filter; + } + } } public void doGet(final HttpServletRequestAdapter req, final HttpServletResponseAdapter resp) throws IOException { @@ -43,7 +70,12 @@ public void doGet(final HttpServletRequestAdapter req, final HttpServletResponse Writer writer = new BufferedWriter(resp.getWriter()); try { - TextFormat.writeFormat(contentType, writer, registry.filteredMetricFamilySamples(parse(req))); + Predicate filter = SampleNameFilter.restrictToNamesEqualTo(sampleNameFilter, parse(req)); + if (filter == null) { + TextFormat.writeFormat(contentType, writer, registry.metricFamilySamples()); + } else { + TextFormat.writeFormat(contentType, writer, registry.filteredMetricFamilySamples(filter)); + } writer.flush(); } finally { writer.close(); diff --git a/simpleclient_servlet_common/src/main/java/io/prometheus/client/servlet/common/exporter/ServletConfigurationException.java b/simpleclient_servlet_common/src/main/java/io/prometheus/client/servlet/common/exporter/ServletConfigurationException.java new file mode 100644 index 000000000..4d99cbb25 --- /dev/null +++ b/simpleclient_servlet_common/src/main/java/io/prometheus/client/servlet/common/exporter/ServletConfigurationException.java @@ -0,0 +1,4 @@ +package io.prometheus.client.servlet.common.exporter; + +public class ServletConfigurationException extends Exception { +} diff --git a/simpleclient_servlet_common/src/test/java/io/prometheus/client/servlet/common/exporter/ExporterTest.java b/simpleclient_servlet_common/src/test/java/io/prometheus/client/servlet/common/exporter/ExporterTest.java index ed10fb10f..3143a55d8 100644 --- a/simpleclient_servlet_common/src/test/java/io/prometheus/client/servlet/common/exporter/ExporterTest.java +++ b/simpleclient_servlet_common/src/test/java/io/prometheus/client/servlet/common/exporter/ExporterTest.java @@ -89,7 +89,7 @@ public void testWriterFiltersBasedOnParameter() throws IOException { StringWriter responseBody = new StringWriter(); HttpServletResponseAdapter resp = mockHttpServletResponse(new PrintWriter(responseBody)); - new Exporter(registry).doGet(req, resp); + new Exporter(registry, null).doGet(req, resp); assertThat(responseBody.toString()).contains("a 0.0"); assertThat(responseBody.toString()).contains("b 0.0"); @@ -110,7 +110,7 @@ public void close() { CollectorRegistry registry = new CollectorRegistry(); Gauge a = Gauge.build("a", "a help").register(registry); - new Exporter(registry).doGet(req, resp); + new Exporter(registry, null).doGet(req, resp); Assert.assertTrue(closed.get()); } @@ -133,7 +133,7 @@ public void close() { Gauge a = Gauge.build("a", "a help").register(registry); try { - new Exporter(registry).doGet(req, resp); + new Exporter(registry, null).doGet(req, resp); fail("Exception expected"); } catch (Exception e) { } @@ -150,7 +150,7 @@ public void testOpenMetricsNegotiated() throws IOException { StringWriter responseBody = new StringWriter(); HttpServletResponseAdapter resp = mockHttpServletResponse(new PrintWriter(responseBody)); - new Exporter(registry).doGet(req, resp); + new Exporter(registry, null).doGet(req, resp); assertThat(responseBody.toString()).contains("a 0.0"); assertThat(responseBody.toString()).contains("# EOF"); diff --git a/simpleclient_servlet_jakarta/src/main/java/io/prometheus/client/servlet/jakarta/Adapter.java b/simpleclient_servlet_jakarta/src/main/java/io/prometheus/client/servlet/jakarta/Adapter.java index 3321a1582..4a4b8bb01 100644 --- a/simpleclient_servlet_jakarta/src/main/java/io/prometheus/client/servlet/jakarta/Adapter.java +++ b/simpleclient_servlet_jakarta/src/main/java/io/prometheus/client/servlet/jakarta/Adapter.java @@ -4,7 +4,9 @@ import io.prometheus.client.servlet.common.adapter.HttpServletRequestAdapter; import io.prometheus.client.servlet.common.adapter.HttpServletResponseAdapter; +import io.prometheus.client.servlet.common.adapter.ServletConfigAdapter; import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletConfig; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; @@ -89,6 +91,20 @@ public String getInitParameter(String name) { } } + private static class ServletConfigAdapterImpl implements ServletConfigAdapter { + + final ServletConfig delegate; + + private ServletConfigAdapterImpl(ServletConfig delegate) { + this.delegate = delegate; + } + + @Override + public String getInitParameter(String name) { + return delegate.getInitParameter(name); + } + } + public static HttpServletRequestAdapter wrap(HttpServletRequest req) { return new HttpServletRequestAdapterImpl(req); } @@ -100,4 +116,8 @@ public static HttpServletResponseAdapter wrap(HttpServletResponse resp) { public static FilterConfigAdapter wrap(FilterConfig filterConfig) { return new FilterConfigAdapterImpl(filterConfig); } + + public static ServletConfigAdapter wrap(ServletConfig servletConfig) { + return new ServletConfigAdapterImpl(servletConfig); + } } \ No newline at end of file diff --git a/simpleclient_servlet_jakarta/src/main/java/io/prometheus/client/servlet/jakarta/exporter/MetricsServlet.java b/simpleclient_servlet_jakarta/src/main/java/io/prometheus/client/servlet/jakarta/exporter/MetricsServlet.java index 749d835f9..bf8646af7 100644 --- a/simpleclient_servlet_jakarta/src/main/java/io/prometheus/client/servlet/jakarta/exporter/MetricsServlet.java +++ b/simpleclient_servlet_jakarta/src/main/java/io/prometheus/client/servlet/jakarta/exporter/MetricsServlet.java @@ -1,7 +1,12 @@ package io.prometheus.client.servlet.jakarta.exporter; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.Predicate; import io.prometheus.client.servlet.common.exporter.Exporter; +import io.prometheus.client.servlet.common.exporter.ServletConfigurationException; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -14,7 +19,32 @@ */ public class MetricsServlet extends HttpServlet { - private final Exporter exporter = new Exporter(); + private final Exporter exporter; + + public MetricsServlet() { + this(CollectorRegistry.defaultRegistry, null); + } + + public MetricsServlet(Predicate sampleNameFilter) { + this(CollectorRegistry.defaultRegistry, sampleNameFilter); + } + + public MetricsServlet(CollectorRegistry registry) { + this(registry, null); + } + + public MetricsServlet(CollectorRegistry registry, Predicate sampleNameFilter) { + exporter = new Exporter(registry, sampleNameFilter); + } + + @Override + public void init(ServletConfig servletConfig) throws ServletException { + try { + exporter.init(wrap(servletConfig)); + } catch (ServletConfigurationException e) { + throw new ServletException(e); + } + } @Override protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {