Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;

import jakarta.enterprise.context.ApplicationScoped;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
import org.jboss.logging.Logger;

import io.smallrye.health.api.AsyncHealthCheck;
import io.smallrye.health.log.ExceptionErrorIdDetail;
import io.smallrye.health.log.ExceptionLogType;
import io.smallrye.mutiny.Uni;

@ApplicationScoped
Expand All @@ -22,13 +28,36 @@ public class AsyncHealthCheckFactory {
private static final String ROOT_CAUSE = "rootCause";
private static final String STACK_TRACE = "stackTrace";

// TODO unify these properties in the next major release
String uncheckedExceptionDataStyle = ROOT_CAUSE;
ExceptionLogType exceptionLogType = ExceptionLogType.NONE;
ExceptionErrorIdDetail exceptionErrorIdDetail = ExceptionErrorIdDetail.STACKTRACE;
Logger.Level exceptionLogLevel = Logger.Level.ERROR;
boolean removeDeprecatedExceptionDataStyle = false;

public AsyncHealthCheckFactory() {
try {
uncheckedExceptionDataStyle = ConfigProvider.getConfig()
.getOptionalValue("io.smallrye.health.uncheckedExceptionDataStyle", String.class)
.orElse(ROOT_CAUSE);
Config config = ConfigProvider.getConfig();
Optional<String> optionalUncheckedExceptionDataStyle = config
.getOptionalValue("io.smallrye.health.uncheckedExceptionDataStyle", String.class);
if (optionalUncheckedExceptionDataStyle.isPresent()) {
HealthLogging.logger
.warn("Configuration property \"io.smallrye.health.uncheckedExceptionDataStyle\" is deprecated. Use " +
"\"io.smallrye.health.exception-log-type\" and \"io.smallrye.health.exception.errorid.detail\" instead.");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this deserve a piece of documentation? I don't think io.smallrye.health.exception-log-type and io.smallrye.health.exception.errorid.detail properties are documented? How would the user know how to use them?

}
uncheckedExceptionDataStyle = optionalUncheckedExceptionDataStyle.orElse(ROOT_CAUSE);

Optional<ExceptionLogType> optionalExceptionLogType = config
.getOptionalValue("io.smallrye.health.exception.log.type", ExceptionLogType.class);
if (optionalExceptionLogType.isPresent()) {
removeDeprecatedExceptionDataStyle = true;
}
exceptionLogType = optionalExceptionLogType.orElse(ExceptionLogType.NONE);
exceptionErrorIdDetail = config
.getOptionalValue("io.smallrye.health.exception.errorid.detail", ExceptionErrorIdDetail.class)
.orElse(ExceptionErrorIdDetail.STACKTRACE);
exceptionLogLevel = config.getOptionalValue("io.smallrye.health.exception.log.level", Logger.Level.class)
.orElse(Logger.Level.ERROR);
} catch (IllegalStateException illegalStateException) {
// OK, no config provider was found, use default values
}
Expand All @@ -54,7 +83,29 @@ private HealthCheckResponse handleFailure(String name, Throwable e) {

HealthCheckResponseBuilder response = HealthCheckResponse.named(name).down();

if (!uncheckedExceptionDataStyle.equals("none")) {
String errorKey = "<error>";
switch (exceptionLogType) {
case NONE:
break;
case ERROR_ID:
UUID uuid = UUID.randomUUID();
response.withData(errorKey, "See error-id " + uuid);
String errorToLog = switch (exceptionErrorIdDetail) {
case STACKTRACE -> getStackTrace(e);
case EXCEPTION_CLASS -> e.getClass().getName();
case EXCEPTION_MESSAGE -> getRootCause(e).toString();
};
HealthLogging.logger.log(exceptionLogLevel,
"Health check \"%s\" failed (error-id %s) %s".formatted(name, uuid, errorToLog));
break;
case EXCEPTION_CLASS:
response.withData(errorKey, e.getClass().getName());
break;
case EXCEPTION_MESSAGE:
response.withData(errorKey, getRootCause(e).toString());
}

if (!removeDeprecatedExceptionDataStyle && !uncheckedExceptionDataStyle.equals("none")) {
response.withData(EXCEPTION_CLASS, e.getClass().getName());
response.withData(EXCEPTION_MESSAGE, e.getMessage());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.smallrye.health.log;

public enum ExceptionErrorIdDetail {
EXCEPTION_CLASS,
EXCEPTION_MESSAGE,
STACKTRACE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.smallrye.health.log;

public enum ExceptionLogType {
NONE,
ERROR_ID,
EXCEPTION_CLASS,
EXCEPTION_MESSAGE
}
180 changes: 180 additions & 0 deletions implementation/src/test/java/io/smallrye/health/ErrorLogTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package io.smallrye.health;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.ByteArrayOutputStream;
import java.util.logging.Level;

import jakarta.json.JsonObject;

import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.jboss.logging.Logger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.smallrye.health.log.ExceptionErrorIdDetail;
import io.smallrye.health.log.ExceptionLogType;
import io.smallrye.testing.logging.LogCapture;

/**
* The new log API test. Will replace the log tests in {@link SmallRyeHealthReporterTest}.
*/
public class ErrorLogTest {

@RegisterExtension
static LogCapture logCapture = LogCapture.with(logRecord -> true, Level.ALL);

public static class FailingHealthCheck implements HealthCheck {
@Override
public HealthCheckResponse call() {
throw new RuntimeException("this health check has failed");
}
}

@BeforeEach
public void createReporter() {
logCapture.records().clear();
}

@Test
public void testReportWhenDownExceptionTypeNone() {
SmallRyeHealth health = reportHealthWithFailure(ExceptionLogType.NONE);

assertNull(health.getPayload().getJsonArray("checks").getJsonObject(0).getJsonObject("data"));
assertAnyLogMessageContains("SRHCK01001: Reporting health down status: {\"status\":\"DOWN\","
+ "\"checks\":[{\"name\":\"io.smallrye.health.ErrorLogTest$FailingHealthCheck\",\"status\":\"DOWN\"}]}");
}

@Test
public void testReportWhenDownExceptionTypeErrorIdStacktrace() {
SmallRyeHealth health = reportHealthWithFailure(ExceptionLogType.ERROR_ID);

JsonObject data = health.getPayload().getJsonArray("checks").getJsonObject(0).getJsonObject("data");
assertNotNull(data);
assertEquals(1, data.size());
assertTrue(data.getString("<error>").contains("See error-id "));
assertAnyLogMessageContains("SRHCK01001: Reporting health down status: {\"status\":\"DOWN\"");
assertAnyLogMessageMatches("Health check \"io\\.smallrye\\.health\\.ErrorLogTest\\$FailingHealthCheck\" failed "
+ "\\(error-id [0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\) "
+ "java\\.lang\\.RuntimeException: this health check has failed\\R"
+ "\\s*at io\\.smallrye\\.health\\.ErrorLogTest\\$FailingHealthCheck\\.call[\\s\\S]*", Logger.Level.ERROR);
}

@Test
public void testReportWhenDownExceptionTypeErrorIdExceptionClass() {
SmallRyeHealth health = reportHealthWithFailure(ExceptionLogType.ERROR_ID, ExceptionErrorIdDetail.EXCEPTION_CLASS);

JsonObject data = health.getPayload().getJsonArray("checks").getJsonObject(0).getJsonObject("data");
assertNotNull(data);
assertEquals(1, data.size());
assertTrue(data.getString("<error>").contains("See error-id "));
assertAnyLogMessageContains("SRHCK01001: Reporting health down status: {\"status\":\"DOWN\"");
assertAnyLogMessageMatches("Health check \"io\\.smallrye\\.health\\.ErrorLogTest\\$FailingHealthCheck\" failed " +
"\\(error-id [0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\) " +
"java\\.lang\\.RuntimeException$", Logger.Level.ERROR);
}

@Test
public void testReportWhenDownExceptionTypeErrorIdExceptionMessage() {
SmallRyeHealth health = reportHealthWithFailure(ExceptionLogType.ERROR_ID, ExceptionErrorIdDetail.EXCEPTION_MESSAGE);

JsonObject data = health.getPayload().getJsonArray("checks").getJsonObject(0).getJsonObject("data");
assertNotNull(data);
assertEquals(1, data.size());
assertTrue(data.getString("<error>").contains("See error-id "));
assertAnyLogMessageContains("SRHCK01001: Reporting health down status: {\"status\":\"DOWN\"");
assertAnyLogMessageMatches("Health check \"io\\.smallrye\\.health\\.ErrorLogTest\\$FailingHealthCheck\" failed " +
"\\(error-id [0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\) " +
"java\\.lang\\.RuntimeException: this health check has failed$", Logger.Level.ERROR);
}

@Test
public void testReportWhenDownExceptionTypeExceptionClass() {
SmallRyeHealth health = reportHealthWithFailure(ExceptionLogType.EXCEPTION_CLASS);

JsonObject data = health.getPayload().getJsonArray("checks").getJsonObject(0).getJsonObject("data");
assertNotNull(data);
assertEquals(1, data.size());
assertTrue(data.getString("<error>").contains("java.lang.RuntimeException"));
assertAnyLogMessageContains("SRHCK01001: Reporting health down status: {\"status\":\"DOWN\"");
}

@Test
public void testReportWhenDownExceptionTypeExceptionMessage() {
SmallRyeHealth health = reportHealthWithFailure(ExceptionLogType.EXCEPTION_MESSAGE);

JsonObject data = health.getPayload().getJsonArray("checks").getJsonObject(0).getJsonObject("data");
assertNotNull(data);
assertEquals(1, data.size());
assertTrue(data.getString("<error>").contains("java.lang.RuntimeException: this health check has failed"));
assertAnyLogMessageContains("SRHCK01001: Reporting health down status: {\"status\":\"DOWN\"");
}

@Test
public void testReportWhenDownExceptionLogLevelWithErrorId() {
AsyncHealthCheckFactory asyncHealthCheckFactory = new AsyncHealthCheckFactory();
asyncHealthCheckFactory.exceptionLogType = ExceptionLogType.ERROR_ID;
asyncHealthCheckFactory.exceptionLogLevel = Logger.Level.DEBUG;
asyncHealthCheckFactory.removeDeprecatedExceptionDataStyle = true;
SmallRyeHealthReporter reporter = new SmallRyeHealthReporter();
reporter.asyncHealthCheckFactory = asyncHealthCheckFactory;
reporter.addHealthCheck(new FailingHealthCheck());
SmallRyeHealth health = reporter.getHealth();
reporter.reportHealth(new ByteArrayOutputStream(), health);

JsonObject data = health.getPayload().getJsonArray("checks").getJsonObject(0).getJsonObject("data");
assertNotNull(data);
assertEquals(1, data.size());
assertTrue(data.getString("<error>").contains("See error-id "));
assertAnyLogMessageContains("SRHCK01001: Reporting health down status: {\"status\":\"DOWN\"");
assertAnyLogMessageMatches("Health check \"io\\.smallrye\\.health\\.ErrorLogTest\\$FailingHealthCheck\" failed "
+ "\\(error-id [0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\) "
+ "java\\.lang\\.RuntimeException: this health check has failed\\R"
+ "\\s*at io\\.smallrye\\.health\\.ErrorLogTest\\$FailingHealthCheck\\.call[\\s\\S]*", Logger.Level.DEBUG);
}

private SmallRyeHealth reportHealthWithFailure(ExceptionLogType type) {
return reportHealthWithFailure(type, ExceptionErrorIdDetail.STACKTRACE);
}

private SmallRyeHealth reportHealthWithFailure(ExceptionLogType exceptionLogType,
ExceptionErrorIdDetail exceptionErrorIdDetail) {
SmallRyeHealthReporter reporter = createReporter(exceptionLogType, exceptionErrorIdDetail);
reporter.addHealthCheck(new FailingHealthCheck());
SmallRyeHealth health = reporter.getHealth();
reporter.reportHealth(new ByteArrayOutputStream(), health);
return health;
}

private static SmallRyeHealthReporter createReporter(ExceptionLogType exceptionLogType,
ExceptionErrorIdDetail exceptionErrorIdDetail) {
AsyncHealthCheckFactory asyncHealthCheckFactory = new AsyncHealthCheckFactory();
asyncHealthCheckFactory.removeDeprecatedExceptionDataStyle = true;
asyncHealthCheckFactory.exceptionLogType = exceptionLogType;
asyncHealthCheckFactory.exceptionErrorIdDetail = exceptionErrorIdDetail;
SmallRyeHealthReporter reporter = new SmallRyeHealthReporter();
reporter.asyncHealthCheckFactory = asyncHealthCheckFactory;
return reporter;
}

private void assertAnyLogMessageContains(String expected) {
assertFalse(logCapture.records().isEmpty(), "Expected that the log is not empty");
assertTrue(logCapture.records().stream().anyMatch(logRecord -> logRecord.getMessage().contains(expected)),
"Log doesn't contain requested message: " + expected);
}

private void assertAnyLogMessageMatches(String regex, Logger.Level level) {
assertFalse(logCapture.records().isEmpty(), "Expected that the log is not empty");
assertTrue(
logCapture.records().stream()
.anyMatch(logRecord -> logRecord.getMessage().matches(regex)
&& (level == null || level.equals(Logger.Level.valueOf(level.toString())))),
"Log doesn't contain requested message matching: " + regex);
}
}