diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d20889fdca1..a0fc1ce4fec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,6 +61,17 @@ jobs: - name: Build # executing ITs requires installing artifacts to the local repository run: mvn -B ${{ env.MVN_GOAL }} ${{ env.MVN_ADDITIONAL_OPTS }} -Pcoverage,integrationTesting,javadoc -Dnsfixtures=SEGMENT_TAR,DOCUMENT_NS + - name: Test SLF4J 2.0 compatibility + # Reuse the SLF4J 1.7-built library classes; do not recompile against the modern API. + run: >- + mvn -B initialize surefire:test -Pslf4j-2.0 + -Dtest=SystemPropertySupplierTest,LogCustomizerTest,TreeTest,LoggingDocumentStoreWrapperTest,LoggingHookTest,AzureHttpRequestLoggingPolicyTest,AzureHttpRequestLoggingPolicyV12Test,LuceneDocumentMakerLargeStringPropertiesLogTest,ElasticDocumentMakerLargeStringPropertiesLogTest + -Dsurefire.failIfNoSpecifiedTests=false + - name: Test embedded logging with SLF4J 1.7 + run: >- + mvn -B verify -pl oak-it-osgi -PintegrationTesting + -Dit.test=OSGiIT#slf4jImportRanges+cloudSlf4jImports+embeddedSlf4jLogging + -Dslf4j.api.version=1.7.36 -Dlogback.version=1.2.13 - name: Upload build result uses: actions/upload-artifact@v4 with: diff --git a/oak-benchmarks-elastic/pom.xml b/oak-benchmarks-elastic/pom.xml index f96101a235e..886795afa71 100644 --- a/oak-benchmarks-elastic/pom.xml +++ b/oak-benchmarks-elastic/pom.xml @@ -28,6 +28,8 @@ oak-benchmarks-elastic + ${slf4j.version} + ${logback.runtime.version} true diff --git a/oak-benchmarks-lucene/pom.xml b/oak-benchmarks-lucene/pom.xml index d46cfce278b..d6d76fec276 100644 --- a/oak-benchmarks-lucene/pom.xml +++ b/oak-benchmarks-lucene/pom.xml @@ -29,6 +29,8 @@ oak-benchmarks-lucene + ${slf4j.version} + ${logback.runtime.version} true diff --git a/oak-benchmarks/pom.xml b/oak-benchmarks/pom.xml index 324abf02ff2..8ea32b2001c 100644 --- a/oak-benchmarks/pom.xml +++ b/oak-benchmarks/pom.xml @@ -31,6 +31,8 @@ Oak Benchmarks Runner + ${slf4j.version} + ${logback.runtime.version} true diff --git a/oak-blob-cloud/pom.xml b/oak-blob-cloud/pom.xml index 474fb5d1f98..71e8d3e6c48 100644 --- a/oak-blob-cloud/pom.xml +++ b/oak-blob-cloud/pom.xml @@ -58,6 +58,8 @@ org.slf4j;version="[1.7,2)", + + org.slf4j.event;version="[1.7,3)", software.amazon.awssdk.crt;resolution:=optional, diff --git a/oak-examples/standalone/pom.xml b/oak-examples/standalone/pom.xml index b279f7cc6d0..3c178a943a0 100644 --- a/oak-examples/standalone/pom.xml +++ b/oak-examples/standalone/pom.xml @@ -35,6 +35,8 @@ Standalone application based on Spring Boot then embeds a Jackrabbit Oak content repository + ${slf4j.version} + ${logback.runtime.version} true 2.7.18 diff --git a/oak-examples/webapp/pom.xml b/oak-examples/webapp/pom.xml index 987c50bda9f..92c9acd5dcf 100644 --- a/oak-examples/webapp/pom.xml +++ b/oak-examples/webapp/pom.xml @@ -35,6 +35,8 @@ Web application that hosts and serves a Jackrabbit Oak content repository + ${slf4j.version} + ${logback.runtime.version} 9.0.118 true diff --git a/oak-it-osgi/pom.xml b/oak-it-osgi/pom.xml index c561fac8fbe..dccb61f7b6a 100644 --- a/oak-it-osgi/pom.xml +++ b/oak-it-osgi/pom.xml @@ -31,6 +31,8 @@ Oak Integration Tests for OSGi deployments + ${slf4j.version} + ${logback.runtime.version} true diff --git a/oak-it-osgi/src/test/java/org/apache/jackrabbit/oak/osgi/OSGiIT.java b/oak-it-osgi/src/test/java/org/apache/jackrabbit/oak/osgi/OSGiIT.java index 64c9e808ff9..9f696b0d6dc 100644 --- a/oak-it-osgi/src/test/java/org/apache/jackrabbit/oak/osgi/OSGiIT.java +++ b/oak-it-osgi/src/test/java/org/apache/jackrabbit/oak/osgi/OSGiIT.java @@ -27,19 +27,41 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; import java.net.URI; import java.net.MalformedURLException; import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; import javax.inject.Inject; import javax.jcr.Repository; import javax.jcr.RepositoryException; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.ThrowableProxy; +import ch.qos.logback.core.read.ListAppender; import org.apache.jackrabbit.oak.plugins.document.spi.lease.LeaseFailureHandler; import org.apache.jackrabbit.oak.spi.state.NodeStore; import org.junit.Test; +import org.junit.Assert; import org.junit.runner.RunWith; import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Constants; import org.ops4j.pax.exam.CoreOptions; import org.ops4j.pax.exam.Option; import org.ops4j.pax.exam.junit.PaxExam; @@ -49,9 +71,18 @@ import org.ops4j.pax.exam.spi.reactors.PerClass; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; import org.osgi.framework.Version; +import org.osgi.framework.namespace.PackageNamespace; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; @RunWith(PaxExam.class) @ExamReactorStrategy(PerClass.class) @@ -86,6 +117,8 @@ public Option[] configuration() throws IOException, URISyntaxException { // required for slf4j 2.0.x spyflyOptions(), + // Keep provider discovery inside OSGi rather than Maven's test classpath. + frameworkProperty("felix.bootdelegation.implicit").value("false"), frameworkProperty("repository.home").value("target"), systemProperties(new SystemPropertyOption("felix.fileinstall.dir").value(getConfigDir())), jarBundles(), @@ -106,6 +139,7 @@ static Option spyflyOptions() { mavenBundle("org.ow2.asm", "asm-commons", "9.7.1"), mavenBundle("org.ow2.asm", "asm-util", "9.7.1"), mavenBundle("org.apache.aries.spifly", "org.apache.aries.spifly.dynamic.bundle", "1.3.7") + .startLevel(1) ); } @@ -136,9 +170,12 @@ static String getConfigDir(){ static Option jarBundles() throws MalformedURLException { DefaultCompositeOption composite = new DefaultCompositeOption(); + List loggingBundles = List.of("slf4j-api.jar", "logback-core.jar", "logback-classic.jar"); for (File bundle : new File("target", "test-bundles").listFiles()) { if (bundle.getName().endsWith(".jar") && bundle.isFile()) { - composite.add(bundle(bundle.toURI().toURL().toString())); + // Make the provider available before framework services initialize SLF4J. + composite.add(bundle(bundle.toURI().toURL().toString()).startLevel( + loggingBundles.contains(bundle.getName()) ? 1 : Constants.START_LEVEL_DEFAULT_PROVISION)); } } return composite; @@ -156,6 +193,206 @@ public void bundleStates() { } } + /** + * Checks library logging contracts and the embedded Elastic client's shared SLF4J SPI. + */ + @Test + public void slf4jImportRanges() throws InvalidSyntaxException, ClassNotFoundException { + int imports = 0; + for (Bundle bundle : context.getBundles()) { + if (!bundle.getSymbolicName().startsWith("org.apache.jackrabbit.oak")) { + continue; + } + BundleWiring wiring = bundle.adapt(BundleWiring.class); + Assert.assertNotNull(bundle.toString(), wiring); + boolean elastic = "org.apache.jackrabbit.oak-search-elastic".equals(bundle.getSymbolicName()); + if (elastic) { + Class.forName("co.elastic.clients.elasticsearch._helpers.bulk.BulkIngester", + true, wiring.getClassLoader()); + } + for (BundleWire wire : wiring.getRequiredWires(PackageNamespace.PACKAGE_NAMESPACE)) { + String packageName = (String) wire.getCapability().getAttributes() + .get(PackageNamespace.PACKAGE_NAMESPACE); + if (!packageName.equals("org.slf4j") && !packageName.startsWith("org.slf4j.")) { + continue; + } + Filter filter = FrameworkUtil.createFilter(wire.getRequirement().getDirectives() + .get(PackageNamespace.REQUIREMENT_FILTER_DIRECTIVE)); + String message = bundle + ": " + filter; + Assert.assertTrue(message, filter.matches(Map.of( + PackageNamespace.PACKAGE_NAMESPACE, packageName, + PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, new Version("1.7.36")))); + Assert.assertEquals(message, elastic && packageName.equals("org.slf4j.spi"), filter.matches(Map.of( + PackageNamespace.PACKAGE_NAMESPACE, packageName, + PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, new Version("2.0.20")))); + Assert.assertFalse(message, filter.matches(Map.of( + PackageNamespace.PACKAGE_NAMESPACE, packageName, + PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, new Version("3.0.0")))); + imports++; + } + } + Assert.assertTrue("No Oak SLF4J imports were checked", imports > 0); + } + + /** + * Checks the embedded AWS SDK's logging requirements against SLF4J alone, without Pax Logging exports. + */ + @Test + public void cloudSlf4jImports() throws BundleException, InvalidSyntaxException { + Bundle provider = Arrays.stream(context.getBundles()) + .filter(bundle -> "slf4j.api".equals(bundle.getSymbolicName())) + .findFirst().orElseThrow(() -> new IllegalStateException("SLF4J API bundle is missing")); + List exports = provider.adapt(BundleRevision.class) + .getDeclaredCapabilities(PackageNamespace.PACKAGE_NAMESPACE); + Bundle cloud = context.installBundle( + new File("target/test-bundles/cloud/oak-blob-cloud.jar").toURI().toString()); + try { + boolean eventChecked = false; + for (BundleRequirement requirement : cloud.adapt(BundleRevision.class) + .getDeclaredRequirements(PackageNamespace.PACKAGE_NAMESPACE)) { + String filterText = requirement.getDirectives() + .get(PackageNamespace.REQUIREMENT_FILTER_DIRECTIVE); + if (!filterText.contains("(osgi.wiring.package=org.slf4j)") + && !filterText.contains("(osgi.wiring.package=org.slf4j.")) { + continue; + } + boolean optional = PackageNamespace.RESOLUTION_OPTIONAL.equals(requirement.getDirectives() + .get(PackageNamespace.REQUIREMENT_RESOLUTION_DIRECTIVE)); + String message = cloud + ": " + filterText + " against " + provider; + if (filterText.contains("(osgi.wiring.package=org.slf4j.event)")) { + Assert.assertFalse(message, optional); + Filter filter = FrameworkUtil.createFilter(filterText); + Assert.assertFalse(message, filter.matches(Map.of( + PackageNamespace.PACKAGE_NAMESPACE, "org.slf4j.event", + PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, new Version("3.0.0")))); + eventChecked = true; + } + if (!optional) { + Assert.assertTrue(message, exports.stream().anyMatch(requirement::matches)); + } + } + Assert.assertTrue("No AWS SDK SLF4J event import was checked", eventChecked); + } finally { + cloud.uninstall(); + } + } + + /** + * Verifies the embedded Commons Logging bridge and AWS logging against the selected runtime. + */ + @Test + public void embeddedSlf4jLogging() throws Exception { + Bundle elastic = Arrays.stream(context.getBundles()) + .filter(bundle -> "org.apache.jackrabbit.oak-search-elastic".equals(bundle.getSymbolicName())) + .findFirst().orElseThrow(() -> new IllegalStateException("Elastic bundle is missing")); + Bundle api = Arrays.stream(context.getBundles()) + .filter(bundle -> "slf4j.api".equals(bundle.getSymbolicName())) + .findFirst().orElseThrow(() -> new IllegalStateException("SLF4J API bundle is missing")); + String loggerName = OSGiIT.class.getName() + ".embedded"; + LoggerContext loggerContext = (LoggerContext) api.loadClass("org.slf4j.LoggerFactory") + .getMethod("getILoggerFactory").invoke(null); + Logger logger = loggerContext.getLogger(loggerName); + Path sdkJar = Files.createTempFile("oak-slf4j-sdk-", ".jar"); + Level previousLevel = logger.getLevel(); + boolean previousAdditive = logger.isAdditive(); + ListAppender appender = new ListAppender<>() { + @Override + protected void append(ILoggingEvent event) { + // Capture caller data while the logging call is still on the stack. + event.getCallerData(); + super.append(event); + } + }; + appender.start(); + logger.addAppender(appender); + logger.setAdditive(false); + logger.setLevel(Level.TRACE); + try (JarFile cloud = new JarFile("target/test-bundles/cloud/oak-blob-cloud.jar")) { + JarEntry utils = cloud.stream() + .filter(entry -> entry.getName().startsWith("utils-") + && !entry.getName().startsWith("utils-lite-") && entry.getName().endsWith(".jar")) + .findFirst().orElseThrow(() -> new IllegalStateException("Embedded AWS SDK utils JAR is missing")); + try (InputStream input = cloud.getInputStream(utils)) { + Files.copy(input, sdkJar, StandardCopyOption.REPLACE_EXISTING); + } + Assert.assertSame(api.loadClass("org.slf4j.LoggerFactory"), + elastic.loadClass("org.slf4j.LoggerFactory")); + Class logFactoryClass = elastic.loadClass("org.apache.commons.logging.LogFactory"); + Object commonsFactory = logFactoryClass.getMethod("getFactory").invoke(null); + Assert.assertEquals("org.apache.commons.logging.impl.Slf4jLogFactory", + commonsFactory.getClass().getName()); + Object commonsLog = logFactoryClass.getMethod("getLog", String.class).invoke(null, loggerName); + Class logClass = elastic.loadClass("org.apache.commons.logging.Log"); + Class levelClass = api.loadClass("org.slf4j.event.Level"); + ClassLoader loggingLoader = new ClassLoader(null) { + @Override + protected Class findClass(String name) throws ClassNotFoundException { + if (name.startsWith("org.slf4j.")) { + return api.loadClass(name); + } + throw new ClassNotFoundException(name); + } + }; + try (URLClassLoader sdkLoader = new URLClassLoader(new URL[] {sdkJar.toUri().toURL()}, loggingLoader)) { + Class sdkClass = sdkLoader.loadClass("software.amazon.awssdk.utils.Logger"); + Assert.assertSame(sdkLoader, sdkClass.getClassLoader()); + Constructor sdkConstructor = sdkClass.getDeclaredConstructor(api.loadClass("org.slf4j.Logger")); + sdkConstructor.setAccessible(true); + Object sdkLog = sdkConstructor.newInstance(logger); + Assert.assertSame(logger, sdkClass.getMethod("logger").invoke(sdkLog)); + for (String level : List.of("TRACE", "DEBUG", "INFO", "WARN", "ERROR")) { + String method = level.toLowerCase(Locale.ROOT); + String enabledMethod = "is" + level.charAt(0) + method.substring(1) + "Enabled"; + appender.list.clear(); + Assert.assertEquals(true, logClass.getMethod(enabledMethod).invoke(commonsLog)); + logClass.getMethod(method, Object.class) + .invoke(commonsLog, "commons-" + level); + Object sdkLevel = levelClass.getField(level).get(null); + Assert.assertEquals(true, sdkClass.getMethod("isLoggingLevelEnabled", levelClass) + .invoke(sdkLog, sdkLevel)); + Supplier message = () -> "aws-" + level; + sdkClass.getMethod("log", levelClass, Supplier.class).invoke(sdkLog, sdkLevel, message); + Assert.assertEquals(2, appender.list.size()); + Assert.assertEquals("commons-" + level, appender.list.get(0).getFormattedMessage()); + Assert.assertEquals("aws-" + level, appender.list.get(1).getFormattedMessage()); + for (ILoggingEvent event : appender.list) { + Assert.assertEquals(Level.toLevel(level), event.getLevel()); + } + StackTraceElement caller = Arrays.stream(appender.list.get(0).getCallerData()) + .filter(frame -> !frame.getClassName().startsWith("java.lang.reflect.") + && !frame.getClassName().startsWith("jdk.internal.reflect.")) + .findFirst().orElseThrow(() -> new AssertionError("Missing bridge caller data")); + Assert.assertEquals(OSGiIT.class.getName(), caller.getClassName()); + Assert.assertEquals("embeddedSlf4jLogging", caller.getMethodName()); + + Throwable failure = new IllegalStateException("commons-failure-" + level); + logClass.getMethod(method, Object.class, Throwable.class) + .invoke(commonsLog, "commons-failure-" + level, failure); + Assert.assertEquals(3, appender.list.size()); + ILoggingEvent event = appender.list.get(2); + Assert.assertEquals("commons-failure-" + level, event.getFormattedMessage()); + Assert.assertEquals(Level.toLevel(level), event.getLevel()); + Assert.assertSame(failure, ((ThrowableProxy) event.getThrowableProxy()).getThrowable()); + + logger.setLevel(Level.OFF); + appender.list.clear(); + Assert.assertEquals(false, logClass.getMethod(enabledMethod).invoke(commonsLog)); + logClass.getMethod(method, Object.class).invoke(commonsLog, "suppressed"); + logClass.getMethod(method, Object.class, Throwable.class) + .invoke(commonsLog, "suppressed", failure); + Assert.assertTrue(appender.list.isEmpty()); + logger.setLevel(Level.TRACE); + } + } + } finally { + logger.detachAppender(appender); + logger.setLevel(previousLevel); + logger.setAdditive(previousAdditive); + appender.stop(); + Files.delete(sdkJar); + } + } + @Test public void listBundles() { for (Bundle bundle : context.getBundles()) { diff --git a/oak-it-osgi/test-bundles.xml b/oak-it-osgi/test-bundles.xml index 39a3aa85675..b008a27ead1 100644 --- a/oak-it-osgi/test-bundles.xml +++ b/oak-it-osgi/test-bundles.xml @@ -23,6 +23,15 @@ false + + cloud + ${artifact.artifactId}.jar + test + + org.apache.jackrabbit:oak-blob-cloud + + false + ${artifact.artifactId}.jar diff --git a/oak-parent/pom.xml b/oak-parent/pom.xml index 2aa8cdd4a07..fc4f3e79a2a 100644 --- a/oak-parent/pom.xml +++ b/oak-parent/pom.xml @@ -58,9 +58,10 @@ SegmentMK 4.7.2 5.5.2 - 2.0.20 - 2.0.20 - 1.6.4 + 1.7.36 + 2.0.20 + 1.2.13 + 1.6.4 2.1.214 1.28.5 10.16.1.1 @@ -599,22 +600,22 @@ org.slf4j slf4j-simple - ${slf4j.version} + ${slf4j.api.version} org.slf4j log4j-over-slf4j - ${slf4j.version} + ${slf4j.api.version} org.slf4j jcl-over-slf4j - ${slf4j.version} + ${slf4j.api.version} org.slf4j jul-to-slf4j - ${slf4j.version} + ${slf4j.api.version} ch.qos.logback @@ -947,6 +948,14 @@ + + + slf4j-2.0 + + ${slf4j.version} + ${logback.runtime.version} + + unittesting diff --git a/oak-run-commons/pom.xml b/oak-run-commons/pom.xml index 3f1faa6a86d..9214a9cb1c5 100644 --- a/oak-run-commons/pom.xml +++ b/oak-run-commons/pom.xml @@ -30,6 +30,11 @@ oak-run-commons Oak Commons for Runnable modules + + ${slf4j.version} + ${logback.runtime.version} + + diff --git a/oak-run-elastic/pom.xml b/oak-run-elastic/pom.xml index 349860d05b5..acf2754572c 100644 --- a/oak-run-elastic/pom.xml +++ b/oak-run-elastic/pom.xml @@ -31,6 +31,8 @@ Oak Elastic Runnable Jar + ${slf4j.version} + ${logback.runtime.version} 8.2.0.v20160908 + org.slf4j.spi;version="[1.7,3)", !com.ibm.*, !org.apache.derby.*, !io.opentelemetry.sdk.autoconfigure.*, diff --git a/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/it/CompositeTestSupport.java b/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/it/CompositeTestSupport.java index 6727a89758c..c7743f9a88c 100644 --- a/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/it/CompositeTestSupport.java +++ b/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/it/CompositeTestSupport.java @@ -108,8 +108,6 @@ protected static Option logging(String level) { return composite( mavenBundle("org.ops4j.pax.logging", "pax-logging-api", "1.11.13"), SlingOptions.spyfly(), - mavenBundle("org.slf4j", "slf4j-api", "2.0.12"), - mavenBundle("org.slf4j", "slf4j-simple", "2.0.12"), systemProperty("org.ops4j.pax.logging.DefaultServiceLog.level").value(level), SlingOptions.logback() ); diff --git a/oak-upgrade/pom.xml b/oak-upgrade/pom.xml index ee4b7a0518f..1939b38976a 100644 --- a/oak-upgrade/pom.xml +++ b/oak-upgrade/pom.xml @@ -31,6 +31,11 @@ Oak upgrade Tooling for upgrading Jackrabbit repositories to Oak + + ${slf4j.version} + ${logback.runtime.version} + +