diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java index 0ad878b4bd..7517c9a0b3 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java @@ -33,10 +33,12 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.actuate.endpoint.EnvironmentEndpoint; +import org.springframework.boot.actuate.endpoint.HealthEndpoint; import org.springframework.boot.actuate.endpoint.MetricsEndpoint; import org.springframework.boot.actuate.endpoint.ShutdownEndpoint; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; import org.springframework.boot.actuate.endpoint.mvc.ShutdownMvcEndpoint; @@ -54,6 +56,7 @@ import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext; import org.springframework.boot.context.embedded.EmbeddedServletContainerException; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; @@ -72,16 +75,18 @@ import org.springframework.web.servlet.DispatcherServlet; * different port to {@link ServerProperties} a new child context is created, otherwise it * is assumed that endpoint requests will be mapped and handled via an already registered * {@link DispatcherServlet}. - * + * * @author Dave Syer * @author Phillip Webb + * @author Christian Dupuis */ @Configuration @ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) @ConditionalOnWebApplication @AutoConfigureAfter({ PropertyPlaceholderAutoConfiguration.class, - EmbeddedServletContainerAutoConfiguration.class, WebMvcAutoConfiguration.class, - ManagementServerPropertiesAutoConfiguration.class }) + EmbeddedServletContainerAutoConfiguration.class, WebMvcAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class }) +@EnableConfigurationProperties(HealthMvcEndpointProperties.class) public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, ApplicationListener { @@ -89,6 +94,9 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, private ApplicationContext applicationContext; + @Autowired + private HealthMvcEndpointProperties healthMvcEndpointProperties; + @Autowired private ManagementServerProperties managementServerProperties; @@ -101,8 +109,8 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, @Bean @ConditionalOnMissingBean public EndpointHandlerMapping endpointHandlerMapping() { - EndpointHandlerMapping mapping = new EndpointHandlerMapping(mvcEndpoints() - .getEndpoints()); + EndpointHandlerMapping mapping = new EndpointHandlerMapping( + mvcEndpoints().getEndpoints()); boolean disabled = ManagementServerPort.get(this.applicationContext) != ManagementServerPort.SAME; mapping.setDisabled(disabled); if (!disabled) { @@ -125,10 +133,12 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, // instantiation of ManagementServerProperties. @Configuration protected static class ApplicationContextFilterConfiguration { + @Bean public Filter applicationContextIdFilter(ApplicationContext context) { final String id = context.getId(); return new OncePerRequestFilter() { + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -153,6 +163,17 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, return new EnvironmentMvcEndpoint(delegate); } + @Bean + @ConditionalOnBean(HealthEndpoint.class) + @ConditionalOnExpression("${endpoints.health.enabled:true}") + public HealthMvcEndpoint healthMvcEndpoint(HealthEndpoint delegate) { + HealthMvcEndpoint healthMvcEndpoint = new HealthMvcEndpoint(delegate); + if (this.healthMvcEndpointProperties.getMapping() != null) { + healthMvcEndpoint.setStatusMapping(this.healthMvcEndpointProperties.getMapping()); + } + return healthMvcEndpoint; + } + @Bean @ConditionalOnBean(MetricsEndpoint.class) @ConditionalOnExpression("${endpoints.metrics.enabled:true}") @@ -184,27 +205,25 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, // Ensure close on the parent also closes the child if (this.applicationContext instanceof ConfigurableApplicationContext) { - ((ConfigurableApplicationContext) this.applicationContext) - .addApplicationListener(new ApplicationListener() { - @Override - public void onApplicationEvent(ContextClosedEvent event) { - if (event.getApplicationContext() == EndpointWebMvcAutoConfiguration.this.applicationContext) { - childContext.close(); - } - } - }); + ((ConfigurableApplicationContext) this.applicationContext).addApplicationListener(new ApplicationListener() { + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + if (event.getApplicationContext() == EndpointWebMvcAutoConfiguration.this.applicationContext) { + childContext.close(); + } + } + }); } try { childContext.refresh(); - } - catch (RuntimeException ex) { + } catch (RuntimeException ex) { // No support currently for deploying a war with management.port=, // and this is the signature of that happening if (ex instanceof EmbeddedServletContainerException || ex.getCause() instanceof EmbeddedServletContainerException) { logger.warn("Could not start embedded container (management endpoints are still available through JMX)"); - } - else { + } else { throw ex; } } @@ -219,17 +238,14 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, ServerProperties serverProperties; try { serverProperties = beanFactory.getBean(ServerProperties.class); - } - catch (NoSuchBeanDefinitionException ex) { + } catch (NoSuchBeanDefinitionException ex) { serverProperties = new ServerProperties(); } ManagementServerProperties managementServerProperties; try { - managementServerProperties = beanFactory - .getBean(ManagementServerProperties.class); - } - catch (NoSuchBeanDefinitionException ex) { + managementServerProperties = beanFactory.getBean(ManagementServerProperties.class); + } catch (NoSuchBeanDefinitionException ex) { managementServerProperties = new ManagementServerProperties(); } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointProperties.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointProperties.java new file mode 100644 index 0000000000..7b147adac9 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointProperties.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.HttpStatus; + +/** + * Configuration properties for the {@link HealthMvcEndpoint}. + * + * @author Christian Dupuis + * @since 1.1.0 + */ +@ConfigurationProperties(prefix = "endpoints.health") +public class HealthMvcEndpointProperties { + + private Map mapping = new HashMap(); + + public Map getMapping() { + return this.mapping; + } + + public void setMapping(Map mapping) { + this.mapping = mapping; + } +} \ No newline at end of file diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/HealthEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/HealthEndpoint.java index ed43df33b4..bfaf672e8c 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/HealthEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/HealthEndpoint.java @@ -31,7 +31,7 @@ import org.springframework.util.Assert; * @author Dave Syer * @author Christian Dupuis */ -@ConfigurationProperties(prefix = "endpoints.health", ignoreUnknownFields = false) +@ConfigurationProperties(prefix = "endpoints.health", ignoreUnknownFields = true) public class HealthEndpoint extends AbstractEndpoint { private final HealthIndicator healthIndicator; @@ -48,8 +48,7 @@ public class HealthEndpoint extends AbstractEndpoint { if (healthIndicators.size() == 1) { this.healthIndicator = healthIndicators.values().iterator().next(); - } - else { + } else { CompositeHealthIndicator healthIndicator = new CompositeHealthIndicator( healthAggregator); for (Map.Entry h : healthIndicators.entrySet()) { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java new file mode 100644 index 0000000000..aa80dca592 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint.mvc; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.actuate.endpoint.HealthEndpoint; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * Adapter to expose {@link HealthEndpoint} as an {@link MvcEndpoint}. + * + * @author Christian Dupuis + * @since 1.1.0 + */ +public class HealthMvcEndpoint extends EndpointMvcAdapter { + + private Map statusMapping; + + public HealthMvcEndpoint(HealthEndpoint delegate) { + super(delegate); + setupDefaultStatusMapping(); + } + + public void setStatusMapping(Map statusMapping) { + Assert.notNull(statusMapping, "StatusMapping must not be null"); + this.statusMapping = statusMapping; + } + + @RequestMapping + @ResponseBody + @Override + public Object invoke() { + if (!this.getDelegate().isEnabled()) { + // Shouldn't happen + return new ResponseEntity>(Collections.singletonMap( + "message", "This endpoint is disabled"), HttpStatus.NOT_FOUND); + } + + Health health = (Health) getDelegate().invoke(); + Status status = health.getStatus(); + if (this.statusMapping.containsKey(status.getCode())) { + return new ResponseEntity(health, + this.statusMapping.get(status.getCode())); + } + return health; + } + + private void setupDefaultStatusMapping() { + this.statusMapping = new HashMap(); + this.statusMapping.put(Status.DOWN.getCode(), HttpStatus.SERVICE_UNAVAILABLE); + this.statusMapping.put(Status.OUT_OF_SERVICE.getCode(), + HttpStatus.SERVICE_UNAVAILABLE); + } +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpointTests.java new file mode 100644 index 0000000000..33e7a8d4db --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpointTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint.mvc; + +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.boot.actuate.endpoint.HealthEndpoint; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link HealthMvcEndpoint}. + * + * @author Christian Dupuis + */ +public class HealthMvcEndpointTests { + + private HealthEndpoint endpoint = null; + + private HealthMvcEndpoint mvc = null; + + @Before + public void init() { + this.endpoint = mock(HealthEndpoint.class); + when(this.endpoint.isEnabled()).thenReturn(true); + this.mvc = new HealthMvcEndpoint(this.endpoint); + } + + @Test + public void up() { + when(this.endpoint.invoke()).thenReturn(new Health().up()); + Object result = this.mvc.invoke(); + assertTrue(result instanceof Health); + assertTrue(((Health) result).getStatus() == Status.UP); + } + + @SuppressWarnings("unchecked") + @Test + public void down() { + when(this.endpoint.invoke()).thenReturn(new Health().down()); + Object result = this.mvc.invoke(); + assertTrue(result instanceof ResponseEntity); + ResponseEntity response = (ResponseEntity) result; + assertTrue(response.getBody().getStatus() == Status.DOWN); + assertEquals(HttpStatus.SERVICE_UNAVAILABLE, response.getStatusCode()); + } + + @SuppressWarnings("unchecked") + @Test + public void customMapping() { + when(this.endpoint.invoke()).thenReturn(new Health().status(new Status("OK"))); + this.mvc.setStatusMapping(Collections.singletonMap("OK", + HttpStatus.INTERNAL_SERVER_ERROR)); + Object result = this.mvc.invoke(); + assertTrue(result instanceof ResponseEntity); + ResponseEntity response = (ResponseEntity) result; + assertTrue(response.getBody().getStatus().equals(new Status("OK"))); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + } +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeHealthIndicatorTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeHealthIndicatorTests.java index fa210dce59..eec4bf866d 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeHealthIndicatorTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeHealthIndicatorTests.java @@ -20,16 +20,15 @@ import java.util.HashMap; import java.util.Map; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; @@ -111,7 +110,6 @@ public class CompositeHealthIndicatorTests { } @Test - @Ignore public void testSerialization() throws Exception { Map indicators = new HashMap(); indicators.put("db1", this.one); @@ -125,9 +123,9 @@ public class CompositeHealthIndicatorTests { Health result = composite.health(); ObjectMapper mapper = new ObjectMapper(); - mapper.configure(SerializationFeature.INDENT_OUTPUT, true); - String test = mapper.writeValueAsString(result); - System.out.println(test); + assertEquals( + "{\"status\":\"UNKOWN\",\"db\":{\"status\":\"UNKOWN\",\"db1\":{\"status\":\"UNKOWN\",\"1\":\"1\"},\"db2\":{\"status\":\"UNKOWN\",\"2\":\"2\"}}}", + mapper.writeValueAsString(result)); } }