Skip to content

Commit 440c3ca

Browse files
elieziobrian-brazil
authored andcommitted
added /prometheus as a Spring Boot Endpoint (#151)
1 parent 3c16c18 commit 440c3ca

File tree

7 files changed

+237
-1
lines changed

7 files changed

+237
-1
lines changed

simpleclient_spring_boot/pom.xml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@
3131
<name>Tokuhiro Matsuno</name>
3232
<email>[email protected]</email>
3333
</developer>
34+
<developer>
35+
<name>Marco Aust</name>
36+
<email>[email protected]</email>
37+
<organization>private</organization>
38+
<organizationUrl>https://github.com/maust</organizationUrl>
39+
</developer>
40+
<developer>
41+
<id>eliezio</id>
42+
<name>Eliezio Oliveira</name>
43+
<email>[email protected]</email>
44+
</developer>
3445
</developers>
3546

3647
<properties>
@@ -48,6 +59,11 @@
4859
<artifactId>simpleclient_common</artifactId>
4960
<version>0.0.18-SNAPSHOT</version>
5061
</dependency>
62+
<dependency>
63+
<groupId>org.springframework</groupId>
64+
<artifactId>spring-web</artifactId>
65+
<version>4.2.5.RELEASE</version>
66+
</dependency>
5167
<dependency>
5268
<groupId>org.springframework.boot</groupId>
5369
<artifactId>spring-boot-actuator</artifactId>
@@ -61,6 +77,12 @@
6177
<version>4.12</version>
6278
<scope>test</scope>
6379
</dependency>
80+
<dependency>
81+
<groupId>org.cthul</groupId>
82+
<artifactId>cthul-matchers</artifactId>
83+
<version>1.1.0</version>
84+
<scope>test</scope>
85+
</dependency>
6486
<dependency>
6587
<groupId>org.springframework.boot</groupId>
6688
<artifactId>spring-boot-starter-test</artifactId>
@@ -69,7 +91,7 @@
6991
</dependency>
7092
<dependency>
7193
<groupId>org.springframework.boot</groupId>
72-
<artifactId>spring-boot-starter-actuator</artifactId>
94+
<artifactId>spring-boot-starter-web</artifactId>
7395
<version>1.3.3.RELEASE</version>
7496
<scope>test</scope>
7597
</dependency>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.prometheus.client.spring.boot;
2+
3+
import org.springframework.context.annotation.Import;
4+
5+
import java.lang.annotation.Documented;
6+
import java.lang.annotation.ElementType;
7+
import java.lang.annotation.Retention;
8+
import java.lang.annotation.RetentionPolicy;
9+
import java.lang.annotation.Target;
10+
11+
/**
12+
* Enable an endpoint that exposes Prometheus metrics from its default collector.
13+
* <p>
14+
* Usage:
15+
* <br>Just add this annotation to the main class of your Spring Boot application, e.g.:
16+
* <pre><code>
17+
* {@literal @}SpringBootApplication
18+
* {@literal @}EnablePrometheusEndpoint
19+
* public class Application {
20+
*
21+
* public static void main(String[] args) {
22+
* SpringApplication.run(Application.class, args);
23+
* }
24+
* }
25+
* </code></pre>
26+
* <p>
27+
* Configuration:
28+
* <br>You can customize this endpoint at runtime using the following spring properties:
29+
* <ul>
30+
* <li>{@code endpoints.prometheus.id} (default: "prometheus")</li>
31+
* <li>{@code endpoints.prometheus.enabled} (default: {@code true})</li>
32+
* <li>{@code endpoints.prometheus.sensitive} (default: {@code true})</li>
33+
* </ul>
34+
*
35+
* @author Marco Aust
36+
* @author Eliezio Oliveira
37+
*/
38+
@Target(ElementType.TYPE)
39+
@Retention(RetentionPolicy.RUNTIME)
40+
@Documented
41+
@Import(PrometheusConfiguration.class)
42+
public @interface EnablePrometheusEndpoint {
43+
44+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.prometheus.client.spring.boot;
2+
3+
import io.prometheus.client.CollectorRegistry;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
7+
@Configuration
8+
class PrometheusConfiguration {
9+
10+
@Bean
11+
public PrometheusEndpoint prometheusEndpoint() {
12+
return new PrometheusEndpoint(CollectorRegistry.defaultRegistry);
13+
}
14+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.prometheus.client.spring.boot;
2+
3+
import io.prometheus.client.CollectorRegistry;
4+
import io.prometheus.client.exporter.common.TextFormat;
5+
import org.springframework.boot.actuate.endpoint.AbstractEndpoint;
6+
import org.springframework.boot.context.properties.ConfigurationProperties;
7+
import org.springframework.http.ResponseEntity;
8+
9+
import java.io.IOException;
10+
import java.io.StringWriter;
11+
import java.io.Writer;
12+
13+
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
14+
15+
@ConfigurationProperties("endpoints.prometheus")
16+
class PrometheusEndpoint extends AbstractEndpoint<ResponseEntity<String>> {
17+
18+
private final CollectorRegistry collectorRegistry;
19+
20+
PrometheusEndpoint(CollectorRegistry collectorRegistry) {
21+
super("prometheus");
22+
this.collectorRegistry = collectorRegistry;
23+
}
24+
25+
@Override
26+
public ResponseEntity<String> invoke() {
27+
try {
28+
Writer writer = new StringWriter();
29+
TextFormat.write004(writer, collectorRegistry.metricFamilySamples());
30+
return ResponseEntity.ok()
31+
.header(CONTENT_TYPE, TextFormat.CONTENT_TYPE_004)
32+
.body(writer.toString());
33+
} catch (IOException e) {
34+
// This actually never happens since StringWriter::write() doesn't throw any IOException
35+
throw new RuntimeException("Writing metrics failed", e);
36+
}
37+
}
38+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.prometheus.client.matchers;
2+
3+
import org.hamcrest.Description;
4+
import org.hamcrest.Matcher;
5+
import org.hamcrest.core.IsCollectionContaining;
6+
7+
/**
8+
* @author <a href="http://stackoverflow.com/users/4483548/bretc">BretC</a>
9+
*
10+
* @see <a href="http://stackoverflow.com/a/29610402/346545">this StackOverflow answer</a>
11+
*
12+
* Licensed under Creative Commons BY-SA 3.0
13+
*/
14+
public final class CustomMatchers {
15+
16+
private CustomMatchers() {
17+
}
18+
19+
public static <T> Matcher<Iterable<? super T>> exactlyNItems(final int n, final Matcher<? super T> elementMatcher) {
20+
return new IsCollectionContaining<T>(elementMatcher) {
21+
@Override
22+
protected boolean matchesSafely(Iterable<? super T> collection, Description mismatchDescription) {
23+
int count = 0;
24+
boolean isPastFirst = false;
25+
26+
for (Object item : collection) {
27+
28+
if (elementMatcher.matches(item)) {
29+
count++;
30+
}
31+
if (isPastFirst) {
32+
mismatchDescription.appendText(", ");
33+
}
34+
elementMatcher.describeMismatch(item, mismatchDescription);
35+
isPastFirst = true;
36+
}
37+
38+
if (count != n) {
39+
mismatchDescription.appendText(". Expected exactly " + n + " but got " + count);
40+
}
41+
return count == n;
42+
}
43+
};
44+
}
45+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.prometheus.client.spring.boot;
2+
3+
import org.springframework.boot.autoconfigure.SpringBootApplication;
4+
5+
/**
6+
* Dummy class to satisfy Spring Boot Test requirement of a class annotated either with {code @SpringBootApplication} or
7+
* {code @SpringBootConfiguration}.
8+
*/
9+
@SpringBootApplication
10+
class DummyBootApplication {
11+
12+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package io.prometheus.client.spring.boot;
2+
3+
import io.prometheus.client.Counter;
4+
import io.prometheus.client.matchers.CustomMatchers;
5+
import org.junit.Test;
6+
import org.junit.runner.RunWith;
7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.boot.test.SpringApplicationConfiguration;
9+
import org.springframework.boot.test.TestRestTemplate;
10+
import org.springframework.boot.test.WebIntegrationTest;
11+
import org.springframework.http.HttpStatus;
12+
import org.springframework.http.MediaType;
13+
import org.springframework.http.ResponseEntity;
14+
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
15+
import org.springframework.web.client.RestTemplate;
16+
17+
import java.util.Arrays;
18+
import java.util.List;
19+
20+
import static org.cthul.matchers.CthulMatchers.matchesPattern;
21+
import static org.hamcrest.MatcherAssert.assertThat;
22+
import static org.junit.Assert.assertEquals;
23+
import static org.junit.Assert.assertTrue;
24+
25+
@RunWith(SpringJUnit4ClassRunner.class)
26+
@SpringApplicationConfiguration(DummyBootApplication.class)
27+
@WebIntegrationTest(randomPort = true)
28+
@EnablePrometheusEndpoint
29+
public class PrometheusEndpointTest {
30+
31+
@Value("${local.server.port}")
32+
int localServerPort;
33+
34+
RestTemplate template = new TestRestTemplate();
35+
36+
@Test
37+
public void testMetricsExportedThroughPrometheusEndpoint() {
38+
// given:
39+
final Counter promCounter = Counter.build()
40+
.name("foo_bar")
41+
.help("a simple prometheus counter")
42+
.labelNames("label1", "label2")
43+
.register();
44+
45+
// when:
46+
promCounter.labels("val1", "val2").inc(3);
47+
ResponseEntity<String> metricsResponse = template.getForEntity(getBaseUrl() + "/prometheus", String.class);
48+
49+
// then:
50+
assertEquals(HttpStatus.OK, metricsResponse.getStatusCode());
51+
assertTrue(MediaType.TEXT_PLAIN.isCompatibleWith(metricsResponse.getHeaders().getContentType()));
52+
53+
List<String> responseLines = Arrays.asList(metricsResponse.getBody().split("\n"));
54+
assertThat(responseLines, CustomMatchers.<String>exactlyNItems(1,
55+
matchesPattern("foo_bar\\{label1=\"val1\",label2=\"val2\",?\\} 3.0")));
56+
}
57+
58+
private String getBaseUrl() {
59+
return "http://localhost:" + localServerPort;
60+
}
61+
}

0 commit comments

Comments
 (0)