Commit ffc33b4c by Ryan Baxter Committed by GitHub

Merge pull request #2105 from gzurowski/routes-endpoint-details

Provide more Zuul route details in the routes actuator endpoint
parents 1d3773ab 781fa3ea
......@@ -1656,7 +1656,37 @@ To not discard these well known security headers in case Spring Security is on t
If you are using `@EnableZuulProxy` with tha Spring Boot Actuator you
will enable (by default) an additional endpoint, available via HTTP as
`/routes`. A GET to this endpoint will return a list of the mapped
routes. A POST will force a refresh of the existing routes (e.g. in
routes:
.GET /routes
[source,json]
----
{
/stores/**: "http://localhost:8081"
}
----
Additional route details can be requested by adding the `?format=details` query
string to `/routes`. This will produce the following output:
.GET /routes?format=details
[source,json]
----
{
"/stores/**": {
"id": "stores",
"fullPath": "/stores/**",
"location": "http://localhost:8081",
"path": "/**",
"prefix": "/stores",
"retryable": false,
"customSensitiveHeaders": false,
"prefixStripped": true
}
}
----
A POST will force a refresh of the existing routes (e.g. in
case there have been changes in the service catalog). You can disable
this endpoint by setting `endpoints.routes.enabled` to `false`.
......
......@@ -18,7 +18,11 @@ package org.springframework.cloud.netflix.zuul;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
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,112 @@ public class RoutesEndpoint extends AbstractEndpoint<Map<String, String>> {
}
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)
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();
}
public String getId() {
return id;
}
public String getFullPath() {
return fullPath;
}
public String getPath() {
return path;
}
public String getLocation() {
return location;
}
public String getPrefix() {
return prefix;
}
public Boolean getRetryable() {
return retryable;
}
public Set<String> getSensitiveHeaders() {
return sensitiveHeaders;
}
public boolean isCustomSensitiveHeaders() {
return customSensitiveHeaders;
}
public boolean isPrefixStripped() {
return prefixStripped;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RouteDetails that = (RouteDetails) o;
return Objects.equals(id, that.id) &&
Objects.equals(fullPath, that.fullPath) &&
Objects.equals(path, that.path) &&
Objects.equals(location, that.location) &&
Objects.equals(prefix, that.prefix) &&
Objects.equals(retryable, that.retryable) &&
Objects.equals(sensitiveHeaders, that.sensitiveHeaders) &&
customSensitiveHeaders == that.customSensitiveHeaders &&
prefixStripped == that.prefixStripped;
}
@Override
public int hashCode() {
return Objects.hash(id, fullPath, path, location, prefix, retryable,
sensitiveHeaders, customSensitiveHeaders, prefixStripped);
}
}
}
......@@ -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
......@@ -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<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
@EnableAutoConfiguration
@RestController
......
......@@ -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<Route> getRoutes() {
List<Route> 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;
}
......@@ -73,6 +74,16 @@ public class RoutesEndpointTests {
}
@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() {
RoutesEndpoint endpoint = new RoutesEndpoint(locator);
assertEquals("routes", endpoint.getId());
......
......@@ -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<Route> getRoutes() {
List<Route> 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<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