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).
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user