Allow health groups to be configured at an additional path

Closes gh-25471

Co-authored-by: Phillip Webb <pwebb@vmware.com>
This commit is contained in:
Madhura Bhave
2021-07-13 15:13:04 -07:00
parent fdde40e4fb
commit 49c86e6e1b
48 changed files with 1659 additions and 160 deletions

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2012-2021 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.endpoint.web;
import org.springframework.util.StringUtils;
/**
* Enumeration of server namespaces.
*
* @author Phillip Webb
* @author Madhura Bhave
* @since 2.6.0
*/
public final class WebServerNamespace {
/**
* {@link WebServerNamespace} that represents the main server.
*/
public static final WebServerNamespace SERVER = new WebServerNamespace("server");
/**
* {@link WebServerNamespace} that represents the management server.
*/
public static final WebServerNamespace MANAGEMENT = new WebServerNamespace("management");
private final String value;
private WebServerNamespace(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
public static WebServerNamespace from(String value) {
if (StringUtils.hasText(value)) {
return new WebServerNamespace(value);
}
return SERVER;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
WebServerNamespace other = (WebServerNamespace) obj;
return this.value.equals(other.value);
}
@Override
public int hashCode() {
return this.value.hashCode();
}
}

View File

@@ -42,6 +42,7 @@ import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.OperationArgumentResolver;
import org.springframework.boot.actuate.endpoint.ProducibleOperationArgumentResolver;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
@@ -52,6 +53,7 @@ import org.springframework.boot.actuate.endpoint.web.Link;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
@@ -91,7 +93,7 @@ public class JerseyEndpointResourceFactory {
return resources;
}
private Resource createResource(EndpointMapping endpointMapping, WebOperation operation) {
protected Resource createResource(EndpointMapping endpointMapping, WebOperation operation) {
WebOperationRequestPredicate requestPredicate = operation.getRequestPredicate();
String path = requestPredicate.getPath();
String matchAllRemainingPathSegmentsVariable = requestPredicate.getMatchAllRemainingPathSegmentsVariable();
@@ -99,11 +101,19 @@ public class JerseyEndpointResourceFactory {
path = path.replace("{*" + matchAllRemainingPathSegmentsVariable + "}",
"{" + matchAllRemainingPathSegmentsVariable + ": .*}");
}
Builder resourceBuilder = Resource.builder().path(endpointMapping.createSubPath(path));
return getResource(endpointMapping, operation, requestPredicate, path, null, null);
}
protected Resource getResource(EndpointMapping endpointMapping, WebOperation operation,
WebOperationRequestPredicate requestPredicate, String path, WebServerNamespace serverNamespace,
JerseyRemainingPathSegmentProvider remainingPathSegmentProvider) {
Builder resourceBuilder = Resource.builder().path(endpointMapping.getPath())
.path(endpointMapping.createSubPath(path));
resourceBuilder.addMethod(requestPredicate.getHttpMethod().name())
.consumes(StringUtils.toStringArray(requestPredicate.getConsumes()))
.produces(StringUtils.toStringArray(requestPredicate.getProduces()))
.handledBy(new OperationInflector(operation, !requestPredicate.getConsumes().isEmpty()));
.handledBy(new OperationInflector(operation, !requestPredicate.getConsumes().isEmpty(), serverNamespace,
remainingPathSegmentProvider));
return resourceBuilder.build();
}
@@ -137,9 +147,16 @@ public class JerseyEndpointResourceFactory {
private final boolean readBody;
private OperationInflector(WebOperation operation, boolean readBody) {
private final WebServerNamespace serverNamespace;
private final JerseyRemainingPathSegmentProvider remainingPathSegmentProvider;
private OperationInflector(WebOperation operation, boolean readBody, WebServerNamespace serverNamespace,
JerseyRemainingPathSegmentProvider remainingPathSegments) {
this.operation = operation;
this.readBody = readBody;
this.serverNamespace = serverNamespace;
this.remainingPathSegmentProvider = remainingPathSegments;
}
@Override
@@ -152,7 +169,10 @@ public class JerseyEndpointResourceFactory {
arguments.putAll(extractQueryParameters(data));
try {
JerseySecurityContext securityContext = new JerseySecurityContext(data.getSecurityContext());
OperationArgumentResolver serverNamespaceArgumentResolver = OperationArgumentResolver
.of(WebServerNamespace.class, () -> this.serverNamespace);
InvocationContext invocationContext = new InvocationContext(securityContext, arguments,
serverNamespaceArgumentResolver,
new ProducibleOperationArgumentResolver(() -> data.getHeaders().get("Accept")));
Object response = this.operation.invoke(invocationContext);
return convertToJaxRsResponse(response, data.getRequest().getMethod());
@@ -173,12 +193,21 @@ public class JerseyEndpointResourceFactory {
String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate()
.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
String remainingPathSegments = (String) pathParameters.get(matchAllRemainingPathSegmentsVariable);
String remainingPathSegments = getRemainingPathSegments(requestContext, pathParameters,
matchAllRemainingPathSegmentsVariable);
pathParameters.put(matchAllRemainingPathSegmentsVariable, tokenizePathSegments(remainingPathSegments));
}
return pathParameters;
}
private String getRemainingPathSegments(ContainerRequestContext requestContext,
Map<String, Object> pathParameters, String matchAllRemainingPathSegmentsVariable) {
if (this.remainingPathSegmentProvider != null) {
return this.remainingPathSegmentProvider.get(requestContext, matchAllRemainingPathSegmentsVariable);
}
return (String) pathParameters.get(matchAllRemainingPathSegmentsVariable);
}
private String[] tokenizePathSegments(String path) {
String[] segments = StringUtils.tokenizeToStringArray(path, PATH_SEPARATOR, false, true);
for (int i = 0; i < segments.length; i++) {

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2012-2021 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.endpoint.web.jersey;
import java.util.Set;
import org.glassfish.jersey.server.model.Resource;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
/**
* A factory for creating Jersey {@link Resource Resources} for health groups with
* additional path.
*
* @author Madhura Bhave
* @since 2.6.0
*/
public class JerseyHealthEndpointAdditionalPathResourceFactory extends JerseyEndpointResourceFactory {
private final Set<HealthEndpointGroup> groups;
private final WebServerNamespace serverNamespace;
public JerseyHealthEndpointAdditionalPathResourceFactory(WebServerNamespace serverNamespace,
HealthEndpointGroups groups) {
this.serverNamespace = serverNamespace;
this.groups = groups.getAllWithAdditionalPath(serverNamespace);
}
@Override
protected Resource createResource(EndpointMapping endpointMapping, WebOperation operation) {
WebOperationRequestPredicate requestPredicate = operation.getRequestPredicate();
String matchAllRemainingPathSegmentsVariable = requestPredicate.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
for (HealthEndpointGroup group : this.groups) {
AdditionalHealthEndpointPath additionalPath = group.getAdditionalPath();
if (additionalPath != null) {
return getResource(endpointMapping, operation, requestPredicate, additionalPath.getValue(),
this.serverNamespace, (data, pathSegmentsVariable) -> data.getUriInfo().getPath());
}
}
}
return null;
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2012-2021 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.endpoint.web.jersey;
import javax.ws.rs.container.ContainerRequestContext;
/**
* Strategy interface used to provide the remaining path segments for a Jersey actuator
* endpoint.
*
* @author Madhura Bhave
*/
interface JerseyRemainingPathSegmentProvider {
String get(ContainerRequestContext requestContext, String matchAllRemainingPathSegmentsVariable);
}

View File

@@ -31,6 +31,7 @@ import reactor.core.scheduler.Schedulers;
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.OperationArgumentResolver;
import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.ProducibleOperationArgumentResolver;
import org.springframework.boot.actuate.endpoint.SecurityContext;
@@ -41,6 +42,8 @@ import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.web.context.WebServerApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -64,6 +67,7 @@ import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.pattern.PathPattern;
/**
* A custom {@link HandlerMapping} that makes web endpoints available over HTTP using
@@ -132,18 +136,25 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
}
private void registerMappingForOperation(ExposableWebEndpoint endpoint, WebOperation operation) {
ReactiveWebOperation reactiveWebOperation = wrapReactiveWebOperation(endpoint, operation,
new ReactiveWebOperationAdapter(operation));
RequestMappingInfo requestMappingInfo = createRequestMappingInfo(operation);
if (operation.getType() == OperationType.WRITE) {
registerMapping(createRequestMappingInfo(operation), new WriteOperationHandler((reactiveWebOperation)),
ReactiveWebOperation reactiveWebOperation = wrapReactiveWebOperation(endpoint, operation,
new ReactiveWebOperationAdapter(operation));
registerMapping(requestMappingInfo, new WriteOperationHandler((reactiveWebOperation)),
this.handleWriteMethod);
}
else {
registerMapping(createRequestMappingInfo(operation), new ReadOperationHandler((reactiveWebOperation)),
this.handleReadMethod);
registerReadMapping(requestMappingInfo, endpoint, operation);
}
}
protected void registerReadMapping(RequestMappingInfo requestMappingInfo, ExposableWebEndpoint endpoint,
WebOperation operation) {
ReactiveWebOperation reactiveWebOperation = wrapReactiveWebOperation(endpoint, operation,
new ReactiveWebOperationAdapter(operation));
registerMapping(requestMappingInfo, new ReadOperationHandler((reactiveWebOperation)), this.handleReadMethod);
}
/**
* Hook point that allows subclasses to wrap the {@link ReactiveWebOperation} before
* it's called. Allows additional features, such as security, to be added.
@@ -299,32 +310,25 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
@Override
public Mono<ResponseEntity<Object>> handle(ServerWebExchange exchange, Map<String, String> body) {
Map<String, Object> arguments = getArguments(exchange, body);
String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate()
.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
arguments.put(matchAllRemainingPathSegmentsVariable,
tokenizePathSegments((String) arguments.get(matchAllRemainingPathSegmentsVariable)));
}
OperationArgumentResolver serverNamespaceArgumentResolver = OperationArgumentResolver
.of(WebServerNamespace.class, () -> WebServerNamespace
.from(WebServerApplicationContext.getServerNamepace(exchange.getApplicationContext())));
return this.securityContextSupplier.get()
.map((securityContext) -> new InvocationContext(securityContext, arguments,
serverNamespaceArgumentResolver,
new ProducibleOperationArgumentResolver(
() -> exchange.getRequest().getHeaders().get("Accept"))))
.flatMap((invocationContext) -> handleResult((Publisher<?>) this.invoker.invoke(invocationContext),
exchange.getRequest().getMethod()));
}
private String[] tokenizePathSegments(String path) {
String[] segments = StringUtils.tokenizeToStringArray(path, PATH_SEPARATOR, false, true);
for (int i = 0; i < segments.length; i++) {
if (segments[i].contains("%")) {
segments[i] = StringUtils.uriDecode(segments[i], StandardCharsets.UTF_8);
}
}
return segments;
}
private Map<String, Object> getArguments(ServerWebExchange exchange, Map<String, String> body) {
Map<String, Object> arguments = new LinkedHashMap<>(getTemplateVariables(exchange));
String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate()
.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
arguments.put(matchAllRemainingPathSegmentsVariable, getRemainingPathSegments(exchange));
}
if (body != null) {
arguments.putAll(body);
}
@@ -333,6 +337,26 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
return arguments;
}
private Object getRemainingPathSegments(ServerWebExchange exchange) {
PathPattern pathPattern = exchange.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
if (pathPattern.hasPatternSyntax()) {
String remainingSegments = pathPattern
.extractPathWithinPattern(exchange.getRequest().getPath().pathWithinApplication()).value();
return tokenizePathSegments(remainingSegments);
}
return tokenizePathSegments(pathPattern.toString());
}
private String[] tokenizePathSegments(String value) {
String[] segments = StringUtils.tokenizeToStringArray(value, PATH_SEPARATOR, false, true);
for (int i = 0; i < segments.length; i++) {
if (segments[i].contains("%")) {
segments[i] = StringUtils.uriDecode(segments[i], StandardCharsets.UTF_8);
}
}
return segments;
}
private Map<String, String> getTemplateVariables(ServerWebExchange exchange) {
return exchange.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2012-2021 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.endpoint.web.reactive;
import java.util.Collections;
import java.util.Set;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
/**
* A custom {@link HandlerMapping} that allows health groups to be mapped to an additional
* path.
*
* @author Madhura Bhave
* @since 2.6.0
*/
public class AdditionalHealthEndpointPathsWebFluxHandlerMapping extends AbstractWebFluxEndpointHandlerMapping {
private final EndpointMapping endpointMapping;
private final ExposableWebEndpoint endpoint;
private final Set<HealthEndpointGroup> groups;
public AdditionalHealthEndpointPathsWebFluxHandlerMapping(EndpointMapping endpointMapping,
ExposableWebEndpoint endpoint, Set<HealthEndpointGroup> groups) {
super(endpointMapping, Collections.singletonList(endpoint), null, null, false);
this.endpointMapping = endpointMapping;
this.groups = groups;
this.endpoint = endpoint;
}
@Override
protected void initHandlerMethods() {
for (WebOperation operation : this.endpoint.getOperations()) {
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
String matchAllRemainingPathSegmentsVariable = predicate.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
for (HealthEndpointGroup group : this.groups) {
AdditionalHealthEndpointPath additionalPath = group.getAdditionalPath();
if (additionalPath != null) {
RequestMappingInfo requestMappingInfo = getRequestMappingInfo(operation,
additionalPath.getValue());
registerReadMapping(requestMappingInfo, this.endpoint, operation);
}
}
}
}
}
private RequestMappingInfo getRequestMappingInfo(WebOperation operation, String additionalPath) {
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
String path = this.endpointMapping.createSubPath(additionalPath);
RequestMethod method = RequestMethod.valueOf(predicate.getHttpMethod().name());
String[] consumes = StringUtils.toStringArray(predicate.getConsumes());
String[] produces = StringUtils.toStringArray(predicate.getProduces());
return RequestMappingInfo.paths(path).methods(method).consumes(consumes).produces(produces).build();
}
@Override
protected LinksHandler getLinksHandler() {
return null;
}
}

View File

@@ -32,6 +32,7 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.OperationArgumentResolver;
import org.springframework.boot.actuate.endpoint.ProducibleOperationArgumentResolver;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
@@ -41,6 +42,8 @@ import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.web.context.WebServerApplicationContext;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
@@ -54,6 +57,8 @@ import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.server.ResponseStatusException;
@@ -172,6 +177,11 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
if (matchAllRemainingPathSegmentsVariable != null) {
path = path.replace("{*" + matchAllRemainingPathSegmentsVariable + "}", "**");
}
registerMapping(endpoint, predicate, operation, path);
}
protected void registerMapping(ExposableWebEndpoint endpoint, WebOperationRequestPredicate predicate,
WebOperation operation, String path) {
ServletWebOperation servletWebOperation = wrapServletWebOperation(endpoint, operation,
new ServletWebOperationAdapter(operation));
registerMapping(createRequestMappingInfo(predicate, path), new OperationHandler(servletWebOperation),
@@ -286,8 +296,17 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
Map<String, Object> arguments = getArguments(request, body);
try {
ServletSecurityContext securityContext = new ServletSecurityContext(request);
ProducibleOperationArgumentResolver producibleOperationArgumentResolver = new ProducibleOperationArgumentResolver(
() -> headers.get("Accept"));
OperationArgumentResolver serverNamespaceArgumentResolver = OperationArgumentResolver
.of(WebServerNamespace.class, () -> {
WebApplicationContext applicationContext = WebApplicationContextUtils
.getRequiredWebApplicationContext(request.getServletContext());
return WebServerNamespace
.from(WebServerApplicationContext.getServerNamepace(applicationContext));
});
InvocationContext invocationContext = new InvocationContext(securityContext, arguments,
new ProducibleOperationArgumentResolver(() -> headers.get("Accept")));
serverNamespaceArgumentResolver, producibleOperationArgumentResolver);
return handleResult(this.operation.invoke(invocationContext), HttpMethod.resolve(request.getMethod()));
}
catch (InvalidEndpointRequestException ex) {

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2012-2021 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.endpoint.web.servlet;
import java.util.Collections;
import java.util.Set;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.web.servlet.HandlerMapping;
/**
* A custom {@link HandlerMapping} that allows health groups to be mapped to an additional
* path.
*
* @author Madhura Bhave
* @since 2.6.0
*/
public class AdditionalHealthEndpointPathsWebMvcHandlerMapping extends AbstractWebMvcEndpointHandlerMapping {
private final ExposableWebEndpoint endpoint;
private final Set<HealthEndpointGroup> groups;
public AdditionalHealthEndpointPathsWebMvcHandlerMapping(ExposableWebEndpoint endpoint,
Set<HealthEndpointGroup> groups) {
super(new EndpointMapping(""), Collections.singletonList(endpoint), null, false);
this.endpoint = endpoint;
this.groups = groups;
}
@Override
protected void initHandlerMethods() {
for (WebOperation operation : this.endpoint.getOperations()) {
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
String matchAllRemainingPathSegmentsVariable = predicate.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
for (HealthEndpointGroup group : this.groups) {
AdditionalHealthEndpointPath additionalPath = group.getAdditionalPath();
if (additionalPath != null) {
registerMapping(this.endpoint, predicate, operation, additionalPath.getValue());
}
}
}
}
}
@Override
protected LinksHandler getLinksHandler() {
return null;
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2012-2021 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.health;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Value object that represents an additional path for a {@link HealthEndpointGroup}.
*
* @author Phillip Webb
* @author Madhura Bhave
* @since 2.6.0
*/
public final class AdditionalHealthEndpointPath {
private final WebServerNamespace namespace;
private final String value;
private final String canonicalValue;
private AdditionalHealthEndpointPath(WebServerNamespace namespace, String value) {
this.namespace = namespace;
this.value = value;
this.canonicalValue = (!value.startsWith("/")) ? "/" + value : value;
}
/**
* Returns the {@link WebServerNamespace} associated with this path.
* @return the server namespace
*/
public WebServerNamespace getNamespace() {
return this.namespace;
}
/**
* Returns the value corresponding to this path.
* @return the path
*/
public String getValue() {
return this.value;
}
/**
* Returns {@code true} if this path has the given {@link WebServerNamespace}.
* @param webServerNamespace the server namespace
* @return the new instance
*/
public boolean hasNamespace(WebServerNamespace webServerNamespace) {
return this.namespace.equals(webServerNamespace);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
AdditionalHealthEndpointPath other = (AdditionalHealthEndpointPath) obj;
boolean result = true;
result = result && this.namespace.equals(other.namespace);
result = result && this.canonicalValue.equals(other.canonicalValue);
return result;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.namespace.hashCode();
result = prime * result + this.canonicalValue.hashCode();
return result;
}
@Override
public String toString() {
return this.namespace.getValue() + ":" + this.value;
}
/**
* Creates an {@link AdditionalHealthEndpointPath} from the given input. The input
* must contain a prefix and value separated by a `:`. The value must be limited to
* one path segment. For example, `server:/healthz`.
* @param value the value to parse
* @return the new instance
*/
public static AdditionalHealthEndpointPath from(String value) {
Assert.hasText(value, "Value must not be null");
String[] values = value.split(":");
Assert.isTrue(values.length == 2, "Value must contain a valid namespace and value separated by ':'.");
Assert.isTrue(StringUtils.hasText(values[0]), "Value must contain a valid namespace.");
WebServerNamespace namespace = WebServerNamespace.from(values[0]);
validateValue(values[1]);
return new AdditionalHealthEndpointPath(namespace, values[1]);
}
/**
* Creates an {@link AdditionalHealthEndpointPath} from the given
* {@link WebServerNamespace} and value.
* @param webServerNamespace the server namespace
* @param value the value
* @return the new instance
*/
public static AdditionalHealthEndpointPath of(WebServerNamespace webServerNamespace, String value) {
Assert.notNull(webServerNamespace, "The server namespace must not be null.");
Assert.notNull(value, "The value must not be null.");
validateValue(value);
return new AdditionalHealthEndpointPath(webServerNamespace, value);
}
private static void validateValue(String value) {
Assert.isTrue(StringUtils.countOccurrencesOf(value, "/") <= 1 && value.indexOf("/") <= 0,
"Value must contain only one segment.");
}
}

View File

@@ -20,6 +20,7 @@ import java.util.Map;
import java.util.Set;
import org.springframework.boot.actuate.endpoint.ApiVersion;
import org.springframework.boot.actuate.endpoint.EndpointId;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
@@ -39,6 +40,11 @@ import org.springframework.boot.actuate.endpoint.annotation.Selector.Match;
@Endpoint(id = "health")
public class HealthEndpoint extends HealthEndpointSupport<HealthContributor, HealthComponent> {
/**
* Health endpoint id.
*/
public static final EndpointId ID = EndpointId.of("health");
private static final String[] EMPTY_PATH = {};
/**
@@ -62,7 +68,7 @@ public class HealthEndpoint extends HealthEndpointSupport<HealthContributor, Hea
}
private HealthComponent health(ApiVersion apiVersion, String... path) {
HealthResult<HealthComponent> result = getHealth(apiVersion, SecurityContext.NONE, true, path);
HealthResult<HealthComponent> result = getHealth(apiVersion, null, SecurityContext.NONE, true, path);
return (result != null) ? result.getHealth() : null;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
@@ -23,6 +23,7 @@ import org.springframework.boot.actuate.endpoint.SecurityContext;
* by the {@link HealthEndpoint}.
*
* @author Phillip Webb
* @author Madhura Bhave
* @since 2.2.0
*/
public interface HealthEndpointGroup {
@@ -62,4 +63,12 @@ public interface HealthEndpointGroup {
*/
HttpCodeStatusMapper getHttpCodeStatusMapper();
/**
* Return an additional path that can be used to map the health group to an
* alternative location.
* @return the additional health path or {@code null}
* @since 2.6.0
*/
AdditionalHealthEndpointPath getAdditionalPath();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2021 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.
@@ -16,9 +16,11 @@
package org.springframework.boot.actuate.health;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.util.Assert;
/**
@@ -48,6 +50,40 @@ public interface HealthEndpointGroups {
*/
HealthEndpointGroup get(String name);
/**
* Return the group with the specified additional path or {@code null} if no group
* with that path is found.
* @param path the additional path
* @return the matching {@link HealthEndpointGroup} or {@code null}
* @since 2.6.0
*/
default HealthEndpointGroup get(AdditionalHealthEndpointPath path) {
Assert.notNull(path, "Path must not be null");
for (String name : getNames()) {
HealthEndpointGroup group = get(name);
if (path.equals(group.getAdditionalPath())) {
return group;
}
}
return null;
}
/**
* Return all the groups with an additional path on the specified
* {@link WebServerNamespace}.
* @param namespace the {@link WebServerNamespace}
* @return the matching groups
* @since 2.6.0
*/
default Set<HealthEndpointGroup> getAllWithAdditionalPath(WebServerNamespace namespace) {
Assert.notNull(namespace, "Namespace must not be null");
Set<HealthEndpointGroup> filteredGroups = new LinkedHashSet<>();
getNames().stream().map(this::get).filter(
(group) -> group.getAdditionalPath() != null && group.getAdditionalPath().hasNamespace(namespace))
.forEach(filteredGroups::add);
return filteredGroups;
}
/**
* Factory method to create a {@link HealthEndpointGroups} instance.
* @param primary the primary group

View File

@@ -23,6 +23,7 @@ import java.util.stream.Collectors;
import org.springframework.boot.actuate.endpoint.ApiVersion;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.util.Assert;
/**
@@ -53,14 +54,28 @@ abstract class HealthEndpointSupport<C, T> {
this.groups = groups;
}
HealthResult<T> getHealth(ApiVersion apiVersion, SecurityContext securityContext, boolean showAll, String... path) {
HealthEndpointGroup group = (path.length > 0) ? this.groups.get(path[0]) : null;
if (group != null) {
return getHealth(apiVersion, group, securityContext, showAll, path, 1);
HealthResult<T> getHealth(ApiVersion apiVersion, WebServerNamespace serverNamespace,
SecurityContext securityContext, boolean showAll, String... path) {
if (path.length > 0) {
HealthEndpointGroup group = getHealthGroup(serverNamespace, path);
if (group != null) {
return getHealth(apiVersion, group, securityContext, showAll, path, 1);
}
}
return getHealth(apiVersion, this.groups.getPrimary(), securityContext, showAll, path, 0);
}
private HealthEndpointGroup getHealthGroup(WebServerNamespace serverNamespace, String... path) {
if (this.groups.get(path[0]) != null) {
return this.groups.get(path[0]);
}
if (serverNamespace != null) {
AdditionalHealthEndpointPath additionalPath = AdditionalHealthEndpointPath.of(serverNamespace, path[0]);
return this.groups.get(additionalPath);
}
return null;
}
private HealthResult<T> getHealth(ApiVersion apiVersion, HealthEndpointGroup group, SecurityContext securityContext,
boolean showAll, String[] path, int pathOffset) {
boolean showComponents = showAll || group.showComponents(securityContext);
@@ -71,8 +86,8 @@ abstract class HealthEndpointSupport<C, T> {
return null;
}
Object contributor = getContributor(path, pathOffset);
T health = getContribution(apiVersion, group, contributor, showComponents, showDetails,
isSystemHealth ? this.groups.getNames() : null, false);
Set<String> groupNames = isSystemHealth ? this.groups.getNames() : null;
T health = getContribution(apiVersion, group, contributor, showComponents, showDetails, groupNames, false);
return (health != null) ? new HealthResult<>(health, group) : null;
}

View File

@@ -26,6 +26,7 @@ import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.annotation.Selector.Match;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
/**
@@ -56,19 +57,20 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport<HealthCont
}
@ReadOperation
public WebEndpointResponse<HealthComponent> health(ApiVersion apiVersion, SecurityContext securityContext) {
return health(apiVersion, securityContext, false, NO_PATH);
public WebEndpointResponse<HealthComponent> health(ApiVersion apiVersion, WebServerNamespace serverNamespace,
SecurityContext securityContext) {
return health(apiVersion, serverNamespace, securityContext, false, NO_PATH);
}
@ReadOperation
public WebEndpointResponse<HealthComponent> health(ApiVersion apiVersion, SecurityContext securityContext,
@Selector(match = Match.ALL_REMAINING) String... path) {
return health(apiVersion, securityContext, false, path);
public WebEndpointResponse<HealthComponent> health(ApiVersion apiVersion, WebServerNamespace serverNamespace,
SecurityContext securityContext, @Selector(match = Match.ALL_REMAINING) String... path) {
return health(apiVersion, serverNamespace, securityContext, false, path);
}
public WebEndpointResponse<HealthComponent> health(ApiVersion apiVersion, SecurityContext securityContext,
boolean showAll, String... path) {
HealthResult<HealthComponent> result = getHealth(apiVersion, securityContext, showAll, path);
public WebEndpointResponse<HealthComponent> health(ApiVersion apiVersion, WebServerNamespace serverNamespace,
SecurityContext securityContext, boolean showAll, String... path) {
HealthResult<HealthComponent> result = getHealth(apiVersion, serverNamespace, securityContext, showAll, path);
if (result == null) {
return (Arrays.equals(path, NO_PATH))
? new WebEndpointResponse<>(DEFAULT_HEALTH, WebEndpointResponse.STATUS_OK)

View File

@@ -29,6 +29,7 @@ import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.annotation.Selector.Match;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
/**
@@ -57,19 +58,21 @@ public class ReactiveHealthEndpointWebExtension
@ReadOperation
public Mono<WebEndpointResponse<? extends HealthComponent>> health(ApiVersion apiVersion,
SecurityContext securityContext) {
return health(apiVersion, securityContext, false, NO_PATH);
WebServerNamespace serverNamespace, SecurityContext securityContext) {
return health(apiVersion, serverNamespace, securityContext, false, NO_PATH);
}
@ReadOperation
public Mono<WebEndpointResponse<? extends HealthComponent>> health(ApiVersion apiVersion,
SecurityContext securityContext, @Selector(match = Match.ALL_REMAINING) String... path) {
return health(apiVersion, securityContext, false, path);
WebServerNamespace serverNamespace, SecurityContext securityContext,
@Selector(match = Match.ALL_REMAINING) String... path) {
return health(apiVersion, serverNamespace, securityContext, false, path);
}
public Mono<WebEndpointResponse<? extends HealthComponent>> health(ApiVersion apiVersion,
SecurityContext securityContext, boolean showAll, String... path) {
HealthResult<Mono<? extends HealthComponent>> result = getHealth(apiVersion, securityContext, showAll, path);
WebServerNamespace serverNamespace, SecurityContext securityContext, boolean showAll, String... path) {
HealthResult<Mono<? extends HealthComponent>> result = getHealth(apiVersion, serverNamespace, securityContext,
showAll, path);
if (result == null) {
return (Arrays.equals(path, NO_PATH))
? Mono.just(new WebEndpointResponse<>(DEFAULT_HEALTH, WebEndpointResponse.STATUS_OK))

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2012-2021 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.endpoint.web;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WebServerNamespace}.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
class WebServerNamespaceTests {
@Test
void fromWhenValueHasText() {
assertThat(WebServerNamespace.from("management")).isEqualTo(WebServerNamespace.MANAGEMENT);
}
@Test
void fromWhenValueIsNull() {
assertThat(WebServerNamespace.from(null)).isEqualTo(WebServerNamespace.SERVER);
}
@Test
void fromWhenValueIsEmpty() {
assertThat(WebServerNamespace.from("")).isEqualTo(WebServerNamespace.SERVER);
}
@Test
void namespaceWithSameValueAreEqual() {
assertThat(WebServerNamespace.from("value")).isEqualTo(WebServerNamespace.from("value"));
}
@Test
void namespaceWithDifferentValuesAreNotEqual() {
assertThat(WebServerNamespace.from("value")).isNotEqualTo(WebServerNamespace.from("other"));
}
}

View File

@@ -137,7 +137,7 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
@Test
void matchAllRemainingPathsSelectorShouldDecodePath() {
load(MatchAllRemainingEndpointConfiguration.class,
(client) -> client.get().uri("/matchallremaining/one/two%20three/").exchange().expectStatus().isOk()
(client) -> client.get().uri("/matchallremaining/one/two three/").exchange().expectStatus().isOk()
.expectBody().jsonPath("selection").isEqualTo("one|two three"));
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2012-2021 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.health;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link AdditionalHealthEndpointPath}.
*
* @author Madhura Bhave
*/
class AdditionalHealthEndpointPathTests {
@Test
void fromValidPathShouldCreatePath() {
AdditionalHealthEndpointPath path = AdditionalHealthEndpointPath.from("server:/my-path");
assertThat(path.getValue()).isEqualTo("/my-path");
assertThat(path.getNamespace()).isEqualTo(WebServerNamespace.SERVER);
}
@Test
void fromValidPathWithoutSlashShouldCreatePath() {
AdditionalHealthEndpointPath path = AdditionalHealthEndpointPath.from("server:my-path");
assertThat(path.getValue()).isEqualTo("my-path");
assertThat(path.getNamespace()).isEqualTo(WebServerNamespace.SERVER);
}
@Test
void fromNullPathShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> AdditionalHealthEndpointPath.from(null));
}
@Test
void fromEmptyPathShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> AdditionalHealthEndpointPath.from(""));
}
@Test
void fromPathWithNoNamespaceShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> AdditionalHealthEndpointPath.from("my-path"));
}
@Test
void fromPathWithEmptyNamespaceShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> AdditionalHealthEndpointPath.from(":my-path"));
}
@Test
void fromPathWithMultipleSegmentsShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> AdditionalHealthEndpointPath.from("server:/my-path/my-sub-path"));
}
@Test
void fromPathWithMultipleSegmentsNotStartingWithSlashShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> AdditionalHealthEndpointPath.from("server:my-path/my-sub-path"));
}
@Test
void pathsWithTheSameNamespaceAndValueAreEqual() {
assertThat(AdditionalHealthEndpointPath.from("server:/my-path"))
.isEqualTo(AdditionalHealthEndpointPath.from("server:/my-path"));
}
@Test
void pathsWithTheDifferentNamespaceAndSameValueAreNotEqual() {
assertThat(AdditionalHealthEndpointPath.from("server:/my-path"))
.isNotEqualTo((AdditionalHealthEndpointPath.from("management:/my-path")));
}
@Test
void pathsWithTheSameNamespaceAndValuesWithNoSlashAreEqual() {
assertThat(AdditionalHealthEndpointPath.from("server:/my-path"))
.isEqualTo((AdditionalHealthEndpointPath.from("server:my-path")));
}
@Test
void ofWithNullNamespaceShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> AdditionalHealthEndpointPath.of(null, "my-sub-path"));
}
@Test
void ofWithNullPathShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> AdditionalHealthEndpointPath.of(WebServerNamespace.SERVER, null));
}
@Test
void ofWithMultipleSegmentValueShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> AdditionalHealthEndpointPath.of(WebServerNamespace.SERVER, "/my-path/my-subpath"));
}
@Test
void ofShouldCreatePath() {
AdditionalHealthEndpointPath additionalPath = AdditionalHealthEndpointPath.of(WebServerNamespace.SERVER,
"my-path");
assertThat(additionalPath.getValue()).isEqualTo("my-path");
assertThat(additionalPath.getNamespace()).isEqualTo(WebServerNamespace.SERVER);
}
}

View File

@@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.ApiVersion;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.health.HealthEndpointSupport.HealthResult;
import static org.assertj.core.api.Assertions.assertThat;
@@ -72,7 +73,7 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
@Test
void getHealthWhenPathIsEmptyUsesPrimaryGroup() {
this.registry.registerContributor("test", createContributor(this.up));
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE,
false);
assertThat(result.getGroup()).isEqualTo(this.primaryGroup);
assertThat(getHealth(result)).isNotSameAs(this.up);
@@ -82,7 +83,7 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
@Test
void getHealthWhenPathIsNotGroupReturnsResultFromPrimaryGroup() {
this.registry.registerContributor("test", createContributor(this.up));
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE,
false, "test");
assertThat(result.getGroup()).isEqualTo(this.primaryGroup);
assertThat(getHealth(result)).isEqualTo(this.up);
@@ -92,7 +93,7 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
@Test
void getHealthWhenPathIsGroupReturnsResultFromGroup() {
this.registry.registerContributor("atest", createContributor(this.up));
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE,
false, "alltheas", "atest");
assertThat(result.getGroup()).isEqualTo(this.allTheAs);
assertThat(getHealth(result)).isEqualTo(this.up);
@@ -103,7 +104,7 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
C contributor = createContributor(this.up);
C compositeContributor = createCompositeContributor(Collections.singletonMap("spring", contributor));
this.registry.registerContributor("test", compositeContributor);
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE,
false, "test");
CompositeHealth health = (CompositeHealth) getHealth(result);
assertThat(health.getComponents()).containsKey("spring");
@@ -116,9 +117,9 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
C compositeContributor = createCompositeContributor(Collections.singletonMap("spring", contributor));
this.registry.registerContributor("test", compositeContributor);
HealthEndpointSupport<C, T> endpoint = create(this.registry, this.groups);
HealthResult<T> rootResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false);
HealthResult<T> rootResult = endpoint.getHealth(ApiVersion.V3, null, SecurityContext.NONE, false);
assertThat(((CompositeHealth) getHealth(rootResult)).getComponents()).isNullOrEmpty();
HealthResult<T> componentResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false, "test");
HealthResult<T> componentResult = endpoint.getHealth(ApiVersion.V3, null, SecurityContext.NONE, false, "test");
assertThat(componentResult).isNull();
}
@@ -129,16 +130,16 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
C compositeContributor = createCompositeContributor(Collections.singletonMap("spring", contributor));
this.registry.registerContributor("test", compositeContributor);
HealthEndpointSupport<C, T> endpoint = create(this.registry, this.groups);
HealthResult<T> rootResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false);
HealthResult<T> rootResult = endpoint.getHealth(ApiVersion.V3, null, SecurityContext.NONE, false);
assertThat(((CompositeHealth) getHealth(rootResult)).getComponents()).containsKey("test");
HealthResult<T> componentResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false, "test");
HealthResult<T> componentResult = endpoint.getHealth(ApiVersion.V3, null, SecurityContext.NONE, false, "test");
assertThat(((CompositeHealth) getHealth(componentResult)).getComponents()).containsKey("spring");
}
@Test
void getHealthWhenAlwaysShowIsFalseAndGroupIsTrueShowsDetails() {
this.registry.registerContributor("test", createContributor(this.up));
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE,
false, "test");
assertThat(((Health) getHealth(result)).getDetails()).containsEntry("spring", "boot");
}
@@ -148,8 +149,8 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
this.primaryGroup.setShowDetails(false);
this.registry.registerContributor("test", createContributor(this.up));
HealthEndpointSupport<C, T> endpoint = create(this.registry, this.groups);
HealthResult<T> rootResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false);
HealthResult<T> componentResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false, "test");
HealthResult<T> rootResult = endpoint.getHealth(ApiVersion.V3, null, SecurityContext.NONE, false);
HealthResult<T> componentResult = endpoint.getHealth(ApiVersion.V3, null, SecurityContext.NONE, false, "test");
assertThat(((CompositeHealth) getHealth(rootResult)).getStatus()).isEqualTo(Status.UP);
assertThat(componentResult).isNull();
}
@@ -158,8 +159,8 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
void getHealthWhenAlwaysShowIsTrueShowsDetails() {
this.primaryGroup.setShowDetails(false);
this.registry.registerContributor("test", createContributor(this.up));
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, true,
"test");
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE,
true, "test");
assertThat(((Health) getHealth(result)).getDetails()).containsEntry("spring", "boot");
}
@@ -169,7 +170,7 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
contributors.put("a", createContributor(this.up));
contributors.put("b", createContributor(this.down));
this.registry.registerContributor("test", createCompositeContributor(contributors));
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE,
false);
CompositeHealth root = (CompositeHealth) getHealth(result);
CompositeHealth component = (CompositeHealth) root.getComponents().get("test");
@@ -180,7 +181,7 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
@Test
void getHealthWhenPathDoesNotExistReturnsNull() {
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE,
false, "missing");
assertThat(result).isNull();
}
@@ -188,7 +189,7 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
@Test
void getHealthWhenPathIsEmptyIncludesGroups() {
this.registry.registerContributor("test", createContributor(this.up));
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE,
false);
assertThat(((SystemHealth) getHealth(result)).getGroups()).containsOnly("alltheas");
}
@@ -196,7 +197,7 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
@Test
void getHealthWhenPathIsGroupDoesNotIncludesGroups() {
this.registry.registerContributor("atest", createContributor(this.up));
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE,
false, "alltheas");
assertThat(getHealth(result)).isNotInstanceOf(SystemHealth.class);
}
@@ -204,7 +205,7 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
@Test
void getHealthWithEmptyCompositeReturnsNullResult() { // gh-18687
this.registry.registerContributor("test", createCompositeContributor(Collections.emptyMap()));
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE,
false);
assertThat(result).isNull();
}
@@ -217,12 +218,53 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
TestHealthEndpointGroup testGroup = new TestHealthEndpointGroup((name) -> name.startsWith("test"));
HealthEndpointGroups groups = HealthEndpointGroups.of(this.primaryGroup,
Collections.singletonMap("testGroup", testGroup));
HealthResult<T> result = create(this.registry, groups).getHealth(ApiVersion.V3, SecurityContext.NONE, false,
"testGroup");
HealthResult<T> result = create(this.registry, groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE,
false, "testGroup");
CompositeHealth health = (CompositeHealth) getHealth(result);
assertThat(health.getComponents()).containsKey("test");
}
@Test
void getHealthWhenGroupHasAdditionalPath() {
this.registry.registerContributor("test", createContributor(this.up));
TestHealthEndpointGroup testGroup = new TestHealthEndpointGroup((name) -> name.startsWith("test"));
testGroup.setAdditionalPath(AdditionalHealthEndpointPath.from("server:/healthz"));
HealthEndpointGroups groups = HealthEndpointGroups.of(this.primaryGroup,
Collections.singletonMap("testGroup", testGroup));
HealthResult<T> result = create(this.registry, groups).getHealth(ApiVersion.V3, WebServerNamespace.SERVER,
SecurityContext.NONE, false, "healthz");
CompositeHealth health = (CompositeHealth) getHealth(result);
assertThat(health.getComponents()).containsKey("test");
}
@Test
void getHealthWhenGroupHasAdditionalPathAndShowComponentsFalse() {
this.registry.registerContributor("test", createContributor(this.up));
TestHealthEndpointGroup testGroup = new TestHealthEndpointGroup((name) -> name.startsWith("test"));
testGroup.setAdditionalPath(AdditionalHealthEndpointPath.from("server:/healthz"));
testGroup.setShowComponents(false);
HealthEndpointGroups groups = HealthEndpointGroups.of(this.primaryGroup,
Collections.singletonMap("testGroup", testGroup));
HealthResult<T> result = create(this.registry, groups).getHealth(ApiVersion.V3, WebServerNamespace.SERVER,
SecurityContext.NONE, false, "healthz");
CompositeHealth health = (CompositeHealth) getHealth(result);
assertThat(health.getStatus().getCode()).isEqualTo("UP");
assertThat(health.getComponents()).isNull();
}
@Test
void getComponentHealthWhenGroupHasAdditionalPathAndShowComponentsFalse() {
this.registry.registerContributor("test", createContributor(this.up));
TestHealthEndpointGroup testGroup = new TestHealthEndpointGroup((name) -> name.startsWith("test"));
testGroup.setAdditionalPath(AdditionalHealthEndpointPath.from("server:/healthz"));
testGroup.setShowComponents(false);
HealthEndpointGroups groups = HealthEndpointGroups.of(this.primaryGroup,
Collections.singletonMap("testGroup", testGroup));
HealthResult<T> result = create(this.registry, groups).getHealth(ApiVersion.V3, WebServerNamespace.SERVER,
SecurityContext.NONE, false, "healthz", "test");
assertThat(result).isEqualTo(null);
}
protected abstract HealthEndpointSupport<C, T> create(R registry, HealthEndpointGroups groups);
protected abstract R createRegistry();

View File

@@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.ApiVersion;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.health.HealthEndpointSupport.HealthResult;
import static org.assertj.core.api.Assertions.assertThat;
@@ -42,7 +43,7 @@ class HealthEndpointWebExtensionTests
void healthReturnsSystemHealth() {
this.registry.registerContributor("test", createContributor(this.up));
WebEndpointResponse<HealthComponent> response = create(this.registry, this.groups).health(ApiVersion.LATEST,
SecurityContext.NONE);
WebServerNamespace.SERVER, SecurityContext.NONE);
HealthComponent health = response.getBody();
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health).isInstanceOf(SystemHealth.class);
@@ -54,7 +55,7 @@ class HealthEndpointWebExtensionTests
assertThat(this.registry).isEmpty();
WebEndpointResponse<HealthComponent> response = create(this.registry,
HealthEndpointGroups.of(mock(HealthEndpointGroup.class), Collections.emptyMap()))
.health(ApiVersion.LATEST, SecurityContext.NONE);
.health(ApiVersion.LATEST, WebServerNamespace.SERVER, SecurityContext.NONE);
assertThat(response.getStatus()).isEqualTo(200);
HealthComponent health = response.getBody();
assertThat(health.getStatus()).isEqualTo(Status.UP);
@@ -65,7 +66,7 @@ class HealthEndpointWebExtensionTests
void healthWhenPathDoesNotExistReturnsHttp404() {
this.registry.registerContributor("test", createContributor(this.up));
WebEndpointResponse<HealthComponent> response = create(this.registry, this.groups).health(ApiVersion.LATEST,
SecurityContext.NONE, "missing");
WebServerNamespace.SERVER, SecurityContext.NONE, "missing");
assertThat(response.getBody()).isNull();
assertThat(response.getStatus()).isEqualTo(404);
}
@@ -74,7 +75,7 @@ class HealthEndpointWebExtensionTests
void healthWhenPathExistsReturnsHealth() {
this.registry.registerContributor("test", createContributor(this.up));
WebEndpointResponse<HealthComponent> response = create(this.registry, this.groups).health(ApiVersion.LATEST,
SecurityContext.NONE, "test");
WebServerNamespace.SERVER, SecurityContext.NONE, "test");
assertThat(response.getBody()).isEqualTo(this.up);
assertThat(response.getStatus()).isEqualTo(200);
}

View File

@@ -43,7 +43,7 @@ class ReactiveHealthEndpointWebExtensionTests extends
void healthReturnsSystemHealth() {
this.registry.registerContributor("test", createContributor(this.up));
WebEndpointResponse<? extends HealthComponent> response = create(this.registry, this.groups)
.health(ApiVersion.LATEST, SecurityContext.NONE).block();
.health(ApiVersion.LATEST, null, SecurityContext.NONE).block();
HealthComponent health = response.getBody();
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health).isInstanceOf(SystemHealth.class);
@@ -55,7 +55,7 @@ class ReactiveHealthEndpointWebExtensionTests extends
assertThat(this.registry).isEmpty();
WebEndpointResponse<? extends HealthComponent> response = create(this.registry,
HealthEndpointGroups.of(mock(HealthEndpointGroup.class), Collections.emptyMap()))
.health(ApiVersion.LATEST, SecurityContext.NONE).block();
.health(ApiVersion.LATEST, null, SecurityContext.NONE).block();
assertThat(response.getStatus()).isEqualTo(200);
HealthComponent health = response.getBody();
assertThat(health.getStatus()).isEqualTo(Status.UP);
@@ -66,7 +66,7 @@ class ReactiveHealthEndpointWebExtensionTests extends
void healthWhenPathDoesNotExistReturnsHttp404() {
this.registry.registerContributor("test", createContributor(this.up));
WebEndpointResponse<? extends HealthComponent> response = create(this.registry, this.groups)
.health(ApiVersion.LATEST, SecurityContext.NONE, "missing").block();
.health(ApiVersion.LATEST, null, SecurityContext.NONE, "missing").block();
assertThat(response.getBody()).isNull();
assertThat(response.getStatus()).isEqualTo(404);
}
@@ -75,7 +75,7 @@ class ReactiveHealthEndpointWebExtensionTests extends
void healthWhenPathExistsReturnsHealth() {
this.registry.registerContributor("test", createContributor(this.up));
WebEndpointResponse<? extends HealthComponent> response = create(this.registry, this.groups)
.health(ApiVersion.LATEST, SecurityContext.NONE, "test").block();
.health(ApiVersion.LATEST, null, SecurityContext.NONE, "test").block();
assertThat(response.getBody()).isEqualTo(this.up);
assertThat(response.getStatus()).isEqualTo(200);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2021 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.
@@ -37,6 +37,8 @@ class TestHealthEndpointGroup implements HealthEndpointGroup {
private boolean showDetails = true;
private AdditionalHealthEndpointPath additionalPath;
TestHealthEndpointGroup() {
this((name) -> true);
}
@@ -78,4 +80,13 @@ class TestHealthEndpointGroup implements HealthEndpointGroup {
return this.httpCodeStatusMapper;
}
@Override
public AdditionalHealthEndpointPath getAdditionalPath() {
return this.additionalPath;
}
void setAdditionalPath(AdditionalHealthEndpointPath additionalPath) {
this.additionalPath = additionalPath;
}
}