Skip to content

Commit 19e211a

Browse files
committed
Add Status endpoint
This commit adds a new `/application/status` endpoint that provides only the Health's status of an application. Previously, `/application/health` was returning full health details or only the status depending on configuration. Those two use cases are now separate in two endpoints that can be configured, secured and enabled separately. Closes gh-9721
1 parent b9bbe9e commit 19e211a

16 files changed

+667
-158
lines changed

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@
3939
import org.springframework.boot.actuate.endpoint.PublicMetrics;
4040
import org.springframework.boot.actuate.endpoint.RequestMappingEndpoint;
4141
import org.springframework.boot.actuate.endpoint.ShutdownEndpoint;
42+
import org.springframework.boot.actuate.endpoint.StatusEndpoint;
4243
import org.springframework.boot.actuate.endpoint.ThreadDumpEndpoint;
4344
import org.springframework.boot.actuate.endpoint.TraceEndpoint;
4445
import org.springframework.boot.actuate.health.HealthAggregator;
4546
import org.springframework.boot.actuate.health.HealthIndicator;
47+
import org.springframework.boot.actuate.health.HealthIndicatorFactory;
4648
import org.springframework.boot.actuate.health.OrderedHealthAggregator;
4749
import org.springframework.boot.actuate.info.InfoContributor;
4850
import org.springframework.boot.actuate.trace.InMemoryTraceRepository;
@@ -90,17 +92,6 @@ public EnvironmentEndpoint environmentEndpoint(Environment environment) {
9092
return new EnvironmentEndpoint(environment);
9193
}
9294

93-
@Bean
94-
@ConditionalOnMissingBean
95-
@ConditionalOnEnabledEndpoint
96-
public HealthEndpoint healthEndpoint(
97-
ObjectProvider<HealthAggregator> healthAggregator,
98-
ObjectProvider<Map<String, HealthIndicator>> healthIndicators) {
99-
return new HealthEndpoint(
100-
healthAggregator.getIfAvailable(() -> new OrderedHealthAggregator()),
101-
healthIndicators.getIfAvailable(Collections::emptyMap));
102-
}
103-
10495
@Bean
10596
@ConditionalOnMissingBean
10697
@ConditionalOnEnabledEndpoint
@@ -182,6 +173,34 @@ public AuditEventsEndpoint auditEventsEndpoint(
182173
return new AuditEventsEndpoint(auditEventRepository);
183174
}
184175

176+
@Configuration
177+
static class HealthEndpointConfiguration {
178+
179+
private final HealthIndicator healthIndicator;
180+
181+
HealthEndpointConfiguration(ObjectProvider<HealthAggregator> healthAggregator,
182+
ObjectProvider<Map<String, HealthIndicator>> healthIndicators) {
183+
this.healthIndicator = new HealthIndicatorFactory().createHealthIndicator(
184+
healthAggregator.getIfAvailable(OrderedHealthAggregator::new),
185+
healthIndicators.getIfAvailable(Collections::emptyMap));
186+
}
187+
188+
@Bean
189+
@ConditionalOnMissingBean
190+
@ConditionalOnEnabledEndpoint
191+
public HealthEndpoint healthEndpoint() {
192+
return new HealthEndpoint(this.healthIndicator);
193+
}
194+
195+
@Bean
196+
@ConditionalOnMissingBean
197+
@ConditionalOnEnabledEndpoint
198+
public StatusEndpoint statusEndpoint() {
199+
return new StatusEndpoint(this.healthIndicator);
200+
}
201+
202+
}
203+
185204
@Configuration
186205
@ConditionalOnBean(Flyway.class)
187206
@ConditionalOnClass(Flyway.class)

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointManagementContextConfiguration.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@
2020
import org.springframework.boot.actuate.autoconfigure.endpoint.ConditionalOnEnabledEndpoint;
2121
import org.springframework.boot.actuate.endpoint.AuditEventsEndpoint;
2222
import org.springframework.boot.actuate.endpoint.HealthEndpoint;
23+
import org.springframework.boot.actuate.endpoint.StatusEndpoint;
2324
import org.springframework.boot.actuate.endpoint.web.AuditEventsWebEndpointExtension;
25+
import org.springframework.boot.actuate.endpoint.web.HealthStatusHttpMapper;
2426
import org.springframework.boot.actuate.endpoint.web.HealthWebEndpointExtension;
2527
import org.springframework.boot.actuate.endpoint.web.HeapDumpWebEndpoint;
2628
import org.springframework.boot.actuate.endpoint.web.LogFileWebEndpoint;
29+
import org.springframework.boot.actuate.endpoint.web.StatusWebEndpointExtension;
2730
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
2831
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
2932
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -61,12 +64,27 @@ public HeapDumpWebEndpoint heapDumpWebEndpoint() {
6164
@ConditionalOnBean(value = HealthEndpoint.class, search = SearchStrategy.CURRENT)
6265
public HealthWebEndpointExtension healthWebEndpointExtension(HealthEndpoint delegate,
6366
HealthWebEndpointExtensionProperties extensionProperties) {
64-
HealthWebEndpointExtension webExtension = new HealthWebEndpointExtension(
65-
delegate);
67+
return new HealthWebEndpointExtension(delegate,
68+
createHealthStatusHttpMapper(extensionProperties));
69+
}
70+
71+
@Bean
72+
@ConditionalOnMissingBean
73+
@ConditionalOnEnabledEndpoint
74+
@ConditionalOnBean(value = StatusEndpoint.class, search = SearchStrategy.CURRENT)
75+
public StatusWebEndpointExtension statusWebEndpointExtension(StatusEndpoint delegate,
76+
HealthWebEndpointExtensionProperties extensionProperties) {
77+
return new StatusWebEndpointExtension(delegate,
78+
createHealthStatusHttpMapper(extensionProperties));
79+
}
80+
81+
private HealthStatusHttpMapper createHealthStatusHttpMapper(
82+
HealthWebEndpointExtensionProperties extensionProperties) {
83+
HealthStatusHttpMapper statusHttpMapper = new HealthStatusHttpMapper();
6684
if (extensionProperties.getMapping() != null) {
67-
webExtension.addStatusMapping(extensionProperties.getMapping());
85+
statusHttpMapper.addStatusMapping(extensionProperties.getMapping());
6886
}
69-
return webExtension;
87+
return statusHttpMapper;
7088
}
7189

7290
@Bean

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

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,10 @@
1616

1717
package org.springframework.boot.actuate.endpoint;
1818

19-
import java.util.Map;
20-
21-
import org.springframework.boot.actuate.health.CompositeHealthIndicator;
2219
import org.springframework.boot.actuate.health.Health;
23-
import org.springframework.boot.actuate.health.HealthAggregator;
2420
import org.springframework.boot.actuate.health.HealthIndicator;
2521
import org.springframework.boot.endpoint.Endpoint;
2622
import org.springframework.boot.endpoint.ReadOperation;
27-
import org.springframework.util.Assert;
2823

2924
/**
3025
* {@link Endpoint} to expose application health.
@@ -40,18 +35,9 @@ public class HealthEndpoint {
4035

4136
/**
4237
* Create a new {@link HealthEndpoint} instance.
43-
* @param healthAggregator the health aggregator
44-
* @param healthIndicators the health indicators
38+
* @param healthIndicator the health indicator
4539
*/
46-
public HealthEndpoint(HealthAggregator healthAggregator,
47-
Map<String, HealthIndicator> healthIndicators) {
48-
Assert.notNull(healthAggregator, "HealthAggregator must not be null");
49-
Assert.notNull(healthIndicators, "HealthIndicators must not be null");
50-
CompositeHealthIndicator healthIndicator = new CompositeHealthIndicator(
51-
healthAggregator);
52-
for (Map.Entry<String, HealthIndicator> entry : healthIndicators.entrySet()) {
53-
healthIndicator.addHealthIndicator(getKey(entry.getKey()), entry.getValue());
54-
}
40+
public HealthEndpoint(HealthIndicator healthIndicator) {
5541
this.healthIndicator = healthIndicator;
5642
}
5743

@@ -60,17 +46,4 @@ public Health health() {
6046
return this.healthIndicator.health();
6147
}
6248

63-
/**
64-
* Turns the bean name into a key that can be used in the map of health information.
65-
* @param name the bean name
66-
* @return the key
67-
*/
68-
private String getKey(String name) {
69-
int index = name.toLowerCase().indexOf("healthindicator");
70-
if (index > 0) {
71-
return name.substring(0, index);
72-
}
73-
return name;
74-
}
75-
7649
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2012-2017 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+
* http://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 org.springframework.boot.actuate.endpoint;
18+
19+
import org.springframework.boot.actuate.health.Health;
20+
import org.springframework.boot.actuate.health.HealthIndicator;
21+
import org.springframework.boot.actuate.health.Status;
22+
import org.springframework.boot.endpoint.Endpoint;
23+
import org.springframework.boot.endpoint.ReadOperation;
24+
25+
/**
26+
* {@link Endpoint} to expose application {@link Status}.
27+
*
28+
* @author Stephane Nicoll
29+
* @since 2.0.0
30+
*/
31+
@Endpoint(id = "status")
32+
public class StatusEndpoint {
33+
34+
private final HealthIndicator healthIndicator;
35+
36+
/**
37+
* Create a new {@link StatusEndpoint} instance.
38+
* @param healthIndicator the health indicator
39+
*/
40+
public StatusEndpoint(HealthIndicator healthIndicator) {
41+
this.healthIndicator = healthIndicator;
42+
}
43+
44+
@ReadOperation
45+
public Health health() {
46+
return Health.status(this.healthIndicator.health().getStatus()).build();
47+
}
48+
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2012-2017 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+
* http://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 org.springframework.boot.actuate.endpoint.web;
18+
19+
import java.util.Collections;
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
23+
import org.springframework.boot.actuate.health.Status;
24+
import org.springframework.http.HttpStatus;
25+
import org.springframework.util.Assert;
26+
27+
/**
28+
* Map a {@link Status} to an HTTP status code.
29+
*
30+
* @author Stephane Nicoll
31+
* @since 2.0.0
32+
*/
33+
public class HealthStatusHttpMapper {
34+
35+
private Map<String, Integer> statusMapping = new HashMap<>();
36+
37+
/**
38+
* Create a new instance.
39+
*/
40+
public HealthStatusHttpMapper() {
41+
setupDefaultStatusMapping();
42+
}
43+
44+
private void setupDefaultStatusMapping() {
45+
addStatusMapping(Status.DOWN, 503);
46+
addStatusMapping(Status.OUT_OF_SERVICE, 503);
47+
}
48+
49+
/**
50+
* Set specific status mappings.
51+
* @param statusMapping a map of status code to {@link HttpStatus}
52+
*/
53+
public void setStatusMapping(Map<String, Integer> statusMapping) {
54+
Assert.notNull(statusMapping, "StatusMapping must not be null");
55+
this.statusMapping = new HashMap<>(statusMapping);
56+
}
57+
58+
/**
59+
* Add specific status mappings to the existing set.
60+
* @param statusMapping a map of status code to {@link HttpStatus}
61+
*/
62+
public void addStatusMapping(Map<String, Integer> statusMapping) {
63+
Assert.notNull(statusMapping, "StatusMapping must not be null");
64+
this.statusMapping.putAll(statusMapping);
65+
}
66+
67+
/**
68+
* Add a status mapping to the existing set.
69+
* @param status the status to map
70+
* @param httpStatus the http status
71+
*/
72+
public void addStatusMapping(Status status, Integer httpStatus) {
73+
Assert.notNull(status, "Status must not be null");
74+
Assert.notNull(httpStatus, "HttpStatus must not be null");
75+
addStatusMapping(status.getCode(), httpStatus);
76+
}
77+
78+
/**
79+
* Add a status mapping to the existing set.
80+
* @param statusCode the status code to map
81+
* @param httpStatus the http status
82+
*/
83+
public void addStatusMapping(String statusCode, Integer httpStatus) {
84+
Assert.notNull(statusCode, "StatusCode must not be null");
85+
Assert.notNull(httpStatus, "HttpStatus must not be null");
86+
this.statusMapping.put(statusCode, httpStatus);
87+
}
88+
89+
/**
90+
* Return an immutable view of the status mapping.
91+
* @return the http status codes mapped by status name
92+
*/
93+
public Map<String, Integer> getStatusMapping() {
94+
return Collections.unmodifiableMap(this.statusMapping);
95+
}
96+
97+
/**
98+
* Map the specified {@link Status} to an HTTP status code.
99+
* @param status the health {@link Status}
100+
* @return the corresponding HTTP status code
101+
*/
102+
public int mapStatus(Status status) {
103+
String code = getUniformValue(status.getCode());
104+
if (code != null) {
105+
return this.statusMapping.keySet().stream()
106+
.filter((key) -> code.equals(getUniformValue(key)))
107+
.map(this.statusMapping::get).findFirst().orElse(200);
108+
}
109+
return 200;
110+
}
111+
112+
113+
114+
private String getUniformValue(String code) {
115+
if (code == null) {
116+
return null;
117+
}
118+
StringBuilder builder = new StringBuilder();
119+
for (char ch : code.toCharArray()) {
120+
if (Character.isAlphabetic(ch) || Character.isDigit(ch)) {
121+
builder.append(Character.toLowerCase(ch));
122+
}
123+
}
124+
return builder.toString();
125+
}
126+
127+
}

0 commit comments

Comments
 (0)