From 24c3add1f1fe0a20bfc873f3a6353b917617a89d Mon Sep 17 00:00:00 2001 From: Marta Medio Date: Wed, 27 Oct 2021 12:52:52 +0200 Subject: [PATCH 1/2] Add metric to include the path as a Gateway metric Tag Fixes gh-2419 --- docs/src/main/asciidoc/_configprops.adoc | 1 + .../main/asciidoc/spring-cloud-gateway.adoc | 4 + .../GatewayMetricsAutoConfiguration.java | 7 ++ .../tagsprovider/GatewayPathTagsProvider.java | 57 ++++++++++++++ .../GatewayPathTagsProviderTests.java | 77 +++++++++++++++++++ 5 files changed, 146 insertions(+) create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/tagsprovider/GatewayPathTagsProvider.java create mode 100644 spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/tagsprovider/GatewayPathTagsProviderTests.java diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 86d1b8f3..8d6460e7 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -107,6 +107,7 @@ |spring.cloud.gateway.metrics.enabled | `false` | Enables the collection of metrics data. |spring.cloud.gateway.metrics.prefix | `spring.cloud.gateway` | The prefix of all metrics emitted by gateway. |spring.cloud.gateway.metrics.tags | | Tags map that added to metrics. +|spring.cloud.gateway.metrics.tags.path.enabled | `false` | If the collection of metrics data is enabled, enables an extra metric data tag by path. |spring.cloud.gateway.predicate.after.enabled | `true` | Enables the after predicate. |spring.cloud.gateway.predicate.before.enabled | `true` | Enables the before predicate. |spring.cloud.gateway.predicate.between.enabled | `true` | Enables the between predicate. diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index e1a33b38..a0b9e846 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -1898,6 +1898,10 @@ To enable gateway metrics, add spring-boot-starter-actuator as a project depende * `httpStatusCode`: The HTTP Status of the request returned to the client. * `httpMethod`: The HTTP method used for the request. +In addition, through the property `spring.cloud.gateway.metrics.tags.path.enabled` (by default, set to false), you can activate an extra metric with the tag: + +* `path`: Path of the request. + These metrics are then available to be scraped from `/actuator/metrics/spring.cloud.gateway.requests` and can be easily integrated with Prometheus to create a link:images/gateway-grafana-dashboard.jpeg[Grafana] link:gateway-grafana-dashboard.json[dashboard]. NOTE: To enable the prometheus endpoint, add `micrometer-registry-prometheus` as a project dependency. 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 6346dde9..c9eaeecc 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 @@ -33,6 +33,7 @@ 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.GatewayPathTagsProvider; import org.springframework.cloud.gateway.support.tagsprovider.GatewayRouteTagsProvider; import org.springframework.cloud.gateway.support.tagsprovider.GatewayTagsProvider; import org.springframework.cloud.gateway.support.tagsprovider.PropertiesTagsProvider; @@ -53,6 +54,12 @@ public class GatewayMetricsAutoConfiguration { return new GatewayHttpTagsProvider(); } + @Bean + @ConditionalOnProperty(name = GatewayProperties.PREFIX + ".metrics.tags.path.enabled") + public GatewayPathTagsProvider gatewayPathTagsProvider() { + return new GatewayPathTagsProvider(); + } + @Bean public GatewayRouteTagsProvider gatewayRouteTagsProvider() { return new GatewayRouteTagsProvider(); diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/tagsprovider/GatewayPathTagsProvider.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/tagsprovider/GatewayPathTagsProvider.java new file mode 100644 index 00000000..bff061b1 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/tagsprovider/GatewayPathTagsProvider.java @@ -0,0 +1,57 @@ +/* + * 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.support.tagsprovider; + +import io.micrometer.core.instrument.Tags; + +import org.springframework.cloud.gateway.route.Route; +import org.springframework.web.server.ServerWebExchange; + +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; + +/** + * @author Marta Medio + * @author Alberto C. Ríos + */ +public class GatewayPathTagsProvider implements GatewayTagsProvider { + + private static final String START_PATH_PATTERN = "Paths: ["; + + private static final String END_PATH_PATTERN = "], match"; + + @Override + public Tags apply(ServerWebExchange exchange) { + Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); + + if (route != null) { + String rawPredicate = route.getPredicate().toString(); + if (predicateContainsPath(rawPredicate)) { + int beginIndex = rawPredicate.indexOf(START_PATH_PATTERN) + START_PATH_PATTERN.length(); + String predicate = rawPredicate.substring(beginIndex, rawPredicate.indexOf(END_PATH_PATTERN)); + return Tags.of("path", predicate); + } + } + + return Tags.empty(); + } + + private boolean predicateContainsPath(String rawPredicate) { + return rawPredicate != null && rawPredicate.contains(START_PATH_PATTERN) + && rawPredicate.contains(END_PATH_PATTERN); + } + +} diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/tagsprovider/GatewayPathTagsProviderTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/tagsprovider/GatewayPathTagsProviderTests.java new file mode 100644 index 00000000..02c5da78 --- /dev/null +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/support/tagsprovider/GatewayPathTagsProviderTests.java @@ -0,0 +1,77 @@ +/* + * 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.support.tagsprovider; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.micrometer.core.instrument.Tags; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.gateway.handler.predicate.MethodRoutePredicateFactory; +import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory; +import org.springframework.cloud.gateway.route.Route; +import org.springframework.http.HttpMethod; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; + +/** + * @author Marta Medio + * @author Alberto C. Ríos + */ +public class GatewayPathTagsProviderTests { + + private final GatewayPathTagsProvider pathTagsProvider = new GatewayPathTagsProvider(); + + private static final String ROUTE_URI = "http://gatewaytagsprovider.org:80"; + + @Test + void addPathToRoutes() { + List pathList = Stream.of("/git/**").collect(Collectors.toList()); + + Route route = Route.async().id("git").uri(ROUTE_URI).predicate( + new PathRoutePredicateFactory().apply(new PathRoutePredicateFactory.Config().setPatterns(pathList))) + .build(); + + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(ROUTE_URI).build()); + exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, route); + + Tags tags = pathTagsProvider.apply(exchange); + assertThat(tags.stream().count()).isEqualTo(1); + assertThat(tags.stream().anyMatch(tag -> "path".equals(tag.getKey()))).isEqualTo(true); + } + + @Test + void ignoreRoutesWithoutPath() { + MethodRoutePredicateFactory.Config config = new MethodRoutePredicateFactory.Config(); + config.setMethods(HttpMethod.GET); + Route route = Route.async().id("empty").uri(ROUTE_URI) + .predicate(new MethodRoutePredicateFactory().apply(config)).build(); + + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(ROUTE_URI).build()); + exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, route); + + Tags tags = pathTagsProvider.apply(exchange); + assertThat(tags.stream().count()).isEqualTo(0); + } + +} From c414f58c7b2338303c67389d63f6ea921a1f5834 Mon Sep 17 00:00:00 2001 From: spencergibb Date: Mon, 1 Nov 2021 14:23:34 -0400 Subject: [PATCH 2/2] Adds path predicate match to server web exchange attributes. GatewayPathTagsProvider then uses that to provide metrics. See gh-2419 --- .../predicate/PathRoutePredicateFactory.java | 9 ++++ .../support/ServerWebExchangeUtils.java | 11 +++++ .../tagsprovider/GatewayPathTagsProvider.java | 23 ++++------ .../GatewayPathTagsProviderTests.java | 43 ++++++++++++++++--- .../gateway/route/builder/RouteDslTests.kt | 6 ++- 5 files changed, 70 insertions(+), 22 deletions(-) diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java index 982492ff..72c575fc 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java @@ -32,6 +32,9 @@ import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPattern.PathMatchInfo; import org.springframework.web.util.pattern.PathPatternParser; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_PREDICATE_MATCHED_PATH_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_PREDICATE_MATCHED_PATH_ROUTE_ID_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.putUriTemplateVariables; import static org.springframework.http.server.PathContainer.parsePath; @@ -101,6 +104,12 @@ public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory pathList = Stream.of("/git/**").collect(Collectors.toList()); + List pathList = Collections.singletonList("/git/**"); - Route route = Route.async().id("git").uri(ROUTE_URI).predicate( - new PathRoutePredicateFactory().apply(new PathRoutePredicateFactory.Config().setPatterns(pathList))) - .build(); + PathRoutePredicateFactory.Config pathConfig = new PathRoutePredicateFactory.Config().setPatterns(pathList); + HostRoutePredicateFactory.Config hostConfig = new HostRoutePredicateFactory.Config() + .setPatterns(Collections.singletonList("**.myhost.com")); + Route route = Route.async().id("git").uri(ROUTE_URI).predicate(new PathRoutePredicateFactory().apply(pathConfig) + .and(new HostRoutePredicateFactory().apply(hostConfig))).build(); ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(ROUTE_URI).build()); exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, route); + exchange.getAttributes().put(GATEWAY_PREDICATE_MATCHED_PATH_ATTR, pathList.get(0)); + exchange.getAttributes().put(GATEWAY_PREDICATE_MATCHED_PATH_ROUTE_ID_ATTR, route.getId()); Tags tags = pathTagsProvider.apply(exchange); assertThat(tags.stream().count()).isEqualTo(1); - assertThat(tags.stream().anyMatch(tag -> "path".equals(tag.getKey()))).isEqualTo(true); + assertThat(tags.stream().anyMatch(tag -> "path".equals(tag.getKey()) && tag.getValue().equals(pathList.get(0)))) + .isEqualTo(true); + } + + @Test + void addsMultiplePathToRoutes() { + List pathList = Collections.singletonList("/git/**"); + List pathList2 = Collections.singletonList("/git2/**"); + + PathRoutePredicateFactory.Config pathConfig = new PathRoutePredicateFactory.Config().setPatterns(pathList); + PathRoutePredicateFactory.Config pathConfig2 = new PathRoutePredicateFactory.Config().setPatterns(pathList2); + Route route = Route.async().id("git").uri(ROUTE_URI).predicate(new PathRoutePredicateFactory().apply(pathConfig) + .or(new PathRoutePredicateFactory().apply(pathConfig2))).build(); + + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(ROUTE_URI).build()); + exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, route); + exchange.getAttributes().put(GATEWAY_PREDICATE_MATCHED_PATH_ATTR, pathList2.get(0)); + exchange.getAttributes().put(GATEWAY_PREDICATE_MATCHED_PATH_ROUTE_ID_ATTR, route.getId()); + + Tags tags = pathTagsProvider.apply(exchange); + assertThat(tags.stream().count()).isEqualTo(1); + assertThat( + tags.stream().anyMatch(tag -> "path".equals(tag.getKey()) && tag.getValue().equals(pathList2.get(0)))) + .isEqualTo(true); } @Test diff --git a/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt b/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt index eafc4002..283c6322 100644 --- a/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt +++ b/spring-cloud-gateway-server/src/test/kotlin/org/springframework/cloud/gateway/route/builder/RouteDslTests.kt @@ -21,6 +21,7 @@ import org.junit.runner.RunWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.EnableAutoConfiguration import org.springframework.boot.test.context.SpringBootTest +import org.springframework.cloud.gateway.support.ServerWebExchangeUtils import org.springframework.context.annotation.Configuration import org.springframework.mock.http.server.reactive.MockServerHttpRequest import org.springframework.mock.web.server.MockServerWebExchange @@ -72,7 +73,10 @@ class RouteDslTests { val sampleExchange: ServerWebExchange = MockServerWebExchange.from(MockServerHttpRequest.get("/image/webp") .header("Host", "test.abc.org").build()) - val filteredRoutes = routeLocator.routes.filter({ it.predicate.apply(sampleExchange).toMono().block() }) + val filteredRoutes = routeLocator.routes.filter({ + sampleExchange.attributes.put(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR, it.id) + it.predicate.apply(sampleExchange).toMono().block() + }) StepVerifier.create(filteredRoutes) .expectNextMatches({