Commit 2c959b8e authored by Phillip Webb's avatar Phillip Webb

Polish health indicators

Align reactive and non-reactive web extensions and update `showDetails`
so that it only applies to web exposure.

See gh-11113
See gh-11192
parent 9e954836
...@@ -23,13 +23,15 @@ import org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet.Cloud ...@@ -23,13 +23,15 @@ import org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet.Cloud
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthStatusHttpMapper; import org.springframework.boot.actuate.health.HealthEndpointWebExtension;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator; import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
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.cloud.CloudPlatform;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
...@@ -39,7 +41,9 @@ import org.springframework.context.annotation.Configuration; ...@@ -39,7 +41,9 @@ import org.springframework.context.annotation.Configuration;
* @author Madhura Bhave * @author Madhura Bhave
*/ */
@Configuration @Configuration
@AutoConfigureBefore({ ReactiveCloudFoundryActuatorAutoConfiguration.class, CloudFoundryActuatorAutoConfiguration.class }) @ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
@AutoConfigureBefore({ ReactiveCloudFoundryActuatorAutoConfiguration.class,
CloudFoundryActuatorAutoConfiguration.class })
@AutoConfigureAfter(HealthEndpointAutoConfiguration.class) @AutoConfigureAfter(HealthEndpointAutoConfiguration.class)
public class CloudFoundryHealthWebEndpointManagementContextConfiguration { public class CloudFoundryHealthWebEndpointManagementContextConfiguration {
...@@ -50,11 +54,10 @@ public class CloudFoundryHealthWebEndpointManagementContextConfiguration { ...@@ -50,11 +54,10 @@ public class CloudFoundryHealthWebEndpointManagementContextConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
@ConditionalOnEnabledEndpoint @ConditionalOnEnabledEndpoint
@ConditionalOnBean(HealthEndpoint.class) @ConditionalOnBean({ HealthEndpoint.class, HealthEndpointWebExtension.class })
public CloudFoundryHealthEndpointWebExtension cloudFoundryHealthEndpointWebExtension( public CloudFoundryHealthEndpointWebExtension cloudFoundryHealthEndpointWebExtension(
HealthEndpoint healthEndpoint, HealthStatusHttpMapper healthStatusHttpMapper) { HealthEndpointWebExtension healthEndpointWebExtension) {
HealthEndpoint delegate = new HealthEndpoint(healthEndpoint.getHealthIndicator(), true); return new CloudFoundryHealthEndpointWebExtension(healthEndpointWebExtension);
return new CloudFoundryHealthEndpointWebExtension(delegate, healthStatusHttpMapper);
} }
} }
...@@ -66,12 +69,12 @@ public class CloudFoundryHealthWebEndpointManagementContextConfiguration { ...@@ -66,12 +69,12 @@ public class CloudFoundryHealthWebEndpointManagementContextConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
@ConditionalOnEnabledEndpoint @ConditionalOnEnabledEndpoint
@ConditionalOnBean(HealthEndpoint.class) @ConditionalOnBean({ HealthEndpoint.class,
ReactiveHealthEndpointWebExtension.class })
public CloudFoundryReactiveHealthEndpointWebExtension cloudFoundryReactiveHealthEndpointWebExtension( public CloudFoundryReactiveHealthEndpointWebExtension cloudFoundryReactiveHealthEndpointWebExtension(
ReactiveHealthIndicator reactiveHealthIndicator, ReactiveHealthEndpointWebExtension reactiveHealthEndpointWebExtension) {
HealthStatusHttpMapper healthStatusHttpMapper) { return new CloudFoundryReactiveHealthEndpointWebExtension(
return new CloudFoundryReactiveHealthEndpointWebExtension(reactiveHealthIndicator, reactiveHealthEndpointWebExtension);
healthStatusHttpMapper);
} }
} }
......
...@@ -17,9 +17,9 @@ ...@@ -17,9 +17,9 @@
package org.springframework.boot.actuate.autoconfigure.cloudfoundry; package org.springframework.boot.actuate.autoconfigure.cloudfoundry;
import java.util.Collection; import java.util.Collection;
import java.util.Map;
import org.springframework.boot.actuate.endpoint.EndpointFilter; import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.EndpointInfo;
import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInvokerAdvisor; import org.springframework.boot.actuate.endpoint.reflect.OperationMethodInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.reflect.ParameterMapper; import org.springframework.boot.actuate.endpoint.reflect.ParameterMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
...@@ -30,34 +30,36 @@ import org.springframework.boot.actuate.health.HealthEndpoint; ...@@ -30,34 +30,36 @@ import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
/** /**
* {@link WebAnnotationEndpointDiscoverer} for Cloud Foundry that uses Cloud Foundry specific * {@link WebAnnotationEndpointDiscoverer} for Cloud Foundry that uses Cloud Foundry
* extensions for the {@link HealthEndpoint}. * specific extensions for the {@link HealthEndpoint}.
* *
* @author Madhura Bhave * @author Madhura Bhave
*/ */
public class CloudFoundryWebAnnotationEndpointDiscoverer extends WebAnnotationEndpointDiscoverer { public class CloudFoundryWebAnnotationEndpointDiscoverer
extends WebAnnotationEndpointDiscoverer {
private final ApplicationContext applicationContext;
private final Class<?> requiredExtensionType; private final Class<?> requiredExtensionType;
public CloudFoundryWebAnnotationEndpointDiscoverer(ApplicationContext applicationContext, ParameterMapper parameterMapper, public CloudFoundryWebAnnotationEndpointDiscoverer(
EndpointMediaTypes endpointMediaTypes, EndpointPathResolver endpointPathResolver, ApplicationContext applicationContext, ParameterMapper parameterMapper,
Collection<? extends OperationMethodInvokerAdvisor> invokerAdvisors, Collection<? extends EndpointFilter<WebOperation>> filters, Class<?> requiredExtensionType) { EndpointMediaTypes endpointMediaTypes,
super(applicationContext, parameterMapper, endpointMediaTypes, endpointPathResolver, invokerAdvisors, filters); EndpointPathResolver endpointPathResolver,
this.applicationContext = applicationContext; Collection<? extends OperationMethodInvokerAdvisor> invokerAdvisors,
Collection<? extends EndpointFilter<WebOperation>> filters,
Class<?> requiredExtensionType) {
super(applicationContext, parameterMapper, endpointMediaTypes,
endpointPathResolver, invokerAdvisors, filters);
this.requiredExtensionType = requiredExtensionType; this.requiredExtensionType = requiredExtensionType;
} }
@Override @Override
protected void addExtension(Map<Class<?>, DiscoveredEndpoint> endpoints, Map<Class<?>, DiscoveredExtension> extensions, String beanName) { protected boolean isExtensionExposed(Class<?> endpointType, Class<?> extensionType,
Class<?> extensionType = this.applicationContext.getType(beanName); EndpointInfo<WebOperation> endpointInfo) {
Class<?> endpointType = getEndpointType(extensionType); if (HealthEndpoint.class.equals(endpointType)
if (HealthEndpoint.class.equals(endpointType) && !this.requiredExtensionType.equals(extensionType)) { && !this.requiredExtensionType.equals(extensionType)) {
return; return false;
} }
super.addExtension(endpoints, extensions, beanName); return super.isExtensionExposed(endpointType, extensionType, endpointInfo);
} }
} }
...@@ -25,36 +25,28 @@ import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; ...@@ -25,36 +25,28 @@ import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthStatusHttpMapper; import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
/** /**
* Reactive {@link EndpointWebExtension} for the {@link HealthEndpoint} * Reactive {@link EndpointWebExtension} for the {@link HealthEndpoint} that always
* that always exposes full health details. * exposes full health details.
* *
* @author Madhura Bhave * @author Madhura Bhave
* @since 2.0.0
*/ */
@EndpointExtension(filter = CloudFoundryEndpointFilter.class, endpoint = HealthEndpoint.class) @EndpointExtension(filter = CloudFoundryEndpointFilter.class, endpoint = HealthEndpoint.class)
public class CloudFoundryReactiveHealthEndpointWebExtension { public class CloudFoundryReactiveHealthEndpointWebExtension {
private final ReactiveHealthIndicator delegate; private final ReactiveHealthEndpointWebExtension delegate;
private final HealthStatusHttpMapper statusHttpMapper; public CloudFoundryReactiveHealthEndpointWebExtension(
ReactiveHealthEndpointWebExtension delegate) {
public CloudFoundryReactiveHealthEndpointWebExtension(ReactiveHealthIndicator delegate,
HealthStatusHttpMapper statusHttpMapper) {
this.delegate = delegate; this.delegate = delegate;
this.statusHttpMapper = statusHttpMapper;
} }
@ReadOperation @ReadOperation
public Mono<WebEndpointResponse<Health>> health() { public Mono<WebEndpointResponse<Health>> health() {
return this.delegate.health().map((health) -> { return this.delegate.health(true);
Integer status = this.statusHttpMapper.mapStatus(health.getStatus());
return new WebEndpointResponse<>(health, status);
});
} }
} }
...@@ -70,7 +70,8 @@ public class ReactiveCloudFoundryActuatorAutoConfiguration { ...@@ -70,7 +70,8 @@ public class ReactiveCloudFoundryActuatorAutoConfiguration {
WebClient.Builder webClientBuilder) { WebClient.Builder webClientBuilder) {
CloudFoundryWebAnnotationEndpointDiscoverer endpointDiscoverer = new CloudFoundryWebAnnotationEndpointDiscoverer( CloudFoundryWebAnnotationEndpointDiscoverer endpointDiscoverer = new CloudFoundryWebAnnotationEndpointDiscoverer(
this.applicationContext, parameterMapper, endpointMediaTypes, this.applicationContext, parameterMapper, endpointMediaTypes,
EndpointPathResolver.useEndpointId(), null, null, CloudFoundryReactiveHealthEndpointWebExtension.class); EndpointPathResolver.useEndpointId(), null, null,
CloudFoundryReactiveHealthEndpointWebExtension.class);
ReactiveCloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor( ReactiveCloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor(
webClientBuilder, this.applicationContext.getEnvironment()); webClientBuilder, this.applicationContext.getEnvironment());
return new CloudFoundryWebFluxEndpointHandlerMapping( return new CloudFoundryWebFluxEndpointHandlerMapping(
......
...@@ -74,7 +74,8 @@ public class CloudFoundryActuatorAutoConfiguration { ...@@ -74,7 +74,8 @@ public class CloudFoundryActuatorAutoConfiguration {
RestTemplateBuilder restTemplateBuilder) { RestTemplateBuilder restTemplateBuilder) {
CloudFoundryWebAnnotationEndpointDiscoverer endpointDiscoverer = new CloudFoundryWebAnnotationEndpointDiscoverer( CloudFoundryWebAnnotationEndpointDiscoverer endpointDiscoverer = new CloudFoundryWebAnnotationEndpointDiscoverer(
this.applicationContext, parameterMapper, endpointMediaTypes, this.applicationContext, parameterMapper, endpointMediaTypes,
EndpointPathResolver.useEndpointId(), null, null, CloudFoundryHealthEndpointWebExtension.class); EndpointPathResolver.useEndpointId(), null, null,
CloudFoundryHealthEndpointWebExtension.class);
CloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor( CloudFoundrySecurityInterceptor securityInterceptor = getSecurityInterceptor(
restTemplateBuilder, this.applicationContext.getEnvironment()); restTemplateBuilder, this.applicationContext.getEnvironment());
return new CloudFoundryWebEndpointServletHandlerMapping( return new CloudFoundryWebEndpointServletHandlerMapping(
......
...@@ -23,33 +23,27 @@ import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; ...@@ -23,33 +23,27 @@ import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthStatusHttpMapper; import org.springframework.boot.actuate.health.HealthEndpointWebExtension;
/** /**
* {@link EndpointWebExtension} for the {@link HealthEndpoint} * {@link EndpointWebExtension} for the {@link HealthEndpoint} that always exposes full
* that always exposes full health details. * health details.
* *
* @author Madhura Bhave * @author Madhura Bhave
* @since 2.0.0
*/ */
@EndpointExtension(filter = CloudFoundryEndpointFilter.class, endpoint = HealthEndpoint.class) @EndpointExtension(filter = CloudFoundryEndpointFilter.class, endpoint = HealthEndpoint.class)
public class CloudFoundryHealthEndpointWebExtension { public class CloudFoundryHealthEndpointWebExtension {
private final HealthEndpoint delegate; private final HealthEndpointWebExtension delegate;
private final HealthStatusHttpMapper statusHttpMapper; public CloudFoundryHealthEndpointWebExtension(HealthEndpointWebExtension delegate) {
public CloudFoundryHealthEndpointWebExtension(HealthEndpoint delegate,
HealthStatusHttpMapper statusHttpMapper) {
this.delegate = delegate; this.delegate = delegate;
this.statusHttpMapper = statusHttpMapper;
} }
@ReadOperation @ReadOperation
public WebEndpointResponse<Health> getHealth() { public WebEndpointResponse<Health> getHealth() {
Health health = this.delegate.health(); return this.delegate.getHealth(true);
Integer status = this.statusHttpMapper.mapStatus(health.getStatus());
return new WebEndpointResponse<>(health, status);
} }
} }
...@@ -16,24 +16,14 @@ ...@@ -16,24 +16,14 @@
package org.springframework.boot.actuate.autoconfigure.health; package org.springframework.boot.actuate.autoconfigure.health;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
import org.springframework.boot.actuate.health.CompositeHealthIndicatorFactory;
import org.springframework.boot.actuate.health.HealthAggregator;
import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.OrderedHealthAggregator;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
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.util.ClassUtils;
/** /**
* {@link EnableAutoConfiguration Auto-configuration} for {@link HealthEndpoint}. * {@link EnableAutoConfiguration Auto-configuration} for {@link HealthEndpoint}.
...@@ -47,46 +37,11 @@ import org.springframework.util.ClassUtils; ...@@ -47,46 +37,11 @@ import org.springframework.util.ClassUtils;
@EnableConfigurationProperties(HealthEndpointProperties.class) @EnableConfigurationProperties(HealthEndpointProperties.class)
public class HealthEndpointAutoConfiguration { public class HealthEndpointAutoConfiguration {
private final HealthIndicator healthIndicator;
public HealthEndpointAutoConfiguration(ApplicationContext applicationContext,
ObjectProvider<HealthAggregator> healthAggregator) {
this.healthIndicator = getHealthIndicator(applicationContext,
healthAggregator.getIfAvailable(OrderedHealthAggregator::new));
}
private HealthIndicator getHealthIndicator(ApplicationContext applicationContext,
HealthAggregator healthAggregator) {
Map<String, HealthIndicator> indicators = new LinkedHashMap<>();
indicators.putAll(applicationContext.getBeansOfType(HealthIndicator.class));
if (ClassUtils.isPresent("reactor.core.publisher.Flux", null)) {
new ReactiveHealthIndicators().get(applicationContext)
.forEach(indicators::putIfAbsent);
}
CompositeHealthIndicatorFactory factory = new CompositeHealthIndicatorFactory();
return factory.createHealthIndicator(healthAggregator, indicators);
}
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
@ConditionalOnEnabledEndpoint @ConditionalOnEnabledEndpoint
public HealthEndpoint healthEndpoint(HealthEndpointProperties properties) { public HealthEndpoint healthEndpoint(ApplicationContext applicationContext) {
return new HealthEndpoint(this.healthIndicator, properties.isShowDetails()); return new HealthEndpoint(HealthIndicatorBeansComposite.get(applicationContext));
}
private static class ReactiveHealthIndicators {
public Map<String, HealthIndicator> get(ApplicationContext applicationContext) {
Map<String, HealthIndicator> indicators = new LinkedHashMap<>();
applicationContext.getBeansOfType(ReactiveHealthIndicator.class)
.forEach((name, indicator) -> indicators.put(name, adapt(indicator)));
return indicators;
}
private HealthIndicator adapt(ReactiveHealthIndicator indicator) {
return () -> indicator.health().block();
}
} }
} }
...@@ -28,7 +28,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties; ...@@ -28,7 +28,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
public class HealthEndpointProperties { public class HealthEndpointProperties {
/** /**
* Whether to show full health details instead of just the status. * Whether to show full health details instead of just the status when exposed over a
* potentially insecure connection.
*/ */
private boolean showDetails; private boolean showDetails;
......
/*
* Copyright 2012-2017 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.health;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.actuate.health.CompositeHealthIndicator;
import org.springframework.boot.actuate.health.CompositeHealthIndicatorFactory;
import org.springframework.boot.actuate.health.HealthAggregator;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.OrderedHealthAggregator;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
import org.springframework.context.ApplicationContext;
import org.springframework.util.ClassUtils;
/**
* Creates a {@link CompositeHealthIndicator} from beans in the
* {@link ApplicationContext}.
*
* @author Phillip Webb
*/
final class HealthIndicatorBeansComposite {
private HealthIndicatorBeansComposite() {
}
public static HealthIndicator get(ApplicationContext applicationContext) {
HealthAggregator healthAggregator = getHealthAggregator(applicationContext);
Map<String, HealthIndicator> indicators = new LinkedHashMap<>();
indicators.putAll(applicationContext.getBeansOfType(HealthIndicator.class));
if (ClassUtils.isPresent("reactor.core.publisher.Flux", null)) {
new ReactiveHealthIndicators().get(applicationContext)
.forEach(indicators::putIfAbsent);
}
CompositeHealthIndicatorFactory factory = new CompositeHealthIndicatorFactory();
return factory.createHealthIndicator(healthAggregator, indicators);
}
private static HealthAggregator getHealthAggregator(
ApplicationContext applicationContext) {
try {
return applicationContext.getBean(HealthAggregator.class);
}
catch (NoSuchBeanDefinitionException ex) {
return new OrderedHealthAggregator();
}
}
private static class ReactiveHealthIndicators {
public Map<String, HealthIndicator> get(ApplicationContext applicationContext) {
Map<String, HealthIndicator> indicators = new LinkedHashMap<>();
applicationContext.getBeansOfType(ReactiveHealthIndicator.class)
.forEach((name, indicator) -> indicators.put(name, adapt(indicator)));
return indicators;
}
private HealthIndicator adapt(ReactiveHealthIndicator indicator) {
return () -> indicator.health().block();
}
}
}
/*
* Copyright 2012-2017 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.health;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.actuate.health.CompositeReactiveHealthIndicator;
import org.springframework.boot.actuate.health.CompositeReactiveHealthIndicatorFactory;
import org.springframework.boot.actuate.health.HealthAggregator;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.OrderedHealthAggregator;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
import org.springframework.context.ApplicationContext;
/**
* Creates a {@link CompositeReactiveHealthIndicator} from beans in the
* {@link ApplicationContext}.
*
* @author Phillip Webb
*/
final class HealthIndicatorBeansReactiveComposite {
private HealthIndicatorBeansReactiveComposite() {
}
public static ReactiveHealthIndicator get(ApplicationContext applicationContext) {
HealthAggregator healthAggregator = getHealthAggregator(applicationContext);
return new CompositeReactiveHealthIndicatorFactory()
.createReactiveHealthIndicator(healthAggregator,
applicationContext.getBeansOfType(ReactiveHealthIndicator.class),
applicationContext.getBeansOfType(HealthIndicator.class));
}
private static HealthAggregator getHealthAggregator(
ApplicationContext applicationContext) {
try {
return applicationContext.getBean(HealthAggregator.class);
}
catch (NoSuchBeanDefinitionException ex) {
return new OrderedHealthAggregator();
}
}
}
...@@ -36,6 +36,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean ...@@ -36,6 +36,7 @@ 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.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
...@@ -65,7 +66,8 @@ public class HealthWebEndpointManagementContextConfiguration { ...@@ -65,7 +66,8 @@ public class HealthWebEndpointManagementContextConfiguration {
static class ReactiveWebHealthConfiguration { static class ReactiveWebHealthConfiguration {
@Bean @Bean
public ReactiveHealthIndicator reactiveHealthIndicator(ObjectProvider<HealthAggregator> healthAggregator, public ReactiveHealthIndicator reactiveHealthIndicator(
ObjectProvider<HealthAggregator> healthAggregator,
ObjectProvider<Map<String, ReactiveHealthIndicator>> reactiveHealthIndicators, ObjectProvider<Map<String, ReactiveHealthIndicator>> reactiveHealthIndicators,
ObjectProvider<Map<String, HealthIndicator>> healthIndicators) { ObjectProvider<Map<String, HealthIndicator>> healthIndicators) {
return new CompositeReactiveHealthIndicatorFactory() return new CompositeReactiveHealthIndicatorFactory()
...@@ -99,8 +101,12 @@ public class HealthWebEndpointManagementContextConfiguration { ...@@ -99,8 +101,12 @@ public class HealthWebEndpointManagementContextConfiguration {
@ConditionalOnEnabledEndpoint @ConditionalOnEnabledEndpoint
@ConditionalOnBean(HealthEndpoint.class) @ConditionalOnBean(HealthEndpoint.class)
public HealthEndpointWebExtension healthEndpointWebExtension( public HealthEndpointWebExtension healthEndpointWebExtension(
HealthEndpoint delegate, HealthStatusHttpMapper healthStatusHttpMapper) { ApplicationContext applicationContext,
return new HealthEndpointWebExtension(delegate, healthStatusHttpMapper); HealthStatusHttpMapper healthStatusHttpMapper,
HealthEndpointProperties properties) {
return new HealthEndpointWebExtension(
HealthIndicatorBeansComposite.get(applicationContext),
healthStatusHttpMapper, properties.isShowDetails());
} }
} }
......
...@@ -49,20 +49,27 @@ public class CloudFoundryEndpointFilterTests { ...@@ -49,20 +49,27 @@ public class CloudFoundryEndpointFilterTests {
@Test @Test
public void matchIfDiscovererCloudFoundryShouldReturnFalse() throws Exception { public void matchIfDiscovererCloudFoundryShouldReturnFalse() throws Exception {
CloudFoundryWebAnnotationEndpointDiscoverer discoverer = Mockito.mock(CloudFoundryWebAnnotationEndpointDiscoverer.class); CloudFoundryWebAnnotationEndpointDiscoverer discoverer = Mockito
.mock(CloudFoundryWebAnnotationEndpointDiscoverer.class);
assertThat(this.filter.match(null, discoverer)).isTrue(); assertThat(this.filter.match(null, discoverer)).isTrue();
} }
@Test @Test
public void matchIfDiscovererNotCloudFoundryShouldReturnFalse() throws Exception { public void matchIfDiscovererNotCloudFoundryShouldReturnFalse() throws Exception {
WebAnnotationEndpointDiscoverer discoverer = Mockito.mock(WebAnnotationEndpointDiscoverer.class); WebAnnotationEndpointDiscoverer discoverer = Mockito
.mock(WebAnnotationEndpointDiscoverer.class);
assertThat(this.filter.match(null, discoverer)).isFalse(); assertThat(this.filter.match(null, discoverer)).isFalse();
} }
static class TestEndpointDiscoverer extends WebAnnotationEndpointDiscoverer { static class TestEndpointDiscoverer extends WebAnnotationEndpointDiscoverer {
TestEndpointDiscoverer(ApplicationContext applicationContext, ParameterMapper parameterMapper, EndpointMediaTypes endpointMediaTypes, EndpointPathResolver endpointPathResolver, Collection<? extends OperationMethodInvokerAdvisor> invokerAdvisors, Collection<? extends EndpointFilter<WebOperation>> filters) { TestEndpointDiscoverer(ApplicationContext applicationContext,
super(applicationContext, parameterMapper, endpointMediaTypes, endpointPathResolver, invokerAdvisors, filters); ParameterMapper parameterMapper, EndpointMediaTypes endpointMediaTypes,
EndpointPathResolver endpointPathResolver,
Collection<? extends OperationMethodInvokerAdvisor> invokerAdvisors,
Collection<? extends EndpointFilter<WebOperation>> filters) {
super(applicationContext, parameterMapper, endpointMediaTypes,
endpointPathResolver, invokerAdvisors, filters);
} }
} }
......
...@@ -50,7 +50,8 @@ public class CloudFoundryWebAnnotationEndpointDiscovererTests { ...@@ -50,7 +50,8 @@ public class CloudFoundryWebAnnotationEndpointDiscovererTests {
@Test @Test
public void discovererShouldAddSuppliedExtensionForHealthEndpoint() throws Exception { public void discovererShouldAddSuppliedExtensionForHealthEndpoint() throws Exception {
load(TestConfiguration.class, endpointDiscoverer -> { load(TestConfiguration.class, endpointDiscoverer -> {
Collection<EndpointInfo<WebOperation>> endpoints = endpointDiscoverer.discoverEndpoints(); Collection<EndpointInfo<WebOperation>> endpoints = endpointDiscoverer
.discoverEndpoints();
assertThat(endpoints.size()).isEqualTo(2); assertThat(endpoints.size()).isEqualTo(2);
}); });
} }
...@@ -92,7 +93,7 @@ public class CloudFoundryWebAnnotationEndpointDiscovererTests { ...@@ -92,7 +93,7 @@ public class CloudFoundryWebAnnotationEndpointDiscovererTests {
@Bean @Bean
public HealthEndpoint healthEndpoint() { public HealthEndpoint healthEndpoint() {
return new HealthEndpoint(null, true); return new HealthEndpoint(null);
} }
@Bean @Bean
......
...@@ -229,14 +229,19 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests { ...@@ -229,14 +229,19 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
@Test @Test
public void healthEndpointInvokerShouldBeCloudFoundryWebExtension() throws Exception { public void healthEndpointInvokerShouldBeCloudFoundryWebExtension() throws Exception {
setupContextWithCloudEnabled(); setupContextWithCloudEnabled();
this.context.register(HealthEndpointAutoConfiguration.class, HealthWebEndpointManagementContextConfiguration.class, this.context.register(HealthEndpointAutoConfiguration.class,
HealthWebEndpointManagementContextConfiguration.class,
CloudFoundryHealthWebEndpointManagementContextConfiguration.class); CloudFoundryHealthWebEndpointManagementContextConfiguration.class);
this.context.refresh(); this.context.refresh();
Collection<EndpointInfo<WebOperation>> endpoints = getHandlerMapping().getEndpoints(); Collection<EndpointInfo<WebOperation>> endpoints = getHandlerMapping()
.getEndpoints();
EndpointInfo endpointInfo = (EndpointInfo) (endpoints.toArray()[0]); EndpointInfo endpointInfo = (EndpointInfo) (endpoints.toArray()[0]);
WebOperation webOperation = (WebOperation) endpointInfo.getOperations().toArray()[0]; WebOperation webOperation = (WebOperation) endpointInfo.getOperations()
ReflectiveOperationInvoker invoker = (ReflectiveOperationInvoker) webOperation.getInvoker(); .toArray()[0];
assertThat(ReflectionTestUtils.getField(invoker, "target")).isInstanceOf(CloudFoundryReactiveHealthEndpointWebExtension.class); ReflectiveOperationInvoker invoker = (ReflectiveOperationInvoker) webOperation
.getInvoker();
assertThat(ReflectionTestUtils.getField(invoker, "target"))
.isInstanceOf(CloudFoundryReactiveHealthEndpointWebExtension.class);
} }
private void setupContextWithCloudEnabled() { private void setupContextWithCloudEnabled() {
......
...@@ -251,15 +251,21 @@ public class CloudFoundryActuatorAutoConfigurationTests { ...@@ -251,15 +251,21 @@ public class CloudFoundryActuatorAutoConfigurationTests {
.of("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id", .of("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id",
"vcap.application.cf_api:http://my-cloud-controller.com") "vcap.application.cf_api:http://my-cloud-controller.com")
.applyTo(this.context); .applyTo(this.context);
this.context.register(HealthEndpointAutoConfiguration.class, HealthWebEndpointManagementContextConfiguration.class, this.context.register(HealthEndpointAutoConfiguration.class,
HealthWebEndpointManagementContextConfiguration.class,
CloudFoundryHealthWebEndpointManagementContextConfiguration.class); CloudFoundryHealthWebEndpointManagementContextConfiguration.class);
this.context.refresh(); this.context.refresh();
Collection<EndpointInfo<WebOperation>> endpoints = this.context.getBean("cloudFoundryWebEndpointServletHandlerMapping", Collection<EndpointInfo<WebOperation>> endpoints = this.context
CloudFoundryWebEndpointServletHandlerMapping.class).getEndpoints(); .getBean("cloudFoundryWebEndpointServletHandlerMapping",
CloudFoundryWebEndpointServletHandlerMapping.class)
.getEndpoints();
EndpointInfo endpointInfo = (EndpointInfo) (endpoints.toArray()[0]); EndpointInfo endpointInfo = (EndpointInfo) (endpoints.toArray()[0]);
WebOperation webOperation = (WebOperation) endpointInfo.getOperations().toArray()[0]; WebOperation webOperation = (WebOperation) endpointInfo.getOperations()
ReflectiveOperationInvoker invoker = (ReflectiveOperationInvoker) webOperation.getInvoker(); .toArray()[0];
assertThat(ReflectionTestUtils.getField(invoker, "target")).isInstanceOf(CloudFoundryHealthEndpointWebExtension.class); ReflectiveOperationInvoker invoker = (ReflectiveOperationInvoker) webOperation
.getInvoker();
assertThat(ReflectionTestUtils.getField(invoker, "target"))
.isInstanceOf(CloudFoundryHealthEndpointWebExtension.class);
} }
private CloudFoundryWebEndpointServletHandlerMapping getHandlerMapping() { private CloudFoundryWebEndpointServletHandlerMapping getHandlerMapping() {
......
...@@ -74,7 +74,7 @@ public class HealthEndpointDocumentationTests extends AbstractEndpointDocumentat ...@@ -74,7 +74,7 @@ public class HealthEndpointDocumentationTests extends AbstractEndpointDocumentat
@Bean @Bean
public HealthEndpoint endpoint(Map<String, HealthIndicator> healthIndicators) { public HealthEndpoint endpoint(Map<String, HealthIndicator> healthIndicators) {
return new HealthEndpoint(new CompositeHealthIndicator( return new HealthEndpoint(new CompositeHealthIndicator(
new OrderedHealthAggregator(), healthIndicators), true); new OrderedHealthAggregator(), healthIndicators));
} }
@Bean @Bean
......
...@@ -57,7 +57,7 @@ public class HealthEndpointAutoConfigurationTests { ...@@ -57,7 +57,7 @@ public class HealthEndpointAutoConfigurationTests {
verify(indicator, times(0)).health(); verify(indicator, times(0)).health();
Health health = context.getBean(HealthEndpoint.class).health(); Health health = context.getBean(HealthEndpoint.class).health();
assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).isEmpty(); assertThat(health.getDetails()).isNotEmpty();
verify(indicator, times(1)).health(); verify(indicator, times(1)).health();
}); });
} }
......
...@@ -173,13 +173,13 @@ public abstract class AnnotationEndpointDiscoverer<K, T extends Operation> ...@@ -173,13 +173,13 @@ public abstract class AnnotationEndpointDiscoverer<K, T extends Operation>
return extensions; return extensions;
} }
protected void addExtension(Map<Class<?>, DiscoveredEndpoint> endpoints, private void addExtension(Map<Class<?>, DiscoveredEndpoint> endpoints,
Map<Class<?>, DiscoveredExtension> extensions, String beanName) { Map<Class<?>, DiscoveredExtension> extensions, String beanName) {
Class<?> extensionType = this.applicationContext.getType(beanName); Class<?> extensionType = this.applicationContext.getType(beanName);
Class<?> endpointType = getEndpointType(extensionType); Class<?> endpointType = getEndpointType(extensionType);
DiscoveredEndpoint endpoint = getExtendingEndpoint(endpoints, extensionType, DiscoveredEndpoint endpoint = getExtendingEndpoint(endpoints, extensionType,
endpointType); endpointType);
if (isExtensionExposed(extensionType, endpoint.getInfo())) { if (isExtensionExposed(endpointType, extensionType, endpoint.getInfo())) {
Assert.state(endpoint.isExposed() || isEndpointFiltered(endpoint.getInfo()), Assert.state(endpoint.isExposed() || isEndpointFiltered(endpoint.getInfo()),
() -> "Invalid extension " + extensionType.getName() + "': endpoint '" () -> "Invalid extension " + extensionType.getName() + "': endpoint '"
+ endpointType.getName() + endpointType.getName()
...@@ -199,7 +199,7 @@ public abstract class AnnotationEndpointDiscoverer<K, T extends Operation> ...@@ -199,7 +199,7 @@ public abstract class AnnotationEndpointDiscoverer<K, T extends Operation>
} }
} }
protected Class<?> getEndpointType(Class<?> extensionType) { private Class<?> getEndpointType(Class<?> extensionType) {
AnnotationAttributes attributes = AnnotatedElementUtils AnnotationAttributes attributes = AnnotatedElementUtils
.getMergedAnnotationAttributes(extensionType, EndpointExtension.class); .getMergedAnnotationAttributes(extensionType, EndpointExtension.class);
Class<?> endpointType = attributes.getClass("endpoint"); Class<?> endpointType = attributes.getClass("endpoint");
...@@ -242,7 +242,14 @@ public abstract class AnnotationEndpointDiscoverer<K, T extends Operation> ...@@ -242,7 +242,14 @@ public abstract class AnnotationEndpointDiscoverer<K, T extends Operation>
return false; return false;
} }
private boolean isExtensionExposed(Class<?> extensionType, /**
* Determines if an extension is exposed.
* @param endpointType the endpoint type
* @param extensionType the extension type
* @param endpointInfo the endpoint info
* @return if the extension is exposed
*/
protected boolean isExtensionExposed(Class<?> endpointType, Class<?> extensionType,
EndpointInfo<T> endpointInfo) { EndpointInfo<T> endpointInfo) {
AnnotationAttributes annotationAttributes = AnnotatedElementUtils AnnotationAttributes annotationAttributes = AnnotatedElementUtils
.getMergedAnnotationAttributes(extensionType, EndpointExtension.class); .getMergedAnnotationAttributes(extensionType, EndpointExtension.class);
......
...@@ -32,29 +32,17 @@ public class HealthEndpoint { ...@@ -32,29 +32,17 @@ public class HealthEndpoint {
private final HealthIndicator healthIndicator; private final HealthIndicator healthIndicator;
private final boolean showDetails;
/** /**
* Create a new {@link HealthEndpoint} instance. * Create a new {@link HealthEndpoint} instance.
* @param healthIndicator the health indicator * @param healthIndicator the health indicator
* @param showDetails if full details should be returned instead of just the status
*/ */
public HealthEndpoint(HealthIndicator healthIndicator, boolean showDetails) { public HealthEndpoint(HealthIndicator healthIndicator) {
this.healthIndicator = healthIndicator; this.healthIndicator = healthIndicator;
this.showDetails = showDetails;
} }
@ReadOperation @ReadOperation
public Health health() { public Health health() {
Health health = this.healthIndicator.health(); return this.healthIndicator.health();
if (this.showDetails) {
return health;
}
return Health.status(health.getStatus()).build();
}
public HealthIndicator getHealthIndicator() {
return this.healthIndicator;
} }
} }
...@@ -34,20 +34,30 @@ import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExten ...@@ -34,20 +34,30 @@ import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExten
@EndpointWebExtension(endpoint = HealthEndpoint.class) @EndpointWebExtension(endpoint = HealthEndpoint.class)
public class HealthEndpointWebExtension { public class HealthEndpointWebExtension {
private final HealthEndpoint delegate; private final HealthIndicator delegate;
private final HealthStatusHttpMapper statusHttpMapper; private final HealthStatusHttpMapper statusHttpMapper;
public HealthEndpointWebExtension(HealthEndpoint delegate, private final boolean showDetails;
HealthStatusHttpMapper statusHttpMapper) {
public HealthEndpointWebExtension(HealthIndicator delegate,
HealthStatusHttpMapper statusHttpMapper, boolean showDetails) {
this.delegate = delegate; this.delegate = delegate;
this.statusHttpMapper = statusHttpMapper; this.statusHttpMapper = statusHttpMapper;
this.showDetails = showDetails;
} }
@ReadOperation @ReadOperation
public WebEndpointResponse<Health> getHealth() { public WebEndpointResponse<Health> getHealth() {
return getHealth(this.showDetails);
}
public WebEndpointResponse<Health> getHealth(boolean showDetails) {
Health health = this.delegate.health(); Health health = this.delegate.health();
Integer status = this.statusHttpMapper.mapStatus(health.getStatus()); Integer status = this.statusHttpMapper.mapStatus(health.getStatus());
if (!showDetails) {
health = Health.status(health.getStatus()).build();
}
return new WebEndpointResponse<>(health, status); return new WebEndpointResponse<>(health, status);
} }
......
...@@ -46,9 +46,13 @@ public class ReactiveHealthEndpointWebExtension { ...@@ -46,9 +46,13 @@ public class ReactiveHealthEndpointWebExtension {
@ReadOperation @ReadOperation
public Mono<WebEndpointResponse<Health>> health() { public Mono<WebEndpointResponse<Health>> health() {
return health(this.showDetails);
}
public Mono<WebEndpointResponse<Health>> health(boolean showDetails) {
return this.delegate.health().map((health) -> { return this.delegate.health().map((health) -> {
Integer status = this.statusHttpMapper.mapStatus(health.getStatus()); Integer status = this.statusHttpMapper.mapStatus(health.getStatus());
if (!this.showDetails) { if (!showDetails) {
health = Health.status(health.getStatus()).build(); health = Health.status(health.getStatus()).build();
} }
return new WebEndpointResponse<>(health, status); return new WebEndpointResponse<>(health, status);
......
...@@ -41,7 +41,7 @@ public class HealthEndpointTests { ...@@ -41,7 +41,7 @@ public class HealthEndpointTests {
healthIndicators.put("upAgain", () -> new Health.Builder().status(Status.UP) healthIndicators.put("upAgain", () -> new Health.Builder().status(Status.UP)
.withDetail("second", "2").build()); .withDetail("second", "2").build());
HealthEndpoint endpoint = new HealthEndpoint( HealthEndpoint endpoint = new HealthEndpoint(
createHealthIndicator(healthIndicators), true); createHealthIndicator(healthIndicators));
Health health = endpoint.health(); Health health = endpoint.health();
assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).containsOnlyKeys("up", "upAgain"); assertThat(health.getDetails()).containsOnlyKeys("up", "upAgain");
...@@ -51,20 +51,6 @@ public class HealthEndpointTests { ...@@ -51,20 +51,6 @@ public class HealthEndpointTests {
assertThat(upAgainHealth.getDetails()).containsOnly(entry("second", "2")); assertThat(upAgainHealth.getDetails()).containsOnly(entry("second", "2"));
} }
@Test
public void onlyStatusIsExposed() {
Map<String, HealthIndicator> healthIndicators = new HashMap<>();
healthIndicators.put("up", () -> new Health.Builder().status(Status.UP)
.withDetail("first", "1").build());
healthIndicators.put("upAgain", () -> new Health.Builder().status(Status.UP)
.withDetail("second", "2").build());
HealthEndpoint endpoint = new HealthEndpoint(
createHealthIndicator(healthIndicators), false);
Health health = endpoint.health();
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).isEmpty();
}
private HealthIndicator createHealthIndicator( private HealthIndicator createHealthIndicator(
Map<String, HealthIndicator> healthIndicators) { Map<String, HealthIndicator> healthIndicators) {
return new CompositeHealthIndicatorFactory() return new CompositeHealthIndicatorFactory()
......
...@@ -66,14 +66,16 @@ public class HealthEndpointWebIntegrationTests { ...@@ -66,14 +66,16 @@ public class HealthEndpointWebIntegrationTests {
Map<String, HealthIndicator> healthIndicators) { Map<String, HealthIndicator> healthIndicators) {
return new HealthEndpoint( return new HealthEndpoint(
new CompositeHealthIndicatorFactory().createHealthIndicator( new CompositeHealthIndicatorFactory().createHealthIndicator(
new OrderedHealthAggregator(), healthIndicators), new OrderedHealthAggregator(), healthIndicators));
true);
} }
@Bean @Bean
public HealthEndpointWebExtension healthWebEndpointExtension( public HealthEndpointWebExtension healthWebEndpointExtension(
HealthEndpoint delegate) { Map<String, HealthIndicator> healthIndicators) {
return new HealthEndpointWebExtension(delegate, new HealthStatusHttpMapper()); return new HealthEndpointWebExtension(
new CompositeHealthIndicatorFactory().createHealthIndicator(
new OrderedHealthAggregator(), healthIndicators),
new HealthStatusHttpMapper(), true);
} }
@Bean @Bean
......
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