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;
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<ContextRefreshedEvent> {
......@@ -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<ContextClosedEvent>() {
@Override
public void onApplicationEvent(ContextClosedEvent event) {
if (event.getApplicationContext() == EndpointWebMvcAutoConfiguration.this.applicationContext) {
childContext.close();
}
}
});
((ConfigurableApplicationContext) this.applicationContext).addApplicationListener(new ApplicationListener<ContextClosedEvent>() {
@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=<different>,
// 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();
}
......
/*
* 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;
* @author Dave Syer
* @author Christian Dupuis
*/
@ConfigurationProperties(prefix = "endpoints.health", ignoreUnknownFields = false)
@ConfigurationProperties(prefix = "endpoints.health", ignoreUnknownFields = true)
public class HealthEndpoint extends AbstractEndpoint<Health> {
private final HealthIndicator healthIndicator;
......@@ -48,8 +48,7 @@ public class HealthEndpoint extends AbstractEndpoint<Health> {
if (healthIndicators.size() == 1) {
this.healthIndicator = healthIndicators.values().iterator().next();
}
else {
} else {
CompositeHealthIndicator healthIndicator = new CompositeHealthIndicator(
healthAggregator);
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;
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<String, HealthIndicator> indicators = new HashMap<String, HealthIndicator>();
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));
}
}
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