Commit eb1b7baf by Gregor Zurowski

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: 's avatarGregor Zurowski <gregor@zurowski.org>
parent c2c63470
...@@ -18,7 +18,11 @@ package org.springframework.cloud.netflix.zuul; ...@@ -18,7 +18,11 @@ package org.springframework.cloud.netflix.zuul;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; 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.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.AbstractEndpoint; import org.springframework.boot.actuate.endpoint.AbstractEndpoint;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
...@@ -34,6 +38,7 @@ import org.springframework.jmx.export.annotation.ManagedResource; ...@@ -34,6 +38,7 @@ import org.springframework.jmx.export.annotation.ManagedResource;
* @author Spencer Gibb * @author Spencer Gibb
* @author Dave Syer * @author Dave Syer
* @author Ryan Baxter * @author Ryan Baxter
* @author Gregor Zurowski
*/ */
@ManagedResource(description = "Can be used to list the reverse proxy routes") @ManagedResource(description = "Can be used to list the reverse proxy routes")
@ConfigurationProperties(prefix = "endpoints.routes") @ConfigurationProperties(prefix = "endpoints.routes")
...@@ -59,4 +64,55 @@ public class RoutesEndpoint extends AbstractEndpoint<Map<String, String>> { ...@@ -59,4 +64,55 @@ public class RoutesEndpoint extends AbstractEndpoint<Map<String, String>> {
} }
return map; return map;
} }
Map<String, RouteDetails> invokeRouteDetails() {
Map<String, RouteDetails> 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<String> 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();
}
}
} }
...@@ -18,28 +18,38 @@ ...@@ -18,28 +18,38 @@
package org.springframework.cloud.netflix.zuul; 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.boot.actuate.endpoint.mvc.EndpointMvcAdapter;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator; import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.http.MediaType;
import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource; 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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
/** /**
* Endpoint used to reset the reverse proxy routes * Endpoint used to reset the reverse proxy routes
* @author Ryan Baxter * @author Ryan Baxter
* @author Gregor Zurowski
*/ */
@ManagedResource(description = "Can be used to reset the reverse proxy routes") @ManagedResource(description = "Can be used to reset the reverse proxy routes")
public class RoutesMvcEndpoint extends EndpointMvcAdapter implements ApplicationEventPublisherAware { public class RoutesMvcEndpoint extends EndpointMvcAdapter implements ApplicationEventPublisherAware {
static final String FORMAT_DETAILS = "details";
private final RoutesEndpoint endpoint;
private RouteLocator routes; private RouteLocator routes;
private ApplicationEventPublisher publisher; private ApplicationEventPublisher publisher;
public RoutesMvcEndpoint(RoutesEndpoint endpoint, RouteLocator routes) { public RoutesMvcEndpoint(RoutesEndpoint endpoint, RouteLocator routes) {
super(endpoint); super(endpoint);
this.endpoint = endpoint;
this.routes = routes; this.routes = routes;
} }
...@@ -55,4 +65,18 @@ public class RoutesMvcEndpoint extends EndpointMvcAdapter implements Application ...@@ -55,4 +65,18 @@ public class RoutesMvcEndpoint extends EndpointMvcAdapter implements Application
this.publisher.publishEvent(new RoutesRefreshedEvent(this.routes)); this.publisher.publishEvent(new RoutesRefreshedEvent(this.routes));
return super.invoke(); 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
...@@ -27,16 +27,23 @@ import org.springframework.boot.test.context.SpringBootTest; ...@@ -27,16 +27,23 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Configuration; 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.stereotype.Component;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
/** /**
* @author Ryan Baxter * @author Ryan Baxter
* @author Gregor Zurowski
*/ */
@RunWith(SpringJUnit4ClassRunner.class) @RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest( @SpringBootTest(
...@@ -64,6 +71,22 @@ public class RoutesEndpointIntegrationTests { ...@@ -64,6 +71,22 @@ public class RoutesEndpointIntegrationTests {
assertTrue(refreshListener.wasCalled()); assertTrue(refreshListener.wasCalled());
} }
@Test
public void getRouteDetailsTest() {
ResponseEntity<Map<String, RoutesEndpoint.RouteDetails>> responseEntity = restTemplate.exchange(
"/admin/routes?format=details", HttpMethod.GET, null, new ParameterizedTypeReference<Map<String, RoutesEndpoint.RouteDetails>>() {
});
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 @Configuration
@EnableAutoConfiguration @EnableAutoConfiguration
@RestController @RestController
......
...@@ -34,6 +34,7 @@ import static org.junit.Assert.assertTrue; ...@@ -34,6 +34,7 @@ import static org.junit.Assert.assertTrue;
/** /**
* @author Ryan Baxter * @author Ryan Baxter
* @author Gregor Zurowski
*/ */
public class RoutesEndpointTests { public class RoutesEndpointTests {
...@@ -51,7 +52,7 @@ public class RoutesEndpointTests { ...@@ -51,7 +52,7 @@ public class RoutesEndpointTests {
public List<Route> getRoutes() { public List<Route> getRoutes() {
List<Route> routes = new ArrayList<>(); List<Route> routes = new ArrayList<>();
routes.add(new Route("foo", "foopath", "foolocation", null, true, Collections.EMPTY_SET)); 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; return routes;
} }
...@@ -73,6 +74,16 @@ public class RoutesEndpointTests { ...@@ -73,6 +74,16 @@ public class RoutesEndpointTests {
} }
@Test @Test
public void testInvokeRouteDetails() {
RoutesEndpoint endpoint = new RoutesEndpoint(locator);
Map<String, RoutesEndpoint.RouteDetails> results = new HashMap<>();
for (Route route : locator.getRoutes()) {
results.put(route.getFullPath(), new RoutesEndpoint.RouteDetails(route));
}
assertEquals(results, endpoint.invokeRouteDetails());
}
@Test
public void testId() { public void testId() {
RoutesEndpoint endpoint = new RoutesEndpoint(locator); RoutesEndpoint endpoint = new RoutesEndpoint(locator);
assertEquals("routes", endpoint.getId()); assertEquals("routes", endpoint.getId());
......
...@@ -42,6 +42,7 @@ import static org.mockito.Mockito.verify; ...@@ -42,6 +42,7 @@ import static org.mockito.Mockito.verify;
/** /**
* @author Ryan Baxter * @author Ryan Baxter
* @author Gregor Zurowski
*/ */
@SpringBootTest @SpringBootTest
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
...@@ -63,7 +64,7 @@ public class RoutesMvcEndpointTests { ...@@ -63,7 +64,7 @@ public class RoutesMvcEndpointTests {
public List<Route> getRoutes() { public List<Route> getRoutes() {
List<Route> routes = new ArrayList<>(); List<Route> routes = new ArrayList<>();
routes.add(new Route("foo", "foopath", "foolocation", null, true, Collections.EMPTY_SET)); 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; return routes;
} }
...@@ -88,4 +89,15 @@ public class RoutesMvcEndpointTests { ...@@ -88,4 +89,15 @@ public class RoutesMvcEndpointTests {
verify(publisher, times(1)).publishEvent(isA(RoutesRefreshedEvent.class)); verify(publisher, times(1)).publishEvent(isA(RoutesRefreshedEvent.class));
} }
@Test
public void routeDetails() throws Exception {
RoutesMvcEndpoint mvcEndpoint = new RoutesMvcEndpoint(endpoint, locator);
Map<String, RoutesEndpoint.RouteDetails> 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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment