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 09d425f63700..d0f564c7fda3 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 @@ -43,6 +43,10 @@ class AvailabilityProbesHealthEndpointGroups implements HealthEndpointGroups { private final Set names; + private static final String LIVENESS = "liveness"; + + private static final String READINESS = "readiness"; + AvailabilityProbesHealthEndpointGroups(HealthEndpointGroups groups, boolean addAdditionalPaths) { Assert.notNull(groups, "Groups must not be null"); this.groups = groups; @@ -54,18 +58,32 @@ class AvailabilityProbesHealthEndpointGroups implements HealthEndpointGroups { private Map createProbeGroups(boolean addAdditionalPaths) { Map probeGroups = new LinkedHashMap<>(); - probeGroups.put("liveness", createProbeGroup(addAdditionalPaths, "/livez", "livenessState")); - probeGroups.put("readiness", createProbeGroup(addAdditionalPaths, "/readyz", "readinessState")); + probeGroups.put(LIVENESS, getOrCreateProbeGroup(addAdditionalPaths, LIVENESS, "/livez", "livenessState")); + probeGroups.put(READINESS, getOrCreateProbeGroup(addAdditionalPaths, READINESS, "/readyz", "readinessState")); return Collections.unmodifiableMap(probeGroups); } - private AvailabilityProbesHealthEndpointGroup createProbeGroup(boolean addAdditionalPath, String path, + private HealthEndpointGroup getOrCreateProbeGroup(boolean addAdditionalPath, String name, String path, String members) { + HealthEndpointGroup group = this.groups.get(name); + if (group != null) { + return determineAdditionalPathForExistingGroup(addAdditionalPath, path, group); + } AdditionalHealthEndpointPath additionalPath = (!addAdditionalPath) ? null : AdditionalHealthEndpointPath.of(WebServerNamespace.SERVER, path); return new AvailabilityProbesHealthEndpointGroup(additionalPath, members); } + private HealthEndpointGroup determineAdditionalPathForExistingGroup(boolean addAdditionalPath, String path, + HealthEndpointGroup group) { + if (addAdditionalPath && group.getAdditionalPath() == null) { + AdditionalHealthEndpointPath additionalPath = AdditionalHealthEndpointPath.of(WebServerNamespace.SERVER, + path); + return new DelegatingAvailabilityProbesHealthEndpointGroup(group, additionalPath); + } + return group; + } + @Override public HealthEndpointGroup getPrimary() { return this.groups.getPrimary(); @@ -79,15 +97,14 @@ public Set getNames() { @Override public HealthEndpointGroup get(String name) { HealthEndpointGroup group = this.groups.get(name); - if (group == null) { + if (group == null || isProbeGroup(name)) { group = this.probeGroups.get(name); } return group; } - static boolean containsAllProbeGroups(HealthEndpointGroups groups) { - Set names = groups.getNames(); - return names.contains("liveness") && names.contains("readiness"); + private boolean isProbeGroup(String name) { + return name.equals(LIVENESS) || name.equals(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 217796fea552..47b269aeb18c 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 @@ -41,9 +41,6 @@ class AvailabilityProbesHealthEndpointGroupsPostProcessor implements HealthEndpo @Override public HealthEndpointGroups postProcessHealthEndpointGroups(HealthEndpointGroups groups) { - if (AvailabilityProbesHealthEndpointGroups.containsAllProbeGroups(groups)) { - return 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/availability/DelegatingAvailabilityProbesHealthEndpointGroup.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/DelegatingAvailabilityProbesHealthEndpointGroup.java new file mode 100644 index 000000000000..66975852e7cd --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/DelegatingAvailabilityProbesHealthEndpointGroup.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2022 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.availability; + +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; +import org.springframework.util.Assert; + +/** + * {@link HealthEndpointGroup} used to support availability probes that delegates to an + * existing group. + * + * @author Madhura Bhave + */ +class DelegatingAvailabilityProbesHealthEndpointGroup implements HealthEndpointGroup { + + private final HealthEndpointGroup delegate; + + private final AdditionalHealthEndpointPath additionalPath; + + DelegatingAvailabilityProbesHealthEndpointGroup(HealthEndpointGroup delegate, + AdditionalHealthEndpointPath additionalPath) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = delegate; + this.additionalPath = additionalPath; + } + + @Override + public boolean isMember(String name) { + return this.delegate.isMember(name); + } + + @Override + public boolean showComponents(SecurityContext securityContext) { + return this.delegate.showComponents(securityContext); + } + + @Override + public boolean showDetails(SecurityContext securityContext) { + return this.delegate.showDetails(securityContext); + } + + @Override + public StatusAggregator getStatusAggregator() { + return this.delegate.getStatusAggregator(); + } + + @Override + public HttpCodeStatusMapper getHttpCodeStatusMapper() { + return this.delegate.getHttpCodeStatusMapper(); + } + + @Override + public AdditionalHealthEndpointPath getAdditionalPath() { + return this.additionalPath; + } + +} 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 246c48b85e79..8db6f6669f59 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 @@ -48,7 +48,8 @@ void postProcessHealthEndpointGroupsWhenGroupsAlreadyContainedReturnsOriginal() names.add("readiness"); names.add("liveness"); given(groups.getNames()).willReturn(names); - assertThat(this.postProcessor.postProcessHealthEndpointGroups(groups)).isSameAs(groups); + assertThat(this.postProcessor.postProcessHealthEndpointGroups(groups)) + .isInstanceOf(AvailabilityProbesHealthEndpointGroups.class); } @Test @@ -83,6 +84,25 @@ void postProcessHealthEndpointGroupsWhenAdditionalPathPropertyIsTrue() { assertThat(readiness.getAdditionalPath().toString()).isEqualTo("server:/readyz"); } + @Test + void postProcessHealthEndpointGroupsWhenGroupsAlreadyContainedAndAdditionalPathPropertyIsTrue() { + HealthEndpointGroups groups = mock(HealthEndpointGroups.class); + Set names = new LinkedHashSet<>(); + names.add("test"); + names.add("readiness"); + names.add("liveness"); + given(groups.getNames()).willReturn(names); + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("management.endpoint.health.probes.add-additional-paths", "true"); + AvailabilityProbesHealthEndpointGroupsPostProcessor postProcessor = new AvailabilityProbesHealthEndpointGroupsPostProcessor( + environment); + HealthEndpointGroups postProcessed = postProcessor.postProcessHealthEndpointGroups(groups); + 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); 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 b5b6be0eb48b..a9547c7050c6 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 @@ -16,15 +16,15 @@ package org.springframework.boot.actuate.autoconfigure.availability; -import java.util.Arrays; import java.util.Collections; -import java.util.LinkedHashSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +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; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -35,6 +35,7 @@ * Tests for {@link AvailabilityProbesHealthEndpointGroups}. * * @author Phillip Webb + * @author Madhura Bhave */ class AvailabilityProbesHealthEndpointGroupsTests { @@ -69,10 +70,32 @@ void getNamesIncludesAvailabilityProbeGroups() { } @Test - void getWhenProbeInDelegateReturnsGroupFromDelegate() { - given(this.delegate.get("liveness")).willReturn(this.group); + void getWhenProbeInDelegateReturnsOriginalGroup() { + HealthEndpointGroup group = mock(HealthEndpointGroup.class); + HttpCodeStatusMapper mapper = mock(HttpCodeStatusMapper.class); + given(group.getHttpCodeStatusMapper()).willReturn(mapper); + given(this.delegate.get("liveness")).willReturn(group); HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate, false); - assertThat(availabilityProbes.get("liveness")).isEqualTo(this.group); + assertThat(availabilityProbes.get("liveness")).isEqualTo(group); + assertThat(group.getHttpCodeStatusMapper()).isEqualTo(mapper); + } + + @Test + void getWhenProbeInDelegateAndExistingAdditionalPathReturnsOriginalGroup() { + HealthEndpointGroup group = mock(HealthEndpointGroup.class); + given(group.getAdditionalPath()).willReturn(AdditionalHealthEndpointPath.from("server:test")); + given(this.delegate.get("liveness")).willReturn(group); + HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate, true); + HealthEndpointGroup liveness = availabilityProbes.get("liveness"); + assertThat(liveness).isEqualTo(group); + assertThat(liveness.getAdditionalPath().getValue()).isEqualTo("test"); + } + + @Test + void getWhenProbeInDelegateAndAdditionalPathReturnsGroupWithAdditionalPath() { + given(this.delegate.get("liveness")).willReturn(this.group); + HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate, true); + assertThat(availabilityProbes.get("liveness").getAdditionalPath().getValue()).isEqualTo("/livez"); } @Test @@ -103,22 +126,4 @@ void getReadinessProbeHasOnlyReadinessStateAsMember() { assertThat(probeGroup.isMember("readinessState")).isTrue(); } - @Test - void containsAllWhenContainsAllReturnTrue() { - given(this.delegate.getNames()).willReturn(new LinkedHashSet<>(Arrays.asList("test", "liveness", "readiness"))); - assertThat(AvailabilityProbesHealthEndpointGroups.containsAllProbeGroups(this.delegate)).isTrue(); - } - - @Test - void containsAllWhenContainsOneReturnFalse() { - given(this.delegate.getNames()).willReturn(new LinkedHashSet<>(Arrays.asList("test", "liveness"))); - assertThat(AvailabilityProbesHealthEndpointGroups.containsAllProbeGroups(this.delegate)).isFalse(); - } - - @Test - void containsAllWhenContainsNoneReturnFalse() { - given(this.delegate.getNames()).willReturn(new LinkedHashSet<>(Arrays.asList("test", "spring"))); - assertThat(AvailabilityProbesHealthEndpointGroups.containsAllProbeGroups(this.delegate)).isFalse(); - } - } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/DelegatingAvailabilityProbesHealthEndpointGroupTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/DelegatingAvailabilityProbesHealthEndpointGroupTests.java new file mode 100644 index 000000000000..724844fb01eb --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/DelegatingAvailabilityProbesHealthEndpointGroupTests.java @@ -0,0 +1,69 @@ +/* + * 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.availability; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DelegatingAvailabilityProbesHealthEndpointGroup}. + * + * @author Madhura Bhave + */ +class DelegatingAvailabilityProbesHealthEndpointGroupTests { + + private DelegatingAvailabilityProbesHealthEndpointGroup group; + + private HttpCodeStatusMapper mapper; + + private StatusAggregator aggregator; + + @BeforeEach + void setup() { + HealthEndpointGroup delegate = mock(HealthEndpointGroup.class); + this.mapper = mock(HttpCodeStatusMapper.class); + this.aggregator = mock(StatusAggregator.class); + given(delegate.getHttpCodeStatusMapper()).willReturn(this.mapper); + given(delegate.getStatusAggregator()).willReturn(this.aggregator); + given(delegate.showComponents(any())).willReturn(true); + given(delegate.showDetails(any())).willReturn(false); + given(delegate.isMember("test")).willReturn(true); + this.group = new DelegatingAvailabilityProbesHealthEndpointGroup(delegate, + AdditionalHealthEndpointPath.from("server:test")); + } + + @Test + void groupDelegatesToDelegate() { + assertThat(this.group.getHttpCodeStatusMapper()).isEqualTo(this.mapper); + assertThat(this.group.getStatusAggregator()).isEqualTo(this.aggregator); + assertThat(this.group.isMember("test")).isTrue(); + assertThat(this.group.showDetails(null)).isFalse(); + assertThat(this.group.showComponents(null)).isTrue(); + assertThat(this.group.getAdditionalPath().getValue()).isEqualTo("test"); + } + +}