diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 70d1be17..dc79b0d8 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -2119,6 +2119,10 @@ spring: For some usages of the gateway, properties are adequate, but some production use cases benefit from loading configuration from an external source, such as a database. Future milestone versions will have `RouteDefinitionLocator` implementations based off of Spring Data Repositories, such as Redis, MongoDB, and Cassandra. +=== RouteDefinition Metrics + +To enable `RouteDefinition` metrics, add spring-boot-starter-actuator as a project dependency. Then, by default, the metrics will be available as long as the property `spring.cloud.gateway.metrics.enabled` is set to `true`. A gauge metric named `spring.cloud.gateway.routes.count` will be added, whose value is the number of `RouteDefinitions`. This metric will be available from `/actuator/metrics/spring.cloud.gateway.routes.count`. + == Route Metadata Configuration You can configure additional parameters for each route by using metadata, as follows: diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayMetricsAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayMetricsAutoConfiguration.java index cf9e9485..6346dde9 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayMetricsAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayMetricsAutoConfiguration.java @@ -30,6 +30,8 @@ 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.cloud.gateway.filter.GatewayMetricsFilter; +import org.springframework.cloud.gateway.route.RouteDefinitionLocator; +import org.springframework.cloud.gateway.route.RouteDefinitionMetrics; import org.springframework.cloud.gateway.support.tagsprovider.GatewayHttpTagsProvider; import org.springframework.cloud.gateway.support.tagsprovider.GatewayRouteTagsProvider; import org.springframework.cloud.gateway.support.tagsprovider.GatewayTagsProvider; @@ -71,4 +73,12 @@ public class GatewayMetricsAutoConfiguration { return new GatewayMetricsFilter(meterRegistry, tagsProviders, properties.getPrefix()); } + @Bean + @ConditionalOnBean(MeterRegistry.class) + @ConditionalOnProperty(name = GatewayProperties.PREFIX + ".metrics.enabled", matchIfMissing = true) + public RouteDefinitionMetrics routeDefinitionMetrics(MeterRegistry meterRegistry, + RouteDefinitionLocator routeDefinitionLocator, GatewayMetricsProperties properties) { + return new RouteDefinitionMetrics(meterRegistry, routeDefinitionLocator, properties.getPrefix()); + } + } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/RouteDefinitionMetrics.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/RouteDefinitionMetrics.java new file mode 100644 index 00000000..211c1213 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/RouteDefinitionMetrics.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013-2020 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 + * + * https://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.route; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.micrometer.core.instrument.MeterRegistry; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.gateway.event.RefreshRoutesEvent; +import org.springframework.cloud.gateway.filter.GatewayMetricsFilter; +import org.springframework.context.ApplicationListener; + +/** + * A metric to track the number of routes definitions on the gateway. + * + * @author Fredrich Ombico + */ +public class RouteDefinitionMetrics implements ApplicationListener { + + private static final Log log = LogFactory.getLog(GatewayMetricsFilter.class); + + private final RouteDefinitionLocator routeLocator; + + private final AtomicInteger routeDefinitionCount; + + private final String metricsPrefix; + + public RouteDefinitionMetrics(MeterRegistry meterRegistry, RouteDefinitionLocator routeLocator, + String metricsPrefix) { + this.routeLocator = routeLocator; + + if (metricsPrefix.endsWith(".")) { + this.metricsPrefix = metricsPrefix.substring(0, metricsPrefix.length() - 1); + } + else { + this.metricsPrefix = metricsPrefix; + } + routeDefinitionCount = meterRegistry.gauge(this.metricsPrefix + ".routes.count", new AtomicInteger(0)); + } + + public String getMetricsPrefix() { + return metricsPrefix; + } + + @Override + public void onApplicationEvent(RefreshRoutesEvent event) { + routeLocator.getRouteDefinitions().count().subscribe(count -> { + routeDefinitionCount.set(count.intValue()); + if (log.isDebugEnabled()) { + log.debug("New routes count: " + routeDefinitionCount); + } + }); + } + +} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayMetricsAutoConfigurationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayMetricsAutoConfigurationTests.java index 95d6cd83..d3be9e85 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayMetricsAutoConfigurationTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayMetricsAutoConfigurationTests.java @@ -28,6 +28,7 @@ import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.gateway.filter.GatewayMetricsFilter; +import org.springframework.cloud.gateway.route.RouteDefinitionMetrics; import org.springframework.cloud.gateway.support.tagsprovider.GatewayTagsProvider; import org.springframework.context.annotation.Bean; import org.springframework.test.context.junit4.SpringRunner; @@ -48,6 +49,9 @@ public class GatewayMetricsAutoConfigurationTests { @Autowired(required = false) private GatewayMetricsFilter filter; + @Autowired(required = false) + private RouteDefinitionMetrics routeDefinitionMetrics; + @Autowired(required = false) private List tagsProviders; @@ -58,6 +62,12 @@ public class GatewayMetricsAutoConfigurationTests { assertThat(tagsProviders).isNotEmpty(); } + @Test + public void routeDefinitionMetricsBeanExists() { + assertThat(routeDefinitionMetrics).isNotNull(); + assertThat(routeDefinitionMetrics.getMetricsPrefix()).isEqualTo("spring.cloud.gateway"); + } + } @RunWith(SpringRunner.class) @@ -67,11 +77,19 @@ public class GatewayMetricsAutoConfigurationTests { @Autowired(required = false) private GatewayMetricsFilter filter; + @Autowired(required = false) + private RouteDefinitionMetrics routeDefinitionMetrics; + @Test public void gatewayMetricsBeanMissing() { assertThat(filter).isNull(); } + @Test + public void routeDefinitionMetricsBeanMissing() { + assertThat(routeDefinitionMetrics).isNull(); + } + } @RunWith(SpringRunner.class) @@ -82,6 +100,9 @@ public class GatewayMetricsAutoConfigurationTests { @Autowired(required = false) private GatewayMetricsFilter filter; + @Autowired(required = false) + private RouteDefinitionMetrics routeDefinitionMetrics; + @Autowired(required = false) private List tagsProviders; @@ -92,6 +113,12 @@ public class GatewayMetricsAutoConfigurationTests { assertThat(tagsProviders).extracting("class").contains(CustomTagsProviderConfig.EmptyTagsProvider.class); } + @Test + public void routeDefinitionMetricsBeanExists() { + assertThat(routeDefinitionMetrics).isNotNull(); + assertThat(routeDefinitionMetrics.getMetricsPrefix()).isEqualTo("myprefix"); + } + } @SpringBootConfiguration diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/route/RouteDefinitionMetricsTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/route/RouteDefinitionMetricsTests.java new file mode 100644 index 00000000..ab7e4549 --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/route/RouteDefinitionMetricsTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2013-2020 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 + * + * https://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.route; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import io.micrometer.core.instrument.MeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.gateway.event.RefreshRoutesEvent; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Fredrich Ombico + */ +@ExtendWith(MockitoExtension.class) +class RouteDefinitionMetricsTests { + + @Mock + private MeterRegistry registry; + + @Mock + private RouteDefinitionLocator routeDefinitionLocator; + + private RouteDefinitionMetrics routeDefinitionMetrics; + + private AtomicInteger routeDefinitionCount; + + @BeforeEach + void setUp() { + routeDefinitionCount = new AtomicInteger(0); + when(registry.gauge(any(String.class), any(AtomicInteger.class))).thenReturn(routeDefinitionCount); + routeDefinitionMetrics = new RouteDefinitionMetrics(registry, routeDefinitionLocator, "prefix."); + } + + @Test + void metricsPrefix() { + assertThat(routeDefinitionMetrics.getMetricsPrefix()).isEqualTo("prefix"); + } + + @Test + void shouldReportOneRoute() { + List oneRoute = Collections.singletonList(new RouteDefinition()); + when(routeDefinitionLocator.getRouteDefinitions()).thenReturn(Flux.fromStream(oneRoute.stream())); + + RefreshRoutesEvent refreshRoutesEvent = new RefreshRoutesEvent(this); + routeDefinitionMetrics.onApplicationEvent(refreshRoutesEvent); + + assertThat(routeDefinitionCount.get()).isEqualTo(1); + } + + @Test + void shouldReportMultipleRoutes() { + List multipleRoutes = Arrays.asList(new RouteDefinition(), new RouteDefinition(), + new RouteDefinition(), new RouteDefinition(), new RouteDefinition()); + when(routeDefinitionLocator.getRouteDefinitions()).thenReturn(Flux.fromStream(multipleRoutes.stream())); + + RefreshRoutesEvent refreshRoutesEvent = new RefreshRoutesEvent(this); + routeDefinitionMetrics.onApplicationEvent(refreshRoutesEvent); + + assertThat(routeDefinitionCount.get()).isEqualTo(5); + } + + @Test + void shouldReportZeroIfNoRoutes() { + RouteDefinition[] zeroRoutes = new RouteDefinition[0]; + when(routeDefinitionLocator.getRouteDefinitions()).thenReturn(Flux.fromArray(zeroRoutes)); + + RefreshRoutesEvent refreshRoutesEvent = new RefreshRoutesEvent(this); + routeDefinitionMetrics.onApplicationEvent(refreshRoutesEvent); + + assertThat(routeDefinitionCount.get()).isEqualTo(0); + } + +}