Support multiple health groups with an additional path with Jersey

This commit knowingly makes breaking API changes to
JerseyHealthEndpointAdditionalPathResourceFactory. We considered
other options but they all had the potential to be backwards
incompatible in one way or another. Faced with that situation we
concluded that the likelihood of anyone using the modified API
directly is small enough to warrant making the breaking changes.
If it becomes apparent that we have misjudged things we can revisit
the changes in the future.

Closes gh-36250
pull/36620/head
Andy Wilkinson 1 year ago
parent 76cd102aa6
commit 52f732920b

@ -184,7 +184,7 @@ class JerseyWebEndpointManagementContextConfiguration {
JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory( JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory(
WebServerNamespace.MANAGEMENT, this.groups); WebServerNamespace.MANAGEMENT, this.groups);
Collection<Resource> endpointResources = resourceFactory Collection<Resource> endpointResources = resourceFactory
.createEndpointResources(mapping, Collections.singletonList(this.endpoint), null, null, false) .createEndpointResources(mapping, Collections.singletonList(this.endpoint))
.stream() .stream()
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toList()); .collect(Collectors.toList());

@ -163,7 +163,7 @@ class HealthEndpointWebExtensionConfiguration {
JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory( JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory(
WebServerNamespace.SERVER, this.groups); WebServerNamespace.SERVER, this.groups);
Collection<Resource> endpointResources = resourceFactory Collection<Resource> endpointResources = resourceFactory
.createEndpointResources(mapping, Collections.singletonList(this.endpoint), null, null, false) .createEndpointResources(mapping, Collections.singletonList(this.endpoint))
.stream() .stream()
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toList()); .collect(Collectors.toList());

@ -27,6 +27,8 @@ import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import static org.assertj.core.api.Assertions.assertThatNoException;
/** /**
* Abstract base class for health groups with an additional path. * Abstract base class for health groups with an additional path.
* *
@ -52,6 +54,18 @@ abstract class AbstractHealthEndpointAdditionalPathIntegrationTests<T extends Ab
.run(withWebTestClient(this::testResponse, "local.server.port")); .run(withWebTestClient(this::testResponse, "local.server.port"));
} }
@Test
void multipleGroupsAreAvailableAtAdditionalPaths() {
this.runner
.withPropertyValues("management.endpoint.health.group.one.include=diskSpace",
"management.endpoint.health.group.two.include=diskSpace",
"management.endpoint.health.group.one.additional-path=server:/alpha",
"management.endpoint.health.group.two.additional-path=server:/bravo",
"management.endpoint.health.group.one.show-components=always",
"management.endpoint.health.group.two.show-components=always")
.run(withWebTestClient((client) -> testResponses(client, "/alpha", "/bravo"), "local.server.port"));
}
@Test @Test
void groupIsAvailableAtAdditionalPathWithoutSlash() { void groupIsAvailableAtAdditionalPathWithoutSlash() {
this.runner this.runner
@ -125,17 +139,24 @@ abstract class AbstractHealthEndpointAdditionalPathIntegrationTests<T extends Ab
} }
private void testResponse(WebTestClient client) { private void testResponse(WebTestClient client) {
client.get() testResponses(client, "/healthz");
.uri("/healthz") }
.accept(MediaType.APPLICATION_JSON)
.exchange() private void testResponses(WebTestClient client, String... paths) {
.expectStatus() for (String path : paths) {
.isOk() assertThatNoException().as(path)
.expectBody() .isThrownBy(() -> client.get()
.jsonPath("status") .uri(path)
.isEqualTo("UP") .accept(MediaType.APPLICATION_JSON)
.jsonPath("components.diskSpace") .exchange()
.exists(); .expectStatus()
.isOk()
.expectBody()
.jsonPath("status")
.isEqualTo("UP")
.jsonPath("components.diskSpace")
.exists());
}
} }
private ContextConsumer<A> withWebTestClient(Consumer<WebTestClient> consumer, String property) { private ContextConsumer<A> withWebTestClient(Consumer<WebTestClient> consumer, String property) {

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2021 the original author or authors. * Copyright 2012-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,11 +16,17 @@
package org.springframework.boot.actuate.endpoint.web.jersey; package org.springframework.boot.actuate.endpoint.web.jersey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.glassfish.jersey.server.model.Resource; import org.glassfish.jersey.server.model.Resource;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping; 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.WebOperation;
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate; import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace; import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
@ -35,7 +41,9 @@ import org.springframework.boot.actuate.health.HealthEndpointGroups;
* @author Madhura Bhave * @author Madhura Bhave
* @since 2.6.0 * @since 2.6.0
*/ */
public class JerseyHealthEndpointAdditionalPathResourceFactory extends JerseyEndpointResourceFactory { public final class JerseyHealthEndpointAdditionalPathResourceFactory {
private final JerseyEndpointResourceFactory delegate = new JerseyEndpointResourceFactory();
private final Set<HealthEndpointGroup> groups; private final Set<HealthEndpointGroup> groups;
@ -47,20 +55,30 @@ public class JerseyHealthEndpointAdditionalPathResourceFactory extends JerseyEnd
this.groups = groups.getAllWithAdditionalPath(serverNamespace); this.groups = groups.getAllWithAdditionalPath(serverNamespace);
} }
@Override public Collection<Resource> createEndpointResources(EndpointMapping endpointMapping,
protected Resource createResource(EndpointMapping endpointMapping, WebOperation operation) { Collection<ExposableWebEndpoint> endpoints) {
return endpoints.stream()
.flatMap((endpoint) -> endpoint.getOperations().stream())
.flatMap((operation) -> createResources(endpointMapping, operation))
.collect(Collectors.toList());
}
private Stream<Resource> createResources(EndpointMapping endpointMapping, WebOperation operation) {
WebOperationRequestPredicate requestPredicate = operation.getRequestPredicate(); WebOperationRequestPredicate requestPredicate = operation.getRequestPredicate();
String matchAllRemainingPathSegmentsVariable = requestPredicate.getMatchAllRemainingPathSegmentsVariable(); String matchAllRemainingPathSegmentsVariable = requestPredicate.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) { if (matchAllRemainingPathSegmentsVariable != null) {
List<Resource> resources = new ArrayList<>();
for (HealthEndpointGroup group : this.groups) { for (HealthEndpointGroup group : this.groups) {
AdditionalHealthEndpointPath additionalPath = group.getAdditionalPath(); AdditionalHealthEndpointPath additionalPath = group.getAdditionalPath();
if (additionalPath != null) { if (additionalPath != null) {
return getResource(endpointMapping, operation, requestPredicate, additionalPath.getValue(), resources.add(this.delegate.getResource(endpointMapping, operation, requestPredicate,
this.serverNamespace, (data, pathSegmentsVariable) -> data.getUriInfo().getPath()); additionalPath.getValue(), this.serverNamespace,
(data, pathSegmentsVariable) -> data.getUriInfo().getPath()));
} }
} }
return resources.stream();
} }
return null; return Stream.empty();
} }
} }

Loading…
Cancel
Save