Merge remote-tracking branch 'origin/2.1.x'
This commit is contained in:
@@ -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].
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String> 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<Meter.Id> meterIds = null;
|
||||
try {
|
||||
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();
|
||||
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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user