From fe9420e0db9db68eec8d0ab7b7b88bc22d4abb4d Mon Sep 17 00:00:00 2001 From: ninjazhou <843520313@qq.com> Date: Fri, 14 Aug 2026 11:30:54 +0800 Subject: [PATCH 01/10] [improve][broker] Bridge JUL to Log4j2 to unify third-party library logging under the project's Log4j2 configuration --- bin/pulsar | 3 ++ bin/pulsar-admin-common.sh | 3 ++ bin/pulsar-perf | 3 ++ .../kotlin/pulsar.java-conventions.gradle.kts | 5 ++ distribution/server/build.gradle.kts | 1 + .../server/src/assemble/LICENSE.bin.txt | 1 + distribution/shell/build.gradle.kts | 1 + .../shell/src/assemble/LICENSE.bin.txt | 1 + gradle/libs.versions.toml | 1 + .../broker/admin/PersistentTopicsTest.java | 46 +++++++++++++++++++ 10 files changed, 65 insertions(+) diff --git a/bin/pulsar b/bin/pulsar index c7b3e1a4a78cc..251341b281b52 100755 --- a/bin/pulsar +++ b/bin/pulsar @@ -358,6 +358,9 @@ OPTS="$OPTS -Dpulsar.log.immediateFlush=$PULSAR_LOG_IMMEDIATE_FLUSH" OPTS="$OPTS -Dpulsar.routing.appender.default=$PULSAR_ROUTING_APPENDER_DEFAULT" # Configure log4j2 to disable servlet webapp detection so that Garbage free logging can be used OPTS="$OPTS -Dlog4j2.is.webapp=false" +# Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries +# (Jersey, gRPC, Guava, etc.) are routed to pulsar.log instead of stdout +OPTS="$OPTS -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" # Functions related logging OPTS="$OPTS -Dpulsar.functions.process.container.log.dir=$PULSAR_LOG_DIR" diff --git a/bin/pulsar-admin-common.sh b/bin/pulsar-admin-common.sh index 47721414066e6..11d34683868bc 100755 --- a/bin/pulsar-admin-common.sh +++ b/bin/pulsar-admin-common.sh @@ -167,3 +167,6 @@ OPTS="$OPTS -Dpulsar.log.level=$PULSAR_LOG_LEVEL" OPTS="$OPTS -Dpulsar.log.root.level=$PULSAR_LOG_ROOT_LEVEL" OPTS="$OPTS -Dpulsar.log.immediateFlush=$PULSAR_LOG_IMMEDIATE_FLUSH" OPTS="$OPTS -Dpulsar.routing.appender.default=$PULSAR_ROUTING_APPENDER_DEFAULT" +# Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries +# (Jersey, gRPC, Guava, etc.) are routed to pulsar.log instead of stdout +OPTS="$OPTS -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" diff --git a/bin/pulsar-perf b/bin/pulsar-perf index 5bc495e8aecf1..85df83e6d9abc 100755 --- a/bin/pulsar-perf +++ b/bin/pulsar-perf @@ -166,6 +166,9 @@ fi OPTS="$OPTS -Dpulsar.log.immediateFlush=$PULSAR_LOG_IMMEDIATE_FLUSH" OPTS="$OPTS -Dpulsar.log.dir=$PULSAR_LOG_DIR" OPTS="$OPTS -Dpulsar.log.file=$PULSAR_LOG_FILE" +# Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries +# (Jersey, gRPC, Guava, etc.) are routed to pulsar.log instead of stdout +OPTS="$OPTS -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" #Change to PULSAR_HOME to support relative paths cd "$PULSAR_HOME" diff --git a/build-logic/conventions/src/main/kotlin/pulsar.java-conventions.gradle.kts b/build-logic/conventions/src/main/kotlin/pulsar.java-conventions.gradle.kts index 51f1e2a484aea..535dd09ad538b 100644 --- a/build-logic/conventions/src/main/kotlin/pulsar.java-conventions.gradle.kts +++ b/build-logic/conventions/src/main/kotlin/pulsar.java-conventions.gradle.kts @@ -193,6 +193,8 @@ dependencies { "testImplementation"(catalog.findLibrary("awaitility").get()) "testImplementation"(catalog.findLibrary("system-lambda").get()) "testImplementation"(catalog.findLibrary("slf4j-api").get()) + // log4j-jul is needed at test runtime to support the JUL bridge JVM argument above + "testRuntimeOnly"(catalog.findLibrary("log4j-jul").get()) } // Allow overriding the JDK used for running tests via -PtestJavaVersion=17 @@ -255,6 +257,9 @@ tasks.withType().configureEach { "-Dpulsar.allocator.exit_on_oom=false", "-Dpulsar.allocator.out_of_memory_policy=FallbackToHeap", "-Dpulsar.test.preventExit=true", + // Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries + // (Jersey, gRPC, Guava, etc.) are routed to Log4j2 instead of stdout + "-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager", // Force IPv4 to match Pulsar's runtime scripts (bin/pulsar, bin/bookkeeper). BookKeeper's // BookieId validation rejects IPv6 zone identifiers (e.g. fe80::1%lo0), so on hosts where the // loopback interface resolves to an IPv6 link-local address (notably macOS) bookies bound to diff --git a/distribution/server/build.gradle.kts b/distribution/server/build.gradle.kts index c0d06376984b1..7be3ae2d74053 100644 --- a/distribution/server/build.gradle.kts +++ b/distribution/server/build.gradle.kts @@ -124,6 +124,7 @@ dependencies { distLib(libs.log4j.web) distLib(libs.log4j.layout.template.json) distLib(libs.log4j.slf4j2.impl) + distLib(libs.log4j.jul) distLib(libs.simpleclient.log4j2) // Metrics diff --git a/distribution/server/src/assemble/LICENSE.bin.txt b/distribution/server/src/assemble/LICENSE.bin.txt index 031f76f017fce..f17a107af5d03 100644 --- a/distribution/server/src/assemble/LICENSE.bin.txt +++ b/distribution/server/src/assemble/LICENSE.bin.txt @@ -350,6 +350,7 @@ The Apache Software License, Version 2.0 * Log4J - org.apache.logging.log4j-log4j-api-2.26.0.jar - org.apache.logging.log4j-log4j-core-2.26.0.jar + - org.apache.logging.log4j-log4j-jul-2.26.0.jar - org.apache.logging.log4j-log4j-slf4j2-impl-2.26.0.jar - org.apache.logging.log4j-log4j-web-2.26.0.jar - org.apache.logging.log4j-log4j-layout-template-json-2.26.0.jar diff --git a/distribution/shell/build.gradle.kts b/distribution/shell/build.gradle.kts index 05452ce0aa55b..25d86b09242ee 100644 --- a/distribution/shell/build.gradle.kts +++ b/distribution/shell/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { distLib(libs.log4j.web) distLib(libs.log4j.layout.template.json) distLib(libs.log4j.slf4j2.impl) + distLib(libs.log4j.jul) distLib(libs.simpleclient.log4j2) // Bouncy Castle (non-FIPS JCA provider for client-side message crypto + TLS) distLib(libs.bcprov.jdk18on) diff --git a/distribution/shell/src/assemble/LICENSE.bin.txt b/distribution/shell/src/assemble/LICENSE.bin.txt index ca6e7e3af473d..efddd4cf04f73 100644 --- a/distribution/shell/src/assemble/LICENSE.bin.txt +++ b/distribution/shell/src/assemble/LICENSE.bin.txt @@ -385,6 +385,7 @@ The Apache Software License, Version 2.0 * Log4J - log4j-api-2.26.0.jar - log4j-core-2.26.0.jar + - log4j-jul-2.26.0.jar - log4j-layout-template-json-2.26.0.jar - log4j-slf4j2-impl-2.26.0.jar - log4j-web-2.26.0.jar diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c94f496000add..1db440b81cf0c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -189,6 +189,7 @@ log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "lo log4j-web = { module = "org.apache.logging.log4j:log4j-web", version.ref = "log4j2" } log4j-layout-template-json = { module = "org.apache.logging.log4j:log4j-layout-template-json", version.ref = "log4j2" } log4j-slf4j2-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j2" } +log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j2" } # slog slog = { module = "io.github.merlimat.slog:slog", version.ref = "slog" } # Lombok diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/PersistentTopicsTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/PersistentTopicsTest.java index 9c785e8a2e4d5..5af24f972e70f 100644 --- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/PersistentTopicsTest.java +++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/PersistentTopicsTest.java @@ -1498,6 +1498,52 @@ public void testGetMessageById4SpecialPropsInMsg() throws Exception { } } + /** + * Verify that: + * 1. JUL bridge (log4j-jul) is active in the test JVM. + * 2. When message properties exceed Jetty's maxResponseHeaderSize (16384 default), + * the getMessageById admin API returns HTTP 500. + */ + @SuppressWarnings("deprecation") + @Test + public void testGetMessageByIdLargePropertiesExceed16KB() throws Exception { + TenantInfoImpl tenantInfo = new TenantInfoImpl(Set.of("role1", "role2"), Set.of("test")); + admin.tenants().createTenant("tenant-xyz", tenantInfo); + admin.namespaces().createNamespace("tenant-xyz/ns-abc", Set.of("test")); + final String topicName = "persistent://tenant-xyz/ns-abc/testGetMessageByIdLargeProps"; + admin.topics().createNonPartitionedTopic(topicName); + + // Build properties whose serialized JSON exceeds Jetty's maxResponseHeaderSize (16384 default) + Map largeProps = new HashMap<>(); + char[] chars = new char[20 * 1024]; + java.util.Arrays.fill(chars, 'A'); + largeProps.put("large-prop", new String(chars)); + + @Cleanup + ProducerBase producer = (ProducerBase) pulsarClient.newProducer().topic(topicName) + .enableBatching(false).create(); + MessageIdImpl id = (MessageIdImpl) producer.newMessage() + .value("test".getBytes()) + .properties(largeProps) + .send(); + + // 1. Verify JUL bridge is active (log4j-jul is on classpath and system property is set) + java.util.logging.LogManager logManager = java.util.logging.LogManager.getLogManager(); + Assert.assertEquals(logManager.getClass().getName(), + "org.apache.logging.log4j.jul.LogManager", + "JUL bridge should be active: -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"); + + // 2. Verify getMessageById returns 500 when response header exceeds maxResponseHeaderSize + try { + admin.topics().getMessageById(topicName, id.getLedgerId(), id.getEntryId()); + Assert.fail("Expected PulsarAdminException due to response header overflow"); + } catch (PulsarAdminException e) { + Assert.assertEquals(e.getStatusCode(), 500, + "Expected status 500 (Jetty response header overflow), got: " + + e.getStatusCode()); + } + } + @Test public void testGetMessageIdByTimestamp() throws Exception { TenantInfoImpl tenantInfo = new TenantInfoImpl(Set.of("role1", "role2"), Set.of("test")); From e83a7135d246b2a9334ddb0b40babb2be15691d0 Mon Sep 17 00:00:00 2001 From: ninjazhou <843520313@qq.com> Date: Fri, 14 Aug 2026 20:10:52 +0800 Subject: [PATCH 02/10] [improve][test] Move JUL bridge test to a separate class and use TestLogAppender for validation --- .../broker/admin/PersistentTopicsTest.java | 46 --------- .../apache/pulsar/utils/JulBridgeTest.java | 95 +++++++++++++++++++ 2 files changed, 95 insertions(+), 46 deletions(-) create mode 100644 pulsar-broker/src/test/java/org/apache/pulsar/utils/JulBridgeTest.java diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/PersistentTopicsTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/PersistentTopicsTest.java index 5af24f972e70f..9c785e8a2e4d5 100644 --- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/PersistentTopicsTest.java +++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/PersistentTopicsTest.java @@ -1498,52 +1498,6 @@ public void testGetMessageById4SpecialPropsInMsg() throws Exception { } } - /** - * Verify that: - * 1. JUL bridge (log4j-jul) is active in the test JVM. - * 2. When message properties exceed Jetty's maxResponseHeaderSize (16384 default), - * the getMessageById admin API returns HTTP 500. - */ - @SuppressWarnings("deprecation") - @Test - public void testGetMessageByIdLargePropertiesExceed16KB() throws Exception { - TenantInfoImpl tenantInfo = new TenantInfoImpl(Set.of("role1", "role2"), Set.of("test")); - admin.tenants().createTenant("tenant-xyz", tenantInfo); - admin.namespaces().createNamespace("tenant-xyz/ns-abc", Set.of("test")); - final String topicName = "persistent://tenant-xyz/ns-abc/testGetMessageByIdLargeProps"; - admin.topics().createNonPartitionedTopic(topicName); - - // Build properties whose serialized JSON exceeds Jetty's maxResponseHeaderSize (16384 default) - Map largeProps = new HashMap<>(); - char[] chars = new char[20 * 1024]; - java.util.Arrays.fill(chars, 'A'); - largeProps.put("large-prop", new String(chars)); - - @Cleanup - ProducerBase producer = (ProducerBase) pulsarClient.newProducer().topic(topicName) - .enableBatching(false).create(); - MessageIdImpl id = (MessageIdImpl) producer.newMessage() - .value("test".getBytes()) - .properties(largeProps) - .send(); - - // 1. Verify JUL bridge is active (log4j-jul is on classpath and system property is set) - java.util.logging.LogManager logManager = java.util.logging.LogManager.getLogManager(); - Assert.assertEquals(logManager.getClass().getName(), - "org.apache.logging.log4j.jul.LogManager", - "JUL bridge should be active: -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"); - - // 2. Verify getMessageById returns 500 when response header exceeds maxResponseHeaderSize - try { - admin.topics().getMessageById(topicName, id.getLedgerId(), id.getEntryId()); - Assert.fail("Expected PulsarAdminException due to response header overflow"); - } catch (PulsarAdminException e) { - Assert.assertEquals(e.getStatusCode(), 500, - "Expected status 500 (Jetty response header overflow), got: " - + e.getStatusCode()); - } - } - @Test public void testGetMessageIdByTimestamp() throws Exception { TenantInfoImpl tenantInfo = new TenantInfoImpl(Set.of("role1", "role2"), Set.of("test")); diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/utils/JulBridgeTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/utils/JulBridgeTest.java new file mode 100644 index 0000000000000..56130c863e5e3 --- /dev/null +++ b/pulsar-broker/src/test/java/org/apache/pulsar/utils/JulBridgeTest.java @@ -0,0 +1,95 @@ +/* + * 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.pulsar.utils; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.logging.log4j.core.LogEvent; +import org.testng.annotations.Test; + +/** + * Verify that the JUL-to-Log4j2 bridge is active in the test JVM and that + * log events emitted via {@link java.util.logging} are routed to Log4j2. + */ +public class JulBridgeTest { + + @Test + public void testJulBridgeIsActive() { + // The JVM should have been started with + // -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager + java.util.logging.LogManager logManager = java.util.logging.LogManager.getLogManager(); + assertEquals(logManager.getClass().getName(), + "org.apache.logging.log4j.jul.LogManager", + "JUL bridge should be active via -Djava.util.logging.manager system property"); + } + + @Test + public void testJulLogsAreRoutedToLog4j2() throws Exception { + String loggerName = "org.apache.pulsar.test.jul.bridge"; + + // Attach a TestLogAppender to capture log events for this logger + try (TestLogAppender appender = TestLogAppender.create(Optional.of(loggerName))) { + // Log via java.util.logging API + Logger julLogger = Logger.getLogger(loggerName); + julLogger.severe("JUL SEVERE test message"); + julLogger.warning("JUL WARNING test message"); + julLogger.info("JUL INFO test message"); + + // Verify that the JUL log events were routed to Log4j2 + assertTrue(appender.getEvents().stream() + .anyMatch(e -> e.getMessage().getFormattedMessage().contains("JUL SEVERE test message") + && e.getLevel() == org.apache.logging.log4j.Level.ERROR), + "JUL SEVERE should be routed to Log4j2 as ERROR"); + + assertTrue(appender.getEvents().stream() + .anyMatch(e -> e.getMessage().getFormattedMessage().contains("JUL WARNING test message") + && e.getLevel() == org.apache.logging.log4j.Level.WARN), + "JUL WARNING should be routed to Log4j2 as WARN"); + + assertTrue(appender.getEvents().stream() + .anyMatch(e -> e.getMessage().getFormattedMessage().contains("JUL INFO test message") + && e.getLevel() == org.apache.logging.log4j.Level.INFO), + "JUL INFO should be routed to Log4j2 as INFO"); + } + } + + @Test + public void testJulExceptionIsPreserved() throws Exception { + String loggerName = "org.apache.pulsar.test.jul.exception"; + + try (TestLogAppender appender = TestLogAppender.create(Optional.of(loggerName))) { + Logger julLogger = Logger.getLogger(loggerName); + RuntimeException testException = new RuntimeException("test exception from JUL"); + julLogger.log(Level.SEVERE, "Error occurred", testException); + + // Verify that the exception is preserved in the Log4j2 event + Optional event = appender.getEvents().stream() + .filter(e -> e.getMessage().getFormattedMessage().contains("Error occurred")) + .findFirst(); + assertTrue(event.isPresent(), "Should find the log event"); + assertTrue(event.get().getThrown() instanceof RuntimeException, + "Exception should be preserved"); + assertEquals(event.get().getThrown().getMessage(), "test exception from JUL"); + } + } +} From c6901af7d4f54f52cc049a1d686b97953fbc569a Mon Sep 17 00:00:00 2001 From: ninjazhou <843520313@qq.com> Date: Fri, 14 Aug 2026 20:41:38 +0800 Subject: [PATCH 03/10] [fix][test] Fix checkstyle ImportOrder violation in JulBridgeTest --- .../src/test/java/org/apache/pulsar/utils/JulBridgeTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/utils/JulBridgeTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/utils/JulBridgeTest.java index 56130c863e5e3..fd294a50eddf0 100644 --- a/pulsar-broker/src/test/java/org/apache/pulsar/utils/JulBridgeTest.java +++ b/pulsar-broker/src/test/java/org/apache/pulsar/utils/JulBridgeTest.java @@ -20,7 +20,6 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; - import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; From f9ea1d1baece0a91f0b44abecb41b7b2265ad564 Mon Sep 17 00:00:00 2001 From: ninjazhou <843520313@qq.com> Date: Tue, 25 Aug 2026 11:15:05 +0800 Subject: [PATCH 04/10] Fix: Function instance JVMs don't get the JUL bridge flag --- pulsar-functions/runtime-all/build.gradle.kts | 2 ++ .../java/org/apache/pulsar/functions/runtime/RuntimeUtils.java | 3 +++ 2 files changed, 5 insertions(+) diff --git a/pulsar-functions/runtime-all/build.gradle.kts b/pulsar-functions/runtime-all/build.gradle.kts index 0f7d3ccc368d1..b56cdaab9a4c5 100644 --- a/pulsar-functions/runtime-all/build.gradle.kts +++ b/pulsar-functions/runtime-all/build.gradle.kts @@ -36,6 +36,8 @@ dependencies { implementation(libs.log4j.slf4j2.impl) implementation(libs.log4j.api) implementation(libs.log4j.core) + // log4j-jul is needed to support the JUL-to-Log4j2 bridge in function instance JVMs + implementation(libs.log4j.jul) } // Build a fat JAR as java-instance.jar using the Shadow plugin. diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeUtils.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeUtils.java index ea0948e7ca595..f40728e764526 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeUtils.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeUtils.java @@ -359,6 +359,9 @@ public static List getCmd(InstanceConfig instanceConfig, args.add(String.format("-D%s=%s", FUNCTIONS_INSTANCE_CLASSPATH, systemFunctionInstanceClasspath)); } args.add("-Dlog4j.configurationFile=" + logConfigFile); + // Enable the JUL-to-Log4j2 bridge so that JUL records (e.g. from gRPC, Guava) + // are routed through Log4j2 in function instance JVMs. + args.add("-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"); // Use a single LoggerContext for the function process. The default // ClassLoaderContextSelector creates separate contexts per classloader, // which causes the LogAppender (added from the instance classloader) to From 8f4c815f82de2435c628dda75a6cc2e992b04045 Mon Sep 17 00:00:00 2001 From: ninjazhou <843520313@qq.com> Date: Tue, 25 Aug 2026 11:25:08 +0800 Subject: [PATCH 05/10] Fix: Several shipped launchers missing JUL bridge flag --- bin/bookkeeper | 2 ++ bin/function-localrunner | 2 ++ bin/pulsar-admin-common.cmd | 2 ++ 3 files changed, 6 insertions(+) diff --git a/bin/bookkeeper b/bin/bookkeeper index 04d642b45a9f3..00b8cecf234cc 100755 --- a/bin/bookkeeper +++ b/bin/bookkeeper @@ -196,6 +196,8 @@ OPTS="-cp $BOOKIE_CLASSPATH $OPTS" # Disable ipv6 as it can cause issues OPTS="-Djava.net.preferIPv4Stack=true $OPTS" +# Enable JUL-to-Log4j2 bridge so that JUL records are routed through Log4j2 +OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager $OPTS" # Required to allow sun.misc.Unsafe on JDK 24 without warnings # Also required for enabling unsafe memory access for Netty since 4.1.121.Final if [[ $JAVA_MAJOR_VERSION -ge 23 ]]; then diff --git a/bin/function-localrunner b/bin/function-localrunner index 4714df74cfda6..92a1de7dd7b71 100755 --- a/bin/function-localrunner +++ b/bin/function-localrunner @@ -121,6 +121,8 @@ fi # rarely needed when trying to list many z-nodes under a # directory) OPTS="-Djava.net.preferIPv4Stack=true $OPTS -Djute.maxbuffer=10485760" +# Enable JUL-to-Log4j2 bridge so that JUL records are routed through Log4j2 +OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager $OPTS" # Required to allow sun.misc.Unsafe on JDK 24 without warnings # Also required for enabling unsafe memory access for Netty since 4.1.121.Final if [[ $JAVA_MAJOR_VERSION -ge 23 ]]; then diff --git a/bin/pulsar-admin-common.cmd b/bin/pulsar-admin-common.cmd index 063f110061923..d28aea4bd8d2f 100644 --- a/bin/pulsar-admin-common.cmd +++ b/bin/pulsar-admin-common.cmd @@ -61,6 +61,8 @@ set "PULSAR_CLASSPATH=%PULSAR_CLASSPATH%;%PULSAR_LOG_CONF_DIR%" set "OPTS=%OPTS% -Dlog4j.configurationFile="%PULSAR_LOG_CONF_BASENAME%"" set "OPTS=-Djava.net.preferIPv4Stack=true %OPTS%" +REM Enable JUL-to-Log4j2 bridge so that JUL records are routed through Log4j2 +set "OPTS=-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager %OPTS%" REM Allow Netty to use reflection access set "OPTS=%OPTS% -Dio.netty.tryReflectionSetAccessible=true" From ef0bc57b62a32e50812ba09dcbd699e55c2fb143 Mon Sep 17 00:00:00 2001 From: ninjazhou <843520313@qq.com> Date: Tue, 25 Aug 2026 12:27:00 +0800 Subject: [PATCH 06/10] Fix: Comment incorrectly says JUL logs are "routed to pulsar.log instead of stdout" --- bin/pulsar | 2 +- bin/pulsar-admin-common.sh | 2 +- bin/pulsar-perf | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/pulsar b/bin/pulsar index 251341b281b52..990b63f481018 100755 --- a/bin/pulsar +++ b/bin/pulsar @@ -359,7 +359,7 @@ OPTS="$OPTS -Dpulsar.routing.appender.default=$PULSAR_ROUTING_APPENDER_DEFAULT" # Configure log4j2 to disable servlet webapp detection so that Garbage free logging can be used OPTS="$OPTS -Dlog4j2.is.webapp=false" # Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries -# (Jersey, gRPC, Guava, etc.) are routed to pulsar.log instead of stdout +# (Jersey, gRPC, Guava, etc.) are bridged into the Log4j2 configuration (conf/log4j2.yaml) OPTS="$OPTS -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" # Functions related logging diff --git a/bin/pulsar-admin-common.sh b/bin/pulsar-admin-common.sh index 11d34683868bc..c9f4cc68a4915 100755 --- a/bin/pulsar-admin-common.sh +++ b/bin/pulsar-admin-common.sh @@ -168,5 +168,5 @@ OPTS="$OPTS -Dpulsar.log.root.level=$PULSAR_LOG_ROOT_LEVEL" OPTS="$OPTS -Dpulsar.log.immediateFlush=$PULSAR_LOG_IMMEDIATE_FLUSH" OPTS="$OPTS -Dpulsar.routing.appender.default=$PULSAR_ROUTING_APPENDER_DEFAULT" # Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries -# (Jersey, gRPC, Guava, etc.) are routed to pulsar.log instead of stdout +# (Jersey, gRPC, Guava, etc.) are bridged into the Log4j2 configuration (conf/log4j2.yaml) OPTS="$OPTS -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" diff --git a/bin/pulsar-perf b/bin/pulsar-perf index 85df83e6d9abc..cd331d90a568e 100755 --- a/bin/pulsar-perf +++ b/bin/pulsar-perf @@ -167,7 +167,7 @@ OPTS="$OPTS -Dpulsar.log.immediateFlush=$PULSAR_LOG_IMMEDIATE_FLUSH" OPTS="$OPTS -Dpulsar.log.dir=$PULSAR_LOG_DIR" OPTS="$OPTS -Dpulsar.log.file=$PULSAR_LOG_FILE" # Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries -# (Jersey, gRPC, Guava, etc.) are routed to pulsar.log instead of stdout +# (Jersey, gRPC, Guava, etc.) are bridged into the Log4j2 configuration (conf/log4j2.yaml) OPTS="$OPTS -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" #Change to PULSAR_HOME to support relative paths From 6ef50916e24b979defe0e75cc5af7cbe01d9cfef Mon Sep 17 00:00:00 2001 From: ninjazhou <843520313@qq.com> Date: Tue, 25 Aug 2026 12:40:55 +0800 Subject: [PATCH 07/10] Fix: No way to override the JUL bridge flag via EXTRA_OPTS --- bin/pulsar | 7 +++---- bin/pulsar-admin-common.sh | 6 +++--- bin/pulsar-perf | 6 +++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/bin/pulsar b/bin/pulsar index 990b63f481018..30d527e997241 100755 --- a/bin/pulsar +++ b/bin/pulsar @@ -267,6 +267,9 @@ OPTS="$OPTS -Dlog4j.configurationFile=`basename $PULSAR_LOG_CONF`" # rarely needed when trying to list many z-nodes under a # directory) OPTS="-Djava.net.preferIPv4Stack=true $OPTS -Djute.maxbuffer=10485760" +# Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries +# (Jersey, gRPC, Guava, etc.) are bridged into the Log4j2 configuration (conf/log4j2.yaml) +OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager $OPTS" # Required to allow sun.misc.Unsafe on JDK 24 without warnings # Also required for enabling unsafe memory access for Netty since 4.1.121.Final if [[ $JAVA_MAJOR_VERSION -ge 23 ]]; then @@ -358,10 +361,6 @@ OPTS="$OPTS -Dpulsar.log.immediateFlush=$PULSAR_LOG_IMMEDIATE_FLUSH" OPTS="$OPTS -Dpulsar.routing.appender.default=$PULSAR_ROUTING_APPENDER_DEFAULT" # Configure log4j2 to disable servlet webapp detection so that Garbage free logging can be used OPTS="$OPTS -Dlog4j2.is.webapp=false" -# Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries -# (Jersey, gRPC, Guava, etc.) are bridged into the Log4j2 configuration (conf/log4j2.yaml) -OPTS="$OPTS -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" - # Functions related logging OPTS="$OPTS -Dpulsar.functions.process.container.log.dir=$PULSAR_LOG_DIR" # instance diff --git a/bin/pulsar-admin-common.sh b/bin/pulsar-admin-common.sh index c9f4cc68a4915..a7b795fe696c8 100755 --- a/bin/pulsar-admin-common.sh +++ b/bin/pulsar-admin-common.sh @@ -97,6 +97,9 @@ PULSAR_CLASSPATH="$PULSAR_JAR:$PULSAR_CLASSPATH:$PULSAR_EXTRA_CLASSPATH" PULSAR_CLASSPATH="`dirname $PULSAR_LOG_CONF`:$PULSAR_CLASSPATH" OPTS="$OPTS -Dlog4j.configurationFile=`basename $PULSAR_LOG_CONF`" OPTS="-Djava.net.preferIPv4Stack=true $OPTS" +# Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries +# (Jersey, gRPC, Guava, etc.) are bridged into the Log4j2 configuration (conf/log4j2.yaml) +OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager $OPTS" # Required to allow sun.misc.Unsafe on JDK 24 without warnings # Also required for enabling unsafe memory access for Netty since 4.1.121.Final if [[ $JAVA_MAJOR_VERSION -ge 23 ]]; then @@ -167,6 +170,3 @@ OPTS="$OPTS -Dpulsar.log.level=$PULSAR_LOG_LEVEL" OPTS="$OPTS -Dpulsar.log.root.level=$PULSAR_LOG_ROOT_LEVEL" OPTS="$OPTS -Dpulsar.log.immediateFlush=$PULSAR_LOG_IMMEDIATE_FLUSH" OPTS="$OPTS -Dpulsar.routing.appender.default=$PULSAR_ROUTING_APPENDER_DEFAULT" -# Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries -# (Jersey, gRPC, Guava, etc.) are bridged into the Log4j2 configuration (conf/log4j2.yaml) -OPTS="$OPTS -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" diff --git a/bin/pulsar-perf b/bin/pulsar-perf index cd331d90a568e..3c0ca100a6b89 100755 --- a/bin/pulsar-perf +++ b/bin/pulsar-perf @@ -94,6 +94,9 @@ fi PULSAR_CLASSPATH="$PULSAR_JAR:$PULSAR_CLASSPATH:$PULSAR_EXTRA_CLASSPATH" PULSAR_CLASSPATH="`dirname $PULSAR_LOG_CONF`:$PULSAR_CLASSPATH" OPTS="-Djava.net.preferIPv4Stack=true $OPTS -Dlog4j.configurationFile=`basename $PULSAR_LOG_CONF`" +# Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries +# (Jersey, gRPC, Guava, etc.) are bridged into the Log4j2 configuration (conf/log4j2.yaml) +OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager $OPTS" # Required to allow sun.misc.Unsafe on JDK 24 without warnings # Also required for enabling unsafe memory access for Netty since 4.1.121.Final if [[ $JAVA_MAJOR_VERSION -ge 23 ]]; then @@ -166,9 +169,6 @@ fi OPTS="$OPTS -Dpulsar.log.immediateFlush=$PULSAR_LOG_IMMEDIATE_FLUSH" OPTS="$OPTS -Dpulsar.log.dir=$PULSAR_LOG_DIR" OPTS="$OPTS -Dpulsar.log.file=$PULSAR_LOG_FILE" -# Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries -# (Jersey, gRPC, Guava, etc.) are bridged into the Log4j2 configuration (conf/log4j2.yaml) -OPTS="$OPTS -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" #Change to PULSAR_HOME to support relative paths cd "$PULSAR_HOME" From 47b426d2ba9bde2cb49612c9fd4f1d6d265345d2 Mon Sep 17 00:00:00 2001 From: ninjazhou <843520313@qq.com> Date: Tue, 25 Aug 2026 12:47:13 +0800 Subject: [PATCH 08/10] fix(scripts): unify JUL bridge comments across all bin scripts --- bin/bookkeeper | 3 ++- bin/function-localrunner | 3 ++- bin/pulsar | 1 + bin/pulsar-admin-common.cmd | 3 ++- .../org/apache/pulsar/functions/runtime/RuntimeUtils.java | 4 ++-- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/bin/bookkeeper b/bin/bookkeeper index 00b8cecf234cc..096beb6ec23dd 100755 --- a/bin/bookkeeper +++ b/bin/bookkeeper @@ -196,7 +196,8 @@ OPTS="-cp $BOOKIE_CLASSPATH $OPTS" # Disable ipv6 as it can cause issues OPTS="-Djava.net.preferIPv4Stack=true $OPTS" -# Enable JUL-to-Log4j2 bridge so that JUL records are routed through Log4j2 +# Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries +# (Jersey, gRPC, Guava, etc.) are bridged into the Log4j2 configuration (conf/log4j2.yaml) OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager $OPTS" # Required to allow sun.misc.Unsafe on JDK 24 without warnings # Also required for enabling unsafe memory access for Netty since 4.1.121.Final diff --git a/bin/function-localrunner b/bin/function-localrunner index 92a1de7dd7b71..45930ca64eca4 100755 --- a/bin/function-localrunner +++ b/bin/function-localrunner @@ -121,7 +121,8 @@ fi # rarely needed when trying to list many z-nodes under a # directory) OPTS="-Djava.net.preferIPv4Stack=true $OPTS -Djute.maxbuffer=10485760" -# Enable JUL-to-Log4j2 bridge so that JUL records are routed through Log4j2 +# Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries +# (Jersey, gRPC, Guava, etc.) are bridged into the Log4j2 configuration (conf/log4j2.yaml) OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager $OPTS" # Required to allow sun.misc.Unsafe on JDK 24 without warnings # Also required for enabling unsafe memory access for Netty since 4.1.121.Final diff --git a/bin/pulsar b/bin/pulsar index 30d527e997241..d433d2bff7867 100755 --- a/bin/pulsar +++ b/bin/pulsar @@ -361,6 +361,7 @@ OPTS="$OPTS -Dpulsar.log.immediateFlush=$PULSAR_LOG_IMMEDIATE_FLUSH" OPTS="$OPTS -Dpulsar.routing.appender.default=$PULSAR_ROUTING_APPENDER_DEFAULT" # Configure log4j2 to disable servlet webapp detection so that Garbage free logging can be used OPTS="$OPTS -Dlog4j2.is.webapp=false" + # Functions related logging OPTS="$OPTS -Dpulsar.functions.process.container.log.dir=$PULSAR_LOG_DIR" # instance diff --git a/bin/pulsar-admin-common.cmd b/bin/pulsar-admin-common.cmd index d28aea4bd8d2f..abd14edd80dd4 100644 --- a/bin/pulsar-admin-common.cmd +++ b/bin/pulsar-admin-common.cmd @@ -61,7 +61,8 @@ set "PULSAR_CLASSPATH=%PULSAR_CLASSPATH%;%PULSAR_LOG_CONF_DIR%" set "OPTS=%OPTS% -Dlog4j.configurationFile="%PULSAR_LOG_CONF_BASENAME%"" set "OPTS=-Djava.net.preferIPv4Stack=true %OPTS%" -REM Enable JUL-to-Log4j2 bridge so that JUL records are routed through Log4j2 +REM Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries +REM (Jersey, gRPC, Guava, etc.) are bridged into the Log4j2 configuration (conf/log4j2.yaml) set "OPTS=-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager %OPTS%" REM Allow Netty to use reflection access diff --git a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeUtils.java b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeUtils.java index f40728e764526..45f687b27ca94 100644 --- a/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeUtils.java +++ b/pulsar-functions/runtime/src/main/java/org/apache/pulsar/functions/runtime/RuntimeUtils.java @@ -359,8 +359,8 @@ public static List getCmd(InstanceConfig instanceConfig, args.add(String.format("-D%s=%s", FUNCTIONS_INSTANCE_CLASSPATH, systemFunctionInstanceClasspath)); } args.add("-Dlog4j.configurationFile=" + logConfigFile); - // Enable the JUL-to-Log4j2 bridge so that JUL records (e.g. from gRPC, Guava) - // are routed through Log4j2 in function instance JVMs. + // Bridge java.util.logging (JUL) to Log4j2 so that JUL logs from third-party libraries + // (Jersey, gRPC, Guava, etc.) are bridged into the Log4j2 configuration args.add("-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"); // Use a single LoggerContext for the function process. The default // ClassLoaderContextSelector creates separate contexts per classloader, From 6e9c307d9de0915c7ee2755710ecafe26d1b6f89 Mon Sep 17 00:00:00 2001 From: ninjazhou <843520313@qq.com> Date: Tue, 25 Aug 2026 15:50:57 +0800 Subject: [PATCH 09/10] fix(test): update ProcessRuntimeTest to account for new JUL bridge JVM argument --- .../functions/runtime/process/ProcessRuntimeTest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/runtime/process/ProcessRuntimeTest.java b/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/runtime/process/ProcessRuntimeTest.java index 4f5dbe0404b52..df968eae7c6f8 100644 --- a/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/runtime/process/ProcessRuntimeTest.java +++ b/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/runtime/process/ProcessRuntimeTest.java @@ -299,7 +299,7 @@ private void verifyJavaInstance(InstanceConfig config, Path depsDir, String webS String extraDepsEnv; int portArg; int metricsPortArg; - int totalArgCount = 55; + int totalArgCount = 56; if (webServiceUrl != null && config.isExposePulsarAdminClientEnabled()) { totalArgCount += 3; } @@ -307,13 +307,13 @@ private void verifyJavaInstance(InstanceConfig config, Path depsDir, String webS assertEquals(args.size(), totalArgCount); extraDepsEnv = " -Dpulsar.functions.extra.dependencies.dir=" + depsDir; classpath = classpath + ":" + depsDir + "/*"; - portArg = 38; - metricsPortArg = 40; + portArg = 39; + metricsPortArg = 41; } else { assertEquals(args.size(), totalArgCount - 1); extraDepsEnv = ""; - portArg = 37; - metricsPortArg = 39; + portArg = 38; + metricsPortArg = 40; } if (webServiceUrl != null && config.isExposePulsarAdminClientEnabled()) { portArg += 3; @@ -327,6 +327,7 @@ private void verifyJavaInstance(InstanceConfig config, Path depsDir, String webS + extraDepsEnv + " -Dpulsar.functions.instance.classpath=/pulsar/lib/*" + " -Dlog4j.configurationFile=java_instance_log4j2.xml" + + " -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" + " -Dlog4j2.contextSelector=org.apache.logging.log4j.core.selector.BasicContextSelector " + "-Dpulsar.function.log.dir=" + logDirectory + "/functions/" + FunctionCommon.getFullyQualifiedName(config.getFunctionDetails()) From 3ebd740101eaf0e6eeab7267ae63ccc500c1bf20 Mon Sep 17 00:00:00 2001 From: ninjazhou <843520313@qq.com> Date: Tue, 25 Aug 2026 16:33:56 +0800 Subject: [PATCH 10/10] fix(test): update KubernetesRuntimeTest to account for new JUL bridge JVM argument --- .../runtime/kubernetes/KubernetesRuntimeTest.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/runtime/kubernetes/KubernetesRuntimeTest.java b/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/runtime/kubernetes/KubernetesRuntimeTest.java index 9efa09a5093f2..a586a86b32d39 100644 --- a/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/runtime/kubernetes/KubernetesRuntimeTest.java +++ b/pulsar-functions/runtime/src/test/java/org/apache/pulsar/functions/runtime/kubernetes/KubernetesRuntimeTest.java @@ -485,14 +485,14 @@ private void verifyJavaInstance(InstanceConfig config, String depsDir, boolean s if (null != depsDir) { extraDepsEnv = " -Dpulsar.functions.extra.dependencies.dir=" + depsDir; classpath = classpath + ":" + depsDir + "/*"; - totalArgs = 53; - portArg = 40; - metricsPortArg = 42; + totalArgs = 54; + portArg = 41; + metricsPortArg = 43; } else { extraDepsEnv = ""; - portArg = 39; - metricsPortArg = 41; - totalArgs = 52; + portArg = 40; + metricsPortArg = 42; + totalArgs = 53; } if (secretsAttached) { totalArgs += 4; @@ -520,6 +520,7 @@ private void verifyJavaInstance(InstanceConfig config, String depsDir, boolean s + extraDepsEnv + " -Dpulsar.functions.instance.classpath=/pulsar/lib/*" + " -Dlog4j.configurationFile=kubernetes_instance_log4j2.xml" + + " -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" + " -Dlog4j2.contextSelector=org.apache.logging.log4j.core.selector.BasicContextSelector " + "-Dpulsar.function.log.dir=" + logDirectory + "/" + FunctionCommon.getFullyQualifiedName(config.getFunctionDetails())