diff --git a/pom.xml b/pom.xml
index 614687e..368b8d8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,6 +36,7 @@
8
8
UTF-8
+ 5.9.0
@@ -199,18 +200,30 @@
io.micrometer
micrometer-registry-prometheus
- 1.9.3
+ 1.9.5
com.github.tomakehurst
wiremock-jre8
- 2.33.2
+ 2.34.0
provided
org.junit.jupiter
junit-jupiter-engine
- 5.9.0
+ ${junit.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ ${junit.version}
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.23.1
test
diff --git a/src/main/java/com/rasklaad/wiremock/metrics/MetricsConfiguration.java b/src/main/java/com/rasklaad/wiremock/metrics/MetricsConfiguration.java
new file mode 100644
index 0000000..64af1bc
--- /dev/null
+++ b/src/main/java/com/rasklaad/wiremock/metrics/MetricsConfiguration.java
@@ -0,0 +1,72 @@
+package com.rasklaad.wiremock.metrics;
+
+public class MetricsConfiguration {
+
+ private boolean useMappingUrlPattern;
+ private boolean useRequestUrl;
+ private boolean registerNotMatchedRequests;
+
+ private boolean ignoreQueryParams;
+
+ private boolean registerAnyUrlMappingAsRequestUrl;
+
+ MetricsConfiguration() {
+
+ }
+ public MetricsConfiguration useRequestUrl() {
+ useRequestUrl = true;
+ return this;
+ }
+
+ public MetricsConfiguration useMappingUrlPattern() {
+ useMappingUrlPattern = true;
+ return this;
+ }
+
+ public MetricsConfiguration registerNotMatchedRequests() {
+ registerNotMatchedRequests = true;
+ return this;
+ }
+
+ public MetricsConfiguration registerAnyUrlMappingAsRequestUrl() {
+ registerAnyUrlMappingAsRequestUrl = true;
+ return this;
+ }
+
+ public MetricsConfiguration ignoreQueryParams() {
+ ignoreQueryParams = true;
+ return this;
+ }
+
+ static MetricsConfiguration defaultConfiguration() {
+ return new MetricsConfiguration().useRequestUrl();
+ }
+
+ void validate() {
+ if (useMappingUrlPattern && useRequestUrl) {
+ throw new IllegalStateException("You can't use both url path and url pattern");
+ }
+ if (!useMappingUrlPattern && !useRequestUrl) {
+ throw new IllegalStateException("You must use either url path or url pattern");
+ }
+ }
+ boolean shouldUseMappingUrlPattern() {
+ return useMappingUrlPattern;
+ }
+
+ boolean shouldUseRequestUrl() {
+ return useRequestUrl;
+ }
+
+ boolean shouldRegisterNotMatchedRequests() {
+ return registerNotMatchedRequests;
+ }
+
+ boolean shouldRegisterAnyUrlMappingAsRequestUrl() {
+ return registerAnyUrlMappingAsRequestUrl;
+ }
+
+ boolean shouldIgnoreQueryParams() {
+ return ignoreQueryParams;
+ }
+}
diff --git a/src/main/java/com/rasklaad/wiremock/metrics/MetricsEndpointExtension.java b/src/main/java/com/rasklaad/wiremock/metrics/MetricsEndpointExtension.java
index b734845..d67365c 100644
--- a/src/main/java/com/rasklaad/wiremock/metrics/MetricsEndpointExtension.java
+++ b/src/main/java/com/rasklaad/wiremock/metrics/MetricsEndpointExtension.java
@@ -8,16 +8,25 @@
import com.github.tomakehurst.wiremock.http.Request;
import com.github.tomakehurst.wiremock.http.RequestMethod;
import com.github.tomakehurst.wiremock.http.ResponseDefinition;
+import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import java.net.HttpURLConnection;
+import java.util.List;
+import java.util.stream.Collectors;
public class MetricsEndpointExtension implements AdminApiExtension {
public static final String EXTENSION_NAME = "metrics-endpoint-extension";
+
+ private final AdminTask adminTask;
+
+ public MetricsEndpointExtension() {
+ adminTask = new PrometheusEndpointAdminTask();
+ }
@Override
public void contributeAdminApiRoutes(Router router) {
- router.add(RequestMethod.GET, "/prometheus-metrics", new PrometheusEndpointAdminTask());
+ router.add(RequestMethod.GET, "/prometheus-metrics", adminTask);
}
@Override
@@ -30,13 +39,14 @@ private final static class PrometheusEndpointAdminTask implements AdminTask {
private final PrometheusMeterRegistry prometheusMeterRegistry;
private PrometheusEndpointAdminTask() {
- prometheusMeterRegistry = (PrometheusMeterRegistry) Metrics.globalRegistry.getRegistries()
+ List registries = Metrics.globalRegistry.getRegistries()
.stream()
.filter(registry -> registry instanceof PrometheusMeterRegistry)
- .findFirst()
- .orElseThrow(() -> new IllegalStateException("PrometheusMeterRegistry not found," +
- " you should define com.rasklaad.wiremock.metrics.PrometheusMetricsExtension" +
- " in your wiremock extensions"));
+ .collect(Collectors.toList());
+ if (registries.size() != 1) {
+ throw new IllegalStateException("Expected exactly one PrometheusMeterRegistry, found " + registries.size());
+ }
+ prometheusMeterRegistry = (PrometheusMeterRegistry) registries.get(0);
}
@Override
diff --git a/src/main/java/com/rasklaad/wiremock/metrics/PrometheusMetricsExtension.java b/src/main/java/com/rasklaad/wiremock/metrics/PrometheusMetricsExtension.java
index c9db637..fa5a546 100644
--- a/src/main/java/com/rasklaad/wiremock/metrics/PrometheusMetricsExtension.java
+++ b/src/main/java/com/rasklaad/wiremock/metrics/PrometheusMetricsExtension.java
@@ -4,8 +4,8 @@
import com.github.tomakehurst.wiremock.core.Admin;
import com.github.tomakehurst.wiremock.extension.PostServeAction;
import com.github.tomakehurst.wiremock.http.LoggedResponse;
+import com.github.tomakehurst.wiremock.matching.UrlPattern;
import com.github.tomakehurst.wiremock.stubbing.ServeEvent;
-import com.github.tomakehurst.wiremock.stubbing.StubMapping;
import com.github.tomakehurst.wiremock.verification.LoggedRequest;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.Metrics;
@@ -16,9 +16,12 @@
import io.micrometer.prometheus.PrometheusConfig;
import io.micrometer.prometheus.PrometheusMeterRegistry;
+import java.net.URI;
+
public class PrometheusMetricsExtension extends PostServeAction {
public static final String EXTENSION_NAME = "prometheus-metrics-extension";
private final PrometheusMeterRegistry registry;
+ private MetricsConfiguration configuration;
public PrometheusMetricsExtension() {
registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
@@ -27,58 +30,108 @@ public PrometheusMetricsExtension() {
new ProcessorMetrics().bindTo(registry);
new JvmGcMetrics().bindTo(registry);
new JvmMemoryMetrics().bindTo(registry);
+ configuration = MetricsConfiguration.defaultConfiguration();
+ }
+
+ public PrometheusMetricsExtension(MetricsConfiguration configuration) {
+ this();
+ configuration.validate();
+ this.configuration = configuration;
}
@Override
public void doGlobalAction(ServeEvent serveEvent, Admin admin) {
super.doGlobalAction(serveEvent, admin);
- StubMapping mapping = serveEvent.getStubMapping();
- if (mapping != null) {
- Timing timing = serveEvent.getTiming();
- LoggedRequest request = serveEvent.getRequest();
- LoggedResponse response = serveEvent.getResponse();
- String urlPath = request.getUrl();
- String method = request.getMethod().getName();
- String status = String.valueOf(response.getStatus());
-
- DistributionSummary totalTimeSummary = DistributionSummary.builder("wiremock.request.totalTime")
- .baseUnit("ms")
- .publishPercentiles(0.5, 0.9, 0.95, 0.99)
- .description("Request time latency")
- .tags("path", urlPath,"method", method, "status", status)
- .register(Metrics.globalRegistry);
-
- DistributionSummary processingTimeSummary = DistributionSummary.builder("wiremock.request.processingTime")
- .baseUnit("ms")
- .publishPercentiles(0.5, 0.9, 0.95, 0.99)
- .description("Processing time latency")
- .tags("path", urlPath,"method", method, "status", status)
- .register(registry);
-
-
- DistributionSummary serveTimeSummary = DistributionSummary.builder("wiremock.request.serveTime")
- .baseUnit("ms")
- .publishPercentiles(0.5, 0.9, 0.95, 0.99)
- .description("Serve time latency")
- .tags("path", urlPath,"method", method, "status", status)
- .register(registry);
-
- DistributionSummary responseSendTime = DistributionSummary.builder("wiremock.request.responseSendTime")
- .baseUnit("ms")
- .publishPercentiles(0.5, 0.9, 0.95, 0.99)
- .description("Response send time latency")
- .tags("path", urlPath,"method", method, "status", status)
- .register(registry);
-
- totalTimeSummary.record(timing.getTotalTime());
- processingTimeSummary.record(timing.getProcessTime());
- serveTimeSummary.record(timing.getServeTime());
- responseSendTime.record(timing.getResponseSendTime());
+ Timing timing = serveEvent.getTiming();
+ LoggedRequest request = serveEvent.getRequest();
+ LoggedResponse response = serveEvent.getResponse();
+
+ String requestUrlPath = request.getUrl();
+ String method = request.getMethod().getName();
+ String status = String.valueOf(response.getStatus());
+
+ if (!serveEvent.getWasMatched()) {
+ if (configuration.shouldRegisterNotMatchedRequests()) {
+ registerByUrlPath(timing, requestUrlPath, method, status);
+ }
+ return;
+ }
+
+ if (configuration.shouldUseRequestUrl()) {
+ registerByUrlPath(timing, requestUrlPath, method, status);
+ return;
+ }
+
+ UrlPattern urlPattern = serveEvent.getStubMapping().getRequest().getUrlMatcher();
+ if (configuration.shouldUseMappingUrlPattern()) {
+ if (urlPattern == UrlPattern.ANY && configuration.shouldRegisterAnyUrlMappingAsRequestUrl()) {
+ registerByUrlPath(timing, requestUrlPath, method, status);
+ } else {
+ registerByUrlMapping(timing, urlPattern, method, status);
+ }
+ }
+
+ }
+
+ private void registerByUrlMapping(Timing timing, UrlPattern urlPattern, String method, String status) {
+ String mappingUrlPath = urlPattern.getExpected();
+ if (configuration.shouldIgnoreQueryParams()) {
+ mappingUrlPath = URI.create(mappingUrlPath).getPath();
}
+ register(timing, mappingUrlPath, method, status);
+
+ }
+
+ private void registerByUrlPath(Timing timing, String urlPath, String method, String statusCode) {
+ String path = urlPath;
+ if (configuration.shouldIgnoreQueryParams()) {
+ path = URI.create(urlPath).getPath();
+ }
+ register(timing, path, method, statusCode);
+ }
+
+ private void register(Timing timing, String path, String method, String statusCode) {
+ DistributionSummary totalTimeSummary = DistributionSummary.builder("wiremock.request.totalTime")
+ .baseUnit("ms")
+ .publishPercentiles(0.5, 0.9, 0.95, 0.99)
+ .description("Request time latency")
+ .tags("path", path, "method", method, "status", statusCode)
+ .register(Metrics.globalRegistry);
+
+ DistributionSummary processingTimeSummary = DistributionSummary.builder("wiremock.request.processingTime")
+ .baseUnit("ms")
+ .publishPercentiles(0.5, 0.9, 0.95, 0.99)
+ .description("Processing time latency")
+ .tags("path", path, "method", method, "status", statusCode)
+ .register(registry);
+
+
+ DistributionSummary serveTimeSummary = DistributionSummary.builder("wiremock.request.serveTime")
+ .baseUnit("ms")
+ .publishPercentiles(0.5, 0.9, 0.95, 0.99)
+ .description("Serve time latency")
+ .tags("path", path, "method", method, "status", statusCode)
+ .register(registry);
+
+ DistributionSummary responseSendTime = DistributionSummary.builder("wiremock.request.responseSendTime")
+ .baseUnit("ms")
+ .publishPercentiles(0.5, 0.9, 0.95, 0.99)
+ .description("Response send time latency")
+ .tags("path", path, "method", method, "status", statusCode)
+ .register(registry);
+
+ totalTimeSummary.record(timing.getTotalTime());
+ processingTimeSummary.record(timing.getProcessTime());
+ serveTimeSummary.record(timing.getServeTime());
+ responseSendTime.record(timing.getResponseSendTime());
}
@Override
public String getName() {
return EXTENSION_NAME;
}
+
+ public static MetricsConfiguration options() {
+ return new MetricsConfiguration();
+ }
}
diff --git a/src/test/java/com/rasklaad/wiremock/metrics/MetricsExtensionTest.java b/src/test/java/com/rasklaad/wiremock/metrics/MetricsExtensionTest.java
index 7cfa49f..2a92880 100644
--- a/src/test/java/com/rasklaad/wiremock/metrics/MetricsExtensionTest.java
+++ b/src/test/java/com/rasklaad/wiremock/metrics/MetricsExtensionTest.java
@@ -3,56 +3,236 @@
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import com.github.tomakehurst.wiremock.matching.UrlPattern;
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Metrics;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeAll;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import java.io.BufferedReader;
import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
public class MetricsExtensionTest {
private static final OkHttpClient client = new OkHttpClient.Builder().build();
- private static WireMockServer server;
- @BeforeAll
- static void createServer() {
- WireMockConfiguration config = new WireMockConfiguration();
- config.extensions(new PrometheusMetricsExtension(), new MetricsEndpointExtension());
- server = new WireMockServer(config);
+ private WireMockServer server;
+
+
+ private WireMockServer startServer(MetricsConfiguration configuration) {
+ WireMockConfiguration config = new WireMockConfiguration()
+ .dynamicPort()
+ .extensions(new PrometheusMetricsExtension(configuration), new MetricsEndpointExtension());
+ WireMockServer server = new WireMockServer(config);
server.start();
+ this.server = server;
+ // Metrics.add add values from previous tests
+ Metrics.globalRegistry.getRegistries().forEach(MeterRegistry::clear);
+
+ return server;
+ }
+
+ private WireMockServer startServer() {
+ WireMockConfiguration config = new WireMockConfiguration()
+ .dynamicPort()
+ .extensions(new PrometheusMetricsExtension(), new MetricsEndpointExtension());
+ WireMockServer server = new WireMockServer(config);
+ server.start();
+ this.server = server;
+ // Metrics.add add values from previous tests
+ Metrics.globalRegistry.getRegistries().forEach(MeterRegistry::clear);
+
+ return server;
}
- @AfterAll
- static void close() {
- server.stop();
+ @AfterEach
+ public void stopServers() {
+ if (server != null) {
+ server.stop();
+ }
+ // Metrics.add add values from previous tests
+ Metrics.globalRegistry.getRegistries().forEach(registry -> {
+ registry.clear();
+ registry.close();
+ Metrics.removeRegistry(registry);
+ });
}
+ private List scrape(WireMockServer server) throws IOException {
+ Response response = client.newCall(new Request.Builder()
+ .url(server.baseUrl() + "/__admin/prometheus-metrics")
+ .build()).execute();
+
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.body().byteStream()))) {
+ return reader.lines().collect(Collectors.toList());
+ }
+ }
+
+ private void httpCall(WireMockServer server, String endpoint) throws IOException {
+ Request request = new Request.Builder()
+ .url(server.baseUrl() + endpoint)
+ .build();
+ client.newCall(request).execute().close();
+ }
+
+ private StubMapping createDefaultMapping() {
+ return WireMock.get(WireMock.urlPathEqualTo("/test"))
+ .willReturn(WireMock.aResponse()
+ .withStatus(200)
+ .withBody("test")).build();
+ }
@Test
void wiremockShouldRegisterMetrics() throws IOException, InterruptedException {
- StubMapping mapping = WireMock.get(WireMock.urlPathEqualTo("/test"))
+ WireMockServer server = startServer();
+
+ server.addStubMapping(WireMock.get(WireMock.urlPathEqualTo("/simple-test"))
+ .willReturn(WireMock.aResponse()
+ .withStatus(200)
+ .withBody("test")).build());
+
+ httpCall(server, "/simple-test");
+ httpCall(server, "/simple-test");
+ Thread.sleep(1_000L);
+
+ List metrics = scrape(server);
+ Assertions.assertThat(metrics).contains("wiremock_request_totalTime_ms_count{method=\"GET\",path=\"/simple-test\",status=\"200\",} 2.0");
+ }
+
+ @Test
+ void shouldThrowExceptionWhenBothMappingAndUrlOptionsAreAbsent() {
+ Assertions.assertThatThrownBy(() -> startServer(new MetricsConfiguration()))
+ .isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ void shouldThrowExceptionWhenBothMappingAndUrlOptionsArePresent() {
+ Assertions.assertThatThrownBy(() ->
+ startServer(new MetricsConfiguration().useMappingUrlPattern().useRequestUrl())
+ ).isInstanceOf(IllegalStateException.class);
+ }
+
+
+ private static Stream urlPathProvider() {
+ return Stream.of(
+ Arguments.of(WireMock.urlPathEqualTo("/some-test"), "/some-test"),
+ Arguments.of(WireMock.urlPathMatching("/some-t.+"), "/some-t.+"),
+ Arguments.of(WireMock.urlEqualTo("/some-test?withQueryParam=true"), "/some-test?withQueryParam=true"),
+ Arguments.of(WireMock.urlMatching("/some-test.+"), "/some-test.+")
+ );
+ }
+ @ParameterizedTest
+ @MethodSource("urlPathProvider")
+ void shouldRegisterMetricsByMapping(UrlPattern pattern, String expectedMetricPath) throws IOException, InterruptedException {
+ WireMockServer server = startServer(
+ new MetricsConfiguration().useMappingUrlPattern()
+ );
+
+ StubMapping mapping = WireMock.get(pattern)
.willReturn(WireMock.aResponse()
.withStatus(200)
- .withFixedDelay(200)
.withBody("test")).build();
server.addStubMapping(mapping);
- Request request = new Request.Builder()
- .url(server.baseUrl() + "/test")
- .build();
+ httpCall(server, "/some-test?withQueryParam=true");
+ Thread.sleep(1000L);
- client.newCall(request).execute();
- client.newCall(request).execute();
- Thread.sleep(1_000L);
+ List metrics = scrape(server);
+ Assertions.assertThat(metrics).contains("wiremock_request_totalTime_ms_count{method=\"GET\",path=\"" + expectedMetricPath + "\",status=\"200\",} 1.0");
+ }
+
+ @Test
+ void shouldIgnoreQueryParamUsingMappingUrl() throws IOException, InterruptedException {
+ WireMockServer server = startServer(
+ new MetricsConfiguration()
+ .useMappingUrlPattern()
+ .ignoreQueryParams()
+ );
+ server.addStubMapping(WireMock.get(WireMock.urlPathEqualTo("/test-something-with-param"))
+ .willReturn(WireMock.aResponse()
+ .withStatus(200)
+ .withBody("test")).build());
+
+ httpCall(server, "/test-something-with-param?withQueryParam=true");
+ Thread.sleep(1000L);
- Response res = client.newCall(new Request.Builder()
- .url(server.baseUrl() + "/__admin/prometheus-metrics")
- .build()).execute();
+ List metrics = scrape(server);
+ Assertions.assertThat(metrics).contains("wiremock_request_totalTime_ms_count{method=\"GET\",path=\"/test-something-with-param\",status=\"200\",} 1.0");
+ }
+ @Test
+ void shouldIgnoreQueryParamUsingRequestUrl() throws IOException, InterruptedException {
+ WireMockServer server = startServer(
+ new MetricsConfiguration()
+ .useRequestUrl()
+ .ignoreQueryParams()
+ );
+ server.addStubMapping(createDefaultMapping());
+
+ httpCall(server, "/test?withQueryParam=true");
+ Thread.sleep(1000L);
+
+ List metrics = scrape(server);
+ Assertions.assertThat(metrics).contains("wiremock_request_totalTime_ms_count{method=\"GET\",path=\"/test\",status=\"200\",} 1.0");
+ }
+
+ @Test
+ void shouldRegisterNonMatchedRequestsByRequestUrl() throws IOException, InterruptedException {
+ WireMockServer server = startServer(
+ new MetricsConfiguration()
+ .useMappingUrlPattern()
+ .registerNotMatchedRequests()
+ );
- String responseBody = res.body().string();
- Assertions.assertTrue(responseBody.contains("wiremock_request_totalTime_ms_count{method=\"GET\",path=\"/test\",status=\"200\",} 2.0"));
+ httpCall(server, "/test?withQueryParam=true");
+ Thread.sleep(1000L);
+ List metrics = scrape(server);
+ Assertions.assertThat(metrics).contains("wiremock_request_totalTime_ms_count{method=\"GET\",path=\"/test?withQueryParam=true\",status=\"404\",} 1.0");
}
+
+ @Test
+ void shouldNotRegisterNonMatchedRequests() throws IOException, InterruptedException {
+ WireMockServer server = startServer(
+ new MetricsConfiguration()
+ .useRequestUrl()
+ );
+
+ httpCall(server, "/some-url-that-should-not-be-matched");
+ Thread.sleep(1000L);
+
+ List metrics = scrape(server);
+ List filtered = metrics.stream().filter(s -> s.startsWith("wiremock")).collect(Collectors.toList());
+ Assertions.assertThat(filtered).isEmpty();
+
+ }
+
+ @Test
+ void shouldRegisterAnyUrlMappingAsRequestUrl() throws IOException, InterruptedException {
+ WireMockServer server = startServer(
+ new MetricsConfiguration()
+ .useMappingUrlPattern()
+ .registerAnyUrlMappingAsRequestUrl()
+ );
+
+ server.addStubMapping(WireMock.any(WireMock.anyUrl())
+ .willReturn(WireMock.aResponse()
+ .withStatus(200)
+ .withBody("test")).build());
+
+ httpCall(server, "/any-url?param=true");
+ Thread.sleep(1000L);
+
+ List metrics = scrape(server);
+ Assertions.assertThat(metrics).contains("wiremock_request_totalTime_ms_count{method=\"GET\",path=\"/any-url?param=true\",status=\"200\",} 1.0");
+ }
+
}