Skip to content

Commit 323a78c

Browse files
committed
Add property to migrate deprecated endoint IDs
Allow legacy actuator endpoint IDs that contain dots to be transparently migrated to the new format. This update will allow Spring Cloud users to proactively migrate from endpoints such as `hystrix.stream` to `hystrixstream`. Closes gh-18148
1 parent 0a70e33 commit 323a78c

File tree

9 files changed

+102
-8
lines changed

9 files changed

+102
-8
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/AbstractEndpointCondition.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ protected ConditionOutcome getEnablementOutcome(ConditionContext context, Annota
6262
Class<? extends Annotation> annotationClass) {
6363
Environment environment = context.getEnvironment();
6464
AnnotationAttributes attributes = getEndpointAttributes(annotationClass, context, metadata);
65-
EndpointId id = EndpointId.of(attributes.getString("id"));
65+
EndpointId id = EndpointId.of(environment, attributes.getString("id"));
6666
String key = "management.endpoint." + id.toLowerCaseString() + ".enabled";
6767
Boolean userDefinedEnabled = environment.getProperty(key, Boolean.class);
6868
if (userDefinedEnabled != null) {

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM
6262
}
6363
AnnotationAttributes attributes = getEndpointAttributes(ConditionalOnAvailableEndpoint.class, context,
6464
metadata);
65-
EndpointId id = EndpointId.of(attributes.getString("id"));
65+
EndpointId id = EndpointId.of(environment, attributes.getString("id"));
6666
Set<ExposureInformation> exposureInformations = getExposureInformation(environment);
6767
for (ExposureInformation exposureInformation : exposureInformations) {
6868
if (exposureInformation.isExposed(id)) {

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointId.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.commons.logging.Log;
2525
import org.apache.commons.logging.LogFactory;
2626

27+
import org.springframework.core.env.Environment;
2728
import org.springframework.util.Assert;
2829

2930
/**
@@ -44,6 +45,8 @@ public final class EndpointId {
4445

4546
private static final Pattern WARNING_PATTERN = Pattern.compile("[\\.\\-]+");
4647

48+
private static final String MIGRATE_LEGACY_NAMES_PROPRTY = "management.endpoints.migrate-legacy-ids";
49+
4750
private final String value;
4851

4952
private final String lowerCaseValue;
@@ -112,6 +115,27 @@ public static EndpointId of(String value) {
112115
return new EndpointId(value);
113116
}
114117

118+
/**
119+
* Factory method to create a new {@link EndpointId} of the specified value. This
120+
* variant will respect the {@code management.endpoints.migrate-legacy-names} property
121+
* if it has been set in the {@link Environment}.
122+
* @param environment the Spring environment
123+
* @param value the endpoint ID value
124+
* @return an {@link EndpointId} instance
125+
* @since 2.2.0
126+
*/
127+
public static EndpointId of(Environment environment, String value) {
128+
Assert.notNull(environment, "Environment must not be null");
129+
return new EndpointId(migrateLegacyId(environment, value));
130+
}
131+
132+
private static String migrateLegacyId(Environment environment, String value) {
133+
if (environment.getProperty(MIGRATE_LEGACY_NAMES_PROPRTY, Boolean.class, false)) {
134+
return value.replace(".", "");
135+
}
136+
return value;
137+
}
138+
115139
/**
116140
* Factory method to create a new {@link EndpointId} from a property value. More
117141
* lenient than {@link #of(String)} to allow for common "relaxed" property variants.

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.springframework.core.annotation.MergedAnnotation;
4848
import org.springframework.core.annotation.MergedAnnotations;
4949
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
50+
import org.springframework.core.env.Environment;
5051
import org.springframework.util.Assert;
5152
import org.springframework.util.CollectionUtils;
5253
import org.springframework.util.LinkedMultiValueMap;
@@ -140,7 +141,7 @@ private Collection<EndpointBean> createEndpointBeans() {
140141

141142
private EndpointBean createEndpointBean(String beanName) {
142143
Object bean = this.applicationContext.getBean(beanName);
143-
return new EndpointBean(beanName, bean);
144+
return new EndpointBean(this.applicationContext.getEnvironment(), beanName, bean);
144145
}
145146

146147
private void addExtensionBeans(Collection<EndpointBean> endpointBeans) {
@@ -159,7 +160,7 @@ private void addExtensionBeans(Collection<EndpointBean> endpointBeans) {
159160

160161
private ExtensionBean createExtensionBean(String beanName) {
161162
Object bean = this.applicationContext.getBean(beanName);
162-
return new ExtensionBean(beanName, bean);
163+
return new ExtensionBean(this.applicationContext.getEnvironment(), beanName, bean);
163164
}
164165

165166
private void addExtensionBean(EndpointBean endpointBean, ExtensionBean extensionBean) {
@@ -401,15 +402,15 @@ private static class EndpointBean {
401402

402403
private Set<ExtensionBean> extensions = new LinkedHashSet<>();
403404

404-
EndpointBean(String beanName, Object bean) {
405+
EndpointBean(Environment environment, String beanName, Object bean) {
405406
MergedAnnotation<Endpoint> annotation = MergedAnnotations
406407
.from(bean.getClass(), SearchStrategy.TYPE_HIERARCHY).get(Endpoint.class);
407408
String id = annotation.getString("id");
408409
Assert.state(StringUtils.hasText(id),
409410
() -> "No @Endpoint id attribute specified for " + bean.getClass().getName());
410411
this.beanName = beanName;
411412
this.bean = bean;
412-
this.id = EndpointId.of(id);
413+
this.id = EndpointId.of(environment, id);
413414
this.enabledByDefault = annotation.getBoolean("enableByDefault");
414415
this.filter = getFilter(this.bean.getClass());
415416
}
@@ -462,7 +463,7 @@ private static class ExtensionBean {
462463

463464
private final Class<?> filter;
464465

465-
ExtensionBean(String beanName, Object bean) {
466+
ExtensionBean(Environment environment, String beanName, Object bean) {
466467
this.bean = bean;
467468
this.beanName = beanName;
468469
MergedAnnotation<EndpointExtension> extensionAnnotation = MergedAnnotations
@@ -472,7 +473,7 @@ private static class ExtensionBean {
472473
.from(endpointType, SearchStrategy.TYPE_HIERARCHY).get(Endpoint.class);
473474
Assert.state(endpointAnnotation.isPresent(),
474475
() -> "Extension " + endpointType.getName() + " does not specify an endpoint");
475-
this.endpointId = EndpointId.of(endpointAnnotation.getString("id"));
476+
this.endpointId = EndpointId.of(environment, endpointAnnotation.getString("id"));
476477
this.filter = extensionAnnotation.getClass("filter");
477478
}
478479

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"properties": [
3+
{
4+
"name": "management.endpoints.migrate-legacy-ids",
5+
"type": "java.lang.Boolean",
6+
"description": "Whether to transparently migrate legacy endpoint IDs.",
7+
"defaultValue": false
8+
}
9+
]
10+
}

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/EndpointIdTests.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.springframework.boot.test.system.CapturedOutput;
2323
import org.springframework.boot.test.system.OutputCaptureExtension;
24+
import org.springframework.mock.env.MockEnvironment;
2425

2526
import static org.assertj.core.api.Assertions.assertThat;
2627
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -92,6 +93,16 @@ void ofWhenContainsDeprecatedCharsLogsWarning(CapturedOutput output) {
9293
.contains("Endpoint ID 'foo-bar' contains invalid characters, please migrate to a valid format");
9394
}
9495

96+
@Test
97+
void ofWhenMigratingLegacyNameRemovesDots(CapturedOutput output) {
98+
EndpointId.resetLoggedWarnings();
99+
MockEnvironment environment = new MockEnvironment();
100+
environment.setProperty("management.endpoints.migrate-legacy-ids", "true");
101+
EndpointId endpointId = EndpointId.of(environment, "foo.bar");
102+
assertThat(endpointId.toString()).isEqualTo("foobar");
103+
assertThat(output).doesNotContain("contains invalid characters");
104+
}
105+
95106
@Test
96107
void equalsAndHashCode() {
97108
EndpointId one = EndpointId.of("foobar1");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package smoketest.actuator;
18+
19+
import java.util.Collections;
20+
import java.util.Map;
21+
22+
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
23+
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
24+
import org.springframework.stereotype.Component;
25+
26+
@Component
27+
@Endpoint(id = "lega.cy")
28+
public class SampleLegacyEndpoint {
29+
30+
@ReadOperation
31+
public Map<String, String> example() {
32+
return Collections.singletonMap("legacy", "legacy");
33+
}
34+
35+
}

spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ management.endpoint.health.show-details=always
2424
management.endpoint.health.group.ready.include=db,diskSpace
2525
management.endpoint.health.group.live.include=example,hello,db
2626
management.endpoint.health.group.live.show-details=never
27+
management.endpoints.migrate-legacy-ids=true

spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/test/java/smoketest/actuator/SampleActuatorApplicationTests.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.springframework.http.ResponseEntity;
3737

3838
import static org.assertj.core.api.Assertions.assertThat;
39+
import static org.assertj.core.api.Assertions.entry;
3940

4041
/**
4142
* Basic integration tests for service demo application.
@@ -188,6 +189,17 @@ void testConfigProps() {
188189
assertThat(beans).containsKey("spring.datasource-" + DataSourceProperties.class.getName());
189190
}
190191

192+
@Test
193+
void testLegacy() {
194+
@SuppressWarnings("rawtypes")
195+
ResponseEntity<Map> entity = this.restTemplate.withBasicAuth("user", getPassword())
196+
.getForEntity("/actuator/legacy", Map.class);
197+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
198+
@SuppressWarnings("unchecked")
199+
Map<String, Object> body = entity.getBody();
200+
assertThat(body).contains(entry("legacy", "legacy"));
201+
}
202+
191203
private String getPassword() {
192204
return "password";
193205
}

0 commit comments

Comments
 (0)