Filter properties with a particular prefix

This commit improves the configprops endpoint to allow filtering
properties based on a particular prefix

See gh-24718
This commit is contained in:
bono007 2021-01-13 09:27:47 -06:00 committed by Stephane Nicoll
parent 0f9fb13141
commit ad7c69a9cd
7 changed files with 391 additions and 11 deletions

View File

@ -6,15 +6,15 @@ The `configprops` endpoint provides information about the application's `@Config
[[configprops-retrieving]]
== Retrieving the @ConfigurationProperties Bean
== Retrieving All @ConfigurationProperties Beans
To retrieve the `@ConfigurationProperties` beans, make a `GET` request to `/actuator/configprops`, as shown in the following curl-based example:
To retrieve all of the `@ConfigurationProperties` beans, make a `GET` request to `/actuator/configprops`, as shown in the following curl-based example:
include::{snippets}/configprops/curl-request.adoc[]
include::{snippets}/configprops/all/curl-request.adoc[]
The resulting response is similar to the following:
include::{snippets}/configprops/http-response.adoc[]
include::{snippets}/configprops/all/http-response.adoc[]
@ -26,3 +26,26 @@ The following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}/configprops/response-fields.adoc[]
[[configprops-retrieving-by-prefix]]
== Retrieving @ConfigurationProperties Beans By Prefix
To retrieve the `@ConfigurationProperties` beans mapped under a certain prefix, make a `GET` request to `/actuator/configprops/{prefix}`, as shown in the following curl-based example:
include::{snippets}/configprops/prefixed/curl-request.adoc[]
The resulting response is similar to the following:
include::{snippets}/configprops/prefixed/http-response.adoc[]
NOTE: The `{prefix}` does not need to be exact, a more general prefix will return all beans mapped under that prefix stem.
[[configprops-retrieving-by-prefix-response-structure]]
=== Response Structure
The response contains details of the application's `@ConfigurationProperties` beans.
The following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}/configprops/prefixed/response-fields.adoc[]

View File

@ -18,7 +18,9 @@ package org.springframework.boot.actuate.autoconfigure.context.properties;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpointWebExtension;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@ -30,6 +32,7 @@ import org.springframework.context.annotation.Configuration;
*
* @author Phillip Webb
* @author Stephane Nicoll
* @author Chris Bono
* @since 2.0.0
*/
@Configuration(proxyBeanMethods = false)
@ -49,4 +52,12 @@ public class ConfigurationPropertiesReportEndpointAutoConfiguration {
return endpoint;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(ConfigurationPropertiesReportEndpoint.class)
public ConfigurationPropertiesReportEndpointWebExtension configurationPropertiesReportEndpointWebExtension(
ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint) {
return new ConfigurationPropertiesReportEndpointWebExtension(configurationPropertiesReportEndpoint);
}
}

View File

@ -36,13 +36,31 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
* {@link ConfigurationPropertiesReportEndpoint}.
*
* @author Andy Wilkinson
* @author Chris Bono
*/
class ConfigurationPropertiesReportEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
@Test
void configProps() throws Exception {
this.mockMvc.perform(get("/actuator/configprops")).andExpect(status().isOk())
.andDo(MockMvcRestDocumentation.document("configprops",
.andDo(MockMvcRestDocumentation.document("configprops/all",
preprocessResponse(limit("contexts", getApplicationContext().getId(), "beans")),
responseFields(fieldWithPath("contexts").description("Application contexts keyed by id."),
fieldWithPath("contexts.*.beans.*")
.description("`@ConfigurationProperties` beans keyed by bean name."),
fieldWithPath("contexts.*.beans.*.prefix")
.description("Prefix applied to the names of the bean's properties."),
subsectionWithPath("contexts.*.beans.*.properties")
.description("Properties of the bean as name-value pairs."),
subsectionWithPath("contexts.*.beans.*.inputs").description(
"Origin and value of the configuration property used when binding to this bean."),
parentIdField())));
}
@Test
void configPropsFilterByPrefix() throws Exception {
this.mockMvc.perform(get("/actuator/configprops/spring.resources")).andExpect(status().isOk())
.andDo(MockMvcRestDocumentation.document("configprops/prefixed",
preprocessResponse(limit("contexts", getApplicationContext().getId(), "beans")),
responseFields(fieldWithPath("contexts").description("Application contexts keyed by id."),
fieldWithPath("contexts.*.beans.*")

View File

@ -26,6 +26,7 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
@ -55,6 +56,7 @@ import org.springframework.beans.BeansException;
import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.context.properties.BoundConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesBean;
@ -90,6 +92,7 @@ import org.springframework.util.StringUtils;
* @author Stephane Nicoll
* @author Madhura Bhave
* @author Andy Wilkinson
* @author Chris Bono
* @since 2.0.0
*/
@Endpoint(id = "configprops")
@ -114,15 +117,21 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
@ReadOperation
public ApplicationConfigurationProperties configurationProperties() {
return extract(this.context);
return extract(this.context, (bean) -> true);
}
private ApplicationConfigurationProperties extract(ApplicationContext context) {
@ReadOperation
public ApplicationConfigurationProperties configurationProperties(@Selector String prefix) {
return extract(this.context, (bean) -> bean.getAnnotation().prefix().startsWith(prefix));
}
private ApplicationConfigurationProperties extract(ApplicationContext context,
Predicate<ConfigurationPropertiesBean> beanFilterPredicate) {
ObjectMapper mapper = getObjectMapper();
Map<String, ContextConfigurationProperties> contexts = new HashMap<>();
ApplicationContext target = context;
while (target != null) {
contexts.put(target.getId(), describeBeans(mapper, target));
contexts.put(target.getId(), describeBeans(mapper, target, beanFilterPredicate));
target = target.getParent();
}
return new ApplicationConfigurationProperties(contexts);
@ -169,10 +178,14 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
mapper.setSerializerFactory(factory);
}
private ContextConfigurationProperties describeBeans(ObjectMapper mapper, ApplicationContext context) {
private ContextConfigurationProperties describeBeans(ObjectMapper mapper, ApplicationContext context,
Predicate<ConfigurationPropertiesBean> beanFilterPredicate) {
Map<String, ConfigurationPropertiesBean> beans = ConfigurationPropertiesBean.getAll(context);
Map<String, ConfigurationPropertiesBeanDescriptor> descriptors = new HashMap<>();
beans.forEach((beanName, bean) -> descriptors.put(beanName, describeBean(mapper, bean)));
Map<String, ConfigurationPropertiesBeanDescriptor> descriptors = beans.values().stream()
.filter(beanFilterPredicate::test)
.collect(Collectors.toMap((bean) -> bean.getName(), (bean) -> describeBean(mapper, bean)));
return new ContextConfigurationProperties(descriptors,
(context.getParent() != null) ? context.getParent().getId() : null);
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2012-2020 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
*
* https://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.context.properties;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
/**
* {@link EndpointWebExtension @EndpointWebExtension} for the
* {@link ConfigurationPropertiesReportEndpoint}.
*
* @author Chris Bono
* @since 2.4
*/
@EndpointWebExtension(endpoint = ConfigurationPropertiesReportEndpoint.class)
public class ConfigurationPropertiesReportEndpointWebExtension {
private final ConfigurationPropertiesReportEndpoint delegate;
public ConfigurationPropertiesReportEndpointWebExtension(ConfigurationPropertiesReportEndpoint delegate) {
this.delegate = delegate;
}
@ReadOperation
public WebEndpointResponse<ApplicationConfigurationProperties> configurationProperties(@Selector String prefix) {
ApplicationConfigurationProperties configurationProperties = this.delegate.configurationProperties(prefix);
boolean foundMatchingBeans = configurationProperties.getContexts().values().stream()
.map(ContextConfigurationProperties::getBeans).anyMatch((beans) -> !beans.isEmpty());
return (foundMatchingBeans) ? new WebEndpointResponse<>(configurationProperties, WebEndpointResponse.STATUS_OK)
: new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND);
}
}

View File

@ -0,0 +1,133 @@
/*
* Copyright 2012-2019 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
*
* https://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.context.properties;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ConfigurationPropertiesReportEndpoint} when filtering by prefix.
*
* @author Chris Bono
*/
class ConfigurationPropertiesReportEndpointFilteringTests {
@Test
void filterByPrefixSingleMatch() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner().withUserConfiguration(Config.class)
.withPropertyValues("foo.primary.name:foo1", "foo.secondary.name:foo2", "only.bar.name:solo1");
contextRunner.run((context) -> {
ConfigurationPropertiesReportEndpoint endpoint = context
.getBean(ConfigurationPropertiesReportEndpoint.class);
ApplicationConfigurationProperties applicationProperties = endpoint.configurationProperties("only.bar");
assertThat(applicationProperties.getContexts()).containsOnlyKeys(context.getId());
ContextConfigurationProperties contextProperties = applicationProperties.getContexts().get(context.getId());
assertThat(contextProperties.getBeans().values()).hasSize(1).first().hasFieldOrPropertyWithValue("prefix",
"only.bar");
});
}
@Test
void filterByPrefixMultipleMatches() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner().withUserConfiguration(Config.class)
.withPropertyValues("foo.primary.name:foo1", "foo.secondary.name:foo2", "only.bar.name:solo1");
contextRunner.run((context) -> {
ConfigurationPropertiesReportEndpoint endpoint = context
.getBean(ConfigurationPropertiesReportEndpoint.class);
ApplicationConfigurationProperties applicationProperties = endpoint.configurationProperties("foo.");
assertThat(applicationProperties.getContexts()).containsOnlyKeys(context.getId());
ContextConfigurationProperties contextProperties = applicationProperties.getContexts().get(context.getId());
assertThat(contextProperties.getBeans()).containsOnlyKeys("primaryFoo", "secondaryFoo");
});
}
@Test
void filterByPrefixNoMatches() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner().withUserConfiguration(Config.class)
.withPropertyValues("foo.primary.name:foo1", "foo.secondary.name:foo2", "only.bar.name:solo1");
contextRunner.run((context) -> {
ConfigurationPropertiesReportEndpoint endpoint = context
.getBean(ConfigurationPropertiesReportEndpoint.class);
ApplicationConfigurationProperties applicationProperties = endpoint.configurationProperties("foo.third");
assertThat(applicationProperties.getContexts()).containsOnlyKeys(context.getId());
ContextConfigurationProperties contextProperties = applicationProperties.getContexts().get(context.getId());
assertThat(contextProperties.getBeans()).isEmpty();
});
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(Bar.class)
static class Config {
@Bean
ConfigurationPropertiesReportEndpoint endpoint() {
return new ConfigurationPropertiesReportEndpoint();
}
@Bean
@ConfigurationProperties(prefix = "foo.primary")
Foo primaryFoo() {
return new Foo();
}
@Bean
@ConfigurationProperties(prefix = "foo.secondary")
Foo secondaryFoo() {
return new Foo();
}
}
public static class Foo {
private String name = "5150";
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
@ConfigurationProperties(prefix = "only.bar")
public static class Bar {
private String name = "123456";
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

@ -0,0 +1,131 @@
/*
* Copyright 2012-2020 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
*
* https://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.context.properties;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasSize;
/**
* Integration tests for {@link ConfigurationPropertiesReportEndpoint} exposed by Jersey,
* Spring MVC, and WebFlux.
*
* @author Chris Bono
*/
class ConfigurationPropertiesReportEndpointWebIntegrationTests {
private WebTestClient client;
@BeforeEach
void prepareEnvironment(ConfigurableApplicationContext context, WebTestClient client) {
TestPropertyValues.of("com.foo.name=fooz", "com.bar.name=barz").applyTo(context);
this.client = client;
}
@WebEndpointTest
void noFilters() {
this.client.get().uri("/actuator/configprops").exchange().expectStatus().isOk().expectBody()
.jsonPath("$..beans[*]").value(hasSize(greaterThanOrEqualTo(2))).jsonPath("$..beans['fooDotCom']")
.exists().jsonPath("$..beans['barDotCom']").exists();
}
@WebEndpointTest
void filterByExactPrefix() {
this.client.get().uri("/actuator/configprops/com.foo").exchange().expectStatus().isOk().expectBody()
.jsonPath("$..beans[*]").value(hasSize(1)).jsonPath("$..beans['fooDotCom']").exists();
}
@WebEndpointTest
void filterByGeneralPrefix() {
this.client.get().uri("/actuator/configprops/com.").exchange().expectStatus().isOk().expectBody()
.jsonPath("$..beans[*]").value(hasSize(2)).jsonPath("$..beans['fooDotCom']").exists()
.jsonPath("$..beans['barDotCom']").exists();
}
@WebEndpointTest
void filterByNonExistentPrefix() {
this.client.get().uri("/actuator/configprops/com.zoo").exchange().expectStatus().isNotFound();
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
static class TestConfiguration {
@Bean
ConfigurationPropertiesReportEndpoint endpoint() {
return new ConfigurationPropertiesReportEndpoint();
}
@Bean
ConfigurationPropertiesReportEndpointWebExtension endpointWebExtension(
ConfigurationPropertiesReportEndpoint endpoint) {
return new ConfigurationPropertiesReportEndpointWebExtension(endpoint);
}
@Bean
@ConfigurationProperties(prefix = "com.foo")
Foo fooDotCom() {
return new Foo();
}
@Bean
@ConfigurationProperties(prefix = "com.bar")
Bar barDotCom() {
return new Bar();
}
}
public static class Foo {
private String name = "5150";
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
public static class Bar {
private String name = "6160";
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
}