This is an automated email from the ASF dual-hosted git repository.
adutra pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git
The following commit(s) were added to refs/heads/main by this push:
new 0c790e0ad Standardize logging libraries in tests (#2268)
0c790e0ad is described below
commit 0c790e0ad48d69d97033bdde08d3bf4bb415210f
Author: Alexandre Dutra <[email protected]>
AuthorDate: Wed Aug 6 14:39:47 2025 +0200
Standardize logging libraries in tests (#2268)
This change enforces the following test logging patterns:
- Non-Quarkus modules use Logback Classic, configured via logback-test.xml
- Quarkus modules use JBoss Logging Manager, configured in Quarkus
configuration files.
This change also introduces a workaround for the "duplicate log messages"
issues with Gradle + JBoss Logging Manager. See this issue for context:
https://github.com/quarkusio/quarkus/issues/22844
The workaround implemented in this PR is very similar to the one proposed
in this comment:
https://github.com/quarkusio/quarkus/issues/22844#issuecomment-1824720923
Note: it's not entirely possible imho to suppress the following message on
the console:
```
The Agroal dependency is present but no JDBC datasources have been defined.
```
This is because:
1. The message happens during augmentation phase, not during tests
2. And it suffers from the "duplicate message" issue with (it's actually
Gradle that prints those messages).
---
.../src/main/kotlin/polaris-java.gradle.kts | 12 +--
.../src/main/kotlin/polaris-runtime.gradle.kts | 25 ++++++
.../src/test/resources/logback-test.xml | 11 +--
plugins/spark/v3.5/integration/build.gradle.kts | 5 +-
.../src/test/resources/logback-test.xml} | 22 ++---
runtime/admin/build.gradle.kts | 2 -
runtime/common/build.gradle.kts | 3 +-
runtime/defaults/build.gradle.kts | 3 +-
.../src/main/resources/application-it.properties | 13 ++-
.../src/main/resources/application-test.properties | 24 +++++-
.../src/main/resources/application.properties | 9 ++-
runtime/service/build.gradle.kts | 6 +-
.../service/exception/IcebergExceptionMapper.java | 15 ++--
.../IcebergJsonProcessingExceptionMapper.java | 15 +++-
.../service/exception/PolarisExceptionMapper.java | 8 +-
.../service/exception/ExceptionMapperTest.java | 94 ++++++++++++++--------
runtime/spark-tests/build.gradle.kts | 2 +-
.../junit/GradleDuplicateLoggingWorkaround.java | 84 +++++++++++++++++++
.../org.junit.jupiter.api.extension.Extension | 20 +++++
19 files changed, 277 insertions(+), 96 deletions(-)
diff --git a/build-logic/src/main/kotlin/polaris-java.gradle.kts
b/build-logic/src/main/kotlin/polaris-java.gradle.kts
index 980a144b7..799234506 100644
--- a/build-logic/src/main/kotlin/polaris-java.gradle.kts
+++ b/build-logic/src/main/kotlin/polaris-java.gradle.kts
@@ -107,11 +107,13 @@ testing {
dependencies {
implementation(project())
implementation(testFixtures(project()))
- runtimeOnly(
- libs.findLibrary("logback-classic").orElseThrow {
- GradleException("logback-classic not declared in
libs.versions.toml")
- }
- )
+ if (!plugins.hasPlugin("io.quarkus")) {
+ implementation(
+ libs.findLibrary("logback-classic").orElseThrow {
+ GradleException("logback-classic not declared in
libs.versions.toml")
+ }
+ )
+ }
implementation(
libs.findLibrary("assertj-core").orElseThrow {
GradleException("assertj-core not declared in libs.versions.toml")
diff --git a/build-logic/src/main/kotlin/polaris-runtime.gradle.kts
b/build-logic/src/main/kotlin/polaris-runtime.gradle.kts
index ee3e547a6..f52b2f16f 100644
--- a/build-logic/src/main/kotlin/polaris-runtime.gradle.kts
+++ b/build-logic/src/main/kotlin/polaris-runtime.gradle.kts
@@ -24,6 +24,14 @@ testing {
suites {
withType<JvmTestSuite> {
targets.all {
+ testTask.configure {
+ systemProperty("java.util.logging.manager",
"org.jboss.logmanager.LogManager")
+ // Enable automatic extension detection to execute
GradleDuplicateLoggingWorkaround
+ // automatically.
+ // See https://github.com/quarkusio/quarkus/issues/22844
+ systemProperty("junit.jupiter.extensions.autodetection.enabled",
"true")
+ }
+
if (testTask.name != "test") {
testTask.configure {
// For Quarkus...
@@ -47,6 +55,23 @@ testing {
}
}
+dependencies {
+ // All Quarkus projects should use JBoss LogManager with SLF4J, instead of
Logback
+ implementation("org.jboss.slf4j:slf4j-jboss-logmanager")
+}
+
+configurations.all {
+ // Validate that Logback dependencies are not used in Quarkus modules.
+ dependencies.configureEach {
+ if (group == "ch.qos.logback") {
+ throw GradleException(
+ "Logback dependencies are not allowed in Quarkus modules. " +
+ "Found $group:$name in ${project.name}."
+ )
+ }
+ }
+}
+
// Let the test's implementation config extend testImplementation, so it also
inherits the
// project's "main" implementation dependencies (not just the "api"
configuration)
configurations.named("intTestImplementation").configure {
diff --git a/runtime/service/src/test/resources/logback-test.xml
b/persistence/relational-jdbc/src/test/resources/logback-test.xml
similarity index 70%
rename from runtime/service/src/test/resources/logback-test.xml
rename to persistence/relational-jdbc/src/test/resources/logback-test.xml
index ddad1db40..b7e97bb82 100644
--- a/runtime/service/src/test/resources/logback-test.xml
+++ b/persistence/relational-jdbc/src/test/resources/logback-test.xml
@@ -19,21 +19,14 @@
under the License.
-->
-<configuration>
+<configuration debug="false">
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
- <root level="${log.level.console:-INFO}">
+ <root level="${test.log.level:-ERROR}">
<appender-ref ref="console"/>
</root>
-
- <!--
- Prevent the 'The Agroal dependency is present but no JDBC datasources have
been defined.' build-time warning.
- See
https://github.com/quarkusio/quarkus/blob/2fbc20f445fad43aaf4f3f984b9ac8319c7c7f0a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java#L111
- THIS IS A TECHNICAL NECESSITY, because of the
- -->
- <logger name="io.quarkus.agroal.deployment" level="ERROR"/>
</configuration>
diff --git a/plugins/spark/v3.5/integration/build.gradle.kts
b/plugins/spark/v3.5/integration/build.gradle.kts
index 462523afe..bcfd7e15f 100644
--- a/plugins/spark/v3.5/integration/build.gradle.kts
+++ b/plugins/spark/v3.5/integration/build.gradle.kts
@@ -43,6 +43,7 @@ dependencies {
exclude(group = "org.scala-lang", module = "scala-reflect")
}
+ implementation(project(":polaris-runtime-defaults"))
implementation(project(":polaris-runtime-service"))
testImplementation(
@@ -52,6 +53,8 @@ dependencies {
testImplementation(project(":polaris-api-management-model"))
+ testImplementation(project(":polaris-runtime-test-common"))
+
testImplementation("org.apache.spark:spark-sql_${scalaVersion}:${spark35Version}")
{
// exclude log4j dependencies. Explicit dependencies for the log4j
libraries are
// enforced below to ensure the version compatibility
@@ -63,7 +66,6 @@ dependencies {
// enforce the usage of log4j 2.24.3. This is for the log4j-api compatibility
// of spark-sql dependency
testRuntimeOnly("org.apache.logging.log4j:log4j-core:2.25.1")
- testRuntimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:2.25.1")
testImplementation("io.delta:delta-spark_${scalaVersion}:3.3.1")
@@ -95,7 +97,6 @@ dependencies {
tasks.named<Test>("intTest").configure {
maxParallelForks = 1
- systemProperty("java.util.logging.manager",
"org.jboss.logmanager.LogManager")
if (System.getenv("AWS_REGION") == null) {
environment("AWS_REGION", "us-west-2")
}
diff --git a/plugins/spark/v3.5/integration/src/intTest/resources/logback.xml
b/plugins/spark/v3.5/spark/src/test/resources/logback-test.xml
similarity index 55%
rename from plugins/spark/v3.5/integration/src/intTest/resources/logback.xml
rename to plugins/spark/v3.5/spark/src/test/resources/logback-test.xml
index 5ec1efd5f..b7e97bb82 100644
--- a/plugins/spark/v3.5/integration/src/intTest/resources/logback.xml
+++ b/plugins/spark/v3.5/spark/src/test/resources/logback-test.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="UTF-8"?>
+<?xml version="1.0" encoding="UTF-8" ?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
@@ -19,20 +19,14 @@
under the License.
-->
-<!--
-Spark by default set the logging at DEBUG level, which dumps a lot of code
details in
-the Intelij console, which impacts the IDE performance significantly when
running the
-tests. This configuration allows only log back the ERROR logs for the IDE, you
can comment
-out the configuration if you would like ot see all spark debug log during the
run.
--->
-<configuration>
- <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
- <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
- <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level - %msg%n</pattern>
+<configuration debug="false">
+ <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
+ <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
-
- <root level="ERROR">
- <appender-ref ref="CONSOLE"/>
+ <root level="${test.log.level:-ERROR}">
+ <appender-ref ref="console"/>
</root>
</configuration>
diff --git a/runtime/admin/build.gradle.kts b/runtime/admin/build.gradle.kts
index f06895ba6..29ac2c856 100644
--- a/runtime/admin/build.gradle.kts
+++ b/runtime/admin/build.gradle.kts
@@ -43,7 +43,6 @@ dependencies {
implementation("io.quarkus:quarkus-container-image-docker")
implementation(project(":polaris-runtime-common"))
- implementation("org.jboss.slf4j:slf4j-jboss-logmanager")
testImplementation(project(":polaris-runtime-test-common"))
testFixturesApi(project(":polaris-core"))
@@ -92,7 +91,6 @@ artifacts {
tasks.withType(Test::class.java).configureEach {
maxParallelForks = 4
forkEvery = 1
- systemProperty("java.util.logging.manager",
"org.jboss.logmanager.LogManager")
}
tasks.named<Test>("test").configure {
diff --git a/runtime/common/build.gradle.kts b/runtime/common/build.gradle.kts
index 79452e80b..f1753882f 100644
--- a/runtime/common/build.gradle.kts
+++ b/runtime/common/build.gradle.kts
@@ -18,9 +18,8 @@
*/
plugins {
- alias(libs.plugins.quarkus)
alias(libs.plugins.jandex)
- id("polaris-runtime")
+ id("polaris-server")
}
dependencies {
diff --git a/runtime/defaults/build.gradle.kts
b/runtime/defaults/build.gradle.kts
index 88134be6e..b61c2458b 100644
--- a/runtime/defaults/build.gradle.kts
+++ b/runtime/defaults/build.gradle.kts
@@ -18,9 +18,8 @@
*/
plugins {
- alias(libs.plugins.quarkus)
alias(libs.plugins.jandex)
- id("polaris-runtime")
+ id("polaris-server")
}
dependencies {
diff --git a/runtime/defaults/src/main/resources/application-it.properties
b/runtime/defaults/src/main/resources/application-it.properties
index dc660f54a..7c3c70f3d 100644
--- a/runtime/defaults/src/main/resources/application-it.properties
+++ b/runtime/defaults/src/main/resources/application-it.properties
@@ -17,15 +17,22 @@
# under the License.
#
-# Configuration common to ALL integration tests (executed with "it" profile).
-# Note: Quarkus integration tests cannot use QuarkusTestProfile
+# Configuration common to ALL integration tests (executed with "it" profile –
Gradle "intTest" tasks).
+# Note: Quarkus integration tests cannot use QuarkusTestProfile.
+
+# Note about logging during integration tests:
+# - Logging for test code is configured in the `application-test.properties`
file (!!).
+# By default, it is set to `ERROR` level.
+# - Logging for application code is configured in the main
`application.properties` file.
+# By default, it is set to `INFO` level.
+# Configuring logging levels or appenders in this file IS INEFFECTIVE!
quarkus.http.limits.max-body-size=1000000
quarkus.http.port=0
quarkus.management.port=0
-# polaris.persistence.type=eclipse-link
+# polaris.persistence.type=jdbc
# polaris.persistence.type=in-memory-atomic
polaris.persistence.type=in-memory
diff --git a/runtime/defaults/src/main/resources/application-test.properties
b/runtime/defaults/src/main/resources/application-test.properties
index ff0f576ca..3ba01dfb0 100644
--- a/runtime/defaults/src/main/resources/application-test.properties
+++ b/runtime/defaults/src/main/resources/application-test.properties
@@ -17,21 +17,37 @@
# under the License.
#
-# Configuration common to ALL unit tests (executed with "test" profile).
+# Configuration common to ALL unit tests (executed with "test" profile –
Gradle "test" tasks).
# Per-test specific configuration should use QuarkusTestProfile
-quarkus.log.file.enable=false
quarkus.datasource.devservices.enabled=false
+quarkus.log.level=ERROR
+quarkus.log.file.enable=false
+quarkus.console.color=true
+
+# Useful loggers for debugging purposes.
+# quarkus.log.category."org.apache.polaris".level=INFO
+# quarkus.log.category."org.apache.iceberg".level=INFO
+# quarkus.log.category."io.quarkus.http.access-log".level=INFO
+# quarkus.log.category."org.apache.hc.client5.http".level=INFO
+
+# Silence a few verbose loggers that are not useful for unit tests.
+quarkus.log.category."org.apache.polaris.core.persistence.transactional.TransactionalMetaStoreManagerImpl".level=ERROR
+quarkus.log.category."org.apache.iceberg.rest.auth.AuthManagers".level=ERROR
+quarkus.log.category."org.apache.iceberg.rest.auth.OAuth2Manager".level=ERROR
+
# Prevent the 'Hibernate Validator does not support constraints on static
methods yet.' warning log messages on
# code generated by the OpenAPI generator. The warning can only be silenced
via a log level setting.
# See
https://github.com/quarkusio/quarkus/blob/15f6b4426512c04a54e7a0bd87f894fabe068c07/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/MethodValidatedAnnotationsTransformer.java#L51-L53
-# and how the 'LOGGER' is setup.
+# and how the 'LOGGER' is set up.
quarkus.log.category."io.quarkus.hibernate.validator.deployment".level=ERROR
+
# Silence the FJP warning, nothing we can do about it.
# See
https://github.com/quarkusio/quarkus/blob/15f6b4426512c04a54e7a0bd87f894fabe068c07/core/deployment/src/main/java/io/quarkus/runner/bootstrap/ForkJoinClassLoading.java#L41-L42
quarkus.log.category."io.quarkus.runner.bootstrap.ForkJoinClassLoading".level=FATAL
+
# Prevent the 'The Agroal dependency is present but no JDBC datasources have
been defined.' build-time warning.
# See
https://github.com/quarkusio/quarkus/blob/2fbc20f445fad43aaf4f3f984b9ac8319c7c7f0a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java#L111
-# THIS IS ALSO CONFIGURED IN
polaris-runtime-service/src/test/resources/logback-test.xml
quarkus.log.category."io.quarkus.agroal.deployment".level=ERROR
+
diff --git a/runtime/defaults/src/main/resources/application.properties
b/runtime/defaults/src/main/resources/application.properties
index a48c4bcbd..794ea1337 100644
--- a/runtime/defaults/src/main/resources/application.properties
+++ b/runtime/defaults/src/main/resources/application.properties
@@ -68,9 +68,11 @@ quarkus.log.file.path=./logs/polaris.log
quarkus.log.file.rotation.file-suffix=.yyyy-MM-dd.gz
quarkus.log.file.rotation.max-file-size=10M
quarkus.log.file.rotation.max-backup-index=14
-quarkus.log.category."org.apache.polaris".level=INFO
-quarkus.log.category."org.apache.iceberg.rest".level=INFO
-quarkus.log.category."io.smallrye.config".level=INFO
+
+# Useful loggers. Set "io.smallrye.config" to DEBUG to print the resolved
configuration properties.
+# quarkus.log.category."io.smallrye.config".level=INFO
+# quarkus.log.category."org.apache.polaris".level=INFO
+# quarkus.log.category."org.apache.iceberg.rest".level=INFO
quarkus.management.port=8182
quarkus.management.test-port=0
@@ -97,6 +99,7 @@ quarkus.otel.sdk.disabled=true
# quarkus.otel.traces.sampler=parentbased_always_on
# quarkus.otel.traces.sampler.arg=1.0d
+# This setting is a build-time setting and MUST be specified in this file.
quarkus.test.integration-test-profile=it
quarkus.fault-tolerance.global.timeout.enabled=false
diff --git a/runtime/service/build.gradle.kts b/runtime/service/build.gradle.kts
index b7a31c753..037776c40 100644
--- a/runtime/service/build.gradle.kts
+++ b/runtime/service/build.gradle.kts
@@ -67,8 +67,6 @@ dependencies {
implementation(libs.guava)
implementation(libs.slf4j.api)
- implementation("org.jboss.slf4j:slf4j-jboss-logmanager")
-
implementation(libs.hadoop.client.api)
implementation(libs.hadoop.client.runtime)
@@ -148,9 +146,8 @@ dependencies {
testImplementation(libs.threeten.extra)
testImplementation(libs.hawkular.agent.prometheus.scraper)
- testImplementation(libs.logback.classic)
-
testImplementation(project(":polaris-runtime-test-common"))
+
testImplementation("io.quarkus:quarkus-junit5")
testImplementation(libs.awaitility)
testImplementation(platform(libs.testcontainers.bom))
@@ -194,7 +191,6 @@ tasks.named("javadoc") { dependsOn("jandex") }
tasks.withType(Test::class.java).configureEach {
forkEvery = 1
- systemProperty("java.util.logging.manager",
"org.jboss.logmanager.LogManager")
if (System.getenv("AWS_REGION") == null) {
environment("AWS_REGION", "us-west-2")
}
diff --git
a/runtime/service/src/main/java/org/apache/polaris/service/exception/IcebergExceptionMapper.java
b/runtime/service/src/main/java/org/apache/polaris/service/exception/IcebergExceptionMapper.java
index 048da3f79..fff99e1f2 100644
---
a/runtime/service/src/main/java/org/apache/polaris/service/exception/IcebergExceptionMapper.java
+++
b/runtime/service/src/main/java/org/apache/polaris/service/exception/IcebergExceptionMapper.java
@@ -87,19 +87,17 @@ public class IcebergExceptionMapper implements
ExceptionMapper<RuntimeException>
private static final Set<String> ACCESS_DENIED_HINTS =
Set.of("access denied", "not authorized", "forbidden");
- public IcebergExceptionMapper() {}
-
@Override
public Response toResponse(RuntimeException runtimeException) {
- LOGGER.info("Handling runtimeException {}", runtimeException.getMessage());
+ getLogger().info("Handling runtimeException {}",
runtimeException.getMessage());
int responseCode = mapExceptionToResponseCode(runtimeException);
- LOGGER
+ getLogger()
.atLevel(responseCode >= 500 ? Level.INFO : Level.DEBUG)
.log("Full RuntimeException", runtimeException);
if (responseCode == Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())
{
- LOGGER.error("Unhandled exception returning INTERNAL_SERVER_ERROR",
runtimeException);
+ getLogger().error("Unhandled exception returning INTERNAL_SERVER_ERROR",
runtimeException);
}
ErrorResponse icebergErrorResponse =
@@ -113,7 +111,7 @@ public class IcebergExceptionMapper implements
ExceptionMapper<RuntimeException>
.entity(icebergErrorResponse)
.type(MediaType.APPLICATION_JSON_TYPE)
.build();
- LOGGER.debug("Mapped exception to errorResp: {}", errorResp);
+ getLogger().debug("Mapped exception to errorResp: {}", errorResp);
return errorResp;
}
@@ -262,4 +260,9 @@ public class IcebergExceptionMapper implements
ExceptionMapper<RuntimeException>
return Optional.of(Status.INTERNAL_SERVER_ERROR.getStatusCode());
}
+
+ @VisibleForTesting
+ Logger getLogger() {
+ return LOGGER;
+ }
}
diff --git
a/runtime/service/src/main/java/org/apache/polaris/service/exception/IcebergJsonProcessingExceptionMapper.java
b/runtime/service/src/main/java/org/apache/polaris/service/exception/IcebergJsonProcessingExceptionMapper.java
index 57dcba32f..10130700c 100644
---
a/runtime/service/src/main/java/org/apache/polaris/service/exception/IcebergJsonProcessingExceptionMapper.java
+++
b/runtime/service/src/main/java/org/apache/polaris/service/exception/IcebergJsonProcessingExceptionMapper.java
@@ -26,6 +26,7 @@ import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import com.fasterxml.jackson.databind.exc.ValueInstantiationException;
+import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Nullable;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@@ -40,7 +41,7 @@ import org.slf4j.LoggerFactory;
/** See Dropwizard's {@code
io.dropwizard.jersey.jackson.JsonProcessingExceptionMapper} */
@Provider
-public final class IcebergJsonProcessingExceptionMapper
+public class IcebergJsonProcessingExceptionMapper
implements ExceptionMapper<JsonProcessingException> {
private static final Logger LOGGER =
@@ -60,7 +61,8 @@ public final class IcebergJsonProcessingExceptionMapper
if (exception instanceof JsonGenerationException
|| exception instanceof InvalidDefinitionException) {
long id = ThreadLocalRandom.current().nextLong();
- LOGGER.error(String.format(Locale.ROOT, "Error handling a request:
%016x", id), exception);
+ getLogger()
+ .error(String.format(Locale.ROOT, "Error handling a request: %016x",
id), exception);
String message =
String.format(
Locale.ROOT,
@@ -75,8 +77,8 @@ public final class IcebergJsonProcessingExceptionMapper
/*
* Otherwise, it's those pesky users.
*/
- LOGGER.info("Unable to process JSON: {}", exception.getMessage());
- LOGGER.debug("Full JsonProcessingException", exception);
+ getLogger().info("Unable to process JSON: {}", exception.getMessage());
+ getLogger().debug("Full JsonProcessingException", exception);
String messagePrefix =
switch (exception) {
@@ -96,4 +98,9 @@ public final class IcebergJsonProcessingExceptionMapper
.entity(icebergErrorResponse)
.build();
}
+
+ @VisibleForTesting
+ Logger getLogger() {
+ return LOGGER;
+ }
}
diff --git
a/runtime/service/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java
b/runtime/service/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java
index 8b68d3890..7d0387f1d 100644
---
a/runtime/service/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java
+++
b/runtime/service/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java
@@ -18,6 +18,7 @@
*/
package org.apache.polaris.service.exception;
+import com.google.common.annotations.VisibleForTesting;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
@@ -64,7 +65,7 @@ public class PolarisExceptionMapper implements
ExceptionMapper<PolarisException>
@Override
public Response toResponse(PolarisException exception) {
Response.Status status = getStatus(exception);
- LOGGER
+ getLogger()
.atLevel(
status.getFamily() == Response.Status.Family.SERVER_ERROR ?
Level.INFO : Level.DEBUG)
.log("Full PolarisException", exception);
@@ -80,4 +81,9 @@ public class PolarisExceptionMapper implements
ExceptionMapper<PolarisException>
.type(MediaType.APPLICATION_JSON_TYPE)
.build();
}
+
+ @VisibleForTesting
+ Logger getLogger() {
+ return LOGGER;
+ }
}
diff --git
a/runtime/service/src/test/java/org/apache/polaris/service/exception/ExceptionMapperTest.java
b/runtime/service/src/test/java/org/apache/polaris/service/exception/ExceptionMapperTest.java
index d0da97fc3..7ba2bd3e6 100644
---
a/runtime/service/src/test/java/org/apache/polaris/service/exception/ExceptionMapperTest.java
+++
b/runtime/service/src/test/java/org/apache/polaris/service/exception/ExceptionMapperTest.java
@@ -18,11 +18,12 @@
*/
package org.apache.polaris.service.exception;
-import ch.qos.logback.classic.Level;
-import ch.qos.logback.classic.Logger;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.classic.spi.IThrowableProxy;
-import ch.qos.logback.core.read.ListAppender;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.ws.rs.core.Response;
@@ -32,50 +33,62 @@ import java.util.stream.Stream;
import org.apache.polaris.core.exceptions.AlreadyExistsException;
import org.apache.polaris.core.exceptions.CommitConflictException;
import org.assertj.core.api.Assertions;
+import org.jboss.logmanager.Level;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
-import org.slf4j.LoggerFactory;
+import org.mockito.Mockito;
+import org.slf4j.Logger;
+import org.slf4j.impl.Slf4jLogger;
/** Unit tests for exception mappers */
+@SuppressWarnings("resource")
public class ExceptionMapperTest {
private static final String MESSAGE = "this is the exception message";
private static final String CAUSE = "this is the exception cause";
+ private static final org.jboss.logmanager.Logger JBOSS_LOGGER =
+ Mockito.mock(org.jboss.logmanager.Logger.class);
+
+ @BeforeEach
+ void setUp() {
+ reset(JBOSS_LOGGER);
+ when(JBOSS_LOGGER.isLoggable(any())).thenReturn(true);
+ }
+
@ParameterizedTest
@MethodSource("testFullExceptionIsLogged")
public void testFullExceptionIsLogged(
ExceptionMapper<Exception> mapper, Exception exception, Level level) {
- Logger logger = (Logger) LoggerFactory.getLogger(mapper.getClass());
- logger.setLevel(level);
- ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
- listAppender.start();
- logger.addAppender(listAppender);
mapper.toResponse(exception);
- Assertions.assertThat(
- listAppender.list.stream()
- .anyMatch(
- log -> {
- if (log.getLevel() != level) {
- return false;
- }
+ verify(JBOSS_LOGGER)
+ .logRaw(
+ argThat(
+ record -> {
+ if (record.getLevel() != level) {
+ return false;
+ }
+
+ String message = record.getMessage();
+ if (message == null) {
+ return false;
+ }
- IThrowableProxy proxy = log.getThrowableProxy();
- if (proxy == null) {
- return false;
- }
+ Throwable error = record.getThrown();
+ if (error == null) {
+ return false;
+ }
- return proxy.getMessage().contains(CAUSE)
- || Optional.ofNullable(proxy.getCause())
- .map(IThrowableProxy::getMessage)
- .orElse("")
- .contains(CAUSE);
- }))
- .as("The exception cause should be logged")
- .isTrue();
+ return message.contains(CAUSE)
+ || Optional.ofNullable(error.getCause())
+ .map(Throwable::getMessage)
+ .orElse("")
+ .contains(CAUSE);
+ }));
}
@Test
@@ -90,15 +103,30 @@ public class ExceptionMapperTest {
// inherited Exception
return Stream.of(
Arguments.of(
- new IcebergExceptionMapper(),
+ new IcebergExceptionMapper() {
+ @Override
+ Logger getLogger() {
+ return new Slf4jLogger(JBOSS_LOGGER);
+ }
+ },
new RuntimeException(MESSAGE, new RuntimeException(CAUSE)),
Level.ERROR),
Arguments.of(
- new IcebergJsonProcessingExceptionMapper(),
+ new IcebergJsonProcessingExceptionMapper() {
+ @Override
+ Logger getLogger() {
+ return new Slf4jLogger(JBOSS_LOGGER);
+ }
+ },
new TestJsonProcessingException(MESSAGE, null, new
RuntimeException(CAUSE)),
Level.DEBUG),
Arguments.of(
- new PolarisExceptionMapper(),
+ new PolarisExceptionMapper() {
+ @Override
+ Logger getLogger() {
+ return new Slf4jLogger(JBOSS_LOGGER);
+ }
+ },
new AlreadyExistsException(MESSAGE, new RuntimeException(CAUSE)),
Level.DEBUG));
}
diff --git a/runtime/spark-tests/build.gradle.kts
b/runtime/spark-tests/build.gradle.kts
index fd130a6c5..57b778f53 100644
--- a/runtime/spark-tests/build.gradle.kts
+++ b/runtime/spark-tests/build.gradle.kts
@@ -36,6 +36,7 @@ dependencies {
testImplementation(project(":polaris-tests"))
testImplementation(testFixtures(project(":polaris-runtime-service")))
+ testImplementation(project(":polaris-runtime-test-common"))
testImplementation(platform(libs.quarkus.bom))
testImplementation("io.quarkus:quarkus-junit5")
@@ -60,7 +61,6 @@ dependencies {
tasks.named<Test>("intTest").configure {
maxParallelForks = 1
- systemProperty("java.util.logging.manager",
"org.jboss.logmanager.LogManager")
if (System.getenv("AWS_REGION") == null) {
environment("AWS_REGION", "us-west-2")
}
diff --git
a/runtime/test-common/src/main/java/org/apache/polaris/test/commons/junit/GradleDuplicateLoggingWorkaround.java
b/runtime/test-common/src/main/java/org/apache/polaris/test/commons/junit/GradleDuplicateLoggingWorkaround.java
new file mode 100644
index 000000000..97e9e0a09
--- /dev/null
+++
b/runtime/test-common/src/main/java/org/apache/polaris/test/commons/junit/GradleDuplicateLoggingWorkaround.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * http://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.apache.polaris.test.commons.junit;
+
+import jakarta.annotation.Priority;
+import jakarta.enterprise.event.Observes;
+import jakarta.enterprise.event.Startup;
+import jakarta.inject.Singleton;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * JUnit extension + Singleton CDI bean that "fixes" test logging
configuration.
+ *
+ * <p>This component removes the SLF4JBridgeHandler installed by Gradle, to
prevent duplicate
+ * logging messages in tests. Without this fix, Gradle tests using JBoss
LogManager (which is the
+ * default in Quarkus) will log each message twice: once via the
SLF4JBridgeHandler, without any
+ * formatting, and once via the JBoss LogManager (with formatting). This is
annoying because the
+ * non-formatted messages appear on the console, regardless of the log level.
+ *
+ * <p>Note: this issue has been reported to Quarkus and appears fixed, but
only the formatting part
+ * is fixed, not the duplicate messages.
+ *
+ * @see <a href="https://github.com/quarkusio/quarkus/issues/22844">Gradle
tests (with JBoss
+ * LogManager setup) output duplicate unformatted messages</a>
+ * @see <a href="https://github.com/quarkusio/quarkus/issues/48763">Gradle
+ * testLogging.showStandardStreams = false ignored by Quarkus tests with
JBoss LogManager</a>
+ */
+@Singleton
+public class GradleDuplicateLoggingWorkaround implements BeforeAllCallback {
+
+ private static final Logger LOGGER =
+ LoggerFactory.getLogger(GradleDuplicateLoggingWorkaround.class);
+
+ private static final AtomicBoolean DONE = new AtomicBoolean(false);
+
+ public void onStartup(@Observes @Priority(1) Startup event) {
+ // Sometimes the application is started before the test extension is
invoked,
+ // so we need to ensure the SLF4JBridgeHandler is removed at startup as
well.
+ removeGradleHandlers();
+ }
+
+ @Override
+ public void beforeAll(ExtensionContext context) {
+ removeGradleHandlers();
+ }
+
+ private static void removeGradleHandlers() {
+ if (!DONE.getAndSet(true)) {
+ var logger =
org.jboss.logmanager.LogManager.getLogManager().getLogger("");
+ var found = false;
+ for (var handler : logger.getHandlers()) {
+ if (handler.getClass().getSimpleName().equals("SLF4JBridgeHandler")) {
+ logger.removeHandler(handler);
+ found = true;
+ }
+ }
+ if (found) {
+ LOGGER.warn(
+ "Removed SLF4JBridgeHandler to fix duplicate logging messages in
Gradle tests.");
+ }
+ }
+ }
+}
diff --git
a/runtime/test-common/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
b/runtime/test-common/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
new file mode 100644
index 000000000..0214d2dd3
--- /dev/null
+++
b/runtime/test-common/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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
+#
+# http://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.
+#
+
+org.apache.polaris.test.commons.junit.GradleDuplicateLoggingWorkaround
\ No newline at end of file