Merge pull request #2105 from gzurowski/routes-endpoint-details
Provide more Zuul route details in the routes actuator endpoint
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -72,6 +73,16 @@ public class RoutesEndpointTests {
|
||||
assertEquals(result , endpoint.invoke());
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user