mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-10-02 23:39:34 +00:00
Add Spring gRPC server health check support
Add integration between Spring Boot's health module and gRPC health checking service support. Closes gh-49048 Co-authored-by: Chris Bono <chris.bono@broadcom.com>
This commit is contained in:
co-authored by
Chris Bono
parent
b650f3c558
commit
67ac57fb77
@@ -29,6 +29,9 @@ dependencies {
|
||||
api("org.springframework.grpc:spring-grpc-core")
|
||||
|
||||
optional(project(":core:spring-boot-autoconfigure"))
|
||||
optional(project(":module:spring-boot-health"))
|
||||
optional("com.fasterxml.jackson.core:jackson-annotations")
|
||||
optional("io.projectreactor:reactor-core")
|
||||
optional("io.grpc:grpc-servlet-jakarta")
|
||||
optional("io.grpc:grpc-services")
|
||||
optional("io.grpc:grpc-netty")
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
|
||||
|
||||
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponent;
|
||||
import org.springframework.boot.grpc.server.health.StatusAggregator;
|
||||
import org.springframework.boot.grpc.server.health.StatusMapper;
|
||||
import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembership;
|
||||
|
||||
/**
|
||||
* Auto-configured {@link HealthCheckedGrpcComponent}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class AutoConfiguredHealthCheckedGrpcComponent implements HealthCheckedGrpcComponent {
|
||||
|
||||
private final HealthContributorMembership membership;
|
||||
|
||||
private final StatusAggregator statusAggregator;
|
||||
|
||||
private final StatusMapper statusMapper;
|
||||
|
||||
AutoConfiguredHealthCheckedGrpcComponent(HealthContributorMembership membership, StatusAggregator statusAggregator,
|
||||
StatusMapper statusMapper) {
|
||||
this.membership = membership;
|
||||
this.statusAggregator = statusAggregator;
|
||||
this.statusMapper = statusMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMember(String healthContributorName) {
|
||||
return this.membership.isMember(healthContributorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatusAggregator getStatusAggregator() {
|
||||
return this.statusAggregator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatusMapper getStatusMapper() {
|
||||
return this.statusMapper;
|
||||
}
|
||||
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthProperties.Service;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthProperties.Status;
|
||||
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponent;
|
||||
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponents;
|
||||
import org.springframework.boot.grpc.server.health.StatusAggregator;
|
||||
import org.springframework.boot.grpc.server.health.StatusMapper;
|
||||
import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembership;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Auto-configured {@link HealthCheckedGrpcComponents}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class AutoConfiguredHealthCheckedGrpcComponents implements HealthCheckedGrpcComponents {
|
||||
|
||||
private final HealthCheckedGrpcComponent server;
|
||||
|
||||
private final Map<String, HealthCheckedGrpcComponent> services;
|
||||
|
||||
/**
|
||||
* Create a new {@link AutoConfiguredHealthCheckedGrpcComponents} instance.
|
||||
* @param applicationContext the application context used to check for override beans
|
||||
* @param properties the grpc server health properties
|
||||
*/
|
||||
AutoConfiguredHealthCheckedGrpcComponents(ApplicationContext applicationContext,
|
||||
GrpcServerHealthProperties properties) {
|
||||
ListableBeanFactory beanFactory = (applicationContext instanceof ConfigurableApplicationContext configurableContext)
|
||||
? configurableContext.getBeanFactory() : applicationContext;
|
||||
StatusAggregator statusAggregator = getNonQualifiedBean(beanFactory, StatusAggregator.class,
|
||||
() -> StatusAggregator.of(properties.getStatus().getOrder()));
|
||||
StatusMapper statusMapper = getNonQualifiedBean(beanFactory, StatusMapper.class,
|
||||
() -> StatusMapper.of(properties.getStatus().getMapping()));
|
||||
this.server = new AutoConfiguredHealthCheckedGrpcComponent(HealthContributorMembership.always(),
|
||||
statusAggregator, statusMapper);
|
||||
this.services = createServices(properties.getService(), beanFactory, statusAggregator, statusMapper);
|
||||
}
|
||||
|
||||
private Map<String, HealthCheckedGrpcComponent> createServices(Map<String, Service> serviceProperties,
|
||||
BeanFactory beanFactory, StatusAggregator defaultStatusAggregator, StatusMapper defaultStatusMapper) {
|
||||
Map<String, HealthCheckedGrpcComponent> services = new TreeMap<>();
|
||||
serviceProperties.forEach((serviceName, service) -> {
|
||||
Status status = service.getStatus();
|
||||
StatusAggregator statusAggregator = getQualifiedBean(beanFactory, StatusAggregator.class, serviceName,
|
||||
() -> createStatusAggregator(status.getOrder(), defaultStatusAggregator));
|
||||
StatusMapper statusMapper = getQualifiedBean(beanFactory, StatusMapper.class, serviceName,
|
||||
() -> createStatusMapper(status.getMapping(), defaultStatusMapper));
|
||||
HealthContributorMembership membership = HealthContributorMembership.byIncludeExclude(service.getInclude(),
|
||||
service.getExclude());
|
||||
services.put(serviceName,
|
||||
new AutoConfiguredHealthCheckedGrpcComponent(membership, statusAggregator, statusMapper));
|
||||
});
|
||||
return Collections.unmodifiableMap(services);
|
||||
}
|
||||
|
||||
private StatusAggregator createStatusAggregator(List<String> order, StatusAggregator defaultStatusAggregator) {
|
||||
return (!CollectionUtils.isEmpty(order)) ? StatusAggregator.of(order) : defaultStatusAggregator;
|
||||
}
|
||||
|
||||
private StatusMapper createStatusMapper(Map<String, ServingStatus> mapping, StatusMapper defaultStatusMapper) {
|
||||
return (!CollectionUtils.isEmpty(mapping)) ? StatusMapper.of(mapping) : defaultStatusMapper;
|
||||
}
|
||||
|
||||
private <T> T getNonQualifiedBean(ListableBeanFactory beanFactory, Class<T> type, Supplier<T> fallback) {
|
||||
List<String> candidates = new ArrayList<>();
|
||||
for (String beanName : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, type)) {
|
||||
String[] aliases = beanFactory.getAliases(beanName);
|
||||
if (!BeanFactoryAnnotationUtils.isQualifierMatch(
|
||||
(qualifier) -> !qualifier.equals(beanName) && !ObjectUtils.containsElement(aliases, qualifier),
|
||||
beanName, beanFactory)) {
|
||||
candidates.add(beanName);
|
||||
}
|
||||
}
|
||||
if (candidates.isEmpty()) {
|
||||
return fallback.get();
|
||||
}
|
||||
if (candidates.size() == 1) {
|
||||
return beanFactory.getBean(candidates.get(0), type);
|
||||
}
|
||||
return beanFactory.getBean(type);
|
||||
}
|
||||
|
||||
private <T> T getQualifiedBean(BeanFactory beanFactory, Class<T> type, String qualifier, Supplier<T> fallback) {
|
||||
try {
|
||||
return BeanFactoryAnnotationUtils.qualifiedBeanOfType(beanFactory, type, qualifier);
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException ex) {
|
||||
return fallback.get();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable HealthCheckedGrpcComponent getServer() {
|
||||
return this.server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getServiceNames() {
|
||||
return this.services.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable HealthCheckedGrpcComponent getService(String serviceName) {
|
||||
return this.services.get(serviceName);
|
||||
}
|
||||
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.Grpc;
|
||||
import io.grpc.protobuf.services.HealthStatusManager;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthAutoConfiguration.NotDisabledAndHasBindableServiceOrExplicitlyEnabledCondition;
|
||||
import org.springframework.boot.grpc.server.health.GrpcServerHealth;
|
||||
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponents;
|
||||
import org.springframework.boot.grpc.server.health.StatusAggregator;
|
||||
import org.springframework.boot.grpc.server.health.StatusMapper;
|
||||
import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembershipValidator;
|
||||
import org.springframework.boot.health.registry.HealthContributorRegistry;
|
||||
import org.springframework.boot.health.registry.ReactiveHealthContributorRegistry;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.grpc.server.GrpcServerFactory;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side health service.
|
||||
*
|
||||
* @author Daniel Theuke
|
||||
* @author Chris Bono
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@AutoConfiguration(
|
||||
afterName = "org.springframework.boot.health.autoconfigure.registry.HealthContributorRegistryAutoConfiguration")
|
||||
@ConditionalOnClass({ GrpcServerFactory.class, Grpc.class, HealthStatusManager.class })
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true)
|
||||
@Conditional(NotDisabledAndHasBindableServiceOrExplicitlyEnabledCondition.class)
|
||||
@EnableConfigurationProperties(GrpcServerHealthProperties.class)
|
||||
public final class GrpcServerHealthAutoConfiguration {
|
||||
|
||||
@Bean(destroyMethod = "enterTerminalState")
|
||||
@ConditionalOnMissingBean
|
||||
HealthStatusManager grpcServerHealthStatusManager() {
|
||||
return new HealthStatusManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
BindableService grpcServerHealthService(HealthStatusManager healthStatusManager) {
|
||||
return healthStatusManager.getHealthService();
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnBean(type = "org.springframework.boot.health.registry.HealthContributorRegistry")
|
||||
static class GrpcServerHealthContributorConfiguration {
|
||||
|
||||
static final String VALIDATE_MEMBERSHIP_PROPERTY = "spring.grpc.server.health.services.validate-membership";
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
StatusAggregator grpcServerHealthStatusAggregator(GrpcServerHealthProperties properties) {
|
||||
return StatusAggregator.of(properties.getStatus().getOrder());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
StatusMapper grpcServerHealthHttpCodeStatusMapper(GrpcServerHealthProperties properties) {
|
||||
return StatusMapper.of(properties.getStatus().getMapping());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(HealthCheckedGrpcComponents.class)
|
||||
AutoConfiguredHealthCheckedGrpcComponents grpcServerHealthCheckedGrpcComponents(
|
||||
ApplicationContext applicationContext, GrpcServerHealthProperties properties) {
|
||||
return new AutoConfiguredHealthCheckedGrpcComponents(applicationContext, properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
GrpcServerHealth grpcServerHealth(HealthContributorRegistry healthContributorRegistry,
|
||||
ObjectProvider<ReactiveHealthContributorRegistry> reactiveHealthContributorRegistry,
|
||||
HealthCheckedGrpcComponents healthCheckedGrpcComponents) {
|
||||
return new GrpcServerHealth(healthContributorRegistry, reactiveHealthContributorRegistry.getIfAvailable(),
|
||||
healthCheckedGrpcComponents);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBooleanProperty(name = VALIDATE_MEMBERSHIP_PROPERTY, matchIfMissing = true)
|
||||
HealthContributorMembershipValidator grpcServerHealthServiceMembershipValidator(
|
||||
GrpcServerHealthProperties properties, HealthContributorRegistry healthContributorRegistry,
|
||||
ObjectProvider<ReactiveHealthContributorRegistry> reactiveHealthContributorRegistry) {
|
||||
return new HealthContributorMembershipValidator(healthContributorRegistry,
|
||||
reactiveHealthContributorRegistry.getIfAvailable(), VALIDATE_MEMBERSHIP_PROPERTY,
|
||||
(members) -> properties.getService().forEach((serviceName, service) -> {
|
||||
String property = "spring.grpc.server.health.service." + serviceName;
|
||||
members.member(property + ".include".formatted(serviceName), service.getInclude());
|
||||
members.member(property + ".exclude".formatted(serviceName), service.getExclude());
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class NotDisabledAndHasBindableServiceOrExplicitlyEnabledCondition extends AnyNestedCondition {
|
||||
|
||||
NotDisabledAndHasBindableServiceOrExplicitlyEnabledCondition() {
|
||||
super(ConfigurationPhase.REGISTER_BEAN);
|
||||
}
|
||||
|
||||
@ConditionalOnBean(BindableService.class)
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.health.enabled", matchIfMissing = true)
|
||||
static class NotDisabledAndHasBindableService {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.health.enabled")
|
||||
static class ExplicitlyEnabled {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* {@link ConfigurationProperties Properties} for Spring gRPC server health.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Chris Bono
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.grpc.server.health")
|
||||
public class GrpcServerHealthProperties {
|
||||
|
||||
/**
|
||||
* Whether to auto-configure Health feature on the gRPC server.
|
||||
*/
|
||||
private @Nullable Boolean enabled;
|
||||
|
||||
/**
|
||||
* Whether to include the overall server health.
|
||||
*/
|
||||
private boolean includeOverallHealth = true;
|
||||
|
||||
/**
|
||||
* Properties that apply to all services.
|
||||
*/
|
||||
private final Services services = new Services();
|
||||
|
||||
/**
|
||||
* Service specific health reporting.
|
||||
*/
|
||||
private final Map<String, Service> service = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Status configuration.
|
||||
*/
|
||||
private final Status status = new Status();
|
||||
|
||||
/**
|
||||
* Schedule configuration.
|
||||
*/
|
||||
private final Schedule schedule = new Schedule();
|
||||
|
||||
public @Nullable Boolean getEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(@Nullable Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isIncludeOverallHealth() {
|
||||
return this.includeOverallHealth;
|
||||
}
|
||||
|
||||
public void setIncludeOverallHealth(boolean includeOverallHealth) {
|
||||
this.includeOverallHealth = includeOverallHealth;
|
||||
}
|
||||
|
||||
public Services getServices() {
|
||||
return this.services;
|
||||
}
|
||||
|
||||
public Map<String, Service> getService() {
|
||||
return this.service;
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public Schedule getSchedule() {
|
||||
return this.schedule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties applying to all services.
|
||||
*/
|
||||
public static class Services {
|
||||
|
||||
/**
|
||||
* Whether to validate health membership for services on startup. Validation fails
|
||||
* if a service includes or excludes a health contributor that does not exist.
|
||||
*/
|
||||
private boolean validateMembership = true;
|
||||
|
||||
public boolean isValidateMembership() {
|
||||
return this.validateMembership;
|
||||
}
|
||||
|
||||
public void setValidateMembership(boolean validateMembership) {
|
||||
this.validateMembership = validateMembership;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A health for a specific service.
|
||||
*/
|
||||
public static class Service {
|
||||
|
||||
/**
|
||||
* Health indicator IDs that should be included or '*' for all.
|
||||
*/
|
||||
private @Nullable Set<String> include;
|
||||
|
||||
/**
|
||||
* Health indicator IDs that should be excluded or '*' for all.
|
||||
*/
|
||||
private @Nullable Set<String> exclude;
|
||||
|
||||
/**
|
||||
* Status configuration.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private final Status status = new Status();
|
||||
|
||||
public Status getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public @Nullable Set<String> getInclude() {
|
||||
return this.include;
|
||||
}
|
||||
|
||||
public void setInclude(@Nullable Set<String> include) {
|
||||
this.include = include;
|
||||
}
|
||||
|
||||
public @Nullable Set<String> getExclude() {
|
||||
return this.exclude;
|
||||
}
|
||||
|
||||
public void setExclude(@Nullable Set<String> exclude) {
|
||||
this.exclude = exclude;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Status properties for the group.
|
||||
*/
|
||||
public static class Status {
|
||||
|
||||
/**
|
||||
* List of health statuses in order of severity.
|
||||
*/
|
||||
private List<String> order = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Mapping of health statuses to gRPC service status. By default, registered
|
||||
* health statuses map to sensible defaults (for example, UP maps to SERVING).
|
||||
*/
|
||||
private final Map<String, ServingStatus> mapping = new HashMap<>();
|
||||
|
||||
public List<String> getOrder() {
|
||||
return this.order;
|
||||
}
|
||||
|
||||
public void setOrder(List<String> statusOrder) {
|
||||
if (!CollectionUtils.isEmpty(statusOrder)) {
|
||||
this.order = statusOrder;
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, ServingStatus> getMapping() {
|
||||
return this.mapping;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Health task scheduling.
|
||||
*/
|
||||
public static class Schedule {
|
||||
|
||||
/**
|
||||
* Whether to schedule updates to gRPC server health based on application health.
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* How often to update the health status.
|
||||
*/
|
||||
private Duration period = Duration.ofSeconds(5);
|
||||
|
||||
/**
|
||||
* The initial delay before updating the health status the very first time.
|
||||
*/
|
||||
private Duration delay = Duration.ofSeconds(5);
|
||||
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public Duration getPeriod() {
|
||||
return this.period;
|
||||
}
|
||||
|
||||
public void setPeriod(Duration period) {
|
||||
this.period = period;
|
||||
}
|
||||
|
||||
public Duration getDelay() {
|
||||
return this.delay;
|
||||
}
|
||||
|
||||
public void setDelay(Duration delay) {
|
||||
this.delay = delay;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
import io.grpc.protobuf.services.HealthStatusManager;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.boot.grpc.server.health.GrpcServerHealth;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
|
||||
/**
|
||||
* Schedules gRPC health updates one the gRPC server has been started.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Chris Bono
|
||||
*/
|
||||
class GrpcServerHealthScheduler implements ApplicationListener<GrpcServerStartedEvent> {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(GrpcServerHealthScheduler.class);
|
||||
|
||||
private final SingletonSupplier<ScheduledFuture<?>> scheduleHealth;
|
||||
|
||||
GrpcServerHealthScheduler(GrpcServerHealth grpcServerHealth, HealthStatusManager grpcServerHealthStatusManager,
|
||||
TaskScheduler taskScheduler, Duration period, Duration delay) {
|
||||
this(Clock.systemDefaultZone(), grpcServerHealth, grpcServerHealthStatusManager, taskScheduler, period, delay);
|
||||
}
|
||||
|
||||
GrpcServerHealthScheduler(Clock clock, GrpcServerHealth grpcServerHealth,
|
||||
HealthStatusManager grpcServerHealthStatusManager, TaskScheduler taskScheduler, Duration period,
|
||||
Duration delay) {
|
||||
this.scheduleHealth = SingletonSupplier.of(() -> {
|
||||
logger.debug(LogMessage
|
||||
.of(() -> "Scheduling gRPC server health updates every %s seconds (after a delay of %s seconds)"
|
||||
.formatted((period.toMillis() / 1000.0), delay.toMillis() / 1000.0)));
|
||||
Runnable task = () -> grpcServerHealth.update(grpcServerHealthStatusManager);
|
||||
return taskScheduler.scheduleAtFixedRate(task, Instant.now(clock).plus(delay), period);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(GrpcServerStartedEvent event) {
|
||||
this.scheduleHealth.get();
|
||||
}
|
||||
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
|
||||
|
||||
import io.grpc.Grpc;
|
||||
import io.grpc.protobuf.services.HealthStatusManager;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.task.DefaultTaskSchedulerConfiguration;
|
||||
import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthProperties.Schedule;
|
||||
import org.springframework.boot.grpc.server.health.GrpcServerHealth;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.grpc.server.GrpcServerFactory;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} to invoke {@link GrpcServerHealth}
|
||||
* updates using a {@link TaskScheduler}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@AutoConfiguration(after = GrpcServerHealthAutoConfiguration.class)
|
||||
@ConditionalOnBooleanProperty(name = "spring.grpc.server.health.schedule.enabled", matchIfMissing = true)
|
||||
@Import(DefaultTaskSchedulerConfiguration.class)
|
||||
@ConditionalOnClass({ GrpcServerFactory.class, Grpc.class, HealthStatusManager.class })
|
||||
public final class GrpcServerHealthSchedulerAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean({ TaskScheduler.class, GrpcServerHealth.class })
|
||||
GrpcServerHealthScheduler grpcServerHealthScheduler(GrpcServerHealth grpcServerHealth,
|
||||
HealthStatusManager grpcServerHealthStatusManager, TaskScheduler taskScheduler,
|
||||
GrpcServerHealthProperties properties) {
|
||||
Schedule schedule = properties.getSchedule();
|
||||
return new GrpcServerHealthScheduler(grpcServerHealth, grpcServerHealthStatusManager, taskScheduler,
|
||||
schedule.getPeriod(), schedule.getDelay());
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for gRPC server health checks.
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.boot.grpc.server.autoconfigure.health;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.health;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
|
||||
import io.grpc.protobuf.services.HealthStatusManager;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.health.contributor.CompositeHealthContributor;
|
||||
import org.springframework.boot.health.contributor.Health;
|
||||
import org.springframework.boot.health.contributor.HealthContributors;
|
||||
import org.springframework.boot.health.contributor.HealthIndicator;
|
||||
import org.springframework.boot.health.contributor.Status;
|
||||
import org.springframework.boot.health.registry.HealthContributorRegistry;
|
||||
import org.springframework.boot.health.registry.ReactiveHealthContributorRegistry;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Provides health information for a gRPC server that can be used to update a
|
||||
* {@link HealthStatusManager}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public class GrpcServerHealth {
|
||||
|
||||
private HealthContributorRegistry registry;
|
||||
|
||||
private @Nullable ReactiveHealthContributorRegistry fallbackRegistry;
|
||||
|
||||
private HealthCheckedGrpcComponents components;
|
||||
|
||||
/**
|
||||
* Create a new {@link GrpcServerHealth} instance.
|
||||
* @param registry the health contributor registry
|
||||
* @param fallbackRegistry the fallback registry or {@code null}
|
||||
* @param components the components used to provide the server health
|
||||
*/
|
||||
public GrpcServerHealth(HealthContributorRegistry registry,
|
||||
@Nullable ReactiveHealthContributorRegistry fallbackRegistry, HealthCheckedGrpcComponents components) {
|
||||
Assert.notNull(registry, "'registry' must not be null");
|
||||
Assert.notNull(components, "'components' must not be null");
|
||||
this.registry = registry;
|
||||
this.fallbackRegistry = fallbackRegistry;
|
||||
this.components = components;
|
||||
}
|
||||
|
||||
public void update(HealthStatusManager manager) {
|
||||
update(manager::setStatus);
|
||||
}
|
||||
|
||||
public void update(BiConsumer<String, ServingStatus> updator) {
|
||||
Cache cache = new Cache();
|
||||
HealthCheckedGrpcComponent serverComponent = this.components.getServer();
|
||||
if (serverComponent != null) {
|
||||
updator.accept("", getServingStatus(cache, serverComponent));
|
||||
}
|
||||
for (String serviceName : this.components.getServiceNames()) {
|
||||
HealthCheckedGrpcComponent serviceComponent = this.components.getService(serviceName);
|
||||
if (!serviceName.isEmpty() && serviceComponent != null) {
|
||||
updator.accept(serviceName, getServingStatus(cache, serviceComponent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ServingStatus getServingStatus(Cache cache, HealthCheckedGrpcComponent component) {
|
||||
Set<Status> statuses = new LinkedHashSet<>();
|
||||
collectStatuses(cache, component, statuses, this.registry, "");
|
||||
if (this.fallbackRegistry != null) {
|
||||
collectStatuses(cache, component, statuses, this.fallbackRegistry.asHealthContributors(), "");
|
||||
}
|
||||
Status status = component.getStatusAggregator().getAggregateStatus(statuses);
|
||||
return component.getStatusMapper().getServingStatus(status);
|
||||
}
|
||||
|
||||
private void collectStatuses(Cache cache, HealthCheckedGrpcComponent component, Set<Status> statuses,
|
||||
HealthContributors contributors, String prefix) {
|
||||
for (HealthContributors.Entry entry : contributors) {
|
||||
String name = (prefix.isEmpty()) ? entry.name() : prefix + "/" + entry.name();
|
||||
if (entry.contributor() instanceof CompositeHealthContributor composite) {
|
||||
collectStatuses(cache, component, statuses, composite, name);
|
||||
}
|
||||
else if (component.isMember(name)) {
|
||||
Health health = cache.getHealth(name, (HealthIndicator) entry.contributor());
|
||||
if (health != null) {
|
||||
statuses.add(health.getStatus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Cache {
|
||||
|
||||
private final Map<String, Health> health = new HashMap<>();
|
||||
|
||||
Health getHealth(String name, HealthIndicator indicator) {
|
||||
return this.health.computeIfAbsent(name, (key) -> indicator.health(false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.health;
|
||||
|
||||
/**
|
||||
* A single gRPC component that can be checked for health.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public interface HealthCheckedGrpcComponent {
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the given health contributor is a member of this component.
|
||||
* @param healthContributorName the contributor name
|
||||
* @return {@code true} if the contributor is a member
|
||||
*/
|
||||
boolean isMember(String healthContributorName);
|
||||
|
||||
/**
|
||||
* Returns the status aggregator that should be used for this component.
|
||||
* @return the status aggregator
|
||||
*/
|
||||
StatusAggregator getStatusAggregator();
|
||||
|
||||
/**
|
||||
* Returns the {@link StatusMapper} that should be used for this component.
|
||||
* @return the status mapper
|
||||
*/
|
||||
StatusMapper getStatusMapper();
|
||||
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.health;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* A collection of {@link HealthCheckedGrpcComponent components} used to check the health
|
||||
* of a gRPC server.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
* @see GrpcServerHealth
|
||||
*/
|
||||
public interface HealthCheckedGrpcComponents {
|
||||
|
||||
/**
|
||||
* Return the component that represents the overall server health or {@code null} if
|
||||
* no overall health should be reported.
|
||||
* @return the server component or {@code null}
|
||||
*/
|
||||
@Nullable HealthCheckedGrpcComponent getServer();
|
||||
|
||||
/**
|
||||
* Return the names of the services that contribute health checks.
|
||||
* @return the service names
|
||||
*/
|
||||
Set<String> getServiceNames();
|
||||
|
||||
/**
|
||||
* Return the component for the service with the specified name or {@code null} if the
|
||||
* name is not known.
|
||||
* @param serviceName the name of the service
|
||||
* @return the service component or {@code null}
|
||||
*/
|
||||
@Nullable HealthCheckedGrpcComponent getService(String serviceName);
|
||||
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.health;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.health.contributor.Status;
|
||||
import org.springframework.lang.Contract;
|
||||
|
||||
/**
|
||||
* Simple {@link StatusMapper} backed by map of {@link Status#getCode() status code} to
|
||||
* {@link ServingStatus}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class SimpleServingStatusMapper implements StatusMapper {
|
||||
|
||||
static final SimpleServingStatusMapper DEFAULT_MAPPINGS;
|
||||
static {
|
||||
Map<String, ServingStatus> mappings = new HashMap<>();
|
||||
mappings.put(Status.DOWN.getCode(), ServingStatus.NOT_SERVING);
|
||||
mappings.put(Status.OUT_OF_SERVICE.getCode(), ServingStatus.NOT_SERVING);
|
||||
mappings.put(Status.UNKNOWN.getCode(), ServingStatus.UNKNOWN);
|
||||
DEFAULT_MAPPINGS = new SimpleServingStatusMapper(mappings);
|
||||
}
|
||||
|
||||
private final Map<String, ServingStatus> mappings;
|
||||
|
||||
SimpleServingStatusMapper(Map<String, ServingStatus> mappings) {
|
||||
this.mappings = getUniformMappings(mappings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServingStatus getServingStatus(Status status) {
|
||||
String code = getUniformCode(status.getCode());
|
||||
return this.mappings.getOrDefault(code, ServingStatus.SERVING);
|
||||
}
|
||||
|
||||
private static Map<String, ServingStatus> getUniformMappings(Map<String, ServingStatus> mappings) {
|
||||
Map<String, ServingStatus> result = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, ServingStatus> entry : mappings.entrySet()) {
|
||||
String code = getUniformCode(entry.getKey());
|
||||
if (code != null) {
|
||||
result.putIfAbsent(code, entry.getValue());
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableMap(result);
|
||||
}
|
||||
|
||||
@Contract("!null -> !null")
|
||||
private static @Nullable String getUniformCode(@Nullable String code) {
|
||||
return (code != null) ? code.codePoints()
|
||||
.filter(Character::isLetterOrDigit)
|
||||
.map(Character::toLowerCase)
|
||||
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
|
||||
.toString() : null;
|
||||
}
|
||||
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.health;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.health.contributor.Status;
|
||||
import org.springframework.lang.Contract;
|
||||
|
||||
/**
|
||||
* {@link StatusAggregator} backed by an ordered status list.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class SimpleStatusAggregator implements StatusAggregator {
|
||||
|
||||
static final SimpleStatusAggregator DEFAULT_ORDER = new SimpleStatusAggregator(
|
||||
Status.DEFAULT_ORDER.stream().map(Status::getCode));
|
||||
|
||||
private final List<String> order;
|
||||
|
||||
private final Comparator<Status> comparator = Comparator.comparingInt(this::orderIndex)
|
||||
.thenComparing(Status::getCode);
|
||||
|
||||
SimpleStatusAggregator(Stream<String> order) {
|
||||
this.order = order.map(SimpleStatusAggregator::getUniformCode).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Status getAggregateStatus(Set<Status> statuses) {
|
||||
return statuses.stream().filter(this::contains).min(this.comparator).orElse(Status.UNKNOWN);
|
||||
}
|
||||
|
||||
private boolean contains(Status status) {
|
||||
return this.order.contains(getUniformCode(status));
|
||||
}
|
||||
|
||||
private int orderIndex(Status status) {
|
||||
return this.order.indexOf(getUniformCode(status));
|
||||
}
|
||||
|
||||
private static @Nullable String getUniformCode(Status status) {
|
||||
return getUniformCode(status.getCode());
|
||||
}
|
||||
|
||||
@Contract("!null -> !null")
|
||||
private static @Nullable String getUniformCode(@Nullable String code) {
|
||||
return (code != null) ? code.codePoints()
|
||||
.filter(Character::isLetterOrDigit)
|
||||
.map(Character::toLowerCase)
|
||||
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
|
||||
.toString() : null;
|
||||
}
|
||||
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.health;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.health.contributor.Health;
|
||||
import org.springframework.boot.health.contributor.Status;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Strategy used to aggregate {@link Status} instances.
|
||||
* <p>
|
||||
* This is required in order to combine subsystem states expressed through
|
||||
* {@link Health#getStatus()} into one state for the gRPC health.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface StatusAggregator {
|
||||
|
||||
/**
|
||||
* Return the aggregate status for the given set of statuses.
|
||||
* @param statuses the statuses to aggregate
|
||||
* @return the aggregate status
|
||||
*/
|
||||
default Status getAggregateStatus(Status... statuses) {
|
||||
return getAggregateStatus(new LinkedHashSet<>(Arrays.asList(statuses)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the aggregate status for the given set of statuses.
|
||||
* @param statuses the statuses to aggregate
|
||||
* @return the aggregate status
|
||||
*/
|
||||
Status getAggregateStatus(Set<Status> statuses);
|
||||
|
||||
/**
|
||||
* Return a {@link StatusAggregator} backed by the given ordered status list.
|
||||
* @param order the status order
|
||||
* @return a {@link StatusAggregator} instance or {@link #getDefault()} if no order is
|
||||
* provided
|
||||
*/
|
||||
static StatusAggregator of(Status... order) {
|
||||
return (!ObjectUtils.isEmpty(order)) ? new SimpleStatusAggregator(Arrays.stream(order).map(Status::getCode))
|
||||
: getDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link StatusAggregator} backed by the given ordered status list.
|
||||
* @param order the status order
|
||||
* @return a {@link StatusAggregator} instance or {@link #getDefault()} if no order is
|
||||
* provided
|
||||
*/
|
||||
static StatusAggregator of(String... order) {
|
||||
return (!ObjectUtils.isEmpty(order)) ? new SimpleStatusAggregator(Arrays.stream(order)) : getDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link StatusAggregator} backed by the given ordered status list.
|
||||
* @param order the status order
|
||||
* @return a {@link StatusAggregator} instance or {@link #getDefault()} if no order is
|
||||
* provided
|
||||
*/
|
||||
static StatusAggregator of(List<String> order) {
|
||||
return (!ObjectUtils.isEmpty(order)) ? new SimpleStatusAggregator(order.stream()) : getDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@link StatusAggregator} instance using default ordering rules.
|
||||
* @return a {@code StatusAggregator} with default ordering rules.
|
||||
*/
|
||||
static StatusAggregator getDefault() {
|
||||
return SimpleStatusAggregator.DEFAULT_ORDER;
|
||||
}
|
||||
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.health;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.health.contributor.Status;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Strategy used to map a {@link Status health status} to a gRPC {@link ServingStatus}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.1.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface StatusMapper {
|
||||
|
||||
/**
|
||||
* Return the HTTP status code that corresponds to the given {@link Status health
|
||||
* status}.
|
||||
* @param status the health status to map
|
||||
* @return the corresponding HTTP status code
|
||||
*/
|
||||
ServingStatus getServingStatus(Status status);
|
||||
|
||||
/**
|
||||
* Create a new {@link StatusMapper} with the specified mappings.
|
||||
* @param mappings the mappings to use or {@code null} to use the default mappings
|
||||
* @return a {@link StatusMapper} or {@link #getDefault()}
|
||||
*/
|
||||
static StatusMapper of(@Nullable Map<String, ServingStatus> mappings) {
|
||||
return CollectionUtils.isEmpty(mappings) ? SimpleServingStatusMapper.DEFAULT_MAPPINGS
|
||||
: new SimpleServingStatusMapper(mappings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an {@link StatusMapper} instance using default mappings.
|
||||
* @return a mapper using default mappings
|
||||
*/
|
||||
static StatusMapper getDefault() {
|
||||
return SimpleServingStatusMapper.DEFAULT_MAPPINGS;
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Support for gRPC server health checks.
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.boot.grpc.server.health;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
+2
@@ -1,2 +1,4 @@
|
||||
org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration
|
||||
org.springframework.boot.grpc.server.autoconfigure.GrpcServerServicesAutoConfiguration
|
||||
org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthAutoConfiguration
|
||||
org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthSchedulerAutoConfiguration
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.boot.grpc.server.health.StatusAggregator;
|
||||
import org.springframework.boot.grpc.server.health.StatusMapper;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link AutoConfiguredHealthCheckedGrpcComponent}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AutoConfiguredHealthCheckedGrpcComponentTests {
|
||||
|
||||
@Mock
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
private StatusAggregator statusAggregator;
|
||||
|
||||
@Mock
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
private StatusMapper statusMapper;
|
||||
|
||||
@Test
|
||||
void isMemberWhenMembershipMatchesAcceptsTrue() {
|
||||
AutoConfiguredHealthCheckedGrpcComponent component = new AutoConfiguredHealthCheckedGrpcComponent(
|
||||
(name) -> name.startsWith("a"), this.statusAggregator, this.statusMapper);
|
||||
assertThat(component.isMember("albert")).isTrue();
|
||||
assertThat(component.isMember("arnold")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isMemberWhenMembershipRejectsReturnsTrue() {
|
||||
AutoConfiguredHealthCheckedGrpcComponent component = new AutoConfiguredHealthCheckedGrpcComponent(
|
||||
(name) -> name.startsWith("a"), this.statusAggregator, this.statusMapper);
|
||||
assertThat(component.isMember("bert")).isFalse();
|
||||
assertThat(component.isMember("ernie")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getStatusAggregatorReturnsStatusAggregator() {
|
||||
AutoConfiguredHealthCheckedGrpcComponent component = new AutoConfiguredHealthCheckedGrpcComponent(
|
||||
(name) -> true, this.statusAggregator, this.statusMapper);
|
||||
assertThat(component.getStatusAggregator()).isSameAs(this.statusAggregator);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getStatusMapperReturnsHttpCodeStatusMapper() {
|
||||
AutoConfiguredHealthCheckedGrpcComponent component = new AutoConfiguredHealthCheckedGrpcComponent(
|
||||
(name) -> true, this.statusAggregator, this.statusMapper);
|
||||
assertThat(component.getStatusMapper()).isSameAs(this.statusMapper);
|
||||
}
|
||||
|
||||
}
|
||||
+437
@@ -0,0 +1,437 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponent;
|
||||
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponents;
|
||||
import org.springframework.boot.grpc.server.health.StatusAggregator;
|
||||
import org.springframework.boot.grpc.server.health.StatusMapper;
|
||||
import org.springframework.boot.health.contributor.Status;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link AutoConfiguredHealthCheckedGrpcComponent}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class AutoConfiguredHealthCheckedGrpcComponentsTests {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(AutoConfiguredHealthCheckedGrpcComponentsTestConfiguration.class));
|
||||
|
||||
@Test
|
||||
void getServerMatchesAllMembers() {
|
||||
this.contextRunner.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
HealthCheckedGrpcComponent server = components.getServer();
|
||||
assertThat(server).isNotNull();
|
||||
assertThat(server.isMember("a")).isTrue();
|
||||
assertThat(server.isMember("b")).isTrue();
|
||||
assertThat(server.isMember("C")).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void getServiceNamesReturnsServiceNames() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.grpc.server.health.service.a.include=*",
|
||||
"spring.grpc.server.health.service.b.include=*")
|
||||
.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
assertThat(components.getServiceNames()).containsExactlyInAnyOrder("a", "b");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void getServiceWhenServiceExistsReturnsService() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.server.health.service.a.include=*").run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
HealthCheckedGrpcComponent component = components.getService("a");
|
||||
assertThat(component).isNotNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void getServiceWhenServiceDoesNotExistReturnsNull() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.server.health.service.a.include=*").run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
HealthCheckedGrpcComponent component = components.getService("b");
|
||||
assertThat(component).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenNoDefinedBeansAdaptsProperties() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.grpc.server.health.status.order=up,down",
|
||||
"spring.grpc.server.health.status.mapping.down=serving")
|
||||
.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
HealthCheckedGrpcComponent server = components.getServer();
|
||||
assertThat(server).isNotNull();
|
||||
assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN))
|
||||
.isEqualTo(Status.UP);
|
||||
assertThat(server.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenHasStatusAggregatorBeanReturnsInstanceWithAggregatorUsedForAllServices() {
|
||||
this.contextRunner.withUserConfiguration(CustomStatusAggregatorConfiguration.class)
|
||||
.withPropertyValues("spring.grpc.server.health.status.order=up,down",
|
||||
"spring.grpc.server.health.service.a.include=*")
|
||||
.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
HealthCheckedGrpcComponent server = components.getServer();
|
||||
HealthCheckedGrpcComponent serviceA = components.getService("a");
|
||||
assertThat(server).isNotNull();
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
|
||||
.isEqualTo(Status.UNKNOWN);
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
|
||||
.isEqualTo(Status.UNKNOWN);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenHasStatusAggregatorBeanAndServiceSpecificPropertyReturnsInstanceThatUsesBeanOnlyForUnconfiguredServices() {
|
||||
this.contextRunner.withUserConfiguration(CustomStatusAggregatorConfiguration.class)
|
||||
.withPropertyValues("spring.grpc.server.health.service.a.include=*",
|
||||
"spring.grpc.server.health.service.a.status.order=up,down",
|
||||
"spring.grpc.server.health.service.b.include=*")
|
||||
.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
HealthCheckedGrpcComponent server = components.getServer();
|
||||
HealthCheckedGrpcComponent serviceA = components.getService("a");
|
||||
HealthCheckedGrpcComponent serviceB = components.getService("b");
|
||||
assertThat(server).isNotNull();
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceB).isNotNull();
|
||||
assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
|
||||
.isEqualTo(Status.UNKNOWN);
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
|
||||
.isEqualTo(Status.UP);
|
||||
assertThat(serviceB).isNotNull();
|
||||
assertThat(serviceB.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
|
||||
.isEqualTo(Status.UNKNOWN);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenHasStatusAggregatorPropertyReturnsInstanceWithPropertyUsedForAllServices() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.grpc.server.health.status.order=up,down",
|
||||
"spring.grpc.server.health.service.a.include=*")
|
||||
.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
HealthCheckedGrpcComponent server = components.getServer();
|
||||
HealthCheckedGrpcComponent serviceA = components.getService("a");
|
||||
assertThat(server).isNotNull();
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN))
|
||||
.isEqualTo(Status.UP);
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN))
|
||||
.isEqualTo(Status.UP);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenHasStatusAggregatorPropertyAndServiceSpecificPropertyReturnsInstanceWithPropertyUsedForExpectedServices() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.grpc.server.health.status.order=up,down",
|
||||
"spring.grpc.server.health.service.a.include=*",
|
||||
"spring.grpc.server.health.service.a.status.order=unknown,up,down",
|
||||
"spring.grpc.server.health.service.b.include=*")
|
||||
.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
HealthCheckedGrpcComponent server = components.getServer();
|
||||
HealthCheckedGrpcComponent serviceA = components.getService("a");
|
||||
HealthCheckedGrpcComponent serviceB = components.getService("b");
|
||||
assertThat(server).isNotNull();
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceB).isNotNull();
|
||||
assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
|
||||
.isEqualTo(Status.UP);
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
|
||||
.isEqualTo(Status.UNKNOWN);
|
||||
assertThat(serviceB).isNotNull();
|
||||
assertThat(serviceB.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
|
||||
.isEqualTo(Status.UP);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenHasStatusAggregatorPropertyAndServiceQualifiedBeanReturnsInstanceWithBeanUsedForExpectedServices() {
|
||||
this.contextRunner.withUserConfiguration(CustomStatusAggregatorServiceAConfiguration.class)
|
||||
.withPropertyValues("spring.grpc.server.health.status.order=up,down",
|
||||
"spring.grpc.server.health.service.a.include=*",
|
||||
"spring.grpc.server.health.service.a.status.order=up,down",
|
||||
"spring.grpc.server.health.service.b.include=*")
|
||||
.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
HealthCheckedGrpcComponent server = components.getServer();
|
||||
HealthCheckedGrpcComponent serviceA = components.getService("a");
|
||||
HealthCheckedGrpcComponent serviceB = components.getService("b");
|
||||
assertThat(server).isNotNull();
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceB).isNotNull();
|
||||
assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
|
||||
.isEqualTo(Status.UP);
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
|
||||
.isEqualTo(Status.UNKNOWN);
|
||||
assertThat(serviceB).isNotNull();
|
||||
assertThat(serviceB.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
|
||||
.isEqualTo(Status.UP);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenHasServiceSpecificStatusAggregatorPropertyAndServiceQualifiedBeanReturnsInstanceWithBeanUsedForExpectedServices() {
|
||||
this.contextRunner.withUserConfiguration(CustomStatusAggregatorServiceAConfiguration.class)
|
||||
.withPropertyValues("spring.grpc.server.health.service.a.include=*",
|
||||
"spring.grpc.server.health.service.a.status.order=up,down",
|
||||
"spring.grpc.server.health.service.b.include=*",
|
||||
"spring.grpc.server.health.service.b.status.order=up,down")
|
||||
.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
HealthCheckedGrpcComponent server = components.getServer();
|
||||
HealthCheckedGrpcComponent serviceA = components.getService("a");
|
||||
HealthCheckedGrpcComponent serviceB = components.getService("b");
|
||||
assertThat(server).isNotNull();
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceB).isNotNull();
|
||||
assertThat(server.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
|
||||
.isEqualTo(Status.DOWN);
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceA.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
|
||||
.isEqualTo(Status.UNKNOWN);
|
||||
assertThat(serviceB).isNotNull();
|
||||
assertThat(serviceB.getStatusAggregator().getAggregateStatus(Status.UP, Status.DOWN, Status.UNKNOWN))
|
||||
.isEqualTo(Status.UP);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenHasStatusMapperBeanReturnsInstanceWithMapperUsedForAllServices() {
|
||||
this.contextRunner.withUserConfiguration(CustomStatusMapperConfiguration.class)
|
||||
.withPropertyValues("spring.grpc.server.health.status.mapping.down=service-unknown",
|
||||
"spring.grpc.server.health.service.a.include=*")
|
||||
.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
HealthCheckedGrpcComponent server = components.getServer();
|
||||
HealthCheckedGrpcComponent serviceA = components.getService("a");
|
||||
assertThat(server).isNotNull();
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(server.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING);
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenHasStatusMapperBeanAndServiceSpecificPropertyReturnsInstanceThatUsesBeanOnlyForUnconfiguredServices() {
|
||||
this.contextRunner.withUserConfiguration(CustomStatusMapperConfiguration.class)
|
||||
.withPropertyValues("spring.grpc.server.health.service.a.include=*",
|
||||
"spring.grpc.server.health.service.a.status.mapping.down=service-unknown",
|
||||
"spring.grpc.server.health.service.b.include=*")
|
||||
.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
HealthCheckedGrpcComponent server = components.getServer();
|
||||
HealthCheckedGrpcComponent serviceA = components.getService("a");
|
||||
HealthCheckedGrpcComponent serviceB = components.getService("b");
|
||||
assertThat(server).isNotNull();
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceB).isNotNull();
|
||||
assertThat(server.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING);
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN))
|
||||
.isEqualTo(ServingStatus.SERVICE_UNKNOWN);
|
||||
assertThat(serviceB).isNotNull();
|
||||
assertThat(serviceB.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenHasStatusMapperPropertyReturnsInstanceWithPropertyUsedForAllServices() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.grpc.server.health.status.mapping.down=service-unknown",
|
||||
"spring.grpc.server.health.service.a.include=*")
|
||||
.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
HealthCheckedGrpcComponent server = components.getServer();
|
||||
HealthCheckedGrpcComponent serviceA = components.getService("a");
|
||||
assertThat(server).isNotNull();
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(server.getStatusMapper().getServingStatus(Status.DOWN))
|
||||
.isEqualTo(ServingStatus.SERVICE_UNKNOWN);
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN))
|
||||
.isEqualTo(ServingStatus.SERVICE_UNKNOWN);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenHasStatusMapperPropertyAndServiceSpecificPropertyReturnsInstanceWithPropertyUsedForExpectedServices() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.grpc.server.health.status.mapping.down=service-unknown",
|
||||
"spring.grpc.server.health.service.a.include=*",
|
||||
"spring.grpc.server.health.service.a.status.mapping.down=unrecognized",
|
||||
"spring.grpc.server.health.service.b.include=*")
|
||||
.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
HealthCheckedGrpcComponent server = components.getServer();
|
||||
HealthCheckedGrpcComponent serviceA = components.getService("a");
|
||||
HealthCheckedGrpcComponent serviceB = components.getService("b");
|
||||
assertThat(server).isNotNull();
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceB).isNotNull();
|
||||
assertThat(server.getStatusMapper().getServingStatus(Status.DOWN))
|
||||
.isEqualTo(ServingStatus.SERVICE_UNKNOWN);
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN))
|
||||
.isEqualTo(ServingStatus.UNRECOGNIZED);
|
||||
assertThat(serviceB).isNotNull();
|
||||
assertThat(serviceB.getStatusMapper().getServingStatus(Status.DOWN))
|
||||
.isEqualTo(ServingStatus.SERVICE_UNKNOWN);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenHasStatusMapperPropertyAndServiceQualifiedBeanReturnsInstanceWithBeanUsedForExpectedServices() {
|
||||
this.contextRunner.withUserConfiguration(CustomStatusMapperServiceAConfiguration.class)
|
||||
.withPropertyValues("spring.grpc.server.health.status.mapping.down=service-unknown",
|
||||
"spring.grpc.server.health.service.a.include=*",
|
||||
"spring.grpc.server.health.service.a.status.mapping.down=service-unknown",
|
||||
"spring.grpc.server.health.service.b.include=*")
|
||||
.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
HealthCheckedGrpcComponent server = components.getServer();
|
||||
HealthCheckedGrpcComponent serviceA = components.getService("a");
|
||||
HealthCheckedGrpcComponent serviceB = components.getService("b");
|
||||
assertThat(server).isNotNull();
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceB).isNotNull();
|
||||
assertThat(server.getStatusMapper().getServingStatus(Status.DOWN))
|
||||
.isEqualTo(ServingStatus.SERVICE_UNKNOWN);
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING);
|
||||
assertThat(serviceB).isNotNull();
|
||||
assertThat(serviceB.getStatusMapper().getServingStatus(Status.DOWN))
|
||||
.isEqualTo(ServingStatus.SERVICE_UNKNOWN);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenHasServiceSpecificStatusMapperPropertyAndServiceQualifiedBeanReturnsInstanceWithBeanUsedForExpectedServices() {
|
||||
this.contextRunner.withUserConfiguration(CustomStatusMapperServiceAConfiguration.class)
|
||||
.withPropertyValues("spring.grpc.server.health.service.a.include=*",
|
||||
"spring.grpc.server.health.service.a.status.mapping.down=service-unknown",
|
||||
"spring.grpc.server.health.service.b.include=*",
|
||||
"spring.grpc.server.health.service.b.status.mapping.down=service-unknown")
|
||||
.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
HealthCheckedGrpcComponent server = components.getServer();
|
||||
HealthCheckedGrpcComponent serviceA = components.getService("a");
|
||||
HealthCheckedGrpcComponent serviceB = components.getService("b");
|
||||
assertThat(server).isNotNull();
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceB).isNotNull();
|
||||
assertThat(server.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.NOT_SERVING);
|
||||
assertThat(serviceA).isNotNull();
|
||||
assertThat(serviceA.getStatusMapper().getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.SERVING);
|
||||
assertThat(serviceB).isNotNull();
|
||||
assertThat(serviceB.getStatusMapper().getServingStatus(Status.DOWN))
|
||||
.isEqualTo(ServingStatus.SERVICE_UNKNOWN);
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties(GrpcServerHealthProperties.class)
|
||||
static class AutoConfiguredHealthCheckedGrpcComponentsTestConfiguration {
|
||||
|
||||
@Bean
|
||||
AutoConfiguredHealthCheckedGrpcComponents healthCheckedGrpcComponents(
|
||||
ConfigurableApplicationContext applicationContext, GrpcServerHealthProperties properties) {
|
||||
return new AutoConfiguredHealthCheckedGrpcComponents(applicationContext, properties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomStatusAggregatorConfiguration {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
StatusAggregator statusAggregator() {
|
||||
return StatusAggregator.of(Status.UNKNOWN, Status.UP, Status.DOWN);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomStatusAggregatorServiceAConfiguration {
|
||||
|
||||
@Bean
|
||||
@Qualifier("a")
|
||||
StatusAggregator statusAggregator() {
|
||||
return StatusAggregator.of(Status.UNKNOWN, Status.UP, Status.DOWN);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomStatusMapperConfiguration {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
StatusMapper httpCodeStatusMapper() {
|
||||
return StatusMapper.of(Collections.singletonMap(Status.DOWN.getCode(), ServingStatus.SERVING));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomStatusMapperServiceAConfiguration {
|
||||
|
||||
@Bean
|
||||
@Qualifier("a")
|
||||
StatusMapper httpCodeStatusMapper() {
|
||||
return StatusMapper.of(Collections.singletonMap(Status.DOWN.getCode(), ServingStatus.SERVING));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+458
@@ -0,0 +1,458 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
|
||||
import io.grpc.protobuf.services.HealthStatusManager;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration;
|
||||
import org.springframework.boot.diagnostics.FailureAnalyzedException;
|
||||
import org.springframework.boot.grpc.server.health.GrpcServerHealth;
|
||||
import org.springframework.boot.grpc.server.health.HealthCheckedGrpcComponents;
|
||||
import org.springframework.boot.grpc.server.health.StatusAggregator;
|
||||
import org.springframework.boot.grpc.server.health.StatusMapper;
|
||||
import org.springframework.boot.health.autoconfigure.contributor.HealthContributorMembershipValidator;
|
||||
import org.springframework.boot.health.autoconfigure.registry.HealthContributorRegistryAutoConfiguration;
|
||||
import org.springframework.boot.health.contributor.CompositeHealthContributor;
|
||||
import org.springframework.boot.health.contributor.CompositeReactiveHealthContributor;
|
||||
import org.springframework.boot.health.contributor.Health;
|
||||
import org.springframework.boot.health.contributor.HealthIndicator;
|
||||
import org.springframework.boot.health.contributor.ReactiveHealthIndicator;
|
||||
import org.springframework.boot.health.contributor.Status;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.assertj.ApplicationContextAssertProvider;
|
||||
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
|
||||
import org.springframework.boot.test.context.runner.AbstractApplicationContextRunner;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.grpc.server.GrpcServerFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.entry;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link GrpcServerHealthAutoConfiguration}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Chris Bono
|
||||
* @author Andrey Litvitski
|
||||
*/
|
||||
class GrpcServerHealthAutoConfigurationTests {
|
||||
|
||||
private static final AutoConfigurations autoConfigurations = AutoConfigurations.of(
|
||||
GrpcServerHealthAutoConfiguration.class, GrpcServerHealthSchedulerAutoConfiguration.class,
|
||||
HealthContributorRegistryAutoConfiguration.class, TaskSchedulingAutoConfiguration.class);
|
||||
|
||||
private final BindableService service = mock();
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withUserConfiguration(HealthIndicatorsConfiguration.class)
|
||||
.withConfiguration(autoConfigurations)
|
||||
.with(this::serviceBean);
|
||||
|
||||
@Test
|
||||
void autoConfiguresBeans() {
|
||||
this.contextRunner.run(this::assertConfigured);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoBindableServiceDefinedDoesNotAutoConfigureBeans() {
|
||||
new ApplicationContextRunner().withConfiguration(autoConfigurations).run(this::assertNotConfigured);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoBindableServiceDefinedButHealthEnabledPropertyIsTrueAutoConfiguresBeans() {
|
||||
new ApplicationContextRunner().withConfiguration(autoConfigurations)
|
||||
.withPropertyValues("spring.grpc.server.health.enabled=true")
|
||||
.run(this::assertConfigured);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenGrpcNotOnClasspathDoesNotAutoConfigureBeans() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(BindableService.class))
|
||||
.run(this::assertNotConfigured);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenSpringGrpcNotOnClasspathDoesNotAutoConfigureBeans() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServerFactory.class))
|
||||
.run(this::assertNotConfigured);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHealthStatusManagerNotOnClasspathDoesNotAutoConfigureBeans() {
|
||||
this.contextRunner.withClassLoader(new FilteredClassLoader(HealthStatusManager.class))
|
||||
.run(this::assertNotConfigured);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoTaskSchedulerDoesNotAutoConfigureHealthScheduler() {
|
||||
new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(GrpcServerHealthAutoConfiguration.class,
|
||||
HealthContributorRegistryAutoConfiguration.class))
|
||||
.with(this::serviceBean)
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerHealthScheduler.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHealthScheduleEnabledPropertyFalseDoesNotAutoConfigureHealthScheduler() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.server.health.schedule.enabled=false")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerHealthScheduler.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHealthScheduleEnabledPropertyTrueDoesAutoConfigureHealthScheduler() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.server.health.schedule.enabled=true")
|
||||
.run((context) -> assertThat(context).hasSingleBean(GrpcServerHealthScheduler.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHealthEnabledPropertyIsTrueAutoConfiguresBeans() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.server.health.enabled=true").run(this::assertConfigured);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHealthEnabledPropertyIsFalseDoesNotAutoConfigureBeans() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.server.health.enabled=false").run(this::assertNotConfigured);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenServerEnabledPropertyIsFalseDoesNotAutoConfigureBeans() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.server.enabled=false").run(this::assertNotConfigured);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenServerEnabledPropertyIsTrueAutoConfiguresBeans() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.server.enabled=true").run(this::assertConfigured);
|
||||
}
|
||||
|
||||
@Test
|
||||
void enterTerminalStateIsCalledOnShutdown() {
|
||||
AtomicReference<HealthStatusManager> manager = new AtomicReference<>();
|
||||
this.contextRunner.run((context) -> {
|
||||
manager.set(context.getBean(HealthStatusManager.class));
|
||||
assertTerminalState(manager.get(), false);
|
||||
});
|
||||
assertTerminalState(manager.get(), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasUserDefinedHealthStatusManagerDoesNotAutoConfigureBean() {
|
||||
HealthStatusManager customHealthStatusManager = mock();
|
||||
this.contextRunner
|
||||
.withBean("customHealthStatusManager", HealthStatusManager.class, () -> customHealthStatusManager)
|
||||
.run((context) -> assertThat(context).getBean(HealthStatusManager.class)
|
||||
.isSameAs(customHealthStatusManager));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasNoHealthContributorRegistryOnlyAutoConfiguresBasicService() {
|
||||
new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(GrpcServerHealthAutoConfiguration.class,
|
||||
TaskSchedulingAutoConfiguration.class))
|
||||
.with(this::serviceBean)
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(HealthStatusManager.class);
|
||||
assertThat(context).hasBean("grpcServerHealthService");
|
||||
assertThat(context).doesNotHaveBean(GrpcServerHealth.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsStatusAggregatorFromProperties() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.server.health.status.order=up,down").run((context) -> {
|
||||
StatusAggregator aggregator = context.getBean(StatusAggregator.class);
|
||||
assertThat(aggregator.getAggregateStatus(Status.UP, Status.DOWN)).isEqualTo(Status.UP);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasStatusAggregatorBeanIgnoresProperties() {
|
||||
this.contextRunner.withUserConfiguration(StatusAggregatorConfiguration.class)
|
||||
.withPropertyValues("spring.grpc.server.health.status.order=up,down")
|
||||
.run((context) -> {
|
||||
StatusAggregator aggregator = context.getBean(StatusAggregator.class);
|
||||
assertThat(aggregator.getAggregateStatus(Status.UP, Status.DOWN)).isEqualTo(Status.UNKNOWN);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsStatusMapperFromProperties() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.server.health.status.mapping.up=not-serving")
|
||||
.run((context) -> {
|
||||
StatusMapper mapper = context.getBean(StatusMapper.class);
|
||||
assertThat(mapper.getServingStatus(Status.UP)).isEqualTo(ServingStatus.NOT_SERVING);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasHttpCodeStatusMapperBeanIgnoresProperties() {
|
||||
this.contextRunner.withUserConfiguration(StatusMapperConfiguration.class)
|
||||
.withPropertyValues("spring.grpc.server.health.status.mapping.up=not-serving")
|
||||
.run((context) -> {
|
||||
StatusMapper mapper = context.getBean(StatusMapper.class);
|
||||
assertThat(mapper.getServingStatus(Status.UP)).isEqualTo(ServingStatus.UNRECOGNIZED);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsHealthCheckedGrpcComponents() {
|
||||
this.contextRunner.withPropertyValues("spring.grpc.server.health.service.test.include=*").run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
assertThat(components).isInstanceOf(AutoConfiguredHealthCheckedGrpcComponents.class);
|
||||
assertThat(components.getServiceNames()).containsOnly("test");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenComponentsIncludesContributorThatExistsDoesNotFail() {
|
||||
this.contextRunner.withUserConfiguration(CompositeHealthIndicatorConfiguration.class)
|
||||
.withPropertyValues("spring.grpc.server.health.service.test.include=composite/b/c")
|
||||
.run((context) -> assertThat(context).hasNotFailed());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHealthCheckedGrpcComponentsIncludesReactiveContributorThatExists() {
|
||||
this.contextRunner.withUserConfiguration(CompositeReactiveHealthIndicatorConfiguration.class)
|
||||
.withPropertyValues("spring.grpc.server.health.service.test.include=composite/b/c")
|
||||
.run((context) -> assertThat(context).hasNotFailed());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHealthCheckedGrpcComponentsIncludesContributorThatDoesNotExistThrowsException() {
|
||||
this.contextRunner.withUserConfiguration(CompositeHealthIndicatorConfiguration.class)
|
||||
.withPropertyValues("spring.grpc.server.health.service.test.include=composite/b/c,nope")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasFailed();
|
||||
assertThat(context.getStartupFailure()).isInstanceOf(FailureAnalyzedException.class)
|
||||
.hasMessage("Health contributor 'nope' defined in "
|
||||
+ "'spring.grpc.server.health.service.test.include' does not exist");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHealthCheckedGrpcComponentsExcludesContributorThatDoesNotExistThrowsException() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.grpc.server.health.service.test.exclude=composite/b/d",
|
||||
"spring.grpc.server.health.service.test.include=*")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasFailed();
|
||||
assertThat(context.getStartupFailure()).isInstanceOf(FailureAnalyzedException.class)
|
||||
.hasMessage("Health contributor 'composite/b/d' defined in "
|
||||
+ "'spring.grpc.server.health.service.test.exclude' does not exist");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHealthCheckedGrpcComponentsIncludesContributorThatDoesNotExistAndValidationDisabledCreatesComponents() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.grpc.server.health.services.validate-membership=false",
|
||||
"spring.grpc.server.health.service.test.include=nope")
|
||||
.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
assertThat(components).isInstanceOf(AutoConfiguredHealthCheckedGrpcComponents.class);
|
||||
assertThat(components.getServiceNames()).containsOnly("test");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasHealthCheckedGrpcComponentsBeanDoesNotCreateAdditional() {
|
||||
this.contextRunner.withUserConfiguration(HealthCheckedGrpcComponentsConfiguration.class)
|
||||
.withPropertyValues("spring.grpc.server.health.service.test.include=*")
|
||||
.run((context) -> {
|
||||
HealthCheckedGrpcComponents components = context.getBean(HealthCheckedGrpcComponents.class);
|
||||
assertThat(components.getServiceNames()).containsOnly("mock");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsGrpcServerHealth() {
|
||||
this.contextRunner.run((context) -> {
|
||||
GrpcServerHealth serverHealth = context.getBean(GrpcServerHealth.class);
|
||||
Map<String, ServingStatus> result = new LinkedHashMap<>();
|
||||
serverHealth.update(result::put);
|
||||
assertThat(result).containsExactly(entry("", ServingStatus.SERVING));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasGrpcServerHealthBeanDoesNotCreateAdditional() {
|
||||
this.contextRunner.withUserConfiguration(GrpcServerHealthConfiguration.class).run((context) -> {
|
||||
GrpcServerHealth serverHealth = context.getBean(GrpcServerHealth.class);
|
||||
Map<String, ServingStatus> result = new LinkedHashMap<>();
|
||||
serverHealth.update(result::put);
|
||||
assertThat(result).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWithIndicatorsInParentContextFindsIndicators() {
|
||||
new ApplicationContextRunner().withUserConfiguration(DownHealthIndicatorConfiguration.class)
|
||||
.run((parent) -> new ApplicationContextRunner().withConfiguration(autoConfigurations)
|
||||
.withUserConfiguration(HealthIndicatorsConfiguration.class)
|
||||
.with(this::serviceBean)
|
||||
.withParent(parent)
|
||||
.run((context) -> {
|
||||
GrpcServerHealth serverHealth = context.getBean(GrpcServerHealth.class);
|
||||
Map<String, ServingStatus> result = new LinkedHashMap<>();
|
||||
serverHealth.update(result::put);
|
||||
assertThat(result).containsExactly(entry("", ServingStatus.NOT_SERVING));
|
||||
}));
|
||||
}
|
||||
|
||||
private void assertTerminalState(HealthStatusManager healthStatusManager, boolean expected) {
|
||||
assertThat(healthStatusManager).extracting("healthService.terminal").isEqualTo(expected);
|
||||
}
|
||||
|
||||
private void assertConfigured(AssertableApplicationContext context) {
|
||||
assertThat(context).hasSingleBean(HealthStatusManager.class);
|
||||
assertThat(context).hasBean("grpcServerHealthService");
|
||||
assertThat(context).hasSingleBean(StatusAggregator.class);
|
||||
assertThat(context).hasSingleBean(StatusMapper.class);
|
||||
assertThat(context).hasSingleBean(HealthCheckedGrpcComponents.class);
|
||||
assertThat(context).hasSingleBean(GrpcServerHealth.class);
|
||||
assertThat(context).hasSingleBean(HealthContributorMembershipValidator.class);
|
||||
assertThat(context).hasSingleBean(GrpcServerHealthScheduler.class);
|
||||
}
|
||||
|
||||
private void assertNotConfigured(AssertableApplicationContext context) {
|
||||
assertThat(context).doesNotHaveBean(HealthStatusManager.class);
|
||||
assertThat(context).doesNotHaveBean("grpcServerHealthService");
|
||||
assertThat(context).doesNotHaveBean(GrpcServerHealthAutoConfiguration.class);
|
||||
}
|
||||
|
||||
private <R extends AbstractApplicationContextRunner<R, C, A>, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider<C>> R serviceBean(
|
||||
R contextRunner) {
|
||||
return contextRunner.withBean(BindableService.class, () -> this.service);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class HealthIndicatorsConfiguration {
|
||||
|
||||
@Bean
|
||||
HealthIndicator simpleHealthIndicator() {
|
||||
return () -> Health.up().withDetail("counter", 42).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
HealthIndicator additionalHealthIndicator() {
|
||||
return () -> Health.up().build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactiveHealthIndicator reactiveHealthIndicator() {
|
||||
return () -> Mono.just(Health.up().build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class DownHealthIndicatorConfiguration {
|
||||
|
||||
@Bean
|
||||
HealthIndicator downHealthIndicator() {
|
||||
return () -> Health.down().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CompositeHealthIndicatorConfiguration {
|
||||
|
||||
@Bean
|
||||
CompositeHealthContributor compositeHealthIndicator() {
|
||||
return CompositeHealthContributor.fromMap(Map.of("a", createHealthIndicator(), "b",
|
||||
CompositeHealthContributor.fromMap(Map.of("c", createHealthIndicator()))));
|
||||
}
|
||||
|
||||
private HealthIndicator createHealthIndicator() {
|
||||
return () -> Health.up().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CompositeReactiveHealthIndicatorConfiguration {
|
||||
|
||||
@Bean
|
||||
CompositeReactiveHealthContributor compositeHealthIndicator() {
|
||||
return CompositeReactiveHealthContributor.fromMap(Map.of("a", createHealthIndicator(), "b",
|
||||
CompositeReactiveHealthContributor.fromMap(Map.of("c", createHealthIndicator()))));
|
||||
}
|
||||
|
||||
private ReactiveHealthIndicator createHealthIndicator() {
|
||||
return () -> Mono.just(Health.up().build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class StatusAggregatorConfiguration {
|
||||
|
||||
@Bean
|
||||
StatusAggregator statusAggregator() {
|
||||
return (statuses) -> Status.UNKNOWN;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class StatusMapperConfiguration {
|
||||
|
||||
@Bean
|
||||
StatusMapper httpCodeStatusMapper() {
|
||||
return (status) -> ServingStatus.UNRECOGNIZED;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class HealthCheckedGrpcComponentsConfiguration {
|
||||
|
||||
@Bean
|
||||
HealthCheckedGrpcComponents healthCheckedGrpcComponents() {
|
||||
HealthCheckedGrpcComponents components = mock();
|
||||
given(components.getServiceNames()).willReturn(Collections.singleton("mock"));
|
||||
return components;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class GrpcServerHealthConfiguration {
|
||||
|
||||
@Bean
|
||||
GrpcServerHealth grpcServerHealth() {
|
||||
return mock();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.autoconfigure.health;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
|
||||
import io.grpc.protobuf.services.HealthStatusManager;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.boot.grpc.server.health.GrpcServerHealth;
|
||||
import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
|
||||
/**
|
||||
* Tests for {@link GrpcServerHealthScheduler}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class GrpcServerHealthSchedulerTests {
|
||||
|
||||
@Test
|
||||
void onApplicationEventWhenEventIsGrpcStartStartsHealth() {
|
||||
Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
|
||||
GrpcServerHealth serverHealth = mock();
|
||||
HealthStatusManager statusManager = mock();
|
||||
TaskScheduler taskScheduler = mock();
|
||||
Duration period = Duration.ofSeconds(10);
|
||||
Duration delay = Duration.ofSeconds(30);
|
||||
GrpcServerHealthScheduler healthScheduler = new GrpcServerHealthScheduler(clock, serverHealth, statusManager,
|
||||
taskScheduler, period, delay);
|
||||
then(serverHealth).should(never()).update(statusManager);
|
||||
healthScheduler.onApplicationEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 123));
|
||||
Instant startTime = Instant.now(clock).plus(delay);
|
||||
ArgumentCaptor<Runnable> runnable = ArgumentCaptor.captor();
|
||||
then(taskScheduler).should().scheduleAtFixedRate(runnable.capture(), eq(startTime), eq(period));
|
||||
then(serverHealth).should(never()).update(statusManager);
|
||||
runnable.getValue().run();
|
||||
then(serverHealth).should().update(statusManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onApplicationEventWhenEventIsGrpcStartAndCalledTwiceStartsHealthOnlyOnce() {
|
||||
GrpcServerHealth serverHealth = mock();
|
||||
HealthStatusManager statusManager = mock();
|
||||
TaskScheduler taskScheduler = mock();
|
||||
GrpcServerHealthScheduler healthScheduler = new GrpcServerHealthScheduler(serverHealth, statusManager,
|
||||
taskScheduler, Duration.ofSeconds(10), Duration.ofSeconds(30));
|
||||
then(serverHealth).should(never()).update(statusManager);
|
||||
healthScheduler.onApplicationEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 123));
|
||||
healthScheduler.onApplicationEvent(new GrpcServerStartedEvent(mock(), mock(), "localhost", 345));
|
||||
then(taskScheduler).should(times(1)).scheduleAtFixedRate(any(), any(), any());
|
||||
}
|
||||
|
||||
}
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.health;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.health.contributor.CompositeHealthContributor;
|
||||
import org.springframework.boot.health.contributor.Health;
|
||||
import org.springframework.boot.health.contributor.HealthContributor;
|
||||
import org.springframework.boot.health.contributor.HealthIndicator;
|
||||
import org.springframework.boot.health.contributor.ReactiveHealthIndicator;
|
||||
import org.springframework.boot.health.contributor.Status;
|
||||
import org.springframework.boot.health.registry.DefaultHealthContributorRegistry;
|
||||
import org.springframework.boot.health.registry.DefaultReactiveHealthContributorRegistry;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.entry;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link GrpcServerHealth}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class GrpcServerHealthTests {
|
||||
|
||||
private static final HealthIndicator UP = () -> Health.up().build();
|
||||
|
||||
private static final HealthIndicator DOWN = () -> Health.down().build();
|
||||
|
||||
private static final ReactiveHealthIndicator REACTIVE_DOWN = () -> Mono.just(Health.down().build());
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("NullAway") // Test null check
|
||||
void createWhenRegistryIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new GrpcServerHealth(null, null, mock()))
|
||||
.withMessage("'registry' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("NullAway") // Test null check
|
||||
void createWhenComponentsIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new GrpcServerHealth(mock(), null, null))
|
||||
.withMessage("'components' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateWhenHasServerComponentAndUp() {
|
||||
assertThat(updateWithServerComponent("test", UP)).containsExactly(entry("", ServingStatus.SERVING));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateWhenHasServerComponentAndDown() {
|
||||
assertThat(updateWithServerComponent("test", DOWN)).containsExactly(entry("", ServingStatus.NOT_SERVING));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateWhenHasServerComponentAndMissing() {
|
||||
assertThat(updateWithServerComponent("other", UP)).containsExactly(entry("", ServingStatus.UNKNOWN));
|
||||
}
|
||||
|
||||
private Map<String, ServingStatus> updateWithServerComponent(String indicatorName, HealthIndicator indicator) {
|
||||
HealthCheckedGrpcComponent server = new TestHealthCheckedGrpcComponent(Set.of("test"));
|
||||
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(server, Collections.emptyMap());
|
||||
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
|
||||
registry.registerContributor(indicatorName, indicator);
|
||||
GrpcServerHealth health = new GrpcServerHealth(registry, null, components);
|
||||
Map<String, ServingStatus> result = new LinkedHashMap<>();
|
||||
health.update(result::put);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateWhenHasServices() {
|
||||
Map<String, HealthCheckedGrpcComponent> services = new LinkedHashMap<>();
|
||||
services.put("one", new TestHealthCheckedGrpcComponent(Set.of("up")));
|
||||
services.put("two", new TestHealthCheckedGrpcComponent(Set.of("down")));
|
||||
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services);
|
||||
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
|
||||
registry.registerContributor("up", UP);
|
||||
registry.registerContributor("down", DOWN);
|
||||
GrpcServerHealth health = new GrpcServerHealth(registry, null, components);
|
||||
Map<String, ServingStatus> result = new LinkedHashMap<>();
|
||||
health.update(result::put);
|
||||
assertThat(result).containsExactly(entry("one", ServingStatus.SERVING),
|
||||
entry("two", ServingStatus.NOT_SERVING));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateUsesCache() {
|
||||
Map<String, HealthCheckedGrpcComponent> services = new LinkedHashMap<>();
|
||||
services.put("one", new TestHealthCheckedGrpcComponent(Set.of("test")));
|
||||
services.put("two", new TestHealthCheckedGrpcComponent(Set.of("test")));
|
||||
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services);
|
||||
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
|
||||
HealthIndicator contributor = mock();
|
||||
given(contributor.health(false)).willReturn(Health.up().build(), Health.down().build());
|
||||
registry.registerContributor("test", contributor);
|
||||
GrpcServerHealth health = new GrpcServerHealth(registry, null, components);
|
||||
Map<String, ServingStatus> result = new LinkedHashMap<>();
|
||||
health.update(result::put);
|
||||
assertThat(result).containsExactly(entry("one", ServingStatus.SERVING), entry("two", ServingStatus.SERVING));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateWhenHasEmptyNamedServicesDoesNotIncludeIt() {
|
||||
Map<String, HealthCheckedGrpcComponent> services = new LinkedHashMap<>();
|
||||
services.put("", new TestHealthCheckedGrpcComponent(Set.of("up")));
|
||||
services.put("one", new TestHealthCheckedGrpcComponent(Set.of("up")));
|
||||
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services);
|
||||
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
|
||||
registry.registerContributor("up", UP);
|
||||
GrpcServerHealth health = new GrpcServerHealth(registry, null, components);
|
||||
Map<String, ServingStatus> result = new LinkedHashMap<>();
|
||||
health.update(result::put);
|
||||
assertThat(result).containsExactly(entry("one", ServingStatus.SERVING));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateWhenHasFallbackRegistry() {
|
||||
Map<String, HealthCheckedGrpcComponent> services = new LinkedHashMap<>();
|
||||
services.put("one", new TestHealthCheckedGrpcComponent(Set.of("1")));
|
||||
services.put("two", new TestHealthCheckedGrpcComponent(Set.of("2")));
|
||||
services.put("three", new TestHealthCheckedGrpcComponent(Set.of("3")));
|
||||
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services);
|
||||
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
|
||||
DefaultReactiveHealthContributorRegistry fallbackRegistry = new DefaultReactiveHealthContributorRegistry();
|
||||
registry.registerContributor("1", UP);
|
||||
registry.registerContributor("2", UP);
|
||||
fallbackRegistry.registerContributor("2", REACTIVE_DOWN);
|
||||
fallbackRegistry.registerContributor("3", REACTIVE_DOWN);
|
||||
GrpcServerHealth health = new GrpcServerHealth(registry, fallbackRegistry, components);
|
||||
Map<String, ServingStatus> result = new LinkedHashMap<>();
|
||||
health.update(result::put);
|
||||
assertThat(result).containsExactly(entry("one", ServingStatus.SERVING), entry("two", ServingStatus.SERVING),
|
||||
entry("three", ServingStatus.NOT_SERVING));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateWhenHasCompositeContributor() {
|
||||
Map<String, HealthCheckedGrpcComponent> services = new LinkedHashMap<>();
|
||||
services.put("one", new TestHealthCheckedGrpcComponent(Set.of("dbs", "dbs/db1", "dbs/db2")));
|
||||
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services);
|
||||
HealthContributor contributor = CompositeHealthContributor.fromMap(Map.of("db1", UP, "db2", DOWN));
|
||||
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
|
||||
registry.registerContributor("dbs", contributor);
|
||||
GrpcServerHealth health = new GrpcServerHealth(registry, null, components);
|
||||
Map<String, ServingStatus> result = new LinkedHashMap<>();
|
||||
health.update(result::put);
|
||||
assertThat(result).containsExactly(entry("one", ServingStatus.NOT_SERVING));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateFiltersIndicatorsAndCompositeParentByName() {
|
||||
Map<String, HealthCheckedGrpcComponent> services = new LinkedHashMap<>();
|
||||
services.put("one", new TestHealthCheckedGrpcComponent(Set.of("dbs", "dbs/db1")));
|
||||
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services);
|
||||
HealthContributor contributor = CompositeHealthContributor.fromMap(Map.of("db1", UP, "db2", DOWN));
|
||||
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
|
||||
registry.registerContributor("dbs", contributor);
|
||||
GrpcServerHealth health = new GrpcServerHealth(registry, null, components);
|
||||
Map<String, ServingStatus> result = new LinkedHashMap<>();
|
||||
health.update(result::put);
|
||||
assertThat(result).containsExactly(entry("one", ServingStatus.SERVING));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void updateCallsStatusAggregator() {
|
||||
StatusAggregator statusAggregator = mock();
|
||||
given(statusAggregator.getAggregateStatus((Set<Status>) any(Set.class))).willReturn(Status.DOWN);
|
||||
Map<String, HealthCheckedGrpcComponent> services = new LinkedHashMap<>();
|
||||
services.put("one",
|
||||
new TestHealthCheckedGrpcComponent(Set.of("up"), statusAggregator, StatusMapper.getDefault()));
|
||||
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services);
|
||||
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
|
||||
registry.registerContributor("up", UP);
|
||||
GrpcServerHealth health = new GrpcServerHealth(registry, null, components);
|
||||
Map<String, ServingStatus> result = new LinkedHashMap<>();
|
||||
health.update(result::put);
|
||||
assertThat(result).containsExactly(entry("one", ServingStatus.NOT_SERVING));
|
||||
then(statusAggregator).should().getAggregateStatus(Set.of(Status.UP));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateCallsStatusMapper() {
|
||||
StatusMapper statusMapper = mock();
|
||||
given(statusMapper.getServingStatus(Status.UP)).willReturn(ServingStatus.UNRECOGNIZED);
|
||||
Map<String, HealthCheckedGrpcComponent> services = new LinkedHashMap<>();
|
||||
services.put("one",
|
||||
new TestHealthCheckedGrpcComponent(Set.of("up"), StatusAggregator.getDefault(), statusMapper));
|
||||
HealthCheckedGrpcComponents components = new TestHealthCheckedGrpcComponents(null, services);
|
||||
DefaultHealthContributorRegistry registry = new DefaultHealthContributorRegistry();
|
||||
registry.registerContributor("up", UP);
|
||||
GrpcServerHealth health = new GrpcServerHealth(registry, null, components);
|
||||
Map<String, ServingStatus> result = new LinkedHashMap<>();
|
||||
health.update(result::put);
|
||||
assertThat(result).containsExactly(entry("one", ServingStatus.UNRECOGNIZED));
|
||||
then(statusMapper).should().getServingStatus(Status.UP);
|
||||
}
|
||||
|
||||
static class TestHealthCheckedGrpcComponents implements HealthCheckedGrpcComponents {
|
||||
|
||||
private final @Nullable HealthCheckedGrpcComponent server;
|
||||
|
||||
private final Map<String, HealthCheckedGrpcComponent> services;
|
||||
|
||||
TestHealthCheckedGrpcComponents(@Nullable HealthCheckedGrpcComponent server,
|
||||
Map<String, HealthCheckedGrpcComponent> services) {
|
||||
this.server = server;
|
||||
this.services = services;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable HealthCheckedGrpcComponent getServer() {
|
||||
return this.server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getServiceNames() {
|
||||
return this.services.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable HealthCheckedGrpcComponent getService(String serviceName) {
|
||||
return this.services.get(serviceName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TestHealthCheckedGrpcComponent implements HealthCheckedGrpcComponent {
|
||||
|
||||
private final Set<String> members;
|
||||
|
||||
private final StatusAggregator statusAggregator;
|
||||
|
||||
private final StatusMapper statusMapper;
|
||||
|
||||
TestHealthCheckedGrpcComponent(Set<String> memebers) {
|
||||
this(memebers, StatusAggregator.getDefault(), StatusMapper.getDefault());
|
||||
}
|
||||
|
||||
TestHealthCheckedGrpcComponent(Set<String> memebers, StatusAggregator statusAggregator,
|
||||
StatusMapper statusMapper) {
|
||||
this.members = memebers;
|
||||
this.statusAggregator = statusAggregator;
|
||||
this.statusMapper = statusMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMember(String healthContributorName) {
|
||||
return this.members.contains(healthContributorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatusAggregator getStatusAggregator() {
|
||||
return this.statusAggregator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatusMapper getStatusMapper() {
|
||||
return this.statusMapper;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.health;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.health.actuate.endpoint.StatusAggregator;
|
||||
import org.springframework.boot.health.contributor.Status;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link StatusAggregator}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class StatusAggregatorTests {
|
||||
|
||||
@Test
|
||||
void ofAndGetAggregateStatusWhenUsingDefaultInstance() {
|
||||
StatusAggregator aggregator = StatusAggregator.getDefault();
|
||||
Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE);
|
||||
assertThat(status).isEqualTo(Status.DOWN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofAndGetAggregateStatusWhenUsingCustomOrder() {
|
||||
StatusAggregator aggregator = StatusAggregator.of(Status.UNKNOWN, Status.UP, Status.OUT_OF_SERVICE,
|
||||
Status.DOWN);
|
||||
Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE);
|
||||
assertThat(status).isEqualTo(Status.UNKNOWN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofAndGetAggregateStatusWhenHasCustomStatusAndUsingDefaultOrder() {
|
||||
StatusAggregator aggregator = StatusAggregator.getDefault();
|
||||
Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE,
|
||||
new Status("CUSTOM"));
|
||||
assertThat(status).isEqualTo(Status.DOWN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofAndGetAggregateStatusWhenHasCustomStatusAndUsingCustomOrder() {
|
||||
StatusAggregator aggregator = StatusAggregator.of("DOWN", "OUT_OF_SERVICE", "UP", "UNKNOWN", "CUSTOM");
|
||||
Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE,
|
||||
new Status("CUSTOM"));
|
||||
assertThat(status).isEqualTo(Status.DOWN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWithNonUniformCodes() {
|
||||
StatusAggregator aggregator = StatusAggregator.of("out-of-service", "up");
|
||||
Status status = aggregator.getAggregateStatus(Status.UP, Status.OUT_OF_SERVICE);
|
||||
assertThat(status).isEqualTo(Status.OUT_OF_SERVICE);
|
||||
}
|
||||
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.grpc.server.health;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.health.contributor.Status;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link StatusMapper}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class StatusMapperTests {
|
||||
|
||||
@Test
|
||||
void createWhenMappingsAreNullUsesDefaultMappings() {
|
||||
StatusMapper mapper = StatusMapper.of(null);
|
||||
assertThat(mapper.getServingStatus(Status.UNKNOWN)).isEqualTo(ServingStatus.UNKNOWN);
|
||||
assertThat(mapper.getServingStatus(Status.UP)).isEqualTo(ServingStatus.SERVING);
|
||||
assertThat(mapper.getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.NOT_SERVING);
|
||||
assertThat(mapper.getServingStatus(Status.OUT_OF_SERVICE)).isEqualTo(ServingStatus.NOT_SERVING);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getStatusCodeReturnsMappedStatus() {
|
||||
Map<String, ServingStatus> map = new LinkedHashMap<>();
|
||||
map.put("up", ServingStatus.UNRECOGNIZED);
|
||||
map.put("down", ServingStatus.UNKNOWN);
|
||||
StatusMapper mapper = StatusMapper.of(map);
|
||||
assertThat(mapper.getServingStatus(Status.UP)).isEqualTo(ServingStatus.UNRECOGNIZED);
|
||||
assertThat(mapper.getServingStatus(Status.DOWN)).isEqualTo(ServingStatus.UNKNOWN);
|
||||
assertThat(mapper.getServingStatus(Status.OUT_OF_SERVICE)).isEqualTo(ServingStatus.SERVING);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getStatusCodeWhenMappingsAreNotUniformReturnsMappedStatus() {
|
||||
Map<String, ServingStatus> map = new LinkedHashMap<>();
|
||||
map.put("out-of-service", ServingStatus.SERVING);
|
||||
StatusMapper mapper = StatusMapper.of(map);
|
||||
assertThat(mapper.getServingStatus(Status.OUT_OF_SERVICE)).isEqualTo(ServingStatus.SERVING);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user