Add a shim Endpoint if management context is child

When management endpoints are on a different port the HandlerMappings
are restricted to a single EndpointHandlerMapping, so the error
controller (which is a normal @Controller with @RequestMappings) does
not get mapped.

Fixed by addinga shim Endpoint on "/error" that delegates to the
ErrorController (which interface picks up an extra method).
pull/152/head
Dave Syer 11 years ago
parent 25d9ac6535
commit bcae284dd9

@ -16,6 +16,8 @@
package org.springframework.boot.actuate.autoconfigure; package org.springframework.boot.actuate.autoconfigure;
import java.util.Map;
import javax.servlet.Filter; import javax.servlet.Filter;
import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactory;
@ -23,18 +25,25 @@ import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.HierarchicalBeanFactory; import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.endpoint.AbstractEndpoint;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerAdapter; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerAdapter;
import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping;
import org.springframework.boot.actuate.properties.ManagementServerProperties; import org.springframework.boot.actuate.properties.ManagementServerProperties;
import org.springframework.boot.actuate.web.ErrorController;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.EmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.ErrorPage;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerAdapter; import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.HandlerMapping;
@ -52,6 +61,9 @@ public class EndpointWebMvcChildContextConfiguration {
protected static class ServerCustomization implements protected static class ServerCustomization implements
EmbeddedServletContainerCustomizer { EmbeddedServletContainerCustomizer {
@Value("${error.path:/error}")
private String errorPath = "/error";
@Autowired @Autowired
private ListableBeanFactory beanFactory; private ListableBeanFactory beanFactory;
@ -69,6 +81,7 @@ public class EndpointWebMvcChildContextConfiguration {
factory.setPort(this.managementServerProperties.getPort()); factory.setPort(this.managementServerProperties.getPort());
factory.setAddress(this.managementServerProperties.getAddress()); factory.setAddress(this.managementServerProperties.getAddress());
factory.setContextPath(this.managementServerProperties.getContextPath()); factory.setContextPath(this.managementServerProperties.getContextPath());
factory.addErrorPages(new ErrorPage(this.errorPath));
} }
} }
@ -96,6 +109,24 @@ public class EndpointWebMvcChildContextConfiguration {
return new EndpointHandlerAdapter(); return new EndpointHandlerAdapter();
} }
/*
* The error controller is present but not mapped as an endpoint in this context
* because of the DispatcherServlet having had it's HandlerMapping explicitly
* disabled. So this tiny shim exposes the same feature but only for machine
* endpoints.
*/
@Bean
public Endpoint<Map<String, Object>> errorEndpoint(final ErrorController controller) {
return new AbstractEndpoint<Map<String, Object>>("/error", false, true) {
@Override
protected Map<String, Object> doInvoke() {
RequestAttributes attributes = RequestContextHolder
.currentRequestAttributes();
return controller.extract(attributes, false);
}
};
}
@Configuration @Configuration
@ConditionalOnClass({ EnableWebSecurity.class, Filter.class }) @ConditionalOnClass({ EnableWebSecurity.class, Filter.class })
@ConditionalOnBean(name = "springSecurityFilterChain", search = SearchStrategy.PARENTS) @ConditionalOnBean(name = "springSecurityFilterChain", search = SearchStrategy.PARENTS)

@ -33,6 +33,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration.DefaultTemplateResolverConfiguration; import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration.DefaultTemplateResolverConfiguration;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
@ -72,7 +73,7 @@ public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustom
private String errorPath = "/error"; private String errorPath = "/error";
@Bean @Bean
@ConditionalOnMissingBean(ErrorController.class) @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController() { public BasicErrorController basicErrorController() {
return new BasicErrorController(); return new BasicErrorController();
} }

@ -33,6 +33,8 @@ import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
/** /**
@ -61,19 +63,27 @@ public class BasicErrorController implements ErrorController {
@RequestMapping(value = "${error.path:/error}", produces = "text/html") @RequestMapping(value = "${error.path:/error}", produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request) { public ModelAndView errorHtml(HttpServletRequest request) {
Map<String, Object> map = error(request); Map<String, Object> map = extract(new ServletRequestAttributes(request), false);
return new ModelAndView(ERROR_KEY, map); return new ModelAndView(ERROR_KEY, map);
} }
@RequestMapping(value = "${error.path:/error}") @RequestMapping(value = "${error.path:/error}")
@ResponseBody @ResponseBody
public Map<String, Object> error(HttpServletRequest request) { public Map<String, Object> error(HttpServletRequest request) {
ServletRequestAttributes attributes = new ServletRequestAttributes(request);
String trace = request.getParameter("trace");
return extract(attributes, trace != null && !"false".equals(trace.toLowerCase()));
}
@Override
public Map<String, Object> extract(RequestAttributes attributes, boolean trace) {
Map<String, Object> map = new LinkedHashMap<String, Object>(); Map<String, Object> map = new LinkedHashMap<String, Object>();
map.put("timestamp", new Date()); map.put("timestamp", new Date());
try { try {
Throwable error = (Throwable) request Throwable error = (Throwable) attributes.getAttribute(
.getAttribute("javax.servlet.error.exception"); "javax.servlet.error.exception", RequestAttributes.SCOPE_REQUEST);
Object obj = request.getAttribute("javax.servlet.error.status_code"); Object obj = attributes.getAttribute("javax.servlet.error.status_code",
RequestAttributes.SCOPE_REQUEST);
int status = 999; int status = 999;
if (obj != null) { if (obj != null) {
status = (Integer) obj; status = (Integer) obj;
@ -89,8 +99,7 @@ public class BasicErrorController implements ErrorController {
} }
map.put("exception", error.getClass().getName()); map.put("exception", error.getClass().getName());
map.put("message", error.getMessage()); map.put("message", error.getMessage());
String trace = request.getParameter("trace"); if (trace) {
if (trace != null && !"false".equals(trace.toLowerCase())) {
StringWriter stackTrace = new StringWriter(); StringWriter stackTrace = new StringWriter();
error.printStackTrace(new PrintWriter(stackTrace)); error.printStackTrace(new PrintWriter(stackTrace));
stackTrace.flush(); stackTrace.flush();
@ -99,7 +108,8 @@ public class BasicErrorController implements ErrorController {
this.logger.error(error); this.logger.error(error);
} }
else { else {
Object message = request.getAttribute("javax.servlet.error.message"); Object message = attributes.getAttribute("javax.servlet.error.message",
RequestAttributes.SCOPE_REQUEST);
map.put("message", message == null ? "No message available" : message); map.put("message", message == null ? "No message available" : message);
} }
return map; return map;

@ -16,7 +16,10 @@
package org.springframework.boot.actuate.web; package org.springframework.boot.actuate.web;
import java.util.Map;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.context.request.RequestAttributes;
/** /**
* Marker interface used to indicate that a {@link Controller @Controller} is used to * Marker interface used to indicate that a {@link Controller @Controller} is used to
@ -31,4 +34,13 @@ public interface ErrorController {
*/ */
public String getErrorPath(); public String getErrorPath();
/**
* Extract a useful model of the error from the request attributes.
*
* @param attributes the request attributes
* @param trace flag to indicate that stack trace information should be included
* @return a model containing error messages and codes etc.
*/
public Map<String, Object> extract(RequestAttributes attributes, boolean trace);
} }

@ -16,6 +16,8 @@
package org.springframework.boot.sample.ops.ui; package org.springframework.boot.sample.ops.ui;
import static org.junit.Assert.assertEquals;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
@ -25,7 +27,6 @@ import java.util.concurrent.TimeUnit;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
@ -35,8 +36,6 @@ import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import static org.junit.Assert.assertEquals;
/** /**
* Integration tests for separate management and main service ports. * Integration tests for separate management and main service ports.
* *
@ -80,9 +79,7 @@ public class SampleActuatorUiApplicationPortTests {
} }
@Test @Test
@Ignore
public void testMetrics() throws Exception { public void testMetrics() throws Exception {
// FIXME broken since error page is not rendered
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
ResponseEntity<Map> entity = getRestTemplate().getForEntity( ResponseEntity<Map> entity = getRestTemplate().getForEntity(
"http://localhost:" + managementPort + "/metrics", Map.class); "http://localhost:" + managementPort + "/metrics", Map.class);

@ -16,6 +16,8 @@
package org.springframework.boot.sample.ops; package org.springframework.boot.sample.ops;
import static org.junit.Assert.assertEquals;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -27,7 +29,6 @@ import java.util.concurrent.TimeUnit;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.security.SecurityProperties;
@ -44,8 +45,6 @@ import org.springframework.security.crypto.codec.Base64;
import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import static org.junit.Assert.assertEquals;
/** /**
* Integration tests for separate management and main service ports. * Integration tests for separate management and main service ports.
* *
@ -93,9 +92,7 @@ public class ManagementAddressSampleActuatorApplicationTests {
} }
@Test @Test
@Ignore
public void testMetrics() throws Exception { public void testMetrics() throws Exception {
// FIXME broken because error page is no longer exposed on management port
testHome(); // makes sure some requests have been made testHome(); // makes sure some requests have been made
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
ResponseEntity<Map> entity = getRestTemplate().getForEntity( ResponseEntity<Map> entity = getRestTemplate().getForEntity(
@ -104,9 +101,7 @@ public class ManagementAddressSampleActuatorApplicationTests {
} }
@Test @Test
@Ignore
public void testHealth() throws Exception { public void testHealth() throws Exception {
// FIXME broken because error page is no longer exposed on management port
ResponseEntity<String> entity = getRestTemplate().getForEntity( ResponseEntity<String> entity = getRestTemplate().getForEntity(
"http://localhost:" + managementPort + "/health", String.class); "http://localhost:" + managementPort + "/health", String.class);
assertEquals(HttpStatus.OK, entity.getStatusCode()); assertEquals(HttpStatus.OK, entity.getStatusCode());
@ -114,9 +109,7 @@ public class ManagementAddressSampleActuatorApplicationTests {
} }
@Test @Test
@Ignore
public void testErrorPage() throws Exception { public void testErrorPage() throws Exception {
// FIXME broken because error page is no longer exposed on management port
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
ResponseEntity<Map> entity = getRestTemplate().getForEntity( ResponseEntity<Map> entity = getRestTemplate().getForEntity(
"http://localhost:" + managementPort + "/error", Map.class); "http://localhost:" + managementPort + "/error", Map.class);

Loading…
Cancel
Save