Allow health groups to be configured at an additional path

Closes gh-25471

Co-authored-by: Phillip Webb <pwebb@vmware.com>
pull/27659/head
Madhura Bhave 3 years ago
parent fdde40e4fb
commit 49c86e6e1b

@ -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.
@ -61,8 +61,9 @@ public class AvailabilityProbesAutoConfiguration {
}
@Bean
public AvailabilityProbesHealthEndpointGroupsPostProcessor availabilityProbesHealthEndpointGroupsPostProcessor() {
return new AvailabilityProbesHealthEndpointGroupsPostProcessor();
public AvailabilityProbesHealthEndpointGroupsPostProcessor availabilityProbesHealthEndpointGroupsPostProcessor(
Environment environment) {
return new AvailabilityProbesHealthEndpointGroupsPostProcessor(environment);
}
/**

@ -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.
@ -21,6 +21,7 @@ import java.util.HashSet;
import java.util.Set;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.StatusAggregator;
@ -35,8 +36,11 @@ class AvailabilityProbesHealthEndpointGroup implements HealthEndpointGroup {
private final Set<String> members;
AvailabilityProbesHealthEndpointGroup(String... members) {
private final AdditionalHealthEndpointPath additionalPath;
AvailabilityProbesHealthEndpointGroup(AdditionalHealthEndpointPath additionalPath, String... members) {
this.members = new HashSet<>(Arrays.asList(members));
this.additionalPath = additionalPath;
}
@Override
@ -64,4 +68,9 @@ class AvailabilityProbesHealthEndpointGroup implements HealthEndpointGroup {
return HttpCodeStatusMapper.DEFAULT;
}
@Override
public AdditionalHealthEndpointPath getAdditionalPath() {
return this.additionalPath;
}
}

@ -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.
@ -22,6 +22,8 @@ import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
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;
import org.springframework.util.Assert;
@ -31,29 +33,39 @@ import org.springframework.util.Assert;
*
* @author Phillip Webb
* @author Brian Clozel
* @author Madhura Bhave
*/
class AvailabilityProbesHealthEndpointGroups implements HealthEndpointGroups {
private static final Map<String, AvailabilityProbesHealthEndpointGroup> GROUPS;
static {
Map<String, AvailabilityProbesHealthEndpointGroup> groups = new LinkedHashMap<>();
groups.put("liveness", new AvailabilityProbesHealthEndpointGroup("livenessState"));
groups.put("readiness", new AvailabilityProbesHealthEndpointGroup("readinessState"));
GROUPS = Collections.unmodifiableMap(groups);
}
private final HealthEndpointGroups groups;
private final Map<String, HealthEndpointGroup> probeGroups;
private final Set<String> names;
AvailabilityProbesHealthEndpointGroups(HealthEndpointGroups groups) {
AvailabilityProbesHealthEndpointGroups(HealthEndpointGroups groups, boolean addAdditionalPaths) {
Assert.notNull(groups, "Groups must not be null");
this.groups = groups;
this.probeGroups = createProbeGroups(addAdditionalPaths);
Set<String> names = new LinkedHashSet<>(groups.getNames());
names.addAll(GROUPS.keySet());
names.addAll(this.probeGroups.keySet());
this.names = Collections.unmodifiableSet(names);
}
private Map<String, HealthEndpointGroup> createProbeGroups(boolean addAdditionalPaths) {
Map<String, HealthEndpointGroup> probeGroups = new LinkedHashMap<>();
probeGroups.put("liveness", createProbeGroup(addAdditionalPaths, "/livez", "livenessState"));
probeGroups.put("readiness", createProbeGroup(addAdditionalPaths, "/readyz", "readinessState"));
return Collections.unmodifiableMap(probeGroups);
}
private AvailabilityProbesHealthEndpointGroup createProbeGroup(boolean addAdditionalPath, String path,
String members) {
AdditionalHealthEndpointPath additionalPath = (!addAdditionalPath) ? null
: AdditionalHealthEndpointPath.of(WebServerNamespace.SERVER, path);
return new AvailabilityProbesHealthEndpointGroup(additionalPath, members);
}
@Override
public HealthEndpointGroup getPrimary() {
return this.groups.getPrimary();
@ -68,13 +80,14 @@ class AvailabilityProbesHealthEndpointGroups implements HealthEndpointGroups {
public HealthEndpointGroup get(String name) {
HealthEndpointGroup group = this.groups.get(name);
if (group == null) {
group = GROUPS.get(name);
group = this.probeGroups.get(name);
}
return group;
}
static boolean containsAllProbeGroups(HealthEndpointGroups groups) {
return groups.getNames().containsAll(GROUPS.keySet());
Set<String> names = groups.getNames();
return names.contains("liveness") && names.contains("readiness");
}
}

@ -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.
@ -20,22 +20,31 @@ import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.boot.actuate.health.HealthEndpointGroupsPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
/**
* {@link HealthEndpointGroupsPostProcessor} to add
* {@link AvailabilityProbesHealthEndpointGroups}.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
@Order(Ordered.LOWEST_PRECEDENCE)
class AvailabilityProbesHealthEndpointGroupsPostProcessor implements HealthEndpointGroupsPostProcessor {
private final boolean addAdditionalPaths;
AvailabilityProbesHealthEndpointGroupsPostProcessor(Environment environment) {
this.addAdditionalPaths = "true"
.equalsIgnoreCase(environment.getProperty("management.endpoint.health.probes.add-additional-paths"));
}
@Override
public HealthEndpointGroups postProcessHealthEndpointGroups(HealthEndpointGroups groups) {
if (AvailabilityProbesHealthEndpointGroups.containsAllProbeGroups(groups)) {
return groups;
}
return new AvailabilityProbesHealthEndpointGroups(groups);
return new AvailabilityProbesHealthEndpointGroups(groups, this.addAdditionalPaths);
}
}

@ -48,13 +48,13 @@ public class CloudFoundryReactiveHealthEndpointWebExtension {
@ReadOperation
public Mono<WebEndpointResponse<? extends HealthComponent>> health(ApiVersion apiVersion) {
return this.delegate.health(apiVersion, SecurityContext.NONE, true);
return this.delegate.health(apiVersion, null, SecurityContext.NONE, true);
}
@ReadOperation
public Mono<WebEndpointResponse<? extends HealthComponent>> health(ApiVersion apiVersion,
@Selector(match = Match.ALL_REMAINING) String... path) {
return this.delegate.health(apiVersion, SecurityContext.NONE, true, path);
return this.delegate.health(apiVersion, null, SecurityContext.NONE, true, path);
}
}

@ -46,13 +46,13 @@ public class CloudFoundryHealthEndpointWebExtension {
@ReadOperation
public WebEndpointResponse<HealthComponent> health(ApiVersion apiVersion) {
return this.delegate.health(apiVersion, SecurityContext.NONE, true);
return this.delegate.health(apiVersion, null, SecurityContext.NONE, true);
}
@ReadOperation
public WebEndpointResponse<HealthComponent> health(ApiVersion apiVersion,
@Selector(match = Match.ALL_REMAINING) String... path) {
return this.delegate.health(apiVersion, SecurityContext.NONE, true, path);
return this.delegate.health(apiVersion, null, SecurityContext.NONE, true, path);
}
}

@ -18,8 +18,11 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.Resource;
@ -27,7 +30,9 @@ import org.glassfish.jersey.server.model.Resource;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.jersey.ManagementContextResourceConfigCustomizer;
import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.EndpointId;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
@ -36,8 +41,12 @@ import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableServletEndpoint;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.jersey.JerseyEndpointResourceFactory;
import org.springframework.boot.actuate.endpoint.web.jersey.JerseyHealthEndpointAdditionalPathResourceFactory;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@ -64,6 +73,8 @@ import org.springframework.util.StringUtils;
@ConditionalOnMissingBean(type = "org.springframework.web.servlet.DispatcherServlet")
class JerseyWebEndpointManagementContextConfiguration {
private static final EndpointId HEALTH_ENDPOINT_ID = EndpointId.of("health");
@Bean
JerseyWebEndpointsResourcesRegistrar jerseyWebEndpointsResourcesRegistrar(Environment environment,
WebEndpointsSupplier webEndpointsSupplier, ServletEndpointsSupplier servletEndpointsSupplier,
@ -74,6 +85,17 @@ class JerseyWebEndpointManagementContextConfiguration {
endpointMediaTypes, basePath, shouldRegisterLinks);
}
@Bean
@ConditionalOnBean(HealthEndpoint.class)
@ConditionalOnManagementPort(ManagementPortType.DIFFERENT)
JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar jerseyDifferentPortAdditionalHealthEndpointPathsResourcesRegistrar(
WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups healthEndpointGroups) {
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
ExposableWebEndpoint health = webEndpoints.stream()
.filter((endpoint) -> endpoint.getEndpointId().equals(HEALTH_ENDPOINT_ID)).findFirst().get();
return new JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(health, healthEndpointGroups);
}
private boolean shouldRegisterLinksMapping(WebEndpointProperties properties, Environment environment,
String basePath) {
return properties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath)
@ -134,4 +156,38 @@ class JerseyWebEndpointManagementContextConfiguration {
}
class JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar
implements ManagementContextResourceConfigCustomizer {
private final ExposableWebEndpoint endpoint;
private final HealthEndpointGroups groups;
JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(ExposableWebEndpoint endpoint,
HealthEndpointGroups groups) {
this.endpoint = endpoint;
this.groups = groups;
}
@Override
public void customize(ResourceConfig config) {
register(config);
}
private void register(ResourceConfig config) {
EndpointMapping mapping = new EndpointMapping("");
JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory(
WebServerNamespace.MANAGEMENT, this.groups);
Collection<Resource> endpointResources = resourceFactory
.createEndpointResources(mapping, Collections.singletonList(this.endpoint), null, null, false)
.stream().filter(Objects::nonNull).collect(Collectors.toList());
register(endpointResources, config);
}
private void register(Collection<Resource> resources, ResourceConfig config) {
config.registerResources(new HashSet<>(resources));
}
}
}

@ -23,6 +23,7 @@ import java.util.List;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
@ -31,9 +32,13 @@ import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.reactive.AdditionalHealthEndpointPathsWebFluxHandlerMapping;
import org.springframework.boot.actuate.endpoint.web.reactive.ControllerEndpointHandlerMapping;
import org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@ -84,6 +89,18 @@ public class WebFluxEndpointManagementContextConfiguration {
|| ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
}
@Bean
@ConditionalOnManagementPort(ManagementPortType.DIFFERENT)
@ConditionalOnBean(HealthEndpoint.class)
public AdditionalHealthEndpointPathsWebFluxHandlerMapping managementHealthEndpointWebFluxHandlerMapping(
WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) {
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
ExposableWebEndpoint health = webEndpoints.stream()
.filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID)).findFirst().get();
return new AdditionalHealthEndpointPathsWebFluxHandlerMapping(new EndpointMapping(""), health,
groups.getAllWithAdditionalPath(WebServerNamespace.MANAGEMENT));
}
@Bean
@ConditionalOnMissingBean
public ControllerEndpointHandlerMapping controllerEndpointHandlerMapping(

@ -23,6 +23,7 @@ import java.util.List;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
@ -31,10 +32,14 @@ import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.servlet.AdditionalHealthEndpointPathsWebMvcHandlerMapping;
import org.springframework.boot.actuate.endpoint.web.servlet.ControllerEndpointHandlerMapping;
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@ -86,6 +91,18 @@ public class WebMvcEndpointManagementContextConfiguration {
|| ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
}
@Bean
@ConditionalOnManagementPort(ManagementPortType.DIFFERENT)
@ConditionalOnBean(HealthEndpoint.class)
public AdditionalHealthEndpointPathsWebMvcHandlerMapping managementHealthEndpointWebMvcHandlerMapping(
WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) {
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
ExposableWebEndpoint health = webEndpoints.stream()
.filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID)).findFirst().get();
return new AdditionalHealthEndpointPathsWebMvcHandlerMapping(health,
groups.getAllWithAdditionalPath(WebServerNamespace.MANAGEMENT));
}
@Bean
@ConditionalOnMissingBean
public ControllerEndpointHandlerMapping controllerEndpointHandlerMapping(

@ -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.
@ -22,6 +22,7 @@ import java.util.function.Predicate;
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Show;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
import org.springframework.boot.actuate.health.StatusAggregator;
@ -35,6 +36,7 @@ import org.springframework.util.CollectionUtils;
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Madhura Bhave
*/
class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup {
@ -50,6 +52,8 @@ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup {
private final Collection<String> roles;
private final AdditionalHealthEndpointPath additionalPath;
/**
* Create a new {@link AutoConfiguredHealthEndpointGroup} instance.
* @param members a predicate used to test for group membership
@ -58,16 +62,18 @@ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup {
* @param showComponents the show components setting
* @param showDetails the show details setting
* @param roles the roles to match
* @param additionalPath the additional path to use for this group
*/
AutoConfiguredHealthEndpointGroup(Predicate<String> members, StatusAggregator statusAggregator,
HttpCodeStatusMapper httpCodeStatusMapper, Show showComponents, Show showDetails,
Collection<String> roles) {
HttpCodeStatusMapper httpCodeStatusMapper, Show showComponents, Show showDetails, Collection<String> roles,
AdditionalHealthEndpointPath additionalPath) {
this.members = members;
this.statusAggregator = statusAggregator;
this.httpCodeStatusMapper = httpCodeStatusMapper;
this.showComponents = showComponents;
this.showDetails = showDetails;
this.roles = roles;
this.additionalPath = additionalPath;
}
@Override
@ -141,4 +147,9 @@ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup {
return this.httpCodeStatusMapper;
}
@Override
public AdditionalHealthEndpointPath getAdditionalPath() {
return this.additionalPath;
}
}

@ -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.
@ -33,6 +33,7 @@ import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties.Group;
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Show;
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Status;
import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
@ -48,6 +49,7 @@ import org.springframework.util.ObjectUtils;
* Auto-configured {@link HealthEndpointGroups}.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups {
@ -77,7 +79,7 @@ class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups {
httpCodeStatusMapper = new SimpleHttpCodeStatusMapper(properties.getStatus().getHttpMapping());
}
this.primaryGroup = new AutoConfiguredHealthEndpointGroup(ALL, statusAggregator, httpCodeStatusMapper,
showComponents, showDetails, roles);
showComponents, showDetails, roles, null);
this.groups = createGroups(properties.getGroup(), beanFactory, statusAggregator, httpCodeStatusMapper,
showComponents, showDetails, roles);
}
@ -106,8 +108,10 @@ class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups {
return defaultHttpCodeStatusMapper;
});
Predicate<String> members = new IncludeExcludeGroupMemberPredicate(group.getInclude(), group.getExclude());
AdditionalHealthEndpointPath additionalPath = (group.getAdditionalPath() != null)
? AdditionalHealthEndpointPath.from(group.getAdditionalPath()) : null;
groups.put(groupName, new AutoConfiguredHealthEndpointGroup(members, statusAggregator, httpCodeStatusMapper,
showComponents, showDetails, roles));
showComponents, showDetails, roles, additionalPath));
});
return Collections.unmodifiableMap(groups);
}

@ -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.
@ -61,6 +61,10 @@ public class HealthEndpointProperties extends HealthProperties {
*/
public static class Group extends HealthProperties {
public static final String SERVER_PREFIX = "server:";
public static final String MANAGEMENT_PREFIX = "management:";
/**
* Health indicator IDs that should be included or '*' for all.
*/
@ -77,6 +81,14 @@ public class HealthEndpointProperties extends HealthProperties {
*/
private Show showDetails;
/**
* Additional path that this group can be made available on. The additional path
* must start with a valid prefix, either `server` or `management` to indicate if
* it will be available on the main port or the management port. For instance,
* `server:/healthz` will configure the group on the main port at `/healthz`.
*/
private String additionalPath;
public Set<String> getInclude() {
return this.include;
}
@ -102,6 +114,14 @@ public class HealthEndpointProperties extends HealthProperties {
this.showDetails = showDetails;
}
public String getAdditionalPath() {
return this.additionalPath;
}
public void setAdditionalPath(String additionalPath) {
this.additionalPath = additionalPath;
}
}
}

@ -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,6 +16,13 @@
package org.springframework.boot.actuate.autoconfigure.health;
import java.util.Collection;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.endpoint.web.reactive.AdditionalHealthEndpointPathsWebFluxHandlerMapping;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.boot.actuate.health.ReactiveHealthContributorRegistry;
@ -31,6 +38,7 @@ import org.springframework.context.annotation.Configuration;
* Configuration for {@link HealthEndpoint} reactive web extensions.
*
* @author Phillip Webb
* @author Madhura Bhave
* @see HealthEndpointAutoConfiguration
*/
@Configuration(proxyBeanMethods = false)
@ -46,4 +54,19 @@ class HealthEndpointReactiveWebExtensionConfiguration {
return new ReactiveHealthEndpointWebExtension(reactiveHealthContributorRegistry, groups);
}
@Configuration(proxyBeanMethods = false)
static class WebFluxAdditionalHealthEndpointPathsConfiguration {
@Bean
AdditionalHealthEndpointPathsWebFluxHandlerMapping healthEndpointWebFluxHandlerMapping(
WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) {
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
ExposableWebEndpoint health = webEndpoints.stream()
.filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID)).findFirst().get();
return new AdditionalHealthEndpointPathsWebFluxHandlerMapping(new EndpointMapping(""), health,
groups.getAllWithAdditionalPath(WebServerNamespace.SERVER));
}
}
}

@ -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,21 +16,48 @@
package org.springframework.boot.actuate.autoconfigure.health;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.stream.Collectors;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.servlet.ServletContainer;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.endpoint.web.jersey.JerseyHealthEndpointAdditionalPathResourceFactory;
import org.springframework.boot.actuate.endpoint.web.servlet.AdditionalHealthEndpointPathsWebMvcHandlerMapping;
import org.springframework.boot.actuate.health.HealthContributorRegistry;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.boot.actuate.health.HealthEndpointWebExtension;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.jersey.JerseyProperties;
import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer;
import org.springframework.boot.autoconfigure.web.servlet.DefaultJerseyApplicationPath;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
/**
* Configuration for {@link HealthEndpoint} web extensions.
*
* @author Phillip Webb
* @author Madhura Bhave
* @see HealthEndpointAutoConfiguration
*/
@Configuration(proxyBeanMethods = false)
@ -46,4 +73,98 @@ class HealthEndpointWebExtensionConfiguration {
return new HealthEndpointWebExtension(healthContributorRegistry, groups);
}
private static ExposableWebEndpoint getHealthEndpoint(WebEndpointsSupplier webEndpointsSupplier) {
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
return webEndpoints.stream().filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID))
.findFirst().get();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(DispatcherServlet.class)
static class MvcAdditionalHealthEndpointPathsConfiguration {
@Bean
AdditionalHealthEndpointPathsWebMvcHandlerMapping healthEndpointWebMvcHandlerMapping(
WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) {
ExposableWebEndpoint health = getHealthEndpoint(webEndpointsSupplier);
return new AdditionalHealthEndpointPathsWebMvcHandlerMapping(health,
groups.getAllWithAdditionalPath(WebServerNamespace.SERVER));
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ResourceConfig.class)
@ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet")
static class JerseyAdditionalHealthEndpointPathsConfiguration {
@Bean
JerseyAdditionalHealthEndpointPathsResourcesRegistrar jerseyAdditionalHealthEndpointPathsResourcesRegistrar(
WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups healthEndpointGroups) {
ExposableWebEndpoint health = getHealthEndpoint(webEndpointsSupplier);
return new JerseyAdditionalHealthEndpointPathsResourcesRegistrar(health, healthEndpointGroups);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(ResourceConfig.class)
@EnableConfigurationProperties(JerseyProperties.class)
static class JerseyInfrastructureConfiguration {
@Bean
@ConditionalOnMissingBean(JerseyApplicationPath.class)
JerseyApplicationPath jerseyApplicationPath(JerseyProperties properties, ResourceConfig config) {
return new DefaultJerseyApplicationPath(properties.getApplicationPath(), config);
}
@Bean
ResourceConfig resourceConfig(ObjectProvider<ResourceConfigCustomizer> resourceConfigCustomizers) {
ResourceConfig resourceConfig = new ResourceConfig();
resourceConfigCustomizers.orderedStream().forEach((customizer) -> customizer.customize(resourceConfig));
return resourceConfig;
}
@Bean
ServletRegistrationBean<ServletContainer> jerseyServletRegistration(
JerseyApplicationPath jerseyApplicationPath, ResourceConfig resourceConfig) {
return new ServletRegistrationBean<>(new ServletContainer(resourceConfig),
jerseyApplicationPath.getUrlMapping());
}
}
}
static class JerseyAdditionalHealthEndpointPathsResourcesRegistrar implements ResourceConfigCustomizer {
private final ExposableWebEndpoint endpoint;
private final HealthEndpointGroups groups;
JerseyAdditionalHealthEndpointPathsResourcesRegistrar(ExposableWebEndpoint endpoint,
HealthEndpointGroups groups) {
this.endpoint = endpoint;
this.groups = groups;
}
@Override
public void customize(ResourceConfig config) {
register(config);
}
private void register(ResourceConfig config) {
EndpointMapping mapping = new EndpointMapping("");
JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory(
WebServerNamespace.SERVER, this.groups);
Collection<Resource> endpointResources = resourceFactory
.createEndpointResources(mapping, Collections.singletonList(this.endpoint), null, null, false)
.stream().filter(Objects::nonNull).collect(Collectors.toList());
register(endpointResources, config);
}
private void register(Collection<Resource> resources, ResourceConfig config) {
config.registerResources(new HashSet<>(resources));
}
}
}

@ -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.
@ -32,7 +32,7 @@ import static org.mockito.Mockito.mock;
*/
class AvailabilityProbesHealthEndpointGroupTests {
private AvailabilityProbesHealthEndpointGroup group = new AvailabilityProbesHealthEndpointGroup("a", "b");
private AvailabilityProbesHealthEndpointGroup group = new AvailabilityProbesHealthEndpointGroup(null, "a", "b");
@Test
void isMemberWhenMemberReturnsTrue() {

@ -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.
@ -21,7 +21,9 @@ import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
@ -31,10 +33,12 @@ import static org.mockito.Mockito.mock;
* Tests for {@link AvailabilityProbesHealthEndpointGroupsPostProcessor}.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
class AvailabilityProbesHealthEndpointGroupsPostProcessorTests {
private AvailabilityProbesHealthEndpointGroupsPostProcessor postProcessor = new AvailabilityProbesHealthEndpointGroupsPostProcessor();
private AvailabilityProbesHealthEndpointGroupsPostProcessor postProcessor = new AvailabilityProbesHealthEndpointGroupsPostProcessor(
new MockEnvironment());
@Test
void postProcessHealthEndpointGroupsWhenGroupsAlreadyContainedReturnsOriginal() {
@ -68,7 +72,43 @@ class AvailabilityProbesHealthEndpointGroupsPostProcessorTests {
given(groups.getNames()).willReturn(names);
assertThat(this.postProcessor.postProcessHealthEndpointGroups(groups))
.isInstanceOf(AvailabilityProbesHealthEndpointGroups.class);
}
@Test
void postProcessHealthEndpointGroupsWhenAdditionalPathPropertyIsTrue() {
HealthEndpointGroups postProcessed = getPostProcessed("true");
HealthEndpointGroup liveness = postProcessed.get("liveness");
HealthEndpointGroup readiness = postProcessed.get("readiness");
assertThat(liveness.getAdditionalPath().toString()).isEqualTo("server:/livez");
assertThat(readiness.getAdditionalPath().toString()).isEqualTo("server:/readyz");
}
private HealthEndpointGroups getPostProcessed(String value) {
MockEnvironment environment = new MockEnvironment();
environment.setProperty("management.endpoint.health.probes.add-additional-paths", value);
AvailabilityProbesHealthEndpointGroupsPostProcessor postProcessor = new AvailabilityProbesHealthEndpointGroupsPostProcessor(
environment);
HealthEndpointGroups groups = mock(HealthEndpointGroups.class);
return postProcessor.postProcessHealthEndpointGroups(groups);
}
@Test
void postProcessHealthEndpointGroupsWhenAdditionalPathPropertyIsFalse() {
HealthEndpointGroups postProcessed = getPostProcessed("false");
HealthEndpointGroup liveness = postProcessed.get("liveness");
HealthEndpointGroup readiness = postProcessed.get("readiness");
assertThat(liveness.getAdditionalPath()).isNull();
assertThat(readiness.getAdditionalPath()).isNull();
}
@Test
void postProcessHealthEndpointGroupsWhenAdditionalPathPropertyIsNull() {
HealthEndpointGroups groups = mock(HealthEndpointGroups.class);
HealthEndpointGroups postProcessed = this.postProcessor.postProcessHealthEndpointGroups(groups);
HealthEndpointGroup liveness = postProcessed.get("liveness");
HealthEndpointGroup readiness = postProcessed.get("readiness");
assertThat(liveness.getAdditionalPath()).isNull();
assertThat(readiness.getAdditionalPath()).isNull();
}
}

@ -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.
@ -50,46 +50,46 @@ class AvailabilityProbesHealthEndpointGroupsTests {
@Test
void createWhenGroupsIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new AvailabilityProbesHealthEndpointGroups(null))
assertThatIllegalArgumentException().isThrownBy(() -> new AvailabilityProbesHealthEndpointGroups(null, false))
.withMessage("Groups must not be null");
}
@Test
void getPrimaryDelegatesToGroups() {
given(this.delegate.getPrimary()).willReturn(this.group);
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate);
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate, false);
assertThat(availabilityProbes.getPrimary()).isEqualTo(this.group);
}
@Test
void getNamesIncludesAvailabilityProbeGroups() {
given(this.delegate.getNames()).willReturn(Collections.singleton("test"));
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate);
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate, false);
assertThat(availabilityProbes.getNames()).containsExactly("test", "liveness", "readiness");
}
@Test
void getWhenProbeInDelegateReturnsGroupFromDelegate() {
given(this.delegate.get("liveness")).willReturn(this.group);
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate);
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate, false);
assertThat(availabilityProbes.get("liveness")).isEqualTo(this.group);
}
@Test
void getWhenProbeNotInDelegateReturnsProbeGroup() {
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate);
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate, false);
assertThat(availabilityProbes.get("liveness")).isInstanceOf(AvailabilityProbesHealthEndpointGroup.class);
}
@Test
void getWhenNotProbeAndNotInDelegateReturnsNull() {
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate);
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate, false);
assertThat(availabilityProbes.get("mygroup")).isNull();
}
@Test
void getLivenessProbeHasOnlyLivenessStateAsMember() {
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate);
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate, false);
HealthEndpointGroup probeGroup = availabilityProbes.get("liveness");
assertThat(probeGroup.isMember("livenessState")).isTrue();
assertThat(probeGroup.isMember("readinessState")).isFalse();
@ -97,7 +97,7 @@ class AvailabilityProbesHealthEndpointGroupsTests {
@Test
void getReadinessProbeHasOnlyReadinessStateAsMember() {
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate);
HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate, false);
HealthEndpointGroup probeGroup = availabilityProbes.get("readiness");
assertThat(probeGroup.isMember("livenessState")).isFalse();
assertThat(probeGroup.isMember("readinessState")).isTrue();

@ -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.
@ -28,6 +28,7 @@ import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath;
import org.springframework.boot.actuate.health.CompositeHealthContributor;
import org.springframework.boot.actuate.health.DefaultHealthContributorRegistry;
import org.springframework.boot.actuate.health.Health;
@ -164,6 +165,11 @@ class HealthEndpointDocumentationTests extends MockMvcEndpointDocumentationTests
return this.httpCodeStatusMapper;
}
@Override
public AdditionalHealthEndpointPath getAdditionalPath() {
return null;
}
}
}

@ -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.
@ -59,7 +59,7 @@ class AutoConfiguredHealthEndpointGroupTests {
@Test
void isMemberWhenMemberPredicateMatchesAcceptsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> name.startsWith("a"),
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet(), null);
assertThat(group.isMember("albert")).isTrue();
assertThat(group.isMember("arnold")).isTrue();
}
@ -67,7 +67,7 @@ class AutoConfiguredHealthEndpointGroupTests {
@Test
void isMemberWhenMemberPredicateRejectsReturnsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> name.startsWith("a"),
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet(), null);
assertThat(group.isMember("bert")).isFalse();
assertThat(group.isMember("ernie")).isFalse();
}
@ -75,21 +75,22 @@ class AutoConfiguredHealthEndpointGroupTests {
@Test
void showDetailsWhenShowDetailsIsNeverReturnsFalse() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.NEVER, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, null, Show.NEVER, Collections.emptySet(), null);
assertThat(group.showDetails(SecurityContext.NONE)).isFalse();
}
@Test
void showDetailsWhenShowDetailsIsAlwaysReturnsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet(), null);
assertThat(group.showDetails(SecurityContext.NONE)).isTrue();
}
@Test
void showDetailsWhenShowDetailsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Collections.emptySet(),
null);
given(this.securityContext.getPrincipal()).willReturn(null);
assertThat(group.showDetails(this.securityContext)).isFalse();
}
@ -97,7 +98,8 @@ class AutoConfiguredHealthEndpointGroupTests {
@Test
void showDetailsWhenShowDetailsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Collections.emptySet(),
null);
given(this.securityContext.getPrincipal()).willReturn(this.principal);
assertThat(group.showDetails(this.securityContext)).isTrue();
}
@ -106,7 +108,7 @@ class AutoConfiguredHealthEndpointGroupTests {
void showDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED,
Arrays.asList("admin", "root", "bossmode"));
Arrays.asList("admin", "root", "bossmode"), null);
given(this.securityContext.getPrincipal()).willReturn(this.principal);
given(this.securityContext.isUserInRole("admin")).willReturn(false);
given(this.securityContext.isUserInRole("root")).willReturn(true);
@ -117,7 +119,7 @@ class AutoConfiguredHealthEndpointGroupTests {
void showDetailsWhenShowDetailsIsWhenAuthorizedAndUserIsNotInRoleReturnsFalse() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED,
Arrays.asList("admin", "root", "bossmode"));
Arrays.asList("admin", "root", "bossmode"), null);
given(this.securityContext.getPrincipal()).willReturn(this.principal);
assertThat(group.showDetails(this.securityContext)).isFalse();
}
@ -126,7 +128,7 @@ class AutoConfiguredHealthEndpointGroupTests {
void showDetailsWhenShowDetailsIsWhenAuthorizedAndUserHasRightAuthorityReturnsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED,
Arrays.asList("admin", "root", "bossmode"));
Arrays.asList("admin", "root", "bossmode"), null);
Authentication principal = mock(Authentication.class);
given(principal.getAuthorities())
.willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("admin")));
@ -138,7 +140,7 @@ class AutoConfiguredHealthEndpointGroupTests {
void showDetailsWhenShowDetailsIsWhenAuthorizedAndUserDoesNotHaveRightAuthoritiesReturnsFalse() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED,
Arrays.asList("admin", "rot", "bossmode"));
Arrays.asList("admin", "rot", "bossmode"), null);
Authentication principal = mock(Authentication.class);
given(principal.getAuthorities())
.willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("other")));
@ -149,24 +151,26 @@ class AutoConfiguredHealthEndpointGroupTests {
@Test
void showComponentsWhenShowComponentsIsNullDelegatesToShowDetails() {
AutoConfiguredHealthEndpointGroup alwaysGroup = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet(), null);
assertThat(alwaysGroup.showComponents(SecurityContext.NONE)).isTrue();
AutoConfiguredHealthEndpointGroup neverGroup = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.NEVER, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, null, Show.NEVER, Collections.emptySet(), null);
assertThat(neverGroup.showComponents(SecurityContext.NONE)).isFalse();
}
@Test
void showComponentsWhenShowComponentsIsNeverReturnsFalse() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.NEVER, Show.ALWAYS, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, Show.NEVER, Show.ALWAYS, Collections.emptySet(),
null);
assertThat(group.showComponents(SecurityContext.NONE)).isFalse();
}
@Test
void showComponentsWhenShowComponentsIsAlwaysReturnsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.ALWAYS, Show.NEVER, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, Show.ALWAYS, Show.NEVER, Collections.emptySet(),
null);
assertThat(group.showComponents(SecurityContext.NONE)).isTrue();
}
@ -174,7 +178,7 @@ class AutoConfiguredHealthEndpointGroupTests {
void showComponentsWhenShowComponentsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
Collections.emptySet());
Collections.emptySet(), null);
given(this.securityContext.getPrincipal()).willReturn(null);
assertThat(group.showComponents(this.securityContext)).isFalse();
}
@ -183,7 +187,7 @@ class AutoConfiguredHealthEndpointGroupTests {
void showComponentsWhenShowComponentsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
Collections.emptySet());
Collections.emptySet(), null);
given(this.securityContext.getPrincipal()).willReturn(this.principal);
assertThat(group.showComponents(this.securityContext)).isTrue();
}
@ -192,7 +196,7 @@ class AutoConfiguredHealthEndpointGroupTests {
void showComponentsWhenShowComponentsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
Arrays.asList("admin", "root", "bossmode"));
Arrays.asList("admin", "root", "bossmode"), null);
given(this.securityContext.getPrincipal()).willReturn(this.principal);
given(this.securityContext.isUserInRole("admin")).willReturn(false);
given(this.securityContext.isUserInRole("root")).willReturn(true);
@ -203,7 +207,7 @@ class AutoConfiguredHealthEndpointGroupTests {
void showComponentsWhenShowComponentsIsWhenAuthorizedAndUserIsNotInRoleReturnsFalse() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
Arrays.asList("admin", "rot", "bossmode"));
Arrays.asList("admin", "rot", "bossmode"), null);
given(this.securityContext.getPrincipal()).willReturn(this.principal);
assertThat(group.showComponents(this.securityContext)).isFalse();
}
@ -212,7 +216,7 @@ class AutoConfiguredHealthEndpointGroupTests {
void showComponentsWhenShowComponentsIsWhenAuthorizedAndUserHasRightAuthoritiesReturnsTrue() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
Arrays.asList("admin", "root", "bossmode"));
Arrays.asList("admin", "root", "bossmode"), null);
Authentication principal = mock(Authentication.class);
given(principal.getAuthorities())
.willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("admin")));
@ -224,7 +228,7 @@ class AutoConfiguredHealthEndpointGroupTests {
void showComponentsWhenShowComponentsIsWhenAuthorizedAndUserDoesNotHaveRightAuthoritiesReturnsFalse() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
Arrays.asList("admin", "rot", "bossmode"));
Arrays.asList("admin", "rot", "bossmode"), null);
Authentication principal = mock(Authentication.class);
given(principal.getAuthorities())
.willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("other")));
@ -235,14 +239,14 @@ class AutoConfiguredHealthEndpointGroupTests {
@Test
void getStatusAggregatorReturnsStatusAggregator() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet(), null);
assertThat(group.getStatusAggregator()).isSameAs(this.statusAggregator);
}
@Test
void getHttpCodeStatusMapperReturnsHttpCodeStatusMapper() {
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet(), null);
assertThat(group.getHttpCodeStatusMapper()).isSameAs(this.httpCodeStatusMapper);
}

@ -22,9 +22,12 @@ import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
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.DefaultHealthContributorRegistry;
import org.springframework.boot.actuate.health.DefaultReactiveHealthContributorRegistry;
import org.springframework.boot.actuate.health.Health;
@ -69,8 +72,10 @@ class HealthEndpointAutoConfigurationTests {
.of(HealthContributorAutoConfiguration.class, HealthEndpointAutoConfiguration.class));
private final ReactiveWebApplicationContextRunner reactiveContextRunner = new ReactiveWebApplicationContextRunner()
.withUserConfiguration(HealthIndicatorsConfiguration.class).withConfiguration(AutoConfigurations
.of(HealthContributorAutoConfiguration.class, HealthEndpointAutoConfiguration.class));
.withUserConfiguration(HealthIndicatorsConfiguration.class)
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthEndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class,
EndpointAutoConfiguration.class));
@Test
void runWhenHealthEndpointIsDisabledDoesNotCreateBeans() {
@ -208,8 +213,8 @@ class HealthEndpointAutoConfigurationTests {
void runCreatesHealthEndpointWebExtension() {
this.contextRunner.run((context) -> {
HealthEndpointWebExtension webExtension = context.getBean(HealthEndpointWebExtension.class);
WebEndpointResponse<HealthComponent> response = webExtension.health(ApiVersion.V3, SecurityContext.NONE,
true, "simple");
WebEndpointResponse<HealthComponent> response = webExtension.health(ApiVersion.V3,
WebServerNamespace.SERVER, SecurityContext.NONE, true, "simple");
Health health = (Health) response.getBody();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(health.getDetails()).containsEntry("counter", 42);
@ -220,8 +225,8 @@ class HealthEndpointAutoConfigurationTests {
void runWhenHasHealthEndpointWebExtensionBeanDoesNotCreateExtraHealthEndpointWebExtension() {
this.contextRunner.withUserConfiguration(HealthEndpointWebExtensionConfiguration.class).run((context) -> {
HealthEndpointWebExtension webExtension = context.getBean(HealthEndpointWebExtension.class);
WebEndpointResponse<HealthComponent> response = webExtension.health(ApiVersion.V3, SecurityContext.NONE,
true, "simple");
WebEndpointResponse<HealthComponent> response = webExtension.health(ApiVersion.V3,
WebServerNamespace.SERVER, SecurityContext.NONE, true, "simple");
assertThat(response).isNull();
});
}
@ -231,7 +236,7 @@ class HealthEndpointAutoConfigurationTests {
this.reactiveContextRunner.run((context) -> {
ReactiveHealthEndpointWebExtension webExtension = context.getBean(ReactiveHealthEndpointWebExtension.class);
Mono<WebEndpointResponse<? extends HealthComponent>> response = webExtension.health(ApiVersion.V3,
SecurityContext.NONE, true, "simple");
WebServerNamespace.SERVER, SecurityContext.NONE, true, "simple");
Health health = (Health) (response.block().getBody());
assertThat(health.getDetails()).containsEntry("counter", 42);
});
@ -244,7 +249,7 @@ class HealthEndpointAutoConfigurationTests {
ReactiveHealthEndpointWebExtension webExtension = context
.getBean(ReactiveHealthEndpointWebExtension.class);
Mono<WebEndpointResponse<? extends HealthComponent>> response = webExtension.health(ApiVersion.V3,
SecurityContext.NONE, true, "simple");
WebServerNamespace.SERVER, SecurityContext.NONE, true, "simple");
assertThat(response).isNull();
});
}

@ -0,0 +1,95 @@
/*
* 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.autoconfigure.integrationtest;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
import org.springframework.boot.test.context.assertj.ApplicationContextAssertProvider;
import org.springframework.boot.test.context.runner.AbstractApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* Abstract base class for health groups with an additional path.
*
* @param <T> the runner
* @param <C> the application context type
* @param <A> the assertions
* @author Madhura Bhave
*/
abstract class AbstractHealthEndpointAdditionalPathIntegrationTests<T extends AbstractApplicationContextRunner<T, C, A>, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider<C>> {
private final T runner;
AbstractHealthEndpointAdditionalPathIntegrationTests(T runner) {
this.runner = runner;
}
@Test
void groupIsAvailableAtAdditionalPath() {
this.runner
.withPropertyValues("management.endpoint.health.group.live.include=diskSpace",
"management.endpoint.health.group.live.additional-path=server:/healthz",
"management.endpoint.health.group.live.show-components=always")
.run(withWebTestClient(this::testResponse, "local.server.port"));
}
@Test
void groupIsAvailableAtAdditionalPathWithoutSlash() {
this.runner
.withPropertyValues("management.endpoint.health.group.live.include=diskSpace",
"management.endpoint.health.group.live.additional-path=server:healthz",
"management.endpoint.health.group.live.show-components=always")
.run(withWebTestClient(this::testResponse, "local.server.port"));
}
@Test
void groupIsAvailableAtAdditionalPathOnManagementPort() {
this.runner.withPropertyValues("management.endpoint.health.group.live.include=diskSpace",
"management.server.port=0", "management.endpoint.health.group.live.additional-path=management:healthz",
"management.endpoint.health.group.live.show-components=always")
.run(withWebTestClient(this::testResponse, "local.management.port"));
}
@Test
void groupIsAvailableAtAdditionalPathOnServerPortWithDifferentManagementPort() {
this.runner.withPropertyValues("management.endpoint.health.group.live.include=diskSpace",
"management.server.port=0", "management.endpoint.health.group.live.additional-path=server:healthz",
"management.endpoint.health.group.live.show-components=always")
.withInitializer(new ConditionEvaluationReportLoggingListener())
.run(withWebTestClient(this::testResponse, "local.server.port"));
}
private void testResponse(WebTestClient client) {
client.get().uri("/healthz").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk().expectBody()
.jsonPath("status").isEqualTo("UP").jsonPath("components.diskSpace").exists();
}
private ContextConsumer<A> withWebTestClient(Consumer<WebTestClient> consumer, String property) {
return (context) -> {
String port = context.getEnvironment().getProperty(property);
WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port).build();
consumer.accept(client);
};
}
}

@ -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.autoconfigure.integrationtest;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
/**
* Integration tests for health groups on an additional path on Jersey.
*
* @author Madhura Bhave
*/
class JerseyHealthEndpointAdditionalPathIntegrationTests extends
AbstractHealthEndpointAdditionalPathIntegrationTests<WebApplicationContextRunner, ConfigurableWebApplicationContext, AssertableWebApplicationContext> {
JerseyHealthEndpointAdditionalPathIntegrationTests() {
super(new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new)
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class, JerseyAutoConfiguration.class,
EndpointAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class,
WebEndpointAutoConfiguration.class, JerseyAutoConfiguration.class,
ManagementContextAutoConfiguration.class, ServletManagementContextAutoConfiguration.class,
HealthEndpointAutoConfiguration.class, DiskSpaceHealthContributorAutoConfiguration.class))
.withInitializer(new ServerPortInfoApplicationContextInitializer())
.withClassLoader(new FilteredClassLoader(DispatcherServlet.class)).withPropertyValues("server.port=0"));
}
}

@ -0,0 +1,57 @@
/*
* 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.autoconfigure.integrationtest;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.web.context.ConfigurableWebApplicationContext;
/**
* Integration tests for MVC health groups on an additional path.
*
* @author Madhura Bhave
*/
class WebMvcHealthEndpointAdditionalPathIntegrationTests extends
AbstractHealthEndpointAdditionalPathIntegrationTests<WebApplicationContextRunner, ConfigurableWebApplicationContext, AssertableWebApplicationContext> {
WebMvcHealthEndpointAdditionalPathIntegrationTests() {
super(new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new)
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, ManagementContextAutoConfiguration.class,
ServletWebServerFactoryAutoConfiguration.class, WebMvcAutoConfiguration.class,
ServletManagementContextAutoConfiguration.class, WebEndpointAutoConfiguration.class,
EndpointAutoConfiguration.class, DispatcherServletAutoConfiguration.class,
HealthEndpointAutoConfiguration.class, DiskSpaceHealthContributorAutoConfiguration.class))
.withInitializer(new ServerPortInfoApplicationContextInitializer())
.withPropertyValues("server.port=0"));
}
}

@ -0,0 +1,58 @@
/*
* 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.autoconfigure.integrationtest;
import org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.reactive.ReactiveManagementContextAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
import org.springframework.boot.web.reactive.context.ConfigurableReactiveWebApplicationContext;
/**
* Integration tests for Webflux health groups on an additional path.
*
* @author Madhura Bhave
*/
class WebfluxHealthEndpointAdditionalPathIntegrationTests extends
AbstractHealthEndpointAdditionalPathIntegrationTests<ReactiveWebApplicationContextRunner, ConfigurableReactiveWebApplicationContext, AssertableReactiveWebApplicationContext> {
WebfluxHealthEndpointAdditionalPathIntegrationTests() {
super(new ReactiveWebApplicationContextRunner(AnnotationConfigReactiveWebServerApplicationContext::new)
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class, CodecsAutoConfiguration.class,
WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class,
EndpointAutoConfiguration.class, HealthEndpointAutoConfiguration.class,
DiskSpaceHealthContributorAutoConfiguration.class, WebEndpointAutoConfiguration.class,
ManagementContextAutoConfiguration.class, ReactiveWebServerFactoryAutoConfiguration.class,
ReactiveManagementContextAutoConfiguration.class, BeansEndpointAutoConfiguration.class))
.withInitializer(new ServerPortInfoApplicationContextInitializer())
.withPropertyValues("server.port=0"));
}
}

@ -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();
}
}

@ -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++) {

@ -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;
}
}

@ -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);
}

@ -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);
}

@ -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;
}
}

@ -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) {

@ -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;
}
}

@ -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.");
}
}

@ -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;
}

@ -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();
}

@ -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

@ -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;
}

@ -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)

@ -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))

@ -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"));
}
}

@ -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"));
}

@ -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);
}
}

@ -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();

@ -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);
}

@ -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);
}

@ -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;
}
}

@ -934,6 +934,20 @@ It's also possible to override the `show-details` and `roles` properties if requ
TIP: You can use `@Qualifier("groupname")` if you need to register custom `StatusAggregator` or `HttpCodeStatusMapper` beans for use with the group.
Health groups can be made available at an additional path on either the main or management port.
This is useful in cloud environments such as Kubernetes, where it is quite common to use a separate management port for the actuator endpoints for security purposes.
Having a separate port could lead to unreliable health checks because the main application might not work properly even if the health check is successful.
The health group can be configured with an additional path as follows:
[source,properties,indent=0,subs="verbatim"]
----
management.endpoint.health.group.live.additional-path="server:/healthz"
----
This would make the `live` health group available on the main server port at `/healthz`.
The prefix is mandatory and must be either `server:` (represents the main server port) or `management:` (represents the management port, if configured.)
The path must be a single path segment.
[[actuator.endpoints.health.datasource]]
@ -983,8 +997,18 @@ You can enable them in any environment using the configprop:management.endpoint.
NOTE: If an application takes longer to start than the configured liveness period, Kubernetes mention the `"startupProbe"` as a possible solution.
The `"startupProbe"` is not necessarily needed here as the `"readinessProbe"` fails until all startup tasks are done, see <<actuator#actuator.endpoints.kubernetes-probes.lifecycle,how Probes behave during the application lifecycle>>.
WARNING: If your Actuator endpoints are deployed on a separate management context, be aware that endpoints are then not using the same web infrastructure (port, connection pools, framework components) as the main application.
If your Actuator endpoints are deployed on a separate management context, the endpoints do not use the same web infrastructure (port, connection pools, framework components) as the main application.
In this case, a probe check could be successful even if the main application does not work properly (for example, it cannot accept new connections).
For this reason, is it a good idea to make the `liveness` and `readiness` health groups available on the main server port.
This can be done by setting the following property:
[source,properties,indent=0,subs="verbatim"]
----
management.endpoint.health.probes.add-additional-paths=true
----
This would make `liveness` available at `/livez` and `readiness` at `readyz` on the main server port.

@ -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.
@ -58,4 +58,18 @@ public interface WebServerApplicationContext extends ApplicationContext {
.nullSafeEquals(((WebServerApplicationContext) context).getServerNamespace(), serverNamespace);
}
/**
* Returns the server namespace if the specified context is a
* {@link WebServerApplicationContext}.
* @param context the context
* @return the server namespace or {@code null} if the context is not a
* {@link WebServerApplicationContext}
* @since 2.6.0
*/
static String getServerNamepace(ApplicationContext context) {
return (context instanceof WebServerApplicationContext)
? ((WebServerApplicationContext) context).getServerNamespace() : null;
}
}

Loading…
Cancel
Save