Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion simpleclient_spring_boot/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@
<name>Tokuhiro Matsuno</name>
<email>tokuhirom@gmail.com</email>
</developer>
<developer>
<name>Marco Aust</name>
<email>github@marcoaust.de</email>
<organization>private</organization>
<organizationUrl>https://github.com/maust</organizationUrl>
</developer>
<developer>
<id>eliezio</id>
<name>Eliezio Oliveira</name>
<email>eliezio.oliveira@gmail.com</email>
</developer>
</developers>

<properties>
Expand All @@ -48,6 +59,11 @@
<artifactId>simpleclient_common</artifactId>
<version>0.0.18-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
Expand All @@ -61,6 +77,12 @@
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.cthul</groupId>
<artifactId>cthul-matchers</artifactId>
<version>1.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand All @@ -69,7 +91,7 @@
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<artifactId>spring-boot-starter-web</artifactId>
<version>1.3.3.RELEASE</version>
<scope>test</scope>
</dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* Usage:
* <br>Just add this annotation to the main class of your Spring Boot application, e.g.:
* <pre><code>
* {@literal @}SpringBootApplication
* {@literal @}EnablePrometheusEndpoint
* public class Application {
*
* public static void main(String[] args) {
* SpringApplication.run(Application.class, args);
* }
* }
* </code></pre>
* <p>
* Configuration:
* <br>You can customize this endpoint at runtime using the following spring properties:
* <ul>
* <li>{@code endpoints.prometheus.id} (default: "prometheus")</li>
* <li>{@code endpoints.prometheus.enabled} (default: {@code true})</li>
* <li>{@code endpoints.prometheus.sensitive} (default: {@code true})</li>
* </ul>
*
* @author Marco Aust
* @author Eliezio Oliveira
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(PrometheusConfiguration.class)
public @interface EnablePrometheusEndpoint {

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<ResponseEntity<String>> {

private final CollectorRegistry collectorRegistry;

PrometheusEndpoint(CollectorRegistry collectorRegistry) {
super("prometheus");
this.collectorRegistry = collectorRegistry;
}

@Override
public ResponseEntity<String> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.prometheus.client.matchers;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.core.IsCollectionContaining;

/**
* @author <a href="http://stackoverflow.com/users/4483548/bretc">BretC</a>
*
* @see <a href="http://stackoverflow.com/a/29610402/346545">this StackOverflow answer</a>
*
* Licensed under Creative Commons BY-SA 3.0
*/
public final class CustomMatchers {

private CustomMatchers() {
}

public static <T> Matcher<Iterable<? super T>> exactlyNItems(final int n, final Matcher<? super T> elementMatcher) {
return new IsCollectionContaining<T>(elementMatcher) {
@Override
protected boolean matchesSafely(Iterable<? super T> 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;
}
};
}
}
Original file line number Diff line number Diff line change
@@ -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 {

}
Original file line number Diff line number Diff line change
@@ -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<String> metricsResponse = template.getForEntity(getBaseUrl() + "/prometheus", String.class);

// then:
assertEquals(HttpStatus.OK, metricsResponse.getStatusCode());
assertTrue(MediaType.TEXT_PLAIN.isCompatibleWith(metricsResponse.getHeaders().getContentType()));

List<String> responseLines = Arrays.asList(metricsResponse.getBody().split("\n"));
assertThat(responseLines, CustomMatchers.<String>exactlyNItems(1,
matchesPattern("foo_bar\\{label1=\"val1\",label2=\"val2\",?\\} 3.0")));
}

private String getBaseUrl() {
return "http://localhost:" + localServerPort;
}
}