Merge remote-tracking branch 'origin/2.0.x' into 2.1.x

This commit is contained in:
Ryan Baxter
2019-04-01 12:19:56 -04:00
3 changed files with 54 additions and 94 deletions

View File

@@ -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 * `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] * `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 * `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]. 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].

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.gateway.filter; package org.springframework.cloud.gateway.filter;
import io.micrometer.core.instrument.MeterRegistry; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;
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 org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.route.Route;
import org.springframework.core.Ordered; 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.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange; 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 { public class GatewayMetricsFilter implements GlobalFilter, Ordered {
private final Log log = LogFactory.getLog(getClass()); private MeterRegistry meterRegistry;
private final MeterRegistry meterRegistry;
public GatewayMetricsFilter(MeterRegistry meterRegistry) { public GatewayMetricsFilter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry; this.meterRegistry = meterRegistry;
@@ -47,7 +44,7 @@ public class GatewayMetricsFilter implements GlobalFilter, Ordered {
public int getOrder() { public int getOrder() {
// start the timer as soon as possible and report the metric event before we write // start the timer as soon as possible and report the metric event before we write
// response to client // response to client
return Ordered.HIGHEST_PRECEDENCE + 10000; return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
} }
@Override @Override
@@ -76,8 +73,12 @@ public class GatewayMetricsFilter implements GlobalFilter, Ordered {
private void endTimerInner(ServerWebExchange exchange, Sample sample) { private void endTimerInner(ServerWebExchange exchange, Sample sample) {
String outcome = "CUSTOM"; String outcome = "CUSTOM";
String status = "CUSTOM"; String status = "CUSTOM";
String httpStatusCodeStr = "NA";
String httpMethod = exchange.getRequest().getMethodValue();
HttpStatus statusCode = exchange.getResponse().getStatusCode(); HttpStatus statusCode = exchange.getResponse().getStatusCode();
if (statusCode != null) { if (statusCode != null) {
httpStatusCodeStr = String.valueOf(statusCode.value());
outcome = statusCode.series().name(); outcome = statusCode.series().name();
status = statusCode.name(); status = statusCode.name();
} }
@@ -87,19 +88,18 @@ public class GatewayMetricsFilter implements GlobalFilter, Ordered {
.getStatusCodeValue(); .getStatusCodeValue();
if (statusInt != null) { if (statusInt != null) {
status = String.valueOf(statusInt); status = String.valueOf(statusInt);
httpStatusCodeStr = status;
} }
else { else {
status = "NA"; status = "NA";
} }
} }
} }
// TODO refactor to allow Tags provider like in MetricsWebFilter
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
Tags tags = Tags.of("outcome", outcome, "status", status, "routeId", Tags tags = Tags.of("outcome", outcome, "status", status, "httpStatusCode",
route.getId(), "routeUri", route.getUri().toString()); httpStatusCodeStr, "routeId", route.getId(), "routeUri",
if (log.isTraceEnabled()) { route.getUri().toString(), "httpMethod", httpMethod);
log.trace("Stopping timer 'gateway.requests' with tags " + tags);
}
sample.stop(meterRegistry.timer("gateway.requests", tags)); sample.stop(meterRegistry.timer("gateway.requests", tags));
} }
} }

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.gateway.filter; package org.springframework.cloud.gateway.filter;
import java.util.Collection; import static org.assertj.core.api.Assertions.assertThat;
import java.util.List; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import java.util.stream.Collectors;
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.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration; 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.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat; import io.micrometer.core.instrument.MeterRegistry;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT) @SpringBootTest(webEnvironment = RANDOM_PORT)
@@ -65,79 +59,49 @@ public class GatewayMetricFilterTests extends BaseWebClientTests {
private String testUri; private String testUri;
@Test @Test
public void gatewayRequestsMeterFilterHasTags() { public void gatewayRequestsMeterFilterHasTags() throws InterruptedException {
testClient.get().uri("/headers") testClient.get().uri("/headers").exchange().expectStatus().isOk();
.header(HttpHeaders.HOST, "www.metricshappypath.org").exchange() assertMetricsContainsTag("outcome", HttpStatus.Series.SUCCESSFUL.name());
.expectStatus().isOk().returnResult(String.class).consumeWith(result -> { assertMetricsContainsTag("status", HttpStatus.OK.name());
assertMetricsContainsTag("outcome", assertMetricsContainsTag("httpStatusCode", String.valueOf(HttpStatus.OK.value()));
HttpStatus.Series.SUCCESSFUL.name()); assertMetricsContainsTag("httpMethod", HttpMethod.GET.toString());
assertMetricsContainsTag("status", HttpStatus.OK.name()); assertMetricsContainsTag("routeId", "default_path_to_httpbin");
assertMetricsContainsTag("routeId", "test_metrics_happy_path"); assertMetricsContainsTag("routeUri", testUri);
assertMetricsContainsTag("routeUri", "lb://testservice");
});
} }
@Test @Test
public void gatewayRequestsMeterFilterHasTagsForBadTargetUri() { public void gatewayRequestsMeterFilterHasTagsForBadTargetUri()
testClient.get().uri("/badtargeturi").exchange().expectStatus().is5xxServerError() throws InterruptedException {
.returnResult(String.class).consumeWith(result -> { testClient.get().uri("/badtargeturi").exchange().expectStatus()
assertMetricsContainsTag("outcome", .is5xxServerError();
HttpStatus.Series.SERVER_ERROR.name()); assertMetricsContainsTag("outcome", HttpStatus.Series.SERVER_ERROR.name());
assertMetricsContainsTag("status", assertMetricsContainsTag("status", HttpStatus.INTERNAL_SERVER_ERROR.name());
HttpStatus.INTERNAL_SERVER_ERROR.name()); assertMetricsContainsTag("httpStatusCode", String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()));
assertMetricsContainsTag("routeId", "default_path_to_httpbin"); assertMetricsContainsTag("httpMethod", HttpMethod.GET.toString());
assertMetricsContainsTag("routeUri", testUri); assertMetricsContainsTag("routeId", "default_path_to_httpbin");
}); assertMetricsContainsTag("routeUri", testUri);
} }
@Test @Test
public void hasMetricsForSetStatusFilter() { public void hasMetricsForSetStatusFilter() throws InterruptedException {
HttpHeaders headers = new HttpHeaders(); 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 // cannot use netty client since we cannot read custom http status
ResponseEntity<String> response = new TestRestTemplate().exchange( ResponseEntity<String> response = new TestRestTemplate().exchange(
baseUri + "/headers", HttpMethod.GET, new HttpEntity<>(headers), baseUri + "/headers", HttpMethod.POST, new HttpEntity<>(headers),
String.class); String.class);
assertThat(response.getStatusCodeValue()).isEqualTo(432); assertThat(response.getStatusCodeValue()).isEqualTo(432);
assertMetricsContainsTag("outcome", "CUSTOM"); assertMetricsContainsTag("outcome", "CUSTOM");
assertMetricsContainsTag("status", "432"); assertMetricsContainsTag("status", "432");
assertMetricsContainsTag("routeId", "test_custom_http_status_metrics"); assertMetricsContainsTag("routeId", "test_custom_http_status");
assertMetricsContainsTag("routeUri", testUri); assertMetricsContainsTag("routeUri", testUri);
assertMetricsContainsTag("httpStatusCode", "432");
assertMetricsContainsTag("httpMethod", HttpMethod.POST.toString());
} }
private void assertMetricsContainsTag(String tagKey, String tagValue) { private void assertMetricsContainsTag(String tagKey, String tagValue) {
List<Meter.Id> meterIds = null; assertThat(this.meterRegistry.get(REQUEST_METRICS_NAME).tag(tagKey, tagValue)
try { .timer().count()).isEqualTo(1);
meterIds = this.meterRegistry.getMeters().stream().map(Meter::getId)
.collect(Collectors.toList());
Collection<Timer> 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();
} }
@EnableAutoConfiguration @EnableAutoConfiguration
@@ -145,18 +109,14 @@ public class GatewayMetricFilterTests extends BaseWebClientTests {
@RestController @RestController
@Import(DefaultTestConfig.class) @Import(DefaultTestConfig.class)
public static class CustomConfig { public static class CustomConfig {
@Value("${test.uri}") @Value("${test.uri}")
protected String testUri; protected String testUri;
@Bean @Bean
public RouteLocator myRouteLocator(RouteLocatorBuilder builder) { public RouteLocator myRouteLocator(RouteLocatorBuilder builder) {
return builder.routes() return builder.routes()
.route("test_metrics_happy_path", .route("test_custom_http_status", r -> r.host("*.setcustomstatus.org")
r -> r.host("*.metricshappypath.org").uri(testUri)) .filters(f -> f.setStatus(432)).uri(testUri))
.route("test_custom_http_status_metrics",
r -> r.host("*.setcustomstatusmetrics.org")
.filters(f -> f.setStatus(432)).uri(testUri))
.build(); .build();
} }
@@ -164,7 +124,5 @@ public class GatewayMetricFilterTests extends BaseWebClientTests {
public String exception() { public String exception() {
throw new RuntimeException("an error"); throw new RuntimeException("an error");
} }
} }
} }