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