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;
import java.util.Map;
import javax.servlet.Filter;
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.ListableBeanFactory;
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.EndpointHandlerMapping;
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.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.EmbeddedServletContainer;
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.Configuration;
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.HandlerAdapter;
import org.springframework.web.servlet.HandlerMapping;
@ -52,6 +61,9 @@ public class EndpointWebMvcChildContextConfiguration {
protected static class ServerCustomization implements
EmbeddedServletContainerCustomizer {
@Value("${error.path:/error}")
private String errorPath = "/error";
@Autowired
private ListableBeanFactory beanFactory;
@ -69,6 +81,7 @@ public class EndpointWebMvcChildContextConfiguration {
factory.setPort(this.managementServerProperties.getPort());
factory.setAddress(this.managementServerProperties.getAddress());
factory.setContextPath(this.managementServerProperties.getContextPath());
factory.addErrorPages(new ErrorPage(this.errorPath));
}
}
@ -96,6 +109,24 @@ public class EndpointWebMvcChildContextConfiguration {
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
@ConditionalOnClass({ EnableWebSecurity.class, Filter.class })
@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.ConditionalOnMissingBean;
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.thymeleaf.ThymeleafAutoConfiguration.DefaultTemplateResolverConfiguration;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
@ -72,7 +73,7 @@ public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustom
private String errorPath = "/error";
@Bean
@ConditionalOnMissingBean(ErrorController.class)
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController() {
return new BasicErrorController();
}

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

@ -16,7 +16,10 @@
package org.springframework.boot.actuate.web;
import java.util.Map;
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
@ -31,4 +34,13 @@ public interface ErrorController {
*/
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;
import static org.junit.Assert.assertEquals;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.Callable;
@ -25,7 +27,6 @@ import java.util.concurrent.TimeUnit;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.boot.SpringApplication;
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.RestTemplate;
import static org.junit.Assert.assertEquals;
/**
* Integration tests for separate management and main service ports.
*
@ -80,9 +79,7 @@ public class SampleActuatorUiApplicationPortTests {
}
@Test
@Ignore
public void testMetrics() throws Exception {
// FIXME broken since error page is not rendered
@SuppressWarnings("rawtypes")
ResponseEntity<Map> entity = getRestTemplate().getForEntity(
"http://localhost:" + managementPort + "/metrics", Map.class);

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

Loading…
Cancel
Save