Commit 0c2bc99a authored by Christian Dupuis's avatar Christian Dupuis

Return different http return status codes for different system health states

fixes #880
parent 4bc6a0f4
...@@ -33,10 +33,12 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; ...@@ -33,10 +33,12 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.actuate.endpoint.EnvironmentEndpoint; 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.MetricsEndpoint;
import org.springframework.boot.actuate.endpoint.ShutdownEndpoint; import org.springframework.boot.actuate.endpoint.ShutdownEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping;
import org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint; 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.MetricsMvcEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints;
import org.springframework.boot.actuate.endpoint.mvc.ShutdownMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.ShutdownMvcEndpoint;
...@@ -54,6 +56,7 @@ import org.springframework.boot.autoconfigure.web.ServerProperties; ...@@ -54,6 +56,7 @@ import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext; import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext;
import org.springframework.boot.context.embedded.EmbeddedServletContainerException; import org.springframework.boot.context.embedded.EmbeddedServletContainerException;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
...@@ -72,16 +75,18 @@ import org.springframework.web.servlet.DispatcherServlet; ...@@ -72,16 +75,18 @@ import org.springframework.web.servlet.DispatcherServlet;
* different port to {@link ServerProperties} a new child context is created, otherwise it * 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 * is assumed that endpoint requests will be mapped and handled via an already registered
* {@link DispatcherServlet}. * {@link DispatcherServlet}.
* *
* @author Dave Syer * @author Dave Syer
* @author Phillip Webb * @author Phillip Webb
* @author Christian Dupuis
*/ */
@Configuration @Configuration
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@ConditionalOnWebApplication @ConditionalOnWebApplication
@AutoConfigureAfter({ PropertyPlaceholderAutoConfiguration.class, @AutoConfigureAfter({ PropertyPlaceholderAutoConfiguration.class,
EmbeddedServletContainerAutoConfiguration.class, WebMvcAutoConfiguration.class, EmbeddedServletContainerAutoConfiguration.class, WebMvcAutoConfiguration.class,
ManagementServerPropertiesAutoConfiguration.class }) ManagementServerPropertiesAutoConfiguration.class })
@EnableConfigurationProperties(HealthMvcEndpointProperties.class)
public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware,
ApplicationListener<ContextRefreshedEvent> { ApplicationListener<ContextRefreshedEvent> {
...@@ -89,6 +94,9 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, ...@@ -89,6 +94,9 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware,
private ApplicationContext applicationContext; private ApplicationContext applicationContext;
@Autowired
private HealthMvcEndpointProperties healthMvcEndpointProperties;
@Autowired @Autowired
private ManagementServerProperties managementServerProperties; private ManagementServerProperties managementServerProperties;
...@@ -101,8 +109,8 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, ...@@ -101,8 +109,8 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware,
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public EndpointHandlerMapping endpointHandlerMapping() { public EndpointHandlerMapping endpointHandlerMapping() {
EndpointHandlerMapping mapping = new EndpointHandlerMapping(mvcEndpoints() EndpointHandlerMapping mapping = new EndpointHandlerMapping(
.getEndpoints()); mvcEndpoints().getEndpoints());
boolean disabled = ManagementServerPort.get(this.applicationContext) != ManagementServerPort.SAME; boolean disabled = ManagementServerPort.get(this.applicationContext) != ManagementServerPort.SAME;
mapping.setDisabled(disabled); mapping.setDisabled(disabled);
if (!disabled) { if (!disabled) {
...@@ -125,10 +133,12 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, ...@@ -125,10 +133,12 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware,
// instantiation of ManagementServerProperties. // instantiation of ManagementServerProperties.
@Configuration @Configuration
protected static class ApplicationContextFilterConfiguration { protected static class ApplicationContextFilterConfiguration {
@Bean @Bean
public Filter applicationContextIdFilter(ApplicationContext context) { public Filter applicationContextIdFilter(ApplicationContext context) {
final String id = context.getId(); final String id = context.getId();
return new OncePerRequestFilter() { return new OncePerRequestFilter() {
@Override @Override
protected void doFilterInternal(HttpServletRequest request, protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain) HttpServletResponse response, FilterChain filterChain)
...@@ -153,6 +163,17 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, ...@@ -153,6 +163,17 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware,
return new EnvironmentMvcEndpoint(delegate); 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 @Bean
@ConditionalOnBean(MetricsEndpoint.class) @ConditionalOnBean(MetricsEndpoint.class)
@ConditionalOnExpression("${endpoints.metrics.enabled:true}") @ConditionalOnExpression("${endpoints.metrics.enabled:true}")
...@@ -184,27 +205,25 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, ...@@ -184,27 +205,25 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware,
// Ensure close on the parent also closes the child // Ensure close on the parent also closes the child
if (this.applicationContext instanceof ConfigurableApplicationContext) { if (this.applicationContext instanceof ConfigurableApplicationContext) {
((ConfigurableApplicationContext) this.applicationContext) ((ConfigurableApplicationContext) this.applicationContext).addApplicationListener(new ApplicationListener<ContextClosedEvent>() {
.addApplicationListener(new ApplicationListener<ContextClosedEvent>() {
@Override @Override
public void onApplicationEvent(ContextClosedEvent event) { public void onApplicationEvent(ContextClosedEvent event) {
if (event.getApplicationContext() == EndpointWebMvcAutoConfiguration.this.applicationContext) { if (event.getApplicationContext() == EndpointWebMvcAutoConfiguration.this.applicationContext) {
childContext.close(); childContext.close();
} }
} }
}); });
} }
try { try {
childContext.refresh(); childContext.refresh();
} } catch (RuntimeException ex) {
catch (RuntimeException ex) {
// No support currently for deploying a war with management.port=<different>, // No support currently for deploying a war with management.port=<different>,
// and this is the signature of that happening // and this is the signature of that happening
if (ex instanceof EmbeddedServletContainerException if (ex instanceof EmbeddedServletContainerException
|| ex.getCause() instanceof EmbeddedServletContainerException) { || ex.getCause() instanceof EmbeddedServletContainerException) {
logger.warn("Could not start embedded container (management endpoints are still available through JMX)"); logger.warn("Could not start embedded container (management endpoints are still available through JMX)");
} } else {
else {
throw ex; throw ex;
} }
} }
...@@ -219,17 +238,14 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, ...@@ -219,17 +238,14 @@ public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware,
ServerProperties serverProperties; ServerProperties serverProperties;
try { try {
serverProperties = beanFactory.getBean(ServerProperties.class); serverProperties = beanFactory.getBean(ServerProperties.class);
} } catch (NoSuchBeanDefinitionException ex) {
catch (NoSuchBeanDefinitionException ex) {
serverProperties = new ServerProperties(); serverProperties = new ServerProperties();
} }
ManagementServerProperties managementServerProperties; ManagementServerProperties managementServerProperties;
try { try {
managementServerProperties = beanFactory managementServerProperties = beanFactory.getBean(ManagementServerProperties.class);
.getBean(ManagementServerProperties.class); } catch (NoSuchBeanDefinitionException ex) {
}
catch (NoSuchBeanDefinitionException ex) {
managementServerProperties = new ManagementServerProperties(); managementServerProperties = new ManagementServerProperties();
} }
......
/*
* 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<String, HttpStatus> mapping = new HashMap<String, HttpStatus>();
public Map<String, HttpStatus> getMapping() {
return this.mapping;
}
public void setMapping(Map<String, HttpStatus> mapping) {
this.mapping = mapping;
}
}
\ No newline at end of file
...@@ -31,7 +31,7 @@ import org.springframework.util.Assert; ...@@ -31,7 +31,7 @@ import org.springframework.util.Assert;
* @author Dave Syer * @author Dave Syer
* @author Christian Dupuis * @author Christian Dupuis
*/ */
@ConfigurationProperties(prefix = "endpoints.health", ignoreUnknownFields = false) @ConfigurationProperties(prefix = "endpoints.health", ignoreUnknownFields = true)
public class HealthEndpoint extends AbstractEndpoint<Health> { public class HealthEndpoint extends AbstractEndpoint<Health> {
private final HealthIndicator healthIndicator; private final HealthIndicator healthIndicator;
...@@ -48,8 +48,7 @@ public class HealthEndpoint extends AbstractEndpoint<Health> { ...@@ -48,8 +48,7 @@ public class HealthEndpoint extends AbstractEndpoint<Health> {
if (healthIndicators.size() == 1) { if (healthIndicators.size() == 1) {
this.healthIndicator = healthIndicators.values().iterator().next(); this.healthIndicator = healthIndicators.values().iterator().next();
} } else {
else {
CompositeHealthIndicator healthIndicator = new CompositeHealthIndicator( CompositeHealthIndicator healthIndicator = new CompositeHealthIndicator(
healthAggregator); healthAggregator);
for (Map.Entry<String, HealthIndicator> h : healthIndicators.entrySet()) { for (Map.Entry<String, HealthIndicator> h : healthIndicators.entrySet()) {
......
/*
* 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<String, HttpStatus> statusMapping;
public HealthMvcEndpoint(HealthEndpoint delegate) {
super(delegate);
setupDefaultStatusMapping();
}
public void setStatusMapping(Map<String, HttpStatus> 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<Map<String, String>>(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>(health,
this.statusMapping.get(status.getCode()));
}
return health;
}
private void setupDefaultStatusMapping() {
this.statusMapping = new HashMap<String, HttpStatus>();
this.statusMapping.put(Status.DOWN.getCode(), HttpStatus.SERVICE_UNAVAILABLE);
this.statusMapping.put(Status.OUT_OF_SERVICE.getCode(),
HttpStatus.SERVICE_UNAVAILABLE);
}
}
/*
* 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<Health> response = (ResponseEntity<Health>) 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<Health> response = (ResponseEntity<Health>) result;
assertTrue(response.getBody().getStatus().equals(new Status("OK")));
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());
}
}
...@@ -20,16 +20,15 @@ import java.util.HashMap; ...@@ -20,16 +20,15 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.hasEntry;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
...@@ -111,7 +110,6 @@ public class CompositeHealthIndicatorTests { ...@@ -111,7 +110,6 @@ public class CompositeHealthIndicatorTests {
} }
@Test @Test
@Ignore
public void testSerialization() throws Exception { public void testSerialization() throws Exception {
Map<String, HealthIndicator> indicators = new HashMap<String, HealthIndicator>(); Map<String, HealthIndicator> indicators = new HashMap<String, HealthIndicator>();
indicators.put("db1", this.one); indicators.put("db1", this.one);
...@@ -125,9 +123,9 @@ public class CompositeHealthIndicatorTests { ...@@ -125,9 +123,9 @@ public class CompositeHealthIndicatorTests {
Health result = composite.health(); Health result = composite.health();
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.INDENT_OUTPUT, true); assertEquals(
String test = mapper.writeValueAsString(result); "{\"status\":\"UNKOWN\",\"db\":{\"status\":\"UNKOWN\",\"db1\":{\"status\":\"UNKOWN\",\"1\":\"1\"},\"db2\":{\"status\":\"UNKOWN\",\"2\":\"2\"}}}",
System.out.println(test); mapper.writeValueAsString(result));
} }
} }
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