diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 3f3389b7b..95a8fda08 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -886,15 +886,17 @@ spring: === Gateway Metrics Filter -The Gateway Metrics Filter runs as long as the property `spring.cloud.gateway.metrics.enabled` is not set to `false`. This filter adds a timer metric named "gateway.requests" with the following tags: +To enable Gateway Metrics add spring-boot-starter-actuator as a project dependency. Then, by default, the Gateway Metrics Filter runs as long as the property `spring.cloud.gateway.metrics.enabled` is not set to `false`. This filter adds a timer metric named "gateway.requests" with the following tags: * `routeId`: The route id * `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 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]. +NOTE: To enable the pometheus endpoint add micrometer-registry-prometheus as a project dependency. + === Making An Exchange As Routed After the Gateway has routed a `ServerWebExchange` it will mark that exchange as "routed" by adding `gatewayAlreadyRouted` diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index c32a3e326..f80754126 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -17,12 +17,7 @@ package org.springframework.cloud.gateway.config; -import java.io.IOException; -import java.net.URL; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -41,6 +36,8 @@ import rx.RxReactiveStreams; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -51,7 +48,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.boot.web.server.WebServerException; import org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint; import org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter; import org.springframework.cloud.gateway.filter.ForwardPathFilter; @@ -128,7 +124,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Primary; import org.springframework.http.codec.ServerCodecConfigurer; -import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; import org.springframework.validation.Validator; import org.springframework.web.reactive.DispatcherHandler; @@ -147,7 +142,9 @@ import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool @ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true) @EnableConfigurationProperties @AutoConfigureBefore(HttpHandlerAutoConfiguration.class) -@AutoConfigureAfter({GatewayLoadBalancerClientAutoConfiguration.class, GatewayClassPathWarningAutoConfiguration.class}) +@AutoConfigureAfter({ GatewayLoadBalancerClientAutoConfiguration.class, + GatewayClassPathWarningAutoConfiguration.class, MetricsAutoConfiguration.class, + CompositeMeterRegistryAutoConfiguration.class }) @ConditionalOnClass(DispatcherHandler.class) public class GatewayAutoConfiguration { @@ -341,15 +338,18 @@ public class GatewayAutoConfiguration { return new XForwardedHeadersFilter(); } - // GlobalFilter beans - @Bean - @ConditionalOnProperty(name = "spring.cloud.gateway.metrics.enabled", matchIfMissing = true) - public GatewayMetricsFilter gatewayMetricFilter(MeterRegistry meterRegistry) { - return new GatewayMetricsFilter(meterRegistry); + @Configuration + @ConditionalOnBean(MeterRegistry.class) + protected static class MetricsConfig { + @Bean + @ConditionalOnProperty(name = "spring.cloud.gateway.metrics.enabled", matchIfMissing = true) + public GatewayMetricsFilter gatewayMetricFilter(MeterRegistry meterRegistry) { + return new GatewayMetricsFilter(meterRegistry); + } } - + @Bean public AdaptCachedBodyGlobalFilter adaptCachedBodyGlobalFilter() { return new AdaptCachedBodyGlobalFilter(); diff --git a/spring-cloud-gateway-sample/pom.xml b/spring-cloud-gateway-sample/pom.xml index a75cf5513..e0efa6268 100644 --- a/spring-cloud-gateway-sample/pom.xml +++ b/spring-cloud-gateway-sample/pom.xml @@ -54,6 +54,11 @@ spring-boot-starter-test test + + org.springframework.cloud + spring-cloud-test-support + test + io.projectreactor reactor-test diff --git a/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/GatewaySampleApplication.java b/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/GatewaySampleApplication.java index b98513ce1..7af93e972 100644 --- a/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/GatewaySampleApplication.java +++ b/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/GatewaySampleApplication.java @@ -45,6 +45,7 @@ import org.springframework.web.reactive.function.server.ServerResponse; @Import(AdditionalRoutes.class) public class GatewaySampleApplication { + public static final String HELLO_FROM_FAKE_ACTUATOR_METRICS_GATEWAY_REQUESTS = "hello from fake /actuator/metrics/gateway.requests"; @Value("${test.uri:http://httpbin.org:80}") String uri; @@ -135,6 +136,14 @@ public class GatewaySampleApplication { return route; } + @Bean + public RouterFunction testWhenMetricPathIsNotMeet() { + RouterFunction route = RouterFunctions.route( + RequestPredicates.path("/actuator/metrics/gateway.requests"), + request -> ServerResponse.ok().body(BodyInserters.fromObject(HELLO_FROM_FAKE_ACTUATOR_METRICS_GATEWAY_REQUESTS))); + return route; + } + static class Hello { String message; diff --git a/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationTests.java b/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationTests.java index fae1d05a1..8cbbf4a91 100644 --- a/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationTests.java +++ b/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationTests.java @@ -17,9 +17,12 @@ package org.springframework.cloud.gateway.sample; +import java.io.IOException; import java.time.Duration; import java.util.Map; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.loadbalancer.Server; import com.netflix.loadbalancer.ServerList; import org.junit.AfterClass; @@ -43,6 +46,7 @@ import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.util.SocketUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; /** @@ -181,6 +185,27 @@ public class GatewaySampleApplicationTests { .expectStatus().isOk(); } + @Test + public void actuatorMetrics() { + contextLoads(); + webClient.get() + .uri("http://localhost:" + managementPort + + "/actuator/metrics/gateway.requests") + .exchange().expectStatus().isOk().expectBody().consumeWith(i -> { + String body = new String(i.getResponseBodyContent()); + ObjectMapper mapper = new ObjectMapper(); + try { + JsonNode actualObj = mapper.readTree(body); + JsonNode findValue = actualObj.findValue("name"); + assertEquals("Expected to find metric with name gateway.requests", + "gateway.requests", findValue.asText()); + } + catch (IOException e) { + throw new IllegalStateException(e); + } + }); + } + @Configuration @EnableAutoConfiguration @RibbonClient(name = "httpbin", configuration = RibbonConfig.class) diff --git a/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationWithoutMetricsTests.java b/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationWithoutMetricsTests.java new file mode 100644 index 000000000..22aa7d7e0 --- /dev/null +++ b/spring-cloud-gateway-sample/src/test/java/org/springframework/cloud/gateway/sample/GatewaySampleApplicationWithoutMetricsTests.java @@ -0,0 +1,80 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * 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.sample; + +import java.time.Duration; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.gateway.sample.GatewaySampleApplicationTests.TestConfig; +import org.springframework.cloud.test.ClassPathExclusions; +import org.springframework.cloud.test.ModifiedClassPathRunner; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.SocketUtils; + +@RunWith(ModifiedClassPathRunner.class) +@ClassPathExclusions({ "micrometer-*.jar" }) +@DirtiesContext +public class GatewaySampleApplicationWithoutMetricsTests { + + static protected int port; + + protected WebTestClient webClient; + protected String baseUri; + + @BeforeClass + public static void beforeClass() { + port = SocketUtils.findAvailableTcpPort(); + System.setProperty("server.port", Integer.toString(port)); + } + + @AfterClass + public static void afterClass() { + System.clearProperty("server.port"); + } + + @Before + public void setup() { + baseUri = "http://localhost:" + port; + this.webClient = WebTestClient.bindToServer() + .responseTimeout(Duration.ofSeconds(10)).baseUrl(baseUri).build(); + } + + protected ConfigurableApplicationContext init(Class config) { + return new SpringApplicationBuilder().web(WebApplicationType.REACTIVE) + .sources(GatewaySampleApplication.class, config).run(); + } + + @Test + public void actuatorMetrics() { + init(TestConfig.class); + webClient.get().uri("/get").exchange().expectStatus().isOk(); + webClient.get() + .uri("http://localhost:" + port + "/actuator/metrics/gateway.requests") + .exchange().expectStatus().isOk().expectBody(String.class).isEqualTo( + GatewaySampleApplication.HELLO_FROM_FAKE_ACTUATOR_METRICS_GATEWAY_REQUESTS); + } + +}