From fe1b9c9501f320112f117692d76726cb3c6f5635 Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Thu, 25 Jul 2019 12:52:15 +0300 Subject: [PATCH 1/2] Log health check failure with Reactive health indicators See gh-17635 --- .../CassandraReactiveHealthIndicator.java | 1 + .../CouchbaseReactiveHealthIndicator.java | 1 + .../AbstractReactiveHealthIndicator.java | 49 ++++++++++ .../mongo/MongoReactiveHealthIndicator.java | 1 + .../redis/RedisReactiveHealthIndicator.java | 1 + .../AbstractReactiveHealthIndicatorTests.java | 92 +++++++++++++++++++ 6 files changed, 145 insertions(+) create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/AbstractReactiveHealthIndicatorTests.java diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java index f8843fbd941..845f2869866 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java @@ -40,6 +40,7 @@ public class CassandraReactiveHealthIndicator extends AbstractReactiveHealthIndi * @param reactiveCassandraOperations the Cassandra operations */ public CassandraReactiveHealthIndicator(ReactiveCassandraOperations reactiveCassandraOperations) { + super("Cassandra health check failed"); Assert.notNull(reactiveCassandraOperations, "ReactiveCassandraOperations must not be null"); this.reactiveCassandraOperations = reactiveCassandraOperations; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseReactiveHealthIndicator.java index 6dc84c3d12a..62f5cd1e11b 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseReactiveHealthIndicator.java @@ -39,6 +39,7 @@ public class CouchbaseReactiveHealthIndicator extends AbstractReactiveHealthIndi * @param cluster the Couchbase cluster */ public CouchbaseReactiveHealthIndicator(Cluster cluster) { + super("Couchbase health check failed"); this.cluster = cluster; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AbstractReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AbstractReactiveHealthIndicator.java index efe9f85bddf..ea819301d64 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AbstractReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AbstractReactiveHealthIndicator.java @@ -16,8 +16,15 @@ package org.springframework.boot.actuate.health; +import java.util.function.Function; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + /** * Base {@link ReactiveHealthIndicator} implementations that encapsulates creation of * {@link Health} instance and error handling. @@ -28,6 +35,44 @@ import reactor.core.publisher.Mono; */ public abstract class AbstractReactiveHealthIndicator implements ReactiveHealthIndicator { + private static final String NO_MESSAGE = null; + + private static final String DEFAULT_MESSAGE = "Health check failed"; + + private final Log logger = LogFactory.getLog(getClass()); + + private final Function healthCheckFailedMessage; + + /** + * Create a new {@link AbstractReactiveHealthIndicator} instance with a default + * {@code healthCheckFailedMessage}. + * @since 2.1.7 + */ + protected AbstractReactiveHealthIndicator() { + this(NO_MESSAGE); + } + + /** + * Create a new {@link AbstractReactiveHealthIndicator} instance with a specific + * message to log when the health check fails. + * @param healthCheckFailedMessage the message to log on health check failure + * @since 2.1.7 + */ + protected AbstractReactiveHealthIndicator(String healthCheckFailedMessage) { + this.healthCheckFailedMessage = (ex) -> healthCheckFailedMessage; + } + + /** + * Create a new {@link AbstractReactiveHealthIndicator} instance with a specific + * message to log when the health check fails. + * @param healthCheckFailedMessage the message to log on health check failure + * @since 2.1.7 + */ + protected AbstractReactiveHealthIndicator(Function healthCheckFailedMessage) { + Assert.notNull(healthCheckFailedMessage, "HealthCheckFailedMessage must not be null"); + this.healthCheckFailedMessage = healthCheckFailedMessage; + } + @Override public final Mono health() { try { @@ -39,6 +84,10 @@ public abstract class AbstractReactiveHealthIndicator implements ReactiveHealthI } private Mono handleFailure(Throwable ex) { + if (this.logger.isWarnEnabled()) { + String message = this.healthCheckFailedMessage.apply(ex); + this.logger.warn(StringUtils.hasText(message) ? message : DEFAULT_MESSAGE, ex); + } return Mono.just(new Health.Builder().down(ex).build()); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/mongo/MongoReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/mongo/MongoReactiveHealthIndicator.java index a0eb6dbdbcd..d518d11a470 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/mongo/MongoReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/mongo/MongoReactiveHealthIndicator.java @@ -36,6 +36,7 @@ public class MongoReactiveHealthIndicator extends AbstractReactiveHealthIndicato private final ReactiveMongoTemplate reactiveMongoTemplate; public MongoReactiveHealthIndicator(ReactiveMongoTemplate reactiveMongoTemplate) { + super("Mongo health check failed"); Assert.notNull(reactiveMongoTemplate, "ReactiveMongoTemplate must not be null"); this.reactiveMongoTemplate = reactiveMongoTemplate; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisReactiveHealthIndicator.java index b9a735d6c06..29f99c52af0 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisReactiveHealthIndicator.java @@ -40,6 +40,7 @@ public class RedisReactiveHealthIndicator extends AbstractReactiveHealthIndicato private final ReactiveRedisConnectionFactory connectionFactory; public RedisReactiveHealthIndicator(ReactiveRedisConnectionFactory connectionFactory) { + super("Redis health check failed"); this.connectionFactory = connectionFactory; } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/AbstractReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/AbstractReactiveHealthIndicatorTests.java new file mode 100644 index 00000000000..bae95261bfe --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/AbstractReactiveHealthIndicatorTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.health; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AbstractReactiveHealthIndicator}. + * + * @author Dmytro Nosan + */ +@ExtendWith(OutputCaptureExtension.class) +class AbstractReactiveHealthIndicatorTests { + + @Test + void doHealth() { + Health health = new SimpleReactiveHealthIndicator().health().block(); + assertThat(health).isEqualTo(Health.up().build()); + } + + @Test + void doHealthCustomErrorMessage(CapturedOutput output) { + Health health = new ErrorMessageReactiveHealthIndicator().health().block(); + assertThat(health).isEqualTo(Health.down(new UnsupportedOperationException()).build()); + assertThat(output).contains("Unsupported Operation"); + } + + @Test + void doHealthCustomErrorMessageFunction(CapturedOutput output) { + Health health = new CustomErrorFunctionReactiveHealthIndicator().health().block(); + assertThat(health).isEqualTo(Health.down(new RuntimeException()).build()); + assertThat(output).contains("Runtime Exception"); + } + + private static final class SimpleReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + + @Override + protected Mono doHealthCheck(Builder builder) { + return Mono.just(builder.up().build()); + } + + } + + private static final class ErrorMessageReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + + ErrorMessageReactiveHealthIndicator() { + super("Unsupported Operation"); + } + + @Override + protected Mono doHealthCheck(Builder builder) { + return Mono.error(new UnsupportedOperationException()); + } + + } + + private static final class CustomErrorFunctionReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + + CustomErrorFunctionReactiveHealthIndicator() { + super((ex) -> "Runtime Exception"); + } + + @Override + protected Mono doHealthCheck(Builder builder) { + throw new RuntimeException(); + } + + } + +} From 6356852e2f97e0c58be105e5561328efe05013ab Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 29 Jul 2019 11:18:07 +0200 Subject: [PATCH 2/2] Polish "Log health check failure with Reactive health indicators" See gh-17635 --- .../AbstractReactiveHealthIndicatorTests.java | 92 ---------------- .../health/ReactiveHealthIndicatorTests.java | 103 ++++++++++++++++++ 2 files changed, 103 insertions(+), 92 deletions(-) delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/AbstractReactiveHealthIndicatorTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorTests.java diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/AbstractReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/AbstractReactiveHealthIndicatorTests.java deleted file mode 100644 index bae95261bfe..00000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/AbstractReactiveHealthIndicatorTests.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import reactor.core.publisher.Mono; - -import org.springframework.boot.actuate.health.Health.Builder; -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link AbstractReactiveHealthIndicator}. - * - * @author Dmytro Nosan - */ -@ExtendWith(OutputCaptureExtension.class) -class AbstractReactiveHealthIndicatorTests { - - @Test - void doHealth() { - Health health = new SimpleReactiveHealthIndicator().health().block(); - assertThat(health).isEqualTo(Health.up().build()); - } - - @Test - void doHealthCustomErrorMessage(CapturedOutput output) { - Health health = new ErrorMessageReactiveHealthIndicator().health().block(); - assertThat(health).isEqualTo(Health.down(new UnsupportedOperationException()).build()); - assertThat(output).contains("Unsupported Operation"); - } - - @Test - void doHealthCustomErrorMessageFunction(CapturedOutput output) { - Health health = new CustomErrorFunctionReactiveHealthIndicator().health().block(); - assertThat(health).isEqualTo(Health.down(new RuntimeException()).build()); - assertThat(output).contains("Runtime Exception"); - } - - private static final class SimpleReactiveHealthIndicator extends AbstractReactiveHealthIndicator { - - @Override - protected Mono doHealthCheck(Builder builder) { - return Mono.just(builder.up().build()); - } - - } - - private static final class ErrorMessageReactiveHealthIndicator extends AbstractReactiveHealthIndicator { - - ErrorMessageReactiveHealthIndicator() { - super("Unsupported Operation"); - } - - @Override - protected Mono doHealthCheck(Builder builder) { - return Mono.error(new UnsupportedOperationException()); - } - - } - - private static final class CustomErrorFunctionReactiveHealthIndicator extends AbstractReactiveHealthIndicator { - - CustomErrorFunctionReactiveHealthIndicator() { - super((ex) -> "Runtime Exception"); - } - - @Override - protected Mono doHealthCheck(Builder builder) { - throw new RuntimeException(); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorTests.java new file mode 100644 index 00000000000..fcd4da3a7ca --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.health; + +import org.junit.Rule; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.boot.test.rule.OutputCapture; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AbstractReactiveHealthIndicator}. + * + * @author Dmytro Nosan + * @author Stephane Nicoll + */ +public class ReactiveHealthIndicatorTests { + + @Rule + public OutputCapture output = new OutputCapture(); + + @Test + public void healthUp() { + StepVerifier.create(new SimpleReactiveHealthIndicator().health()) + .consumeNextWith((health) -> assertThat(health).isEqualTo(Health.up().build())).verifyComplete(); + assertThat(this.output.toString()).doesNotContain("Health check failed for simple"); + } + + @Test + public void healthDownWithCustomErrorMessage() { + StepVerifier.create(new CustomErrorMessageReactiveHealthIndicator().health()).consumeNextWith( + (health) -> assertThat(health).isEqualTo(Health.down(new UnsupportedOperationException()).build())) + .verifyComplete(); + assertThat(this.output.toString()).contains("Health check failed for custom"); + } + + @Test + public void healthDownWithCustomErrorMessageFunction() { + StepVerifier.create(new CustomErrorMessageFunctionReactiveHealthIndicator().health()) + .consumeNextWith((health) -> assertThat(health).isEqualTo(Health.down(new RuntimeException()).build())) + .verifyComplete(); + assertThat(this.output.toString()).contains("Health check failed with RuntimeException"); + } + + private static final class SimpleReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + + SimpleReactiveHealthIndicator() { + super("Health check failed for simple"); + } + + @Override + protected Mono doHealthCheck(Builder builder) { + return Mono.just(builder.up().build()); + } + + } + + private static final class CustomErrorMessageReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + + CustomErrorMessageReactiveHealthIndicator() { + super("Health check failed for custom"); + } + + @Override + protected Mono doHealthCheck(Builder builder) { + return Mono.error(new UnsupportedOperationException()); + } + + } + + private static final class CustomErrorMessageFunctionReactiveHealthIndicator + extends AbstractReactiveHealthIndicator { + + CustomErrorMessageFunctionReactiveHealthIndicator() { + super((ex) -> "Health check failed with " + ex.getClass().getSimpleName()); + } + + @Override + protected Mono doHealthCheck(Builder builder) { + throw new RuntimeException(); + } + + } + +}