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 ...@@ -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 If you are using `@EnableZuulProxy` with tha Spring Boot Actuator you
will enable (by default) an additional endpoint, available via HTTP as 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 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 case there have been changes in the service catalog). You can disable
this endpoint by setting `endpoints.routes.enabled` to `false`. this endpoint by setting `endpoints.routes.enabled` to `false`.
......
...@@ -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.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.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,112 @@ public class RoutesEndpoint extends AbstractEndpoint<Map<String, String>> { ...@@ -59,4 +64,112 @@ 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)
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 @@ ...@@ -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