From 60aaeb9abc96dcc9590a2ddf6c6a6501d0b84e74 Mon Sep 17 00:00:00 2001 From: Eliezio Oliveira Date: Thu, 13 Oct 2016 00:15:56 -0300 Subject: [PATCH] added /prometheus as a Spring Boot Endpoint --- simpleclient_spring_boot/pom.xml | 24 +++++++- .../spring/boot/EnablePrometheusEndpoint.java | 44 +++++++++++++ .../spring/boot/PrometheusConfiguration.java | 14 +++++ .../spring/boot/PrometheusEndpoint.java | 38 ++++++++++++ .../client/matchers/CustomMatchers.java | 45 ++++++++++++++ .../spring/boot/DummyBootApplication.java | 12 ++++ .../spring/boot/PrometheusEndpointTest.java | 61 +++++++++++++++++++ 7 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/EnablePrometheusEndpoint.java create mode 100644 simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/PrometheusConfiguration.java create mode 100644 simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/PrometheusEndpoint.java create mode 100644 simpleclient_spring_boot/src/test/java/io/prometheus/client/matchers/CustomMatchers.java create mode 100644 simpleclient_spring_boot/src/test/java/io/prometheus/client/spring/boot/DummyBootApplication.java create mode 100644 simpleclient_spring_boot/src/test/java/io/prometheus/client/spring/boot/PrometheusEndpointTest.java diff --git a/simpleclient_spring_boot/pom.xml b/simpleclient_spring_boot/pom.xml index 8917280fd..8e425ed5b 100644 --- a/simpleclient_spring_boot/pom.xml +++ b/simpleclient_spring_boot/pom.xml @@ -31,6 +31,17 @@ Tokuhiro Matsuno tokuhirom@gmail.com + + Marco Aust + github@marcoaust.de + private + https://github.com/maust + + + eliezio + Eliezio Oliveira + eliezio.oliveira@gmail.com + @@ -48,6 +59,11 @@ simpleclient_common 0.0.18-SNAPSHOT + + org.springframework + spring-web + 4.2.5.RELEASE + org.springframework.boot spring-boot-actuator @@ -61,6 +77,12 @@ 4.12 test + + org.cthul + cthul-matchers + 1.1.0 + test + org.springframework.boot spring-boot-starter-test @@ -69,7 +91,7 @@ org.springframework.boot - spring-boot-starter-actuator + spring-boot-starter-web 1.3.3.RELEASE test diff --git a/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/EnablePrometheusEndpoint.java b/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/EnablePrometheusEndpoint.java new file mode 100644 index 000000000..3b2e893f7 --- /dev/null +++ b/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/EnablePrometheusEndpoint.java @@ -0,0 +1,44 @@ +package io.prometheus.client.spring.boot; + +import org.springframework.context.annotation.Import; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Enable an endpoint that exposes Prometheus metrics from its default collector. + *

+ * Usage: + *
Just add this annotation to the main class of your Spring Boot application, e.g.: + *


+ * {@literal @}SpringBootApplication
+ * {@literal @}EnablePrometheusEndpoint
+ *  public class Application {
+ *
+ *    public static void main(String[] args) {
+ *      SpringApplication.run(Application.class, args);
+ *    }
+ *  }
+ * 
+ *

+ * Configuration: + *
You can customize this endpoint at runtime using the following spring properties: + *

+ * + * @author Marco Aust + * @author Eliezio Oliveira + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(PrometheusConfiguration.class) +public @interface EnablePrometheusEndpoint { + +} diff --git a/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/PrometheusConfiguration.java b/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/PrometheusConfiguration.java new file mode 100644 index 000000000..f6666f984 --- /dev/null +++ b/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/PrometheusConfiguration.java @@ -0,0 +1,14 @@ +package io.prometheus.client.spring.boot; + +import io.prometheus.client.CollectorRegistry; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +class PrometheusConfiguration { + + @Bean + public PrometheusEndpoint prometheusEndpoint() { + return new PrometheusEndpoint(CollectorRegistry.defaultRegistry); + } +} diff --git a/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/PrometheusEndpoint.java b/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/PrometheusEndpoint.java new file mode 100644 index 000000000..5ad70f9bc --- /dev/null +++ b/simpleclient_spring_boot/src/main/java/io/prometheus/client/spring/boot/PrometheusEndpoint.java @@ -0,0 +1,38 @@ +package io.prometheus.client.spring.boot; + +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.exporter.common.TextFormat; +import org.springframework.boot.actuate.endpoint.AbstractEndpoint; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.ResponseEntity; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; + +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; + +@ConfigurationProperties("endpoints.prometheus") +class PrometheusEndpoint extends AbstractEndpoint> { + + private final CollectorRegistry collectorRegistry; + + PrometheusEndpoint(CollectorRegistry collectorRegistry) { + super("prometheus"); + this.collectorRegistry = collectorRegistry; + } + + @Override + public ResponseEntity invoke() { + try { + Writer writer = new StringWriter(); + TextFormat.write004(writer, collectorRegistry.metricFamilySamples()); + return ResponseEntity.ok() + .header(CONTENT_TYPE, TextFormat.CONTENT_TYPE_004) + .body(writer.toString()); + } catch (IOException e) { + // This actually never happens since StringWriter::write() doesn't throw any IOException + throw new RuntimeException("Writing metrics failed", e); + } + } +} diff --git a/simpleclient_spring_boot/src/test/java/io/prometheus/client/matchers/CustomMatchers.java b/simpleclient_spring_boot/src/test/java/io/prometheus/client/matchers/CustomMatchers.java new file mode 100644 index 000000000..dddec7b61 --- /dev/null +++ b/simpleclient_spring_boot/src/test/java/io/prometheus/client/matchers/CustomMatchers.java @@ -0,0 +1,45 @@ +package io.prometheus.client.matchers; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.core.IsCollectionContaining; + +/** + * @author BretC + * + * @see this StackOverflow answer + * + * Licensed under Creative Commons BY-SA 3.0 + */ +public final class CustomMatchers { + + private CustomMatchers() { + } + + public static Matcher> exactlyNItems(final int n, final Matcher elementMatcher) { + return new IsCollectionContaining(elementMatcher) { + @Override + protected boolean matchesSafely(Iterable collection, Description mismatchDescription) { + int count = 0; + boolean isPastFirst = false; + + for (Object item : collection) { + + if (elementMatcher.matches(item)) { + count++; + } + if (isPastFirst) { + mismatchDescription.appendText(", "); + } + elementMatcher.describeMismatch(item, mismatchDescription); + isPastFirst = true; + } + + if (count != n) { + mismatchDescription.appendText(". Expected exactly " + n + " but got " + count); + } + return count == n; + } + }; + } +} diff --git a/simpleclient_spring_boot/src/test/java/io/prometheus/client/spring/boot/DummyBootApplication.java b/simpleclient_spring_boot/src/test/java/io/prometheus/client/spring/boot/DummyBootApplication.java new file mode 100644 index 000000000..a011d137d --- /dev/null +++ b/simpleclient_spring_boot/src/test/java/io/prometheus/client/spring/boot/DummyBootApplication.java @@ -0,0 +1,12 @@ +package io.prometheus.client.spring.boot; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Dummy class to satisfy Spring Boot Test requirement of a class annotated either with {code @SpringBootApplication} or + * {code @SpringBootConfiguration}. + */ +@SpringBootApplication +class DummyBootApplication { + +} diff --git a/simpleclient_spring_boot/src/test/java/io/prometheus/client/spring/boot/PrometheusEndpointTest.java b/simpleclient_spring_boot/src/test/java/io/prometheus/client/spring/boot/PrometheusEndpointTest.java new file mode 100644 index 000000000..682eef40f --- /dev/null +++ b/simpleclient_spring_boot/src/test/java/io/prometheus/client/spring/boot/PrometheusEndpointTest.java @@ -0,0 +1,61 @@ +package io.prometheus.client.spring.boot; + +import io.prometheus.client.Counter; +import io.prometheus.client.matchers.CustomMatchers; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.TestRestTemplate; +import org.springframework.boot.test.WebIntegrationTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.client.RestTemplate; + +import java.util.Arrays; +import java.util.List; + +import static org.cthul.matchers.CthulMatchers.matchesPattern; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(DummyBootApplication.class) +@WebIntegrationTest(randomPort = true) +@EnablePrometheusEndpoint +public class PrometheusEndpointTest { + + @Value("${local.server.port}") + int localServerPort; + + RestTemplate template = new TestRestTemplate(); + + @Test + public void testMetricsExportedThroughPrometheusEndpoint() { + // given: + final Counter promCounter = Counter.build() + .name("foo_bar") + .help("a simple prometheus counter") + .labelNames("label1", "label2") + .register(); + + // when: + promCounter.labels("val1", "val2").inc(3); + ResponseEntity metricsResponse = template.getForEntity(getBaseUrl() + "/prometheus", String.class); + + // then: + assertEquals(HttpStatus.OK, metricsResponse.getStatusCode()); + assertTrue(MediaType.TEXT_PLAIN.isCompatibleWith(metricsResponse.getHeaders().getContentType())); + + List responseLines = Arrays.asList(metricsResponse.getBody().split("\n")); + assertThat(responseLines, CustomMatchers.exactlyNItems(1, + matchesPattern("foo_bar\\{label1=\"val1\",label2=\"val2\",?\\} 3.0"))); + } + + private String getBaseUrl() { + return "http://localhost:" + localServerPort; + } +}