diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 12914c65..ff1c2bc9 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -1155,6 +1155,8 @@ To enable Gateway Metrics add spring-boot-starter-actuator as a project dependen * `routeUri`: The URI that the API will be routed to * `outcome`: Outcome as classified by link:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/HttpStatus.Series.html[HttpStatus.Series] * `status`: Http Status of the request returned to the client +* `httpStatusCode`: Http Status of the request returned to the client +* `httpMethod`: The Http method used for the request These metrics are then available to be scraped from ``/actuator/metrics/gateway.requests`` and can be easily integated with Prometheus to create a link:images/gateway-grafana-dashboard.jpeg[Grafana] link:gateway-grafana-dashboard.json[dashboard]. diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/GatewayMetricsFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/GatewayMetricsFilter.java index 2d8c0363..314e84e8 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/GatewayMetricsFilter.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/GatewayMetricsFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,17 +12,12 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ package org.springframework.cloud.gateway.filter; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tags; -import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.Timer.Sample; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Mono; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; import org.springframework.cloud.gateway.route.Route; import org.springframework.core.Ordered; @@ -31,13 +26,15 @@ import org.springframework.http.server.reactive.AbstractServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.server.ServerWebExchange; -import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.Timer.Sample; +import reactor.core.publisher.Mono; public class GatewayMetricsFilter implements GlobalFilter, Ordered { - private final Log log = LogFactory.getLog(getClass()); - - private final MeterRegistry meterRegistry; + private MeterRegistry meterRegistry; public GatewayMetricsFilter(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; @@ -47,7 +44,7 @@ public class GatewayMetricsFilter implements GlobalFilter, Ordered { public int getOrder() { // start the timer as soon as possible and report the metric event before we write // response to client - return Ordered.HIGHEST_PRECEDENCE + 10000; + return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1; } @Override @@ -76,8 +73,12 @@ public class GatewayMetricsFilter implements GlobalFilter, Ordered { private void endTimerInner(ServerWebExchange exchange, Sample sample) { String outcome = "CUSTOM"; String status = "CUSTOM"; + String httpStatusCodeStr = "NA"; + + String httpMethod = exchange.getRequest().getMethodValue(); HttpStatus statusCode = exchange.getResponse().getStatusCode(); if (statusCode != null) { + httpStatusCodeStr = String.valueOf(statusCode.value()); outcome = statusCode.series().name(); status = statusCode.name(); } @@ -87,19 +88,18 @@ public class GatewayMetricsFilter implements GlobalFilter, Ordered { .getStatusCodeValue(); if (statusInt != null) { status = String.valueOf(statusInt); + httpStatusCodeStr = status; } else { status = "NA"; } } } + // TODO refactor to allow Tags provider like in MetricsWebFilter Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); - Tags tags = Tags.of("outcome", outcome, "status", status, "routeId", - route.getId(), "routeUri", route.getUri().toString()); - if (log.isTraceEnabled()) { - log.trace("Stopping timer 'gateway.requests' with tags " + tags); - } + Tags tags = Tags.of("outcome", outcome, "status", status, "httpStatusCode", + httpStatusCodeStr, "routeId", route.getId(), "routeUri", + route.getUri().toString(), "httpMethod", httpMethod); sample.stop(meterRegistry.timer("gateway.requests", tags)); } - } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/GatewayMetricFilterTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/GatewayMetricFilterTests.java index cbe1cc7a..eac3017e 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/GatewayMetricFilterTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/GatewayMetricFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,21 +12,16 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ package org.springframework.cloud.gateway.filter; -import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; -import io.micrometer.core.instrument.Meter; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.search.MeterNotFoundException; import org.junit.Test; import org.junit.runner.RunWith; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringBootConfiguration; @@ -48,8 +43,7 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import io.micrometer.core.instrument.MeterRegistry; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = RANDOM_PORT) @@ -65,79 +59,49 @@ public class GatewayMetricFilterTests extends BaseWebClientTests { private String testUri; @Test - public void gatewayRequestsMeterFilterHasTags() { - testClient.get().uri("/headers") - .header(HttpHeaders.HOST, "www.metricshappypath.org").exchange() - .expectStatus().isOk().returnResult(String.class).consumeWith(result -> { - assertMetricsContainsTag("outcome", - HttpStatus.Series.SUCCESSFUL.name()); - assertMetricsContainsTag("status", HttpStatus.OK.name()); - assertMetricsContainsTag("routeId", "test_metrics_happy_path"); - assertMetricsContainsTag("routeUri", "lb://testservice"); - }); + public void gatewayRequestsMeterFilterHasTags() throws InterruptedException { + testClient.get().uri("/headers").exchange().expectStatus().isOk(); + assertMetricsContainsTag("outcome", HttpStatus.Series.SUCCESSFUL.name()); + assertMetricsContainsTag("status", HttpStatus.OK.name()); + assertMetricsContainsTag("httpStatusCode", String.valueOf(HttpStatus.OK.value())); + assertMetricsContainsTag("httpMethod", HttpMethod.GET.toString()); + assertMetricsContainsTag("routeId", "default_path_to_httpbin"); + assertMetricsContainsTag("routeUri", testUri); } @Test - public void gatewayRequestsMeterFilterHasTagsForBadTargetUri() { - testClient.get().uri("/badtargeturi").exchange().expectStatus().is5xxServerError() - .returnResult(String.class).consumeWith(result -> { - assertMetricsContainsTag("outcome", - HttpStatus.Series.SERVER_ERROR.name()); - assertMetricsContainsTag("status", - HttpStatus.INTERNAL_SERVER_ERROR.name()); - assertMetricsContainsTag("routeId", "default_path_to_httpbin"); - assertMetricsContainsTag("routeUri", testUri); - }); + public void gatewayRequestsMeterFilterHasTagsForBadTargetUri() + throws InterruptedException { + testClient.get().uri("/badtargeturi").exchange().expectStatus() + .is5xxServerError(); + assertMetricsContainsTag("outcome", HttpStatus.Series.SERVER_ERROR.name()); + assertMetricsContainsTag("status", HttpStatus.INTERNAL_SERVER_ERROR.name()); + assertMetricsContainsTag("httpStatusCode", String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value())); + assertMetricsContainsTag("httpMethod", HttpMethod.GET.toString()); + assertMetricsContainsTag("routeId", "default_path_to_httpbin"); + assertMetricsContainsTag("routeUri", testUri); } @Test - public void hasMetricsForSetStatusFilter() { + public void hasMetricsForSetStatusFilter() throws InterruptedException { HttpHeaders headers = new HttpHeaders(); - headers.set(HttpHeaders.HOST, "www.setcustomstatusmetrics.org"); + headers.set(HttpHeaders.HOST, "www.setcustomstatus.org"); // cannot use netty client since we cannot read custom http status ResponseEntity response = new TestRestTemplate().exchange( - baseUri + "/headers", HttpMethod.GET, new HttpEntity<>(headers), + baseUri + "/headers", HttpMethod.POST, new HttpEntity<>(headers), String.class); assertThat(response.getStatusCodeValue()).isEqualTo(432); assertMetricsContainsTag("outcome", "CUSTOM"); assertMetricsContainsTag("status", "432"); - assertMetricsContainsTag("routeId", "test_custom_http_status_metrics"); + assertMetricsContainsTag("routeId", "test_custom_http_status"); assertMetricsContainsTag("routeUri", testUri); + assertMetricsContainsTag("httpStatusCode", "432"); + assertMetricsContainsTag("httpMethod", HttpMethod.POST.toString()); } private void assertMetricsContainsTag(String tagKey, String tagValue) { - List meterIds = null; - try { - meterIds = this.meterRegistry.getMeters().stream().map(Meter::getId) - .collect(Collectors.toList()); - Collection timers = this.meterRegistry.get(REQUEST_METRICS_NAME) - .timers(); - System.err.println("Looking for gateway.requests: tag: " + tagKey - + ", value: " + tagValue); - timers.forEach(timer -> System.err - .println(timer.getId() + timer.getClass().getSimpleName())); - long count = getCount(tagKey, tagValue); - assertThat(count).isEqualTo(1); - } - catch (MeterNotFoundException e) { - System.err.println("\n\n\nError finding gatway.requests meter: tag: " + tagKey - + ", value: " + tagValue); - System.err.println( - "\n\n\nMeter ids prior to search: " + meterIds + "\n\n\n and after:"); - this.meterRegistry.forEachMeter(meter -> System.err - .println(meter.getId() + meter.getClass().getSimpleName())); - - // try again? - long count = getCount(tagKey, tagValue); - if (count != 1) { - throw e; - } - } - } - - private long getCount(String tagKey, String tagValue) { - return this.meterRegistry.get(REQUEST_METRICS_NAME).tag(tagKey, tagValue).timer() - .count(); + assertThat(this.meterRegistry.get(REQUEST_METRICS_NAME).tag(tagKey, tagValue) + .timer().count()).isEqualTo(1); } @EnableAutoConfiguration @@ -145,18 +109,14 @@ public class GatewayMetricFilterTests extends BaseWebClientTests { @RestController @Import(DefaultTestConfig.class) public static class CustomConfig { - @Value("${test.uri}") protected String testUri; @Bean public RouteLocator myRouteLocator(RouteLocatorBuilder builder) { return builder.routes() - .route("test_metrics_happy_path", - r -> r.host("*.metricshappypath.org").uri(testUri)) - .route("test_custom_http_status_metrics", - r -> r.host("*.setcustomstatusmetrics.org") - .filters(f -> f.setStatus(432)).uri(testUri)) + .route("test_custom_http_status", r -> r.host("*.setcustomstatus.org") + .filters(f -> f.setStatus(432)).uri(testUri)) .build(); } @@ -164,7 +124,5 @@ public class GatewayMetricFilterTests extends BaseWebClientTests { public String exception() { throw new RuntimeException("an error"); } - } - }