diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/CommonsClientAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/CommonsClientAutoConfiguration.java index 656f301c..27a23090 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/CommonsClientAutoConfiguration.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/CommonsClientAutoConfiguration.java @@ -16,8 +16,12 @@ package org.springframework.cloud.client; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.actuate.health.HealthAggregator; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigureOrder; @@ -25,10 +29,13 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.client.actuator.FeaturesEndpoint; +import org.springframework.cloud.client.actuator.HasFeatures; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.health.DiscoveryClientHealthIndicator; import org.springframework.cloud.client.discovery.health.DiscoveryCompositeHealthIndicator; import org.springframework.cloud.client.discovery.health.DiscoveryHealthIndicator; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -56,4 +63,22 @@ public class CommonsClientAutoConfiguration { return new DiscoveryCompositeHealthIndicator(aggregator, indicators); } + @Bean + public HasFeatures commonsFeatures() { + return HasFeatures.abstractFeatures(DiscoveryClient.class, + LoadBalancerClient.class); + } + + @Configuration + @ConditionalOnClass(Endpoint.class) + protected static class ActuatorConfiguration { + @Autowired(required = false) + private List hasFeatures = new ArrayList<>(); + + @Bean + public FeaturesEndpoint featuresEndpoint() { + return new FeaturesEndpoint(hasFeatures); + } + } + } diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/actuator/FeaturesEndpoint.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/actuator/FeaturesEndpoint.java new file mode 100644 index 00000000..1cb5464d --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/actuator/FeaturesEndpoint.java @@ -0,0 +1,87 @@ +package org.springframework.cloud.client.actuator; + +import java.util.ArrayList; +import java.util.List; + +import lombok.Value; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.actuate.endpoint.AbstractEndpoint; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +/** + * @author Spencer Gibb + */ +@ConfigurationProperties(prefix = "endpoints.features", ignoreUnknownFields = false) +public class FeaturesEndpoint extends AbstractEndpoint implements ApplicationContextAware { + + private final List hasFeaturesList; + private ApplicationContext context; + + public FeaturesEndpoint(List hasFeaturesList) { + super("features", false); + this.hasFeaturesList = hasFeaturesList; + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + this.context = context; + } + + @Override + public Features invoke() { + Features features = new Features(); + + for (HasFeatures hasFeatures : hasFeaturesList) { + List abstractFeatures = hasFeatures.getAbstractFeatures(); + if (abstractFeatures != null) { + for (Class clazz : abstractFeatures) { + addAbstractFeature(features, clazz); + } + } + + List namedFeatures = hasFeatures.getNamedFeatures(); + if (namedFeatures != null) { + for (NamedFeature namedFeature : namedFeatures) { + addFeature(features, namedFeature); + } + } + } + + return features; + } + + private void addAbstractFeature(Features features, Class type) { + addFeature(features, new NamedFeature(type.getSimpleName(), type)); + } + + private void addFeature(Features features, NamedFeature feature) { + try { + Object bean = context.getBean(feature.getType()); + Class beanClass = bean.getClass(); + features.getEnabled().add(new Feature(feature.getName(), + beanClass.getCanonicalName(), + beanClass.getPackage().getImplementationVersion(), + beanClass.getPackage().getImplementationVendor())); + } catch (NoSuchBeanDefinitionException e) { + features.getDisabled().add(feature.getName()); + } + } + + @Value + class Features { + List enabled = new ArrayList<>(); + List disabled = new ArrayList<>(); + } + + @Value + class Feature { + final String type; + final String name; + final String version; + final String vendor; + } +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/actuator/HasFeatures.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/actuator/HasFeatures.java new file mode 100644 index 00000000..59b2e343 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/actuator/HasFeatures.java @@ -0,0 +1,29 @@ +package org.springframework.cloud.client.actuator; + +import lombok.Builder; +import lombok.Singular; +import lombok.Value; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * @author Spencer Gibb + */ +@Value +@Builder +public class HasFeatures { + @Singular + private final List abstractFeatures; + @Singular + private final List namedFeatures; + + public static HasFeatures abstractFeatures(Class... abstractFeatures) { + return new HasFeatures(Arrays.asList(abstractFeatures), Collections.emptyList()); + } + + public static HasFeatures namedFeatures(NamedFeature... namedFeatures) { + return new HasFeatures(Collections.emptyList(), Arrays.asList(namedFeatures)); + } +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/actuator/NamedFeature.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/actuator/NamedFeature.java new file mode 100644 index 00000000..f5062052 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/actuator/NamedFeature.java @@ -0,0 +1,12 @@ +package org.springframework.cloud.client.actuator; + +import lombok.Value; + +/** + * @author Spencer Gibb + */ +@Value +public class NamedFeature { + private final String name; + private final Class type; +} diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/actuator/FeaturesEndpointTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/actuator/FeaturesEndpointTests.java new file mode 100644 index 00000000..31bce206 --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/actuator/FeaturesEndpointTests.java @@ -0,0 +1,89 @@ +package org.springframework.cloud.client.actuator; + + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Spencer Gibb + */ +public class FeaturesEndpointTests { + + private AnnotationConfigApplicationContext context; + + @Before + public void setup() { + this.context = new AnnotationConfigApplicationContext(); + this.context.register(JacksonAutoConfiguration.class, FeaturesConfig.class, Config.class); + this.context.refresh(); + } + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void invokeWorks() { + FeaturesEndpoint.Features features = this.context.getBean(FeaturesEndpoint.class).invoke(); + assertThat(features, is(notNullValue())); + assertThat(features.getEnabled().size(), is(equalTo(2))); + assertThat(features.getDisabled().size(), is(equalTo(1))); + } + + @Configuration + public static class FeaturesConfig { + @Bean + Foo foo() { + return new Foo(); + } + + @Bean + Bar bar() { + return new Bar(); + } + + @Bean + HasFeatures localFeatures() { + return HasFeatures.builder() + .abstractFeature(Foo.class) + .namedFeature(new NamedFeature("Bar Feature", Bar.class)) + .abstractFeature(Baz.class) + .build(); + } + + } + + @Configuration + @EnableConfigurationProperties + public static class Config { + @Autowired(required = false) + private List hasFeatures = new ArrayList<>(); + + @Bean + public FeaturesEndpoint cloudEndpoint() { + return new FeaturesEndpoint(hasFeatures); + } + } + + public static class Foo {} + public static class Bar {} + public static class Baz {} +}