Skip to content

Commit 8dcf1e4

Browse files
mbhavewilkinsona
andcommitted
Provide end-to-end traceability for config properties
Closes gh-14880 Co-authored-by: Andy Wilkinson <[email protected]>
1 parent 8f5777c commit 8dcf1e4

File tree

16 files changed

+452
-42
lines changed

16 files changed

+452
-42
lines changed

Diff for: spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ConfigurationPropertiesReportEndpointDocumentationTests.java

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ void configProps() throws Exception {
5151
.description("Prefix applied to the names of the bean's properties."),
5252
subsectionWithPath("contexts.*.beans.*.properties")
5353
.description("Properties of the bean as name-value pairs."),
54+
subsectionWithPath("contexts.*.beans.*.inputs").description(
55+
"Origin and value of the configuration property used when binding to this bean."),
5456
parentIdField())));
5557
}
5658

Diff for: spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java

+80-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Collection;
2323
import java.util.Collections;
2424
import java.util.HashMap;
25+
import java.util.LinkedHashMap;
2526
import java.util.List;
2627
import java.util.Map;
2728
import java.util.stream.Collectors;
@@ -55,7 +56,10 @@
5556
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
5657
import org.springframework.boot.context.properties.ConfigurationProperties;
5758
import org.springframework.boot.context.properties.ConfigurationPropertiesBean;
59+
import org.springframework.boot.context.properties.ConfigurationPropertiesBoundPropertiesHolder;
5860
import org.springframework.boot.context.properties.ConstructorBinding;
61+
import org.springframework.boot.context.properties.source.ConfigurationProperty;
62+
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
5963
import org.springframework.context.ApplicationContext;
6064
import org.springframework.context.ApplicationContextAware;
6165
import org.springframework.core.KotlinDetector;
@@ -77,6 +81,8 @@
7781
* @author Christian Dupuis
7882
* @author Dave Syer
7983
* @author Stephane Nicoll
84+
* @author Madhura Bhave
85+
* @author Andy Wilkinson
8086
* @since 2.0.0
8187
*/
8288
@Endpoint(id = "configprops")
@@ -120,8 +126,10 @@ private ContextConfigurationProperties describeConfigurationProperties(Applicati
120126
Map<String, ConfigurationPropertiesBeanDescriptor> descriptors = new HashMap<>();
121127
beans.forEach((beanName, bean) -> {
122128
String prefix = bean.getAnnotation().prefix();
123-
descriptors.put(beanName, new ConfigurationPropertiesBeanDescriptor(prefix,
124-
sanitize(prefix, safeSerialize(mapper, bean.getInstance(), prefix))));
129+
descriptors.put(beanName,
130+
new ConfigurationPropertiesBeanDescriptor(prefix,
131+
sanitize(prefix, safeSerialize(mapper, bean.getInstance(), prefix)),
132+
getInputs(prefix, safeSerialize(mapper, bean.getInstance(), prefix))));
125133
});
126134
return new ContextConfigurationProperties(descriptors,
127135
(context.getParent() != null) ? context.getParent().getId() : null);
@@ -229,6 +237,67 @@ else if (item instanceof List) {
229237
return sanitized;
230238
}
231239

240+
@SuppressWarnings("unchecked")
241+
private Map<String, Object> getInputs(String prefix, Map<String, Object> map) {
242+
map.forEach((key, value) -> {
243+
String qualifiedKey = (prefix.isEmpty() ? prefix : prefix + ".") + key;
244+
if (value instanceof Map) {
245+
map.put(key, getInputs(qualifiedKey, (Map<String, Object>) value));
246+
}
247+
else if (value instanceof List) {
248+
map.put(key, getInputs(qualifiedKey, (List<Object>) value));
249+
}
250+
else {
251+
map.put(key, applyInput(qualifiedKey));
252+
}
253+
});
254+
return map;
255+
}
256+
257+
@SuppressWarnings("unchecked")
258+
private List<Object> getInputs(String prefix, List<Object> list) {
259+
List<Object> augmented = new ArrayList<>();
260+
int index = 0;
261+
for (Object item : list) {
262+
String name = prefix + "[" + index++ + "]";
263+
if (item instanceof Map) {
264+
augmented.add(getInputs(name, (Map<String, Object>) item));
265+
}
266+
else if (item instanceof List) {
267+
augmented.add(getInputs(name, (List<Object>) item));
268+
}
269+
else {
270+
augmented.add(applyInput(name));
271+
}
272+
}
273+
return augmented;
274+
}
275+
276+
private Map<String, Object> applyInput(String qualifiedKey) {
277+
if (!this.context.containsBean(ConfigurationPropertiesBoundPropertiesHolder.BEAN_NAME)) {
278+
return Collections.emptyMap();
279+
}
280+
ConfigurationPropertiesBoundPropertiesHolder bean = this.context.getBean(
281+
ConfigurationPropertiesBoundPropertiesHolder.BEAN_NAME,
282+
ConfigurationPropertiesBoundPropertiesHolder.class);
283+
Map<ConfigurationPropertyName, ConfigurationProperty> boundProperties = bean.getProperties();
284+
ConfigurationPropertyName currentName = ConfigurationPropertyName.adapt(qualifiedKey, '.');
285+
ConfigurationProperty candidate = boundProperties.get(currentName);
286+
if (candidate == null && currentName.isLastElementIndexed()) {
287+
candidate = boundProperties.get(currentName.chop(currentName.getNumberOfElements() - 1));
288+
}
289+
return (candidate != null) ? getInput(currentName.toString(), candidate) : Collections.emptyMap();
290+
}
291+
292+
private Map<String, Object> getInput(String property, ConfigurationProperty candidate) {
293+
Map<String, Object> input = new LinkedHashMap<>();
294+
String origin = (candidate.getOrigin() != null) ? candidate.getOrigin().toString() : "none";
295+
Object value = candidate.getValue();
296+
input.put("origin", origin);
297+
input.put("value", this.sanitizer.sanitize(property, value));
298+
return input;
299+
}
300+
232301
/**
233302
* Extension to {@link JacksonAnnotationIntrospector} to suppress CGLIB generated bean
234303
* properties.
@@ -457,9 +526,13 @@ public static final class ConfigurationPropertiesBeanDescriptor {
457526

458527
private final Map<String, Object> properties;
459528

460-
private ConfigurationPropertiesBeanDescriptor(String prefix, Map<String, Object> properties) {
529+
private final Map<String, Object> inputs;
530+
531+
private ConfigurationPropertiesBeanDescriptor(String prefix, Map<String, Object> properties,
532+
Map<String, Object> inputs) {
461533
this.prefix = prefix;
462534
this.properties = properties;
535+
this.inputs = inputs;
463536
}
464537

465538
public String getPrefix() {
@@ -470,6 +543,10 @@ public Map<String, Object> getProperties() {
470543
return this.properties;
471544
}
472545

546+
public Map<String, Object> getInputs() {
547+
return this.inputs;
548+
}
549+
473550
}
474551

475552
}

0 commit comments

Comments
 (0)