This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new f3e62d5733dc CAMEL-26429: camel-jbang - A CLI command that turns the 
logging off configures log4j only when something logs (#28764)
f3e62d5733dc is described below

commit f3e62d5733dc55f5ba51ee46a544343cfe784705
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 31 10:25:35 2026 +0100

    CAMEL-26429: camel-jbang - A CLI command that turns the logging off 
configures log4j only when something logs (#28764)
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../src/main/resources/log4j2-off.properties       |  36 +++++
 .../jbang/core/common/LogConfigurationTest.java    |   1 +
 .../common/RuntimeUtilDeferredLoggingTest.java     | 156 +++++++++++++++++++++
 .../camel/dsl/jbang/run/common/RuntimeUtil.java    |  63 ++++++++-
 4 files changed, 254 insertions(+), 2 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/log4j2-off.properties 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/log4j2-off.properties
new file mode 100644
index 000000000000..32aefb0dcf6f
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/log4j2-off.properties
@@ -0,0 +1,36 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+
+# log4j2-no-color.properties with the logging off. A command that turns the 
logging off does not configure log4j:
+# log4j configures itself with this file when something first logs (see 
RuntimeUtil.configureLog).
+name = CamelJBangLoggingOff
+
+appender.stdout.type = Console
+appender.stdout.name = out
+appender.stdout.layout.type = PatternLayout
+appender.stdout.layout.pattern = %d{yyyy-MM-dd HH:mm:ss.SSS} %5p %pid --- 
[%15.15t] %-35.35c : %m%n
+
+appender.file.type = File
+appender.file.name = file
+appender.file.fileName = 
${sys:camel.jbang.logDir:-${sys:user.home}${sys:file.separator}.camel}${sys:file.separator}${sys:pid}.log
+appender.file.createOnDemand = true
+appender.file.append = false
+appender.file.layout.type = PatternLayout
+appender.file.layout.pattern = %d{yyyy-MM-dd HH:mm:ss.SSS} %5p %pid --- 
[%15.15t] %-35.35c : %m%n
+
+rootLogger = OFF,out,file
+rootLogger.level = OFF
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/LogConfigurationTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/LogConfigurationTest.java
index 367890b7364a..a63e72824af2 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/LogConfigurationTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/LogConfigurationTest.java
@@ -66,6 +66,7 @@ class LogConfigurationTest {
     @CsvSource({
             "log4j2-color.properties, PID.log",
             "log4j2-no-color.properties, PID.log",
+            "log4j2-off.properties, PID.log",
             "log4j2-export.properties, camel-export.log",
             "log4j2-script.properties, camel-script.log" })
     void theLogFileIsInTheCamelDirectoryOfTheCli(String config, String file) 
throws Exception {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtilDeferredLoggingTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtilDeferredLoggingTest.java
new file mode 100644
index 000000000000..09c0c27ce93c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtilDeferredLoggingTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.camel.dsl.jbang.core.common;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.dsl.jbang.run.common.RuntimeUtil;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * A command that turns the logging off does not configure log4j, which takes 
about 200 ms, as most commands log
+ * nothing: log4j configures itself, with the logging off, when something 
first logs. Each case runs in a new JVM, as
+ * log4j is configured once in a JVM.
+ */
+class RuntimeUtilDeferredLoggingTest {
+
+    @TempDir
+    Path dir;
+
+    @Test
+    void theLoggingOffDoesNotConfigureLog4j() throws Exception {
+        assertThat(run("off")).containsExactly("configured=false");
+    }
+
+    @Test
+    void log4jConfiguresItselfWithTheLoggingOffWhenSomethingLogs() throws 
Exception {
+        assertThat(run("off-then-log"))
+                .containsExactly("configured=true", 
"name=CamelJBangLoggingOff", "root=OFF", "appenders=[file, out]");
+    }
+
+    @Test
+    void 
theCommandThatConfiguresTheLoggingAgainLogsWithTheConfigurationItStartedWith() 
throws Exception {
+        // as when log4j was configured at first: the appenders of the 
configuration the command started with
+        assertThat(run("off-then-info")).containsExactly("root=INFO", 
"appenders=[file, out]");
+        assertThat(run("off-then-log-then-info")).containsExactly("root=INFO", 
"appenders=[file, out]");
+    }
+
+    @Test
+    void theNextCommandOfACliThatRunsSeveralCommandsLogsAsOnItsOwn() throws 
Exception {
+        
assertThat(run("off-then-new-command-off")).containsExactly("configured=false");
+        
assertThat(run("off-then-new-command-export")).containsExactly("root=INFO", 
"appenders=[file]");
+    }
+
+    @Test
+    void whenSomethingLoggedBeforeTheLoggingIsOffAtOnce() throws Exception {
+        assertThat(run("log-then-off")).containsExactly("configured=true", 
"root=OFF");
+    }
+
+    // the lines the scenario prints: nothing that is logged reaches the 
console
+    private List<String> run(String scenario) throws Exception {
+        String java = Path.of(System.getProperty("java.home"), "bin", 
"java").toString();
+        Path out = dir.resolve(scenario + ".out");
+        Process process = new ProcessBuilder(
+                java, "-cp", System.getProperty("java.class.path"), 
"-Duser.home=" + dir, Scenario.class.getName(),
+                scenario)
+                .redirectErrorStream(true)
+                .redirectOutput(out.toFile())
+                .start();
+        assertThat(process.waitFor(30, TimeUnit.SECONDS)).isTrue();
+        List<String> lines = Files.readAllLines(out, StandardCharsets.UTF_8);
+        assertThat(process.exitValue()).as("exit code, output: " + 
lines).isZero();
+        return lines;
+    }
+
+    /**
+     * Configures the logging as the commands of the CLI do, and prints what 
log4j is configured with.
+     */
+    public static final class Scenario {
+
+        public static void main(String... args) throws Exception {
+            switch (args[0]) {
+                case "off" -> {
+                    off();
+                    printConfigured();
+                }
+                case "off-then-log" -> {
+                    off();
+                    LogManager.getLogger("test").error("not printed");
+                    printConfigured();
+                    LoggerContext context = (LoggerContext) 
LogManager.getContext(false);
+                    System.out.println("name=" + 
context.getConfiguration().getName());
+                    printRootAndAppenders();
+                }
+                case "off-then-info" -> {
+                    off();
+                    RuntimeUtil.configureLog("info", false, false, false, 
true, null, null);
+                    printRootAndAppenders();
+                }
+                case "off-then-log-then-info" -> {
+                    off();
+                    LogManager.getLogger("test").error("not printed");
+                    RuntimeUtil.configureLog("info", false, false, false, 
true, null, null);
+                    printRootAndAppenders();
+                }
+                case "off-then-new-command-off" -> {
+                    off();
+                    RuntimeUtil.newCommand();
+                    off();
+                    printConfigured();
+                }
+                case "off-then-new-command-export" -> {
+                    off();
+                    RuntimeUtil.newCommand();
+                    RuntimeUtil.configureLog("info", false, false, false, 
true, null, null);
+                    printRootAndAppenders();
+                }
+                case "log-then-off" -> {
+                    LogManager.getLogger("test").debug("before");
+                    off();
+                    printConfigured();
+                    System.out.println("root=" + ((LoggerContext) 
LogManager.getContext(false)).getRootLogger().getLevel());
+                }
+                default -> throw new IllegalArgumentException(args[0]);
+            }
+        }
+
+        private static void off() throws Exception {
+            RuntimeUtil.configureLog("off", false, false, false, false, null, 
null);
+        }
+
+        private static void printConfigured() {
+            System.out.println("configured=" + LogManager.getFactory()
+                    .hasContext(RuntimeUtil.class.getName(), 
RuntimeUtil.class.getClassLoader(), false));
+        }
+
+        private static void printRootAndAppenders() {
+            LoggerContext context = (LoggerContext) 
LogManager.getContext(false);
+            System.out.println("root=" + context.getRootLogger().getLevel());
+            System.out.println("appenders=" + new 
TreeSet<>(context.getConfiguration().getAppenders().keySet()));
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/common/RuntimeUtil.java
 
b/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/common/RuntimeUtil.java
index 40d300ca9005..8d84034ed326 100644
--- 
a/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/common/RuntimeUtil.java
+++ 
b/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/common/RuntimeUtil.java
@@ -32,6 +32,8 @@ import org.apache.camel.util.IOHelper;
 import org.apache.camel.util.OrderedProperties;
 import org.apache.camel.util.StringHelper;
 import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LoggerContext;
 import org.apache.logging.log4j.core.config.Configurator;
 
 public final class RuntimeUtil {
@@ -50,8 +52,28 @@ public final class RuntimeUtil {
      */
     static final String COLOR_CONFIG = "log4j2-color.properties";
 
+    /**
+     * The logging configuration of the CLI without color, which a command 
that turns the logging off starts with.
+     */
+    static final String NO_COLOR_CONFIG = "log4j2-no-color.properties";
+
+    /**
+     * The logging configuration that log4j configures itself with when 
something first logs, after a command turned the
+     * logging off: the one without color, with the logging off.
+     */
+    static final String OFF_CONFIG = "log4j2-off.properties";
+
+    // the name of the configuration in OFF_CONFIG
+    private static final String OFF_CONFIG_NAME = "CamelJBangLoggingOff";
+
+    // the system property with the configuration log4j configures itself with
+    private static final String CONFIGURATION_FILE = 
"log4j2.configurationFile";
+
     // the logging configuration log4j was configured with, or null when not 
yet
     private static String configured;
+    // the logging configuration of a command that turned the logging off, 
which log4j is not configured with yet: log4j
+    // configures itself with OFF_CONFIG when something first logs (see 
configureLog)
+    private static String deferred;
     // whether the command that runs next may configure the logging again (see 
newCommand)
     private static boolean newCommand;
 
@@ -133,7 +155,7 @@ public final class RuntimeUtil {
         } else if (color) {
             location = COLOR_CONFIG;
         } else {
-            location = "log4j2-no-color.properties";
+            location = NO_COLOR_CONFIG;
         }
 
         synchronized (RuntimeUtil.class) {
@@ -145,9 +167,26 @@ public final class RuntimeUtil {
                 System.setProperty("pid", Long.toString(pid));
                 // make it possible to manage log4j via JMX and camel-cli
                 System.setProperty("log4j2.disable.jmx", "false");
+                if (isOff(level, location) && !isLog4jConfigured()) {
+                    // most commands log nothing with the logging off, and 
log4j takes about 200 ms to configure: log4j
+                    // configures itself, with the logging off, when something 
first logs
+                    System.setProperty(CONFIGURATION_FILE, OFF_CONFIG);
+                    deferred = location;
+                    configured = location;
+                    newCommand = false;
+                    return;
+                }
                 Configurator.initialize("CamelJBang", withoutHash(location));
                 configured = location;
-            } else if (newCommand && !configured.equals(location)) {
+            } else if (deferred != null && isOff(level, location)) {
+                // the logging is still off
+                newCommand = false;
+                return;
+            } else if (deferred != null) {
+                // configure log4j as the command that turned the logging off 
would have, before this configuration
+                configureDeferred();
+            }
+            if (newCommand && !configured.equals(location)) {
                 // a CLI that runs several commands in this JVM (camel ask, 
camel shell, the monitor) runs one that
                 // logs otherwise, such as export to its log file: log4j is 
configured once, so configure it again
                 Configurator.reconfigure(toUri(withoutHash(location)));
@@ -160,6 +199,26 @@ public final class RuntimeUtil {
         setRootLoggingLevel(level);
     }
 
+    private static boolean isOff(String level, String location) {
+        return "off".equalsIgnoreCase(level) && 
NO_COLOR_CONFIG.equals(location);
+    }
+
+    // whether something logged already, so log4j configured itself
+    private static boolean isLog4jConfigured() {
+        return LogManager.getFactory().hasContext(RuntimeUtil.class.getName(), 
RuntimeUtil.class.getClassLoader(), false);
+    }
+
+    private static void configureDeferred() throws URISyntaxException {
+        System.clearProperty(CONFIGURATION_FILE);
+        LoggerContext context = Configurator.initialize("CamelJBang", 
deferred);
+        if (context != null && 
OFF_CONFIG_NAME.equals(context.getConfiguration().getName())) {
+            // log4j configured itself, as something logged
+            Configurator.reconfigure(toUri(deferred));
+        }
+        Configurator.setRootLevel(Level.OFF);
+        deferred = null;
+    }
+
     /**
      * A CLI that runs several commands in this JVM (camel ask, camel shell, 
the monitor) runs another one: its first
      * logging configuration is applied, as for a command run on its own, also 
when log4j was configured otherwise by a

Reply via email to