From eb1b7baf1cbbe296260d88ab55a27142fe8dd382 Mon Sep 17 00:00:00 2001 From: Gregor Zurowski Date: Sat, 8 Jul 2017 22:40:39 +0200 Subject: [PATCH] Provide more Zuul route details in the routes actuator endpoint Add a parameter to control the level of details returned with the routes actuator endpoint. If requested, return all available Zuul route details including ID, prefix, sensitive headers, etc. Signed-off-by: Gregor Zurowski --- .../cloud/netflix/zuul/RoutesEndpoint.java | 56 +++++++++++++++++++ .../cloud/netflix/zuul/RoutesMvcEndpoint.java | 26 ++++++++- .../zuul/RoutesEndpointIntegrationTests.java | 23 ++++++++ .../netflix/zuul/RoutesEndpointTests.java | 13 ++++- .../netflix/zuul/RoutesMvcEndpointTests.java | 14 ++++- 5 files changed, 129 insertions(+), 3 deletions(-) diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RoutesEndpoint.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RoutesEndpoint.java index 39b688e8..d10bc9ba 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RoutesEndpoint.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RoutesEndpoint.java @@ -18,7 +18,11 @@ package org.springframework.cloud.netflix.zuul; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Data; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.endpoint.AbstractEndpoint; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -34,6 +38,7 @@ import org.springframework.jmx.export.annotation.ManagedResource; * @author Spencer Gibb * @author Dave Syer * @author Ryan Baxter + * @author Gregor Zurowski */ @ManagedResource(description = "Can be used to list the reverse proxy routes") @ConfigurationProperties(prefix = "endpoints.routes") @@ -59,4 +64,55 @@ public class RoutesEndpoint extends AbstractEndpoint> { } return map; } + + Map invokeRouteDetails() { + Map map = new LinkedHashMap<>(); + for (Route route : this.routes.getRoutes()) { + map.put(route.getFullPath(), new RouteDetails(route)); + } + return map; + } + + /** + * Container for exposing Zuul {@link Route} details as JSON. + */ + @JsonPropertyOrder({ "id", "fullPath", "location" }) + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @Data + public static class RouteDetails { + + private String id; + + private String fullPath; + + private String path; + + private String location; + + private String prefix; + + private Boolean retryable; + + private Set sensitiveHeaders; + + private boolean customSensitiveHeaders; + + private boolean prefixStripped; + + public RouteDetails() { + } + + RouteDetails(final Route route) { + this.id = route.getId(); + this.fullPath = route.getFullPath(); + this.path = route.getPath(); + this.location = route.getLocation(); + this.prefix = route.getPrefix(); + this.retryable = route.getRetryable(); + this.sensitiveHeaders = route.getSensitiveHeaders(); + this.customSensitiveHeaders = route.isCustomSensitiveHeaders(); + this.prefixStripped = route.isPrefixStripped(); + } + } + } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RoutesMvcEndpoint.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RoutesMvcEndpoint.java index 17e05640..c0f801e5 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RoutesMvcEndpoint.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RoutesMvcEndpoint.java @@ -18,28 +18,38 @@ package org.springframework.cloud.netflix.zuul; +import org.springframework.boot.actuate.endpoint.mvc.ActuatorMediaTypes; import org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter; +import org.springframework.cloud.netflix.zuul.filters.Route; import org.springframework.cloud.netflix.zuul.filters.RouteLocator; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.http.MediaType; import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; /** * Endpoint used to reset the reverse proxy routes * @author Ryan Baxter + * @author Gregor Zurowski */ @ManagedResource(description = "Can be used to reset the reverse proxy routes") public class RoutesMvcEndpoint extends EndpointMvcAdapter implements ApplicationEventPublisherAware { + static final String FORMAT_DETAILS = "details"; + + private final RoutesEndpoint endpoint; private RouteLocator routes; private ApplicationEventPublisher publisher; public RoutesMvcEndpoint(RoutesEndpoint endpoint, RouteLocator routes) { super(endpoint); + this.endpoint = endpoint; this.routes = routes; } @@ -55,4 +65,18 @@ public class RoutesMvcEndpoint extends EndpointMvcAdapter implements Application this.publisher.publishEvent(new RoutesRefreshedEvent(this.routes)); return super.invoke(); } -} + + /** + * Expose Zuul {@link Route} information with details. + */ + @GetMapping(params = "format", produces = { ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON_VALUE, + MediaType.APPLICATION_JSON_VALUE }) + @ResponseBody + public Object invokeRouteDetails(@RequestParam String format) { + if (FORMAT_DETAILS.equalsIgnoreCase(format)) { + return endpoint.invokeRouteDetails(); + } else { + return super.invoke(); + } + } +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesEndpointIntegrationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesEndpointIntegrationTests.java index b5a54c60..d2386d23 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesEndpointIntegrationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesEndpointIntegrationTests.java @@ -27,16 +27,23 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Configuration; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.web.bind.annotation.RestController; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; /** * @author Ryan Baxter + * @author Gregor Zurowski */ @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest( @@ -64,6 +71,22 @@ public class RoutesEndpointIntegrationTests { assertTrue(refreshListener.wasCalled()); } + @Test + public void getRouteDetailsTest() { + ResponseEntity> responseEntity = restTemplate.exchange( + "/admin/routes?format=details", HttpMethod.GET, null, new ParameterizedTypeReference>() { + }); + + assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK)); + + RoutesEndpoint.RouteDetails details = responseEntity.getBody().get("/sslservice/**"); + assertThat(details.getPath(), is("/**")); + assertThat(details.getFullPath(), is("/sslservice/**")); + assertThat(details.getLocation(), is("https://localhost:8443")); + assertThat(details.getPrefix(), is("/sslservice")); + assertTrue(details.isPrefixStripped()); + } + @Configuration @EnableAutoConfiguration @RestController diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesEndpointTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesEndpointTests.java index 08340e14..53a90fbc 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesEndpointTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesEndpointTests.java @@ -34,6 +34,7 @@ import static org.junit.Assert.assertTrue; /** * @author Ryan Baxter + * @author Gregor Zurowski */ public class RoutesEndpointTests { @@ -51,7 +52,7 @@ public class RoutesEndpointTests { public List getRoutes() { List routes = new ArrayList<>(); routes.add(new Route("foo", "foopath", "foolocation", null, true, Collections.EMPTY_SET)); - routes.add(new Route("bar", "barpath", "barlocation", null, true, Collections.EMPTY_SET)); + routes.add(new Route("bar", "barpath", "barlocation", "/bar-prefix", true, Collections.EMPTY_SET)); return routes; } @@ -72,6 +73,16 @@ public class RoutesEndpointTests { assertEquals(result , endpoint.invoke()); } + @Test + public void testInvokeRouteDetails() { + RoutesEndpoint endpoint = new RoutesEndpoint(locator); + Map results = new HashMap<>(); + for (Route route : locator.getRoutes()) { + results.put(route.getFullPath(), new RoutesEndpoint.RouteDetails(route)); + } + assertEquals(results, endpoint.invokeRouteDetails()); + } + @Test public void testId() { RoutesEndpoint endpoint = new RoutesEndpoint(locator); diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesMvcEndpointTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesMvcEndpointTests.java index e59ebfec..248ecad6 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesMvcEndpointTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RoutesMvcEndpointTests.java @@ -42,6 +42,7 @@ import static org.mockito.Mockito.verify; /** * @author Ryan Baxter + * @author Gregor Zurowski */ @SpringBootTest @RunWith(MockitoJUnitRunner.class) @@ -63,7 +64,7 @@ public class RoutesMvcEndpointTests { public List getRoutes() { List routes = new ArrayList<>(); routes.add(new Route("foo", "foopath", "foolocation", null, true, Collections.EMPTY_SET)); - routes.add(new Route("bar", "barpath", "barlocation", null, true, Collections.EMPTY_SET)); + routes.add(new Route("bar", "barpath", "barlocation", "bar-prefix", true, Collections.EMPTY_SET)); return routes; } @@ -88,4 +89,15 @@ public class RoutesMvcEndpointTests { verify(publisher, times(1)).publishEvent(isA(RoutesRefreshedEvent.class)); } + @Test + public void routeDetails() throws Exception { + RoutesMvcEndpoint mvcEndpoint = new RoutesMvcEndpoint(endpoint, locator); + Map results = new HashMap<>(); + for (Route route : locator.getRoutes()) { + results.put(route.getFullPath(), new RoutesEndpoint.RouteDetails(route)); + } + assertEquals(results, mvcEndpoint.invokeRouteDetails(RoutesMvcEndpoint.FORMAT_DETAILS)); + verify(endpoint, times(1)).invokeRouteDetails(); + } + } \ No newline at end of file