diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesAutoConfiguration.java index 99e0e84d1004..79e126f7cf94 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesAutoConfiguration.java @@ -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 ReadinessStateHealthIndicator readinessStateHealthIndicator( } @Bean - public AvailabilityProbesHealthEndpointGroupsPostProcessor availabilityProbesHealthEndpointGroupsPostProcessor() { - return new AvailabilityProbesHealthEndpointGroupsPostProcessor(); + public AvailabilityProbesHealthEndpointGroupsPostProcessor availabilityProbesHealthEndpointGroupsPostProcessor( + Environment environment) { + return new AvailabilityProbesHealthEndpointGroupsPostProcessor(environment); } /** diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroup.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroup.java index c8a42ca3bbff..2576f6a45b7a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroup.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroup.java @@ -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.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 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 @@ public HttpCodeStatusMapper getHttpCodeStatusMapper() { return HttpCodeStatusMapper.DEFAULT; } + @Override + public AdditionalHealthEndpointPath getAdditionalPath() { + return this.additionalPath; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroups.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroups.java index 0da69e7b5e44..09d425f63700 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroups.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroups.java @@ -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.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 @@ * * @author Phillip Webb * @author Brian Clozel + * @author Madhura Bhave */ class AvailabilityProbesHealthEndpointGroups implements HealthEndpointGroups { - private static final Map GROUPS; - static { - Map 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 probeGroups; + private final Set 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 names = new LinkedHashSet<>(groups.getNames()); - names.addAll(GROUPS.keySet()); + names.addAll(this.probeGroups.keySet()); this.names = Collections.unmodifiableSet(names); } + private Map createProbeGroups(boolean addAdditionalPaths) { + Map 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 @@ public Set getNames() { 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 names = groups.getNames(); + return names.contains("liveness") && names.contains("readiness"); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessor.java index fcd36c99d9f1..217796fea552 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessor.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessor.java @@ -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.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); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtension.java index 67f1708b5175..1542b36f7107 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtension.java @@ -48,13 +48,13 @@ public CloudFoundryReactiveHealthEndpointWebExtension(ReactiveHealthEndpointWebE @ReadOperation public Mono> health(ApiVersion apiVersion) { - return this.delegate.health(apiVersion, SecurityContext.NONE, true); + return this.delegate.health(apiVersion, null, SecurityContext.NONE, true); } @ReadOperation public Mono> 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); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtension.java index 7ec0b9772e97..abde0d96582e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtension.java @@ -46,13 +46,13 @@ public CloudFoundryHealthEndpointWebExtension(HealthEndpointWebExtension delegat @ReadOperation public WebEndpointResponse health(ApiVersion apiVersion) { - return this.delegate.health(apiVersion, SecurityContext.NONE, true); + return this.delegate.health(apiVersion, null, SecurityContext.NONE, true); } @ReadOperation public WebEndpointResponse 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); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java index c5b55c1e5140..c72b50e0bb42 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java @@ -18,8 +18,11 @@ 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.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.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 @@ @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 @@ JerseyWebEndpointsResourcesRegistrar jerseyWebEndpointsResourcesRegistrar(Enviro endpointMediaTypes, basePath, shouldRegisterLinks); } + @Bean + @ConditionalOnBean(HealthEndpoint.class) + @ConditionalOnManagementPort(ManagementPortType.DIFFERENT) + JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar jerseyDifferentPortAdditionalHealthEndpointPathsResourcesRegistrar( + WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups healthEndpointGroups) { + Collection 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 @@ private void register(Collection resources, ResourceConfig config) { } + 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 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 resources, ResourceConfig config) { + config.registerResources(new HashSet<>(resources)); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java index e4794e10c8fd..ac2deabe0774 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java @@ -23,6 +23,7 @@ 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.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 @@ private boolean shouldRegisterLinksMapping(WebEndpointProperties properties, Env || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT)); } + @Bean + @ConditionalOnManagementPort(ManagementPortType.DIFFERENT) + @ConditionalOnBean(HealthEndpoint.class) + public AdditionalHealthEndpointPathsWebFluxHandlerMapping managementHealthEndpointWebFluxHandlerMapping( + WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) { + Collection 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( diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java index 75fab2727432..c0ee3e87a42e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java @@ -23,6 +23,7 @@ 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.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 @@ private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProp || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT)); } + @Bean + @ConditionalOnManagementPort(ManagementPortType.DIFFERENT) + @ConditionalOnBean(HealthEndpoint.class) + public AdditionalHealthEndpointPathsWebMvcHandlerMapping managementHealthEndpointWebMvcHandlerMapping( + WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) { + Collection 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( diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java index efabc7152933..1992f814e3c3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java @@ -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 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 @@ * * @author Phillip Webb * @author Andy Wilkinson + * @author Madhura Bhave */ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup { @@ -50,6 +52,8 @@ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup { private final Collection 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 members, StatusAggregator statusAggregator, - HttpCodeStatusMapper httpCodeStatusMapper, Show showComponents, Show showDetails, - Collection roles) { + HttpCodeStatusMapper httpCodeStatusMapper, Show showComponents, Show showDetails, Collection 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 @@ public HttpCodeStatusMapper getHttpCodeStatusMapper() { return this.httpCodeStatusMapper; } + @Override + public AdditionalHealthEndpointPath getAdditionalPath() { + return this.additionalPath; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java index 31346fecb6e2..58a1f0e3f636 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java @@ -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.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 @@ * 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 @@ private Map createGroups(Map groupPr return defaultHttpCodeStatusMapper; }); Predicate 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); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java index 6ccf2ae5a133..d536a89f8cb0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java @@ -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 Map getGroup() { */ 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 static class Group 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 getInclude() { return this.include; } @@ -102,6 +114,14 @@ public void setShowDetails(Show showDetails) { this.showDetails = showDetails; } + public String getAdditionalPath() { + return this.additionalPath; + } + + public void setAdditionalPath(String additionalPath) { + this.additionalPath = additionalPath; + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java index d556e1904c62..5836f6d9927c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java @@ -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 @@ * Configuration for {@link HealthEndpoint} reactive web extensions. * * @author Phillip Webb + * @author Madhura Bhave * @see HealthEndpointAutoConfiguration */ @Configuration(proxyBeanMethods = false) @@ -46,4 +54,19 @@ ReactiveHealthEndpointWebExtension reactiveHealthEndpointWebExtension( return new ReactiveHealthEndpointWebExtension(reactiveHealthContributorRegistry, groups); } + @Configuration(proxyBeanMethods = false) + static class WebFluxAdditionalHealthEndpointPathsConfiguration { + + @Bean + AdditionalHealthEndpointPathsWebFluxHandlerMapping healthEndpointWebFluxHandlerMapping( + WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) { + Collection 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)); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java index 777c07551c17..26da1253e3b5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java @@ -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 @@ HealthEndpointWebExtension healthEndpointWebExtension(HealthContributorRegistry return new HealthEndpointWebExtension(healthContributorRegistry, groups); } + private static ExposableWebEndpoint getHealthEndpoint(WebEndpointsSupplier webEndpointsSupplier) { + Collection 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 resourceConfigCustomizers) { + ResourceConfig resourceConfig = new ResourceConfig(); + resourceConfigCustomizers.orderedStream().forEach((customizer) -> customizer.customize(resourceConfig)); + return resourceConfig; + } + + @Bean + ServletRegistrationBean 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 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 resources, ResourceConfig config) { + config.registerResources(new HashSet<>(resources)); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupTests.java index 1a80c04dc124..61d36f9d66f1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupTests.java @@ -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 @@ */ class AvailabilityProbesHealthEndpointGroupTests { - private AvailabilityProbesHealthEndpointGroup group = new AvailabilityProbesHealthEndpointGroup("a", "b"); + private AvailabilityProbesHealthEndpointGroup group = new AvailabilityProbesHealthEndpointGroup(null, "a", "b"); @Test void isMemberWhenMemberReturnsTrue() { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessorTests.java index d7994b896a00..246c48b85e79 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessorTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessorTests.java @@ -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 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 @@ * 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 @@ void postProcessHealthEndpointGroupsWhenGroupsContainsNoneReturnsProcessed() { 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(); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsTests.java index 1ab7a565e8a4..b5b6be0eb48b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsTests.java @@ -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 @@ void setup() { @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 @@ void getLivenessProbeHasOnlyLivenessStateAsMember() { @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(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java index df3a8f30d631..dae4df571097 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java @@ -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 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 @@ public HttpCodeStatusMapper getHttpCodeStatusMapper() { return this.httpCodeStatusMapper; } + @Override + public AdditionalHealthEndpointPath getAdditionalPath() { + return null; + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java index 3c22e80dae0d..7c30ed2ed108 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java @@ -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 @@ void isMemberWhenMemberPredicateMatchesAcceptsTrue() { @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 @@ void isMemberWhenMemberPredicateRejectsReturnsTrue() { @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 @@ void showDetailsWhenShowDetailsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() @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 @@ void showDetailsWhenShowDetailsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() { 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 @@ void showDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() { 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 @@ void showDetailsWhenShowDetailsIsWhenAuthorizedAndUserIsNotInRoleReturnsFalse() 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 @@ void showDetailsWhenShowDetailsIsWhenAuthorizedAndUserHasRightAuthorityReturnsTr 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 @@ void showDetailsWhenShowDetailsIsWhenAuthorizedAndUserDoesNotHaveRightAuthoritie @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 @@ void showComponentsWhenShowComponentsIsAlwaysReturnsTrue() { 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 @@ void showComponentsWhenShowComponentsIsWhenAuthorizedAndPrincipalIsNullReturnsFa 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 @@ void showComponentsWhenShowComponentsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue 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 @@ void showComponentsWhenShowComponentsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() 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 @@ void showComponentsWhenShowComponentsIsWhenAuthorizedAndUserIsNotInRoleReturnsFa 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 @@ void showComponentsWhenShowComponentsIsWhenAuthorizedAndUserHasRightAuthoritiesR 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 @@ void showComponentsWhenShowComponentsIsWhenAuthorizedAndUserDoesNotHaveRightAuth @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); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java index d3f134ea6ad5..4f43c0b95f0d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java @@ -22,9 +22,12 @@ 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 @@ void runWhenHasReactiveHealthContributorRegistryBeanDoesNotCreateAdditionalReact void runCreatesHealthEndpointWebExtension() { this.contextRunner.run((context) -> { HealthEndpointWebExtension webExtension = context.getBean(HealthEndpointWebExtension.class); - WebEndpointResponse response = webExtension.health(ApiVersion.V3, SecurityContext.NONE, - true, "simple"); + WebEndpointResponse 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 @@ void runCreatesHealthEndpointWebExtension() { void runWhenHasHealthEndpointWebExtensionBeanDoesNotCreateExtraHealthEndpointWebExtension() { this.contextRunner.withUserConfiguration(HealthEndpointWebExtensionConfiguration.class).run((context) -> { HealthEndpointWebExtension webExtension = context.getBean(HealthEndpointWebExtension.class); - WebEndpointResponse response = webExtension.health(ApiVersion.V3, SecurityContext.NONE, - true, "simple"); + WebEndpointResponse response = webExtension.health(ApiVersion.V3, + WebServerNamespace.SERVER, SecurityContext.NONE, true, "simple"); assertThat(response).isNull(); }); } @@ -231,7 +236,7 @@ void runCreatesReactiveHealthEndpointWebExtension() { this.reactiveContextRunner.run((context) -> { ReactiveHealthEndpointWebExtension webExtension = context.getBean(ReactiveHealthEndpointWebExtension.class); Mono> 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 @@ void runWhenHasReactiveHealthEndpointWebExtensionBeanDoesNotCreateExtraReactiveH ReactiveHealthEndpointWebExtension webExtension = context .getBean(ReactiveHealthEndpointWebExtension.class); Mono> response = webExtension.health(ApiVersion.V3, - SecurityContext.NONE, true, "simple"); + WebServerNamespace.SERVER, SecurityContext.NONE, true, "simple"); assertThat(response).isNull(); }); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/AbstractHealthEndpointAdditionalPathIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/AbstractHealthEndpointAdditionalPathIntegrationTests.java new file mode 100644 index 000000000000..0a73565b5813 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/AbstractHealthEndpointAdditionalPathIntegrationTests.java @@ -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 the runner + * @param the application context type + * @param the assertions + * @author Madhura Bhave + */ +abstract class AbstractHealthEndpointAdditionalPathIntegrationTests, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> { + + 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 withWebTestClient(Consumer consumer, String property) { + return (context) -> { + String port = context.getEnvironment().getProperty(property); + WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port).build(); + consumer.accept(client); + }; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyHealthEndpointAdditionalPathIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyHealthEndpointAdditionalPathIntegrationTests.java new file mode 100644 index 000000000000..aa9f9ce0d2d0 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyHealthEndpointAdditionalPathIntegrationTests.java @@ -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 { + + 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")); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcHealthEndpointAdditionalPathIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcHealthEndpointAdditionalPathIntegrationTests.java new file mode 100644 index 000000000000..03bc1904a2cc --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcHealthEndpointAdditionalPathIntegrationTests.java @@ -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 { + + 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")); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebfluxHealthEndpointAdditionalPathIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebfluxHealthEndpointAdditionalPathIntegrationTests.java new file mode 100644 index 000000000000..09a63fe1b177 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebfluxHealthEndpointAdditionalPathIntegrationTests.java @@ -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 { + + 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")); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebServerNamespace.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebServerNamespace.java new file mode 100644 index 000000000000..97b1ccee36d0 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebServerNamespace.java @@ -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(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java index f540699f744b..5b60c31dbd8d 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java @@ -42,6 +42,7 @@ 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.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 Collection createEndpointResources(EndpointMapping endpointMapp 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 @@ private Resource createResource(EndpointMapping endpointMapping, WebOperation op 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 @@ private static final class OperationInflector implements Inflector 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 @@ private Map extractPathParameters(ContainerRequestContext reques 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 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++) { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyHealthEndpointAdditionalPathResourceFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyHealthEndpointAdditionalPathResourceFactory.java new file mode 100644 index 000000000000..3b7033753b86 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyHealthEndpointAdditionalPathResourceFactory.java @@ -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 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; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyRemainingPathSegmentProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyRemainingPathSegmentProvider.java new file mode 100644 index 000000000000..e36806a16624 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyRemainingPathSegmentProvider.java @@ -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); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java index 118066e1970a..e790822d3df3 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java @@ -31,6 +31,7 @@ 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.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.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 @@ protected HandlerMethod createHandlerMethod(Object handler, Method method) { } 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 @@ Mono emptySecurityContext() { @Override public Mono> handle(ServerWebExchange exchange, Map body) { Map 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 getArguments(ServerWebExchange exchange, Map body) { Map 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 @@ private Map getArguments(ServerWebExchange exchange, Map getTemplateVariables(ServerWebExchange exchange) { return exchange.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AdditionalHealthEndpointPathsWebFluxHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AdditionalHealthEndpointPathsWebFluxHandlerMapping.java new file mode 100644 index 000000000000..464842616702 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AdditionalHealthEndpointPathsWebFluxHandlerMapping.java @@ -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 groups; + + public AdditionalHealthEndpointPathsWebFluxHandlerMapping(EndpointMapping endpointMapping, + ExposableWebEndpoint endpoint, Set 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; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java index 669b53a791af..b6c67e636e82 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java @@ -32,6 +32,7 @@ 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.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.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 @@ private void registerMappingForOperation(ExposableWebEndpoint endpoint, WebOpera 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 Object handle(HttpServletRequest request, @RequestBody(required = false) Map 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) { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AdditionalHealthEndpointPathsWebMvcHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AdditionalHealthEndpointPathsWebMvcHandlerMapping.java new file mode 100644 index 000000000000..48b717cb343f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AdditionalHealthEndpointPathsWebMvcHandlerMapping.java @@ -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 groups; + + public AdditionalHealthEndpointPathsWebMvcHandlerMapping(ExposableWebEndpoint endpoint, + Set 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; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AdditionalHealthEndpointPath.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AdditionalHealthEndpointPath.java new file mode 100644 index 000000000000..2b3cccd09695 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AdditionalHealthEndpointPath.java @@ -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."); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java index da130063dd76..4fcd7d4b7d34 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java @@ -20,6 +20,7 @@ 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 @@ @Endpoint(id = "health") public class HealthEndpoint extends HealthEndpointSupport { + /** + * Health endpoint id. + */ + public static final EndpointId ID = EndpointId.of("health"); + private static final String[] EMPTY_PATH = {}; /** @@ -62,7 +68,7 @@ public HealthComponent healthForPath(@Selector(match = Match.ALL_REMAINING) Stri } private HealthComponent health(ApiVersion apiVersion, String... path) { - HealthResult result = getHealth(apiVersion, SecurityContext.NONE, true, path); + HealthResult result = getHealth(apiVersion, null, SecurityContext.NONE, true, path); return (result != null) ? result.getHealth() : null; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroup.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroup.java index 0fa53c5df232..b0fbcd49f029 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroup.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroup.java @@ -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 @@ * 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(); + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroups.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroups.java index 1b50c1b4996c..26765672ca57 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroups.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointGroups.java @@ -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 getAllWithAdditionalPath(WebServerNamespace namespace) { + Assert.notNull(namespace, "Namespace must not be null"); + Set 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 diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointSupport.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointSupport.java index d36991774985..17b51970176c 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointSupport.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointSupport.java @@ -23,6 +23,7 @@ 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 { this.groups = groups; } - HealthResult 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 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 getHealth(ApiVersion apiVersion, HealthEndpointGroup group, SecurityContext securityContext, boolean showAll, String[] path, int pathOffset) { boolean showComponents = showAll || group.showComponents(securityContext); @@ -71,8 +86,8 @@ private HealthResult getHealth(ApiVersion apiVersion, HealthEndpointGroup gro return null; } Object contributor = getContributor(path, pathOffset); - T health = getContribution(apiVersion, group, contributor, showComponents, showDetails, - isSystemHealth ? this.groups.getNames() : null, false); + Set groupNames = isSystemHealth ? this.groups.getNames() : null; + T health = getContribution(apiVersion, group, contributor, showComponents, showDetails, groupNames, false); return (health != null) ? new HealthResult<>(health, group) : null; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java index 11eea5fb9995..3c44d51137cd 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java @@ -26,6 +26,7 @@ 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 HealthEndpointWebExtension(HealthContributorRegistry registry, HealthEndp } @ReadOperation - public WebEndpointResponse health(ApiVersion apiVersion, SecurityContext securityContext) { - return health(apiVersion, securityContext, false, NO_PATH); + public WebEndpointResponse health(ApiVersion apiVersion, WebServerNamespace serverNamespace, + SecurityContext securityContext) { + return health(apiVersion, serverNamespace, securityContext, false, NO_PATH); } @ReadOperation - public WebEndpointResponse health(ApiVersion apiVersion, SecurityContext securityContext, - @Selector(match = Match.ALL_REMAINING) String... path) { - return health(apiVersion, securityContext, false, path); + public WebEndpointResponse health(ApiVersion apiVersion, WebServerNamespace serverNamespace, + SecurityContext securityContext, @Selector(match = Match.ALL_REMAINING) String... path) { + return health(apiVersion, serverNamespace, securityContext, false, path); } - public WebEndpointResponse health(ApiVersion apiVersion, SecurityContext securityContext, - boolean showAll, String... path) { - HealthResult result = getHealth(apiVersion, securityContext, showAll, path); + public WebEndpointResponse health(ApiVersion apiVersion, WebServerNamespace serverNamespace, + SecurityContext securityContext, boolean showAll, String... path) { + HealthResult result = getHealth(apiVersion, serverNamespace, securityContext, showAll, path); if (result == null) { return (Arrays.equals(path, NO_PATH)) ? new WebEndpointResponse<>(DEFAULT_HEALTH, WebEndpointResponse.STATUS_OK) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java index 483e0a9e45b8..4d74549a39fc 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java @@ -29,6 +29,7 @@ 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 ReactiveHealthEndpointWebExtension(ReactiveHealthContributorRegistry regi @ReadOperation public Mono> 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> 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> health(ApiVersion apiVersion, - SecurityContext securityContext, boolean showAll, String... path) { - HealthResult> result = getHealth(apiVersion, securityContext, showAll, path); + WebServerNamespace serverNamespace, SecurityContext securityContext, boolean showAll, String... path) { + HealthResult> 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)) diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebServerNamespaceTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebServerNamespaceTests.java new file mode 100644 index 000000000000..91eaebb7f0e9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebServerNamespaceTests.java @@ -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")); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java index 0afb46250eee..db7e96b8be22 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java @@ -137,7 +137,7 @@ void matchAllRemainingPathsSelectorShouldMatchFullPath() { @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")); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/AdditionalHealthEndpointPathTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/AdditionalHealthEndpointPathTests.java new file mode 100644 index 000000000000..a9d02b061b5d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/AdditionalHealthEndpointPathTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointSupportTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointSupportTests.java index f60de783bc12..40a7d5dd9863 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointSupportTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointSupportTests.java @@ -24,6 +24,7 @@ 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 @@ void createWhenGroupsIsNullThrowsException() { @Test void getHealthWhenPathIsEmptyUsesPrimaryGroup() { this.registry.registerContributor("test", createContributor(this.up)); - HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, + HealthResult 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 @@ void getHealthWhenPathIsEmptyUsesPrimaryGroup() { @Test void getHealthWhenPathIsNotGroupReturnsResultFromPrimaryGroup() { this.registry.registerContributor("test", createContributor(this.up)); - HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, + HealthResult 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 @@ void getHealthWhenPathIsNotGroupReturnsResultFromPrimaryGroup() { @Test void getHealthWhenPathIsGroupReturnsResultFromGroup() { this.registry.registerContributor("atest", createContributor(this.up)); - HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, + HealthResult 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 @@ void getHealthWhenAlwaysShowIsFalseAndGroupIsTrueShowsComponents() { C contributor = createContributor(this.up); C compositeContributor = createCompositeContributor(Collections.singletonMap("spring", contributor)); this.registry.registerContributor("test", compositeContributor); - HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, + HealthResult 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 @@ void getHealthWhenAlwaysShowIsFalseAndGroupIsFalseCannotAccessComponent() { C compositeContributor = createCompositeContributor(Collections.singletonMap("spring", contributor)); this.registry.registerContributor("test", compositeContributor); HealthEndpointSupport endpoint = create(this.registry, this.groups); - HealthResult rootResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false); + HealthResult rootResult = endpoint.getHealth(ApiVersion.V3, null, SecurityContext.NONE, false); assertThat(((CompositeHealth) getHealth(rootResult)).getComponents()).isNullOrEmpty(); - HealthResult componentResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false, "test"); + HealthResult componentResult = endpoint.getHealth(ApiVersion.V3, null, SecurityContext.NONE, false, "test"); assertThat(componentResult).isNull(); } @@ -129,16 +130,16 @@ void getHealthWhenAlwaysShowIsTrueShowsComponents() { C compositeContributor = createCompositeContributor(Collections.singletonMap("spring", contributor)); this.registry.registerContributor("test", compositeContributor); HealthEndpointSupport endpoint = create(this.registry, this.groups); - HealthResult rootResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false); + HealthResult rootResult = endpoint.getHealth(ApiVersion.V3, null, SecurityContext.NONE, false); assertThat(((CompositeHealth) getHealth(rootResult)).getComponents()).containsKey("test"); - HealthResult componentResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false, "test"); + HealthResult 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 result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, + HealthResult 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 @@ void getHealthWhenAlwaysShowIsFalseAndGroupIsFalseShowsNoDetails() { this.primaryGroup.setShowDetails(false); this.registry.registerContributor("test", createContributor(this.up)); HealthEndpointSupport endpoint = create(this.registry, this.groups); - HealthResult rootResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false); - HealthResult componentResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false, "test"); + HealthResult rootResult = endpoint.getHealth(ApiVersion.V3, null, SecurityContext.NONE, false); + HealthResult 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 @@ void getHealthWhenAlwaysShowIsFalseAndGroupIsFalseShowsNoDetails() { void getHealthWhenAlwaysShowIsTrueShowsDetails() { this.primaryGroup.setShowDetails(false); this.registry.registerContributor("test", createContributor(this.up)); - HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, true, - "test"); + HealthResult 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 @@ void getHealthWhenCompositeReturnsAggregateResult() { contributors.put("a", createContributor(this.up)); contributors.put("b", createContributor(this.down)); this.registry.registerContributor("test", createCompositeContributor(contributors)); - HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, + HealthResult 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 @@ void getHealthWhenCompositeReturnsAggregateResult() { @Test void getHealthWhenPathDoesNotExistReturnsNull() { - HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, + HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE, false, "missing"); assertThat(result).isNull(); } @@ -188,7 +189,7 @@ void getHealthWhenPathDoesNotExistReturnsNull() { @Test void getHealthWhenPathIsEmptyIncludesGroups() { this.registry.registerContributor("test", createContributor(this.up)); - HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, + HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE, false); assertThat(((SystemHealth) getHealth(result)).getGroups()).containsOnly("alltheas"); } @@ -196,7 +197,7 @@ void getHealthWhenPathIsEmptyIncludesGroups() { @Test void getHealthWhenPathIsGroupDoesNotIncludesGroups() { this.registry.registerContributor("atest", createContributor(this.up)); - HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, + HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE, false, "alltheas"); assertThat(getHealth(result)).isNotInstanceOf(SystemHealth.class); } @@ -204,7 +205,7 @@ void getHealthWhenPathIsGroupDoesNotIncludesGroups() { @Test void getHealthWithEmptyCompositeReturnsNullResult() { // gh-18687 this.registry.registerContributor("test", createCompositeContributor(Collections.emptyMap())); - HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, + HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, null, SecurityContext.NONE, false); assertThat(result).isNull(); } @@ -217,12 +218,53 @@ void getHealthWhenGroupContainsCompositeContributorReturnsHealth() { TestHealthEndpointGroup testGroup = new TestHealthEndpointGroup((name) -> name.startsWith("test")); HealthEndpointGroups groups = HealthEndpointGroups.of(this.primaryGroup, Collections.singletonMap("testGroup", testGroup)); - HealthResult result = create(this.registry, groups).getHealth(ApiVersion.V3, SecurityContext.NONE, false, - "testGroup"); + HealthResult 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 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 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 result = create(this.registry, groups).getHealth(ApiVersion.V3, WebServerNamespace.SERVER, + SecurityContext.NONE, false, "healthz", "test"); + assertThat(result).isEqualTo(null); + } + protected abstract HealthEndpointSupport create(R registry, HealthEndpointGroups groups); protected abstract R createRegistry(); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebExtensionTests.java index d791a7686b34..778b2592f886 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebExtensionTests.java @@ -24,6 +24,7 @@ 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 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 @@ void healthWithNoContributorReturnsUp() { assertThat(this.registry).isEmpty(); WebEndpointResponse 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 @@ void healthWithNoContributorReturnsUp() { void healthWhenPathDoesNotExistReturnsHttp404() { this.registry.registerContributor("test", createContributor(this.up)); WebEndpointResponse 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 @@ void healthWhenPathDoesNotExistReturnsHttp404() { void healthWhenPathExistsReturnsHealth() { this.registry.registerContributor("test", createContributor(this.up)); WebEndpointResponse 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); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtensionTests.java index 1d3a5718db00..38acbd1acee9 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtensionTests.java @@ -43,7 +43,7 @@ class ReactiveHealthEndpointWebExtensionTests extends void healthReturnsSystemHealth() { this.registry.registerContributor("test", createContributor(this.up)); WebEndpointResponse 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 @@ void healthWithNoContributorReturnsUp() { assertThat(this.registry).isEmpty(); WebEndpointResponse 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 @@ void healthWithNoContributorReturnsUp() { void healthWhenPathDoesNotExistReturnsHttp404() { this.registry.registerContributor("test", createContributor(this.up)); WebEndpointResponse 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 @@ void healthWhenPathDoesNotExistReturnsHttp404() { void healthWhenPathExistsReturnsHealth() { this.registry.registerContributor("test", createContributor(this.up)); WebEndpointResponse 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); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroup.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroup.java index eb041bf90841..156c4982edea 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroup.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroup.java @@ -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 @@ public HttpCodeStatusMapper getHttpCodeStatusMapper() { return this.httpCodeStatusMapper; } + @Override + public AdditionalHealthEndpointPath getAdditionalPath() { + return this.additionalPath; + } + + void setAdditionalPath(AdditionalHealthEndpointPath additionalPath) { + this.additionalPath = additionalPath; + } + } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc index 3149a7c18e55..bc589f60915a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc @@ -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 <>. -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. + diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/context/WebServerApplicationContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/context/WebServerApplicationContext.java index 7d0e99022406..eb97ccdbaac9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/context/WebServerApplicationContext.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/context/WebServerApplicationContext.java @@ -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 @@ static boolean hasServerNamespace(ApplicationContext context, String serverNames .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; + + } + }