From a07cfd53e4a3122dc83c4ad36b96f6f6fc78375c Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Fri, 25 Jul 2025 14:00:04 -0700 Subject: [PATCH 01/36] logback-core: fix spelling errors (#956) --- .../main/java/ch/qos/logback/core/AsyncAppenderBase.java | 6 +++--- .../src/main/java/ch/qos/logback/core/util/ContextUtil.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/logback-core/src/main/java/ch/qos/logback/core/AsyncAppenderBase.java b/logback-core/src/main/java/ch/qos/logback/core/AsyncAppenderBase.java index a51f6efec3..ee577f85f7 100755 --- a/logback-core/src/main/java/ch/qos/logback/core/AsyncAppenderBase.java +++ b/logback-core/src/main/java/ch/qos/logback/core/AsyncAppenderBase.java @@ -113,8 +113,8 @@ public void start() { addInfo("Setting discardingThreshold to " + discardingThreshold); worker.setDaemon(true); worker.setName("AsyncAppender-Worker-" + getName()); - // make sure this instance is marked as "started" before staring the worker - // Thread + // make sure this instance is marked as "started" before starting the + // worker Thread super.start(); worker.start(); } @@ -244,7 +244,7 @@ public boolean isNeverBlock() { * BlockingQueue#remainingCapacity()} * * @return the remaining capacity - * + * */ public int getRemainingCapacity() { return blockingQueue.remainingCapacity(); diff --git a/logback-core/src/main/java/ch/qos/logback/core/util/ContextUtil.java b/logback-core/src/main/java/ch/qos/logback/core/util/ContextUtil.java index 4ad3abb283..54883d9375 100755 --- a/logback-core/src/main/java/ch/qos/logback/core/util/ContextUtil.java +++ b/logback-core/src/main/java/ch/qos/logback/core/util/ContextUtil.java @@ -85,10 +85,10 @@ public void addFrameworkPackage(List frameworkPackages, String packageNa public void addOrReplaceShutdownHook(ShutdownHook hook) { Runtime runtime = Runtime.getRuntime(); - Thread oldShutdownHookTread = (Thread) context.getObject(CoreConstants.SHUTDOWN_HOOK_THREAD); - if(oldShutdownHookTread != null) { + Thread oldShutdownHookThread = (Thread) context.getObject(CoreConstants.SHUTDOWN_HOOK_THREAD); + if(oldShutdownHookThread != null) { addInfo("Removing old shutdown hook from JVM runtime"); - runtime.removeShutdownHook(oldShutdownHookTread); + runtime.removeShutdownHook(oldShutdownHookThread); } Thread hookThread = new Thread(hook, "Logback shutdown hook [" + context.getName() + "]"); From 61f6a2544f36b3016e0efd434ee21f19269f1df7 Mon Sep 17 00:00:00 2001 From: ceki Date: Mon, 29 Sep 2025 19:28:08 +0200 Subject: [PATCH 02/36] disallow new in if condition attribute in config files Signed-off-by: ceki --- .../blackboxInput/joran/conditional/ifNew.xml | 14 +++++++++++++ .../joran/conditional/IfThenElseTest.java | 11 +++++++++- .../processor/conditional/IfModelHandler.java | 20 ++++++++++++++++--- 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifNew.xml diff --git a/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifNew.xml b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifNew.xml new file mode 100644 index 0000000000..fae14c7484 --- /dev/null +++ b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifNew.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/joran/conditional/IfThenElseTest.java b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/joran/conditional/IfThenElseTest.java index d993418962..72e9d66cda 100644 --- a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/joran/conditional/IfThenElseTest.java +++ b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/joran/conditional/IfThenElseTest.java @@ -51,7 +51,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.Stack; @@ -129,6 +128,16 @@ public void whenContextPropertyIsSet_IfThenBranchIsEvaluated() throws JoranExcep verifyConfig(new String[] { "BEGIN", "a", "END" }); } + @Test + public void ifWithNew() throws JoranException { + context.putProperty(ki1, val1); + simpleConfigurator.doConfigure(CONDITIONAL_DIR_PREFIX + "ifNew.xml"); + checker.containsMatch(Status.ERROR, IfModelHandler.NEW_OPERATOR_DISALLOWED_MSG); + checker.containsMatch(Status.ERROR, IfModelHandler.NEW_OPERATOR_DISALLOWED_SEE); + verifyConfig(new String[] { "BEGIN", "END" }); + } + + @Test public void whenLocalPropertyIsSet_IfThenBranchIsEvaluated() throws JoranException { simpleConfigurator.doConfigure(CONDITIONAL_DIR_PREFIX + "if_localProperty.xml"); diff --git a/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/IfModelHandler.java b/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/IfModelHandler.java index 0b9a39a375..5bc101dbc7 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/IfModelHandler.java +++ b/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/IfModelHandler.java @@ -1,6 +1,6 @@ /** * Logback: the reliable, generic, fast and flexible logging framework. - * Copyright (C) 1999-2022, QOS.ch. All rights reserved. + * Copyright (C) 1999-2025, QOS.ch. All rights reserved. * * This program and the accompanying materials are dual-licensed under * either the terms of the Eclipse Public License v1.0 as published by @@ -33,6 +33,9 @@ public class IfModelHandler extends ModelHandlerBase { public static final String MISSING_JANINO_MSG = "Could not find Janino library on the class path. Skipping conditional processing."; public static final String MISSING_JANINO_SEE = "See also " + CoreConstants.CODES_URL + "#ifJanino"; + public static final String NEW_OPERATOR_DISALLOWED_MSG = "The 'condition' attribute may not contain the 'new' operator."; + public static final String NEW_OPERATOR_DISALLOWED_SEE = "See also " + CoreConstants.CODES_URL + "#conditionNew"; + enum Branch {IF_BRANCH, ELSE_BRANCH; } IfModel ifModel = null; @@ -75,6 +78,13 @@ public void handle(ModelInterpretationContext mic, Model model) throws ModelHand return; } + // do not allow 'new' operator + if(hasNew(conditionStr)) { + addError(NEW_OPERATOR_DISALLOWED_MSG); + addError(NEW_OPERATOR_DISALLOWED_SEE); + return; + } + try { PropertyEvalScriptBuilder pesb = new PropertyEvalScriptBuilder(mic); pesb.setContext(context); @@ -96,8 +106,12 @@ public void handle(ModelInterpretationContext mic, Model model) throws ModelHand } } } - - + + private boolean hasNew(String conditionStr) { + return conditionStr.contains("new "); + } + + @Override public void postHandle(ModelInterpretationContext mic, Model model) throws ModelHandlerException { From c76fed3c01f389e4c18db914bcba1e72bccc2d1e Mon Sep 17 00:00:00 2001 From: Ralf Wiebicke Date: Mon, 29 Sep 2025 19:28:57 +0200 Subject: [PATCH 03/36] ViewStatusMessagesServlet requires method POST for button 'Clear' (#971) The button 'Clear' has a side-effect and should not work with GET, as GET is considered a Safe Method not taking an action other than retrieval. https://www.rfc-editor.org/rfc/rfc2616#section-9.1.1 I'd like to restrict users from doing any changes by restricting them to method GET. With that said one might consider this change as a security fix. Signed-off-by: Ralf Wiebicke --- .../qos/logback/core/status/ViewStatusMessagesServletBase.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logback-core/src/main/java/ch/qos/logback/core/status/ViewStatusMessagesServletBase.java b/logback-core/src/main/java/ch/qos/logback/core/status/ViewStatusMessagesServletBase.java index f7f1a88a1e..353ae6b8ef 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/status/ViewStatusMessagesServletBase.java +++ b/logback-core/src/main/java/ch/qos/logback/core/status/ViewStatusMessagesServletBase.java @@ -62,7 +62,7 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws output.append(""); output.append("\r\n"); - if (CLEAR.equalsIgnoreCase(req.getParameter(SUBMIT))) { + if ("POST".equals(req.getMethod()) && CLEAR.equalsIgnoreCase(req.getParameter(SUBMIT))) { sm.clear(); sm.add(new InfoStatus("Cleared all status messages", this)); } From 8d2262d3c5227f209905ac1705a3333ebd8a33c8 Mon Sep 17 00:00:00 2001 From: ceki Date: Mon, 29 Sep 2025 22:52:11 +0200 Subject: [PATCH 04/36] soften warning on using ConsoleAppender Signed-off-by: ceki --- .../qos/logback/core/blackbox}/COWArrayListConcurrencyTest.java | 0 .../src/main/java/ch/qos/logback/core/ConsoleAppender.java | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {logback-core/src/test/java/ch/qos/logback/core/util => logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox}/COWArrayListConcurrencyTest.java (100%) diff --git a/logback-core/src/test/java/ch/qos/logback/core/util/COWArrayListConcurrencyTest.java b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/COWArrayListConcurrencyTest.java similarity index 100% rename from logback-core/src/test/java/ch/qos/logback/core/util/COWArrayListConcurrencyTest.java rename to logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/COWArrayListConcurrencyTest.java diff --git a/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java b/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java index 8200b05015..eec96b5240 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java +++ b/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java @@ -88,7 +88,7 @@ private void targetWarn(String val) { @Override public void start() { - addInfo("BEWARE: Writing to the console can be very slow. Avoid logging to the "); + addInfo("NOTE: Writing to the console can be slow. Try to avoid logging to the "); addInfo("console in production environments, especially in high volume systems."); addInfo("See also "+CONSOLE_APPENDER_WARNING_URL); OutputStream targetStream = target.getStream(); From 7f653409c95e40efd79b2b1bbeefde6dd649ceab Mon Sep 17 00:00:00 2001 From: ceki Date: Mon, 29 Sep 2025 22:53:09 +0200 Subject: [PATCH 05/36] minor changes Signed-off-by: ceki --- .../logback/core/rolling/RenameUtilTest.java | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/logback-core/src/test/java/ch/qos/logback/core/rolling/RenameUtilTest.java b/logback-core/src/test/java/ch/qos/logback/core/rolling/RenameUtilTest.java index 1460ecd77f..e8fdf16728 100755 --- a/logback-core/src/test/java/ch/qos/logback/core/rolling/RenameUtilTest.java +++ b/logback-core/src/test/java/ch/qos/logback/core/rolling/RenameUtilTest.java @@ -21,8 +21,10 @@ import ch.qos.logback.core.testUtil.CoreTestConstants; import ch.qos.logback.core.testUtil.RandomUtil; import ch.qos.logback.core.status.testUtil.StatusChecker; +import ch.qos.logback.core.util.EnvUtil; import ch.qos.logback.core.util.StatusPrinter; +import ch.qos.logback.core.util.StatusPrinter2; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -32,6 +34,7 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.channels.FileLock; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -41,6 +44,7 @@ public class RenameUtilTest { Encoder encoder; Context context = new ContextBase(); StatusChecker statusChecker = new StatusChecker(context); + StatusPrinter2 statusPrinter2 = new StatusPrinter2(); long currentTime = System.currentTimeMillis(); int diff = RandomUtil.getPositiveInt(); @@ -66,12 +70,18 @@ public void renameToNonExistingDirectory() throws IOException, RolloverFailure { String randomTARGETDir = CoreTestConstants.OUTPUT_DIR_PREFIX + diff2; renameUtil.rename(fromFile.toString(), new File(randomTARGETDir + "/to.test").toString()); - StatusPrinter.printInCaseOfErrorsOrWarnings(context); + statusPrinter2.printInCaseOfErrorsOrWarnings(context); assertTrue(statusChecker.isErrorFree(0)); } @Test // LOGBACK-1054 public void renameLockedAbstractFile_LOGBACK_1054() throws IOException, RolloverFailure { + + // this tests only works on windows + if(!EnvUtil.isWindows()) { + return; + } + RenameUtil renameUtil = new RenameUtil(); renameUtil.setContext(context); @@ -82,12 +92,15 @@ public void renameLockedAbstractFile_LOGBACK_1054() throws IOException, Rollover makeFile(src); - FileInputStream fisLock = new FileInputStream(src); + // open file in a way preventing simple rename in order to force call to + // areOnDifferentVolumes() method. This only works on Windows + + FileInputStream fis = new FileInputStream(src); renameUtil.rename(src, target); // release the lock - fisLock.close(); + fis.close(); - StatusPrinter.print(context); + statusPrinter2.print(context); assertEquals(0, statusChecker.matchCount("Parent of target file ." + target + ". is null")); } @@ -101,7 +114,7 @@ public void MANUAL_renamingOnDifferentVolumesOnLinux() throws IOException, Rollo makeFile(src); renameUtil.rename(src, "/tmp/foo" + diff + ".txt"); - StatusPrinter.print(context); + statusPrinter2.print(context); } @Test @@ -114,7 +127,7 @@ public void MANUAL_renamingOnDifferentVolumesOnWindows() throws IOException, Rol makeFile(src); renameUtil.rename(src, "d:/tmp/foo" + diff + ".txt"); - StatusPrinter.print(context); + statusPrinter2.print(context); assertTrue(statusChecker.isErrorFree(0)); } From 81160699fcecbefdecf79ea44c0f7f2877d9eb8d Mon Sep 17 00:00:00 2001 From: ceki Date: Mon, 29 Sep 2025 22:53:44 +0200 Subject: [PATCH 06/36] comment out code in COWArrayListConcurrencyTest to make IDE happy Signed-off-by: ceki --- .../blackbox/COWArrayListConcurrencyTest.java | 330 +++++++++--------- 1 file changed, 165 insertions(+), 165 deletions(-) diff --git a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/COWArrayListConcurrencyTest.java b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/COWArrayListConcurrencyTest.java index 8d8a2e7261..683cd67be7 100644 --- a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/COWArrayListConcurrencyTest.java +++ b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/COWArrayListConcurrencyTest.java @@ -12,177 +12,177 @@ * as published by the Free Software Foundation. */ -package ch.qos.logback.core.util; +package ch.qos.logback.core.blackbox; -import ch.qos.logback.core.Appender; -import ch.qos.logback.core.AppenderBase; -import ch.qos.logback.core.Context; -import ch.qos.logback.core.ContextBase; -import ch.qos.logback.core.spi.AppenderAttachableImpl; +//import ch.qos.logback.core.Appender; +//import ch.qos.logback.core.AppenderBase; +//import ch.qos.logback.core.Context; +//import ch.qos.logback.core.ContextBase; +//import ch.qos.logback.core.spi.AppenderAttachableImpl; import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.Future; -import java.util.concurrent.locks.ReentrantLock; - -import static org.assertj.core.api.Fail.fail; +//import org.junit.jupiter.api.Test; +// +//import java.util.concurrent.ExecutionException; +//import java.util.concurrent.ExecutorService; +//import java.util.concurrent.Executors; +// +//import java.util.ArrayList; +//import java.util.Arrays; +//import java.util.List; +//import java.util.concurrent.Future; +//import java.util.concurrent.locks.ReentrantLock; +// +//import static org.assertj.core.api.Fail.fail; @Disabled public class COWArrayListConcurrencyTest { - - //private static final int LIST_SIZE = 1_000_000; - private static final int LOOP_LEN = 1_0; - private static final int RECONFIGURE_DELAY = 1; - - ReentrantLock reconfigureLock = new ReentrantLock(true); - ReentrantLock writeLock = new ReentrantLock(true); - - private static int THREAD_COUNT = 200; //Runtime.getRuntime().availableProcessors()*200; - //private static int THREAD_COUNT = 5000; - - private final ExecutorService tasksExecutor = Executors.newVirtualThreadPerTaskExecutor(); - LoopingRunnable[] loopingThreads = new LoopingRunnable[THREAD_COUNT]; - ReconfiguringThread[] reconfiguringThreads = new ReconfiguringThread[THREAD_COUNT]; - Future[] futures = new Future[THREAD_COUNT]; - - AppenderAttachableImpl aai = new AppenderAttachableImpl<>(); - Context context = new ContextBase(); - - void reconfigureWithDelay(AppenderAttachableImpl aai) { - try { - reconfigureLock.lock(); - aai.addAppender(makeNewNOPAppender()); - aai.addAppender(makeNewNOPAppender()); - delay(RECONFIGURE_DELAY); - aai.detachAndStopAllAppenders(); - } finally { - reconfigureLock.unlock(); - } - } - - private Appender makeNewNOPAppender() { - List longList = new ArrayList<>(); -// for (int j = 0; j < LIST_SIZE; j++) { -// longList.add(0L); +// +// //private static final int LIST_SIZE = 1_000_000; +// private static final int LOOP_LEN = 1_0; +// private static final int RECONFIGURE_DELAY = 1; +// +// ReentrantLock reconfigureLock = new ReentrantLock(true); +// ReentrantLock writeLock = new ReentrantLock(true); +// +// private static int THREAD_COUNT = 200; //Runtime.getRuntime().availableProcessors()*200; +// //private static int THREAD_COUNT = 5000; +// +// private final ExecutorService tasksExecutor = Executors.newVirtualThreadPerTaskExecutor(); +// LoopingRunnable[] loopingThreads = new LoopingRunnable[THREAD_COUNT]; +// ReconfiguringThread[] reconfiguringThreads = new ReconfiguringThread[THREAD_COUNT]; +// Future[] futures = new Future[THREAD_COUNT]; +// +// AppenderAttachableImpl aai = new AppenderAttachableImpl<>(); +// Context context = new ContextBase(); +// +// void reconfigureWithDelay(AppenderAttachableImpl aai) { +// try { +// reconfigureLock.lock(); +// aai.addAppender(makeNewNOPAppender()); +// aai.addAppender(makeNewNOPAppender()); +// delay(RECONFIGURE_DELAY); +// aai.detachAndStopAllAppenders(); +// } finally { +// reconfigureLock.unlock(); // } - Appender nopAppenderWithDelay = new NOPAppenderWithDelay<>(longList); - nopAppenderWithDelay.setContext(context); - nopAppenderWithDelay.start(); - return nopAppenderWithDelay; - } - - private void delay(int delay) { - try { - Thread.sleep(delay); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - @Test - void smoke() throws InterruptedException, ExecutionException { - - for (int i = 0; i < THREAD_COUNT; i++) { - System.out.println("i="+i); - ReconfiguringThread rt = new ReconfiguringThread(aai); - futures[i] = tasksExecutor.submit(rt); - reconfiguringThreads[i] = rt; - } - - for (int i = 0; i < THREAD_COUNT; i++) { - LoopingRunnable loopingThread = new LoopingRunnable(i, aai); - tasksExecutor.submit(loopingThread); - loopingThreads[i] = loopingThread; - } - - for (int i = 0; i < THREAD_COUNT; i++) { - futures[i].get(); - } - - //reconfiguringThread.join(); - Arrays.stream(loopingThreads).forEach(lt -> lt.active = false); - - } - - public class NOPAppenderWithDelay extends AppenderBase { - - List longList; - - NOPAppenderWithDelay(List longList) { - this.longList = new ArrayList<>(longList); - } - - int i = 0; - - @Override - protected void append(E eventObject) { - i++; - try { - writeLock.lock(); - if ((i & 0xF) == 0) { - delay(1); - } else { - //longList.stream().map(x-> x+1); - } - } finally { - writeLock.unlock(); - } - - } - - } - - class ReconfiguringThread extends Thread { - - AppenderAttachableImpl aai; - - ReconfiguringThread(AppenderAttachableImpl aai) { - this.aai = aai; - } - - public void run() { - Thread.yield(); - for (int i = 0; i < LOOP_LEN; i++) { - reconfigureWithDelay(aai); - } - } - - - } - - - class LoopingRunnable implements Runnable { - - int num; - AppenderAttachableImpl aai; - public boolean active = true; - - LoopingRunnable(int num, AppenderAttachableImpl aai) { - this.num = num; - this.aai = aai; - } - - public void run() { - System.out.println("LoopingRunnable.run.num="+num); - int i = 0; - while (active) { - if ((i & 0xFFFFF) == 0) { - long id = Thread.currentThread().threadId(); - System.out.println("thread=" + id + " reconfigure=" + i); - } - aai.appendLoopOnAppenders(Integer.toString(i)); - i++; - //Thread.yield(); - } - } - } +// } +// +// private Appender makeNewNOPAppender() { +// List longList = new ArrayList<>(); +//// for (int j = 0; j < LIST_SIZE; j++) { +//// longList.add(0L); +//// } +// Appender nopAppenderWithDelay = new NOPAppenderWithDelay<>(longList); +// nopAppenderWithDelay.setContext(context); +// nopAppenderWithDelay.start(); +// return nopAppenderWithDelay; +// } +// +// private void delay(int delay) { +// try { +// Thread.sleep(delay); +// } catch (InterruptedException e) { +// throw new RuntimeException(e); +// } +// } +// +// @Test +// void smoke() throws InterruptedException, ExecutionException { +// +// for (int i = 0; i < THREAD_COUNT; i++) { +// System.out.println("i="+i); +// ReconfiguringThread rt = new ReconfiguringThread(aai); +// futures[i] = tasksExecutor.submit(rt); +// reconfiguringThreads[i] = rt; +// } +// +// for (int i = 0; i < THREAD_COUNT; i++) { +// LoopingRunnable loopingThread = new LoopingRunnable(i, aai); +// tasksExecutor.submit(loopingThread); +// loopingThreads[i] = loopingThread; +// } +// +// for (int i = 0; i < THREAD_COUNT; i++) { +// futures[i].get(); +// } +// +// //reconfiguringThread.join(); +// Arrays.stream(loopingThreads).forEach(lt -> lt.active = false); +// +// } +// +// public class NOPAppenderWithDelay extends AppenderBase { +// +// List longList; +// +// NOPAppenderWithDelay(List longList) { +// this.longList = new ArrayList<>(longList); +// } +// +// int i = 0; +// +// @Override +// protected void append(E eventObject) { +// i++; +// try { +// writeLock.lock(); +// if ((i & 0xF) == 0) { +// delay(1); +// } else { +// //longList.stream().map(x-> x+1); +// } +// } finally { +// writeLock.unlock(); +// } +// +// } +// +// } +// +// class ReconfiguringThread extends Thread { +// +// AppenderAttachableImpl aai; +// +// ReconfiguringThread(AppenderAttachableImpl aai) { +// this.aai = aai; +// } +// +// public void run() { +// Thread.yield(); +// for (int i = 0; i < LOOP_LEN; i++) { +// reconfigureWithDelay(aai); +// } +// } +// +// +// } +// +// +// class LoopingRunnable implements Runnable { +// +// int num; +// AppenderAttachableImpl aai; +// public boolean active = true; +// +// LoopingRunnable(int num, AppenderAttachableImpl aai) { +// this.num = num; +// this.aai = aai; +// } +// +// public void run() { +// System.out.println("LoopingRunnable.run.num="+num); +// int i = 0; +// while (active) { +// if ((i & 0xFFFFF) == 0) { +// long id = Thread.currentThread().threadId(); +// System.out.println("thread=" + id + " reconfigure=" + i); +// } +// aai.appendLoopOnAppenders(Integer.toString(i)); +// i++; +// //Thread.yield(); +// } +// } +// } } From 20802cff1dc1ba3bd73b9d7a93102f3b6fd16e2a Mon Sep 17 00:00:00 2001 From: ceki Date: Tue, 30 Sep 2025 11:04:53 +0200 Subject: [PATCH 07/36] mindor javadoc changes Signed-off-by: ceki --- .../main/java/ch/qos/logback/classic/PatternLayout.java | 7 ++++++- .../core/pattern/color/ConverterSupplierByClassName.java | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/PatternLayout.java b/logback-classic/src/main/java/ch/qos/logback/classic/PatternLayout.java index 15b7becc95..d2cc244f4c 100644 --- a/logback-classic/src/main/java/ch/qos/logback/classic/PatternLayout.java +++ b/logback-classic/src/main/java/ch/qos/logback/classic/PatternLayout.java @@ -42,11 +42,16 @@ public class PatternLayout extends PatternLayoutBase { public static final Map> DEFAULT_CONVERTER_SUPPLIER_MAP = new HashMap<>(); + /** + * @deprecated replaced by {@link #DEFAULT_CONVERTER_SUPPLIER_MAP} + */ + @Deprecated public static final Map DEFAULT_CONVERTER_MAP = new HashMap<>(); public static final Map CONVERTER_CLASS_TO_KEY_MAP = new HashMap(); /** - * @deprecated replaced by DEFAULT_CONVERTER_MAP + * @deprecated replaced by {@link #DEFAULT_CONVERTER_MAP} in turn itself replaced by + * {@link #DEFAULT_CONVERTER_SUPPLIER_MAP} */ @Deprecated public static final Map defaultConverterMap = DEFAULT_CONVERTER_MAP; diff --git a/logback-core/src/main/java/ch/qos/logback/core/pattern/color/ConverterSupplierByClassName.java b/logback-core/src/main/java/ch/qos/logback/core/pattern/color/ConverterSupplierByClassName.java index 02929ed23a..2cbc35e197 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/pattern/color/ConverterSupplierByClassName.java +++ b/logback-core/src/main/java/ch/qos/logback/core/pattern/color/ConverterSupplierByClassName.java @@ -20,6 +20,13 @@ import java.util.function.Supplier; +/** + * + *

Implements the {@link Supplier} interface in order to cater for legacy code using the class name + * of a converter. + *

+ *

Should not be used in non-legacy code.

+ */ public class ConverterSupplierByClassName extends ContextAwareBase implements Supplier { String conversionWord; From ee70cf4cd99774ea5fe1f7e2d928061126e45eeb Mon Sep 17 00:00:00 2001 From: ceki Date: Tue, 30 Sep 2025 13:06:53 +0200 Subject: [PATCH 08/36] prepare release 1.5.19 Signed-off-by: ceki --- logback-access/pom.xml | 2 +- logback-classic-blackbox/pom.xml | 2 +- logback-classic/pom.xml | 2 +- logback-core-blackbox/pom.xml | 2 +- logback-core/pom.xml | 2 +- logback-examples/pom.xml | 2 +- pom.xml | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/logback-access/pom.xml b/logback-access/pom.xml index f719baaa3a..853a61ca36 100644 --- a/logback-access/pom.xml +++ b/logback-access/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.19-SNAPSHOT + 1.5.19 logback-access diff --git a/logback-classic-blackbox/pom.xml b/logback-classic-blackbox/pom.xml index 0790f753d8..17834b5495 100644 --- a/logback-classic-blackbox/pom.xml +++ b/logback-classic-blackbox/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.19-SNAPSHOT + 1.5.19 logback-classic-blackbox diff --git a/logback-classic/pom.xml b/logback-classic/pom.xml index 6e0e1506fd..f2332c2b2b 100755 --- a/logback-classic/pom.xml +++ b/logback-classic/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.19-SNAPSHOT + 1.5.19 logback-classic diff --git a/logback-core-blackbox/pom.xml b/logback-core-blackbox/pom.xml index 92cf68f677..1347fc8c24 100644 --- a/logback-core-blackbox/pom.xml +++ b/logback-core-blackbox/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.19-SNAPSHOT + 1.5.19 logback-core-blackbox diff --git a/logback-core/pom.xml b/logback-core/pom.xml index 85d2e49105..e00248f250 100755 --- a/logback-core/pom.xml +++ b/logback-core/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.19-SNAPSHOT + 1.5.19 logback-core diff --git a/logback-examples/pom.xml b/logback-examples/pom.xml index 9e217d1f98..a32982266d 100755 --- a/logback-examples/pom.xml +++ b/logback-examples/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.19-SNAPSHOT + 1.5.19 logback-examples diff --git a/pom.xml b/pom.xml index 55600bc39d..c2c9c3b376 100755 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.19-SNAPSHOT + 1.5.19 pom Logback-Parent @@ -50,7 +50,7 @@ - 2025-03-18T13:01:31Z + 2025-09-30T11:04:00Z 11 From 4adae8bdcdcf018bb29e51387175412bd9c6d546 Mon Sep 17 00:00:00 2001 From: Ceki Gulcu Date: Tue, 30 Sep 2025 18:10:10 +0200 Subject: [PATCH 09/36] add plugin for Maven Central deployment Signed-off-by: Ceki Gulcu --- pom.xml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pom.xml b/pom.xml index c2c9c3b376..e927d4367f 100755 --- a/pom.xml +++ b/pom.xml @@ -105,6 +105,9 @@ 1.10.13 2.7 12.0.13 + + 0.9.0 + @@ -353,6 +356,7 @@ maven-bundle-plugin ${maven-bundle-plugin.version} + @@ -437,6 +441,15 @@ + + org.sonatype.central + central-publishing-maven-plugin + ${central-publishing-maven-plugin.version} + true + + central + + From e572d4f87f06674788eb3ca7148e8d1dffc615fa Mon Sep 17 00:00:00 2001 From: Ceki Gulcu Date: Tue, 30 Sep 2025 18:42:39 +0200 Subject: [PATCH 10/36] skip deployment of blackbox and example modules, published as version 1.5.9 Signed-off-by: Ceki Gulcu --- logback-classic-blackbox/pom.xml | 8 -------- logback-core-blackbox/pom.xml | 8 -------- logback-examples/pom.xml | 8 -------- pom.xml | 4 ++-- 4 files changed, 2 insertions(+), 26 deletions(-) diff --git a/logback-classic-blackbox/pom.xml b/logback-classic-blackbox/pom.xml index 17834b5495..49a38ff155 100644 --- a/logback-classic-blackbox/pom.xml +++ b/logback-classic-blackbox/pom.xml @@ -130,14 +130,6 @@ - - org.apache.maven.plugins - maven-deploy-plugin - - true - - - diff --git a/logback-core-blackbox/pom.xml b/logback-core-blackbox/pom.xml index 1347fc8c24..ae292bc41f 100644 --- a/logback-core-blackbox/pom.xml +++ b/logback-core-blackbox/pom.xml @@ -70,14 +70,6 @@ - - org.apache.maven.plugins - maven-deploy-plugin - - true - - - diff --git a/logback-examples/pom.xml b/logback-examples/pom.xml index a32982266d..d91d080c93 100755 --- a/logback-examples/pom.xml +++ b/logback-examples/pom.xml @@ -80,14 +80,6 @@ - - - org.apache.maven.plugins - maven-deploy-plugin - - true - - diff --git a/pom.xml b/pom.xml index e927d4367f..01ae16ff57 100755 --- a/pom.xml +++ b/pom.xml @@ -356,7 +356,6 @@ maven-bundle-plugin ${maven-bundle-plugin.version} - @@ -444,10 +443,11 @@ org.sonatype.central central-publishing-maven-plugin - ${central-publishing-maven-plugin.version} + ${central-publishing-maven-plugin.version} true central + logback-core-blackbox,logback-classic-blackbox,logback-examples From aa5eeb1f0d38cc195e7eab183d79f9d0c4f07c0a Mon Sep 17 00:00:00 2001 From: ceki Date: Wed, 1 Oct 2025 11:36:24 +0200 Subject: [PATCH 11/36] start work on version 1.5.20-SNAPSHOT Signed-off-by: ceki --- logback-access/pom.xml | 2 +- logback-classic-blackbox/pom.xml | 2 +- logback-classic/pom.xml | 2 +- logback-core-blackbox/pom.xml | 2 +- logback-core/pom.xml | 2 +- logback-examples/pom.xml | 2 +- pom.xml | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/logback-access/pom.xml b/logback-access/pom.xml index 853a61ca36..e3eb32a060 100644 --- a/logback-access/pom.xml +++ b/logback-access/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.19 + 1.5.20-SNAPSHOT logback-access diff --git a/logback-classic-blackbox/pom.xml b/logback-classic-blackbox/pom.xml index 49a38ff155..654f8d560d 100644 --- a/logback-classic-blackbox/pom.xml +++ b/logback-classic-blackbox/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.19 + 1.5.20-SNAPSHOT logback-classic-blackbox diff --git a/logback-classic/pom.xml b/logback-classic/pom.xml index f2332c2b2b..d8a3ee457c 100755 --- a/logback-classic/pom.xml +++ b/logback-classic/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.19 + 1.5.20-SNAPSHOT logback-classic diff --git a/logback-core-blackbox/pom.xml b/logback-core-blackbox/pom.xml index ae292bc41f..d6e7c61422 100644 --- a/logback-core-blackbox/pom.xml +++ b/logback-core-blackbox/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.19 + 1.5.20-SNAPSHOT logback-core-blackbox diff --git a/logback-core/pom.xml b/logback-core/pom.xml index e00248f250..f46203bf55 100755 --- a/logback-core/pom.xml +++ b/logback-core/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.19 + 1.5.20-SNAPSHOT logback-core diff --git a/logback-examples/pom.xml b/logback-examples/pom.xml index d91d080c93..1d0960e7a0 100755 --- a/logback-examples/pom.xml +++ b/logback-examples/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.19 + 1.5.20-SNAPSHOT logback-examples diff --git a/pom.xml b/pom.xml index 01ae16ff57..d2b71f12d8 100755 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.19 + 1.5.20-SNAPSHOT pom Logback-Parent @@ -50,7 +50,7 @@ - 2025-09-30T11:04:00Z + 2025-10-01T09:35:18Z 11 From 728803f660e07e495843d8aee43ae353c8390973 Mon Sep 17 00:00:00 2001 From: ceki Date: Tue, 14 Oct 2025 13:11:42 +0200 Subject: [PATCH 12/36] fix typo Signed-off-by: ceki --- .../main/java/ch/qos/logback/classic/spi/LoggingEvent.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/spi/LoggingEvent.java b/logback-classic/src/main/java/ch/qos/logback/classic/spi/LoggingEvent.java index be4cd2ecee..e7d146a850 100755 --- a/logback-classic/src/main/java/ch/qos/logback/classic/spi/LoggingEvent.java +++ b/logback-classic/src/main/java/ch/qos/logback/classic/spi/LoggingEvent.java @@ -138,7 +138,7 @@ public LoggingEvent(String fqcn, Logger logger, Level level, String message, Thr } if (throwable == null) { - throwable = extractThrowableAnRearrangeArguments(argArray); + throwable = extractThrowableAndRearrangeArguments(argArray); } if (throwable != null) { @@ -158,7 +158,7 @@ void initTmestampFields(Instant instant) { this.timeStamp = (epochSecond * 1000) + (milliseconds); } - private Throwable extractThrowableAnRearrangeArguments(Object[] argArray) { + private Throwable extractThrowableAndRearrangeArguments(Object[] argArray) { Throwable extractedThrowable = EventArgUtil.extractThrowable(argArray); if (EventArgUtil.successfulExtraction(extractedThrowable)) { this.argumentArray = EventArgUtil.trimmedCopy(argArray); From 5ca7ce8a86cdf28f2d389c3d7dc780f538f3d059 Mon Sep 17 00:00:00 2001 From: ceki Date: Wed, 15 Oct 2025 21:33:34 +0200 Subject: [PATCH 13/36] provide an alternative to Janino based conditional configuration processing - Part 1 Signed-off-by: ceki --- .../qos/logback/classic/spi/Configurator.java | 12 +- .../core/boolex/PropertyEvaluator.java | 62 ++++++++ .../core/boolex/PropertyEvaluatorBase.java | 134 ++++++++++++++++++ .../core/joran/JoranConfiguratorBase.java | 5 +- .../ModelClassToModelHandlerLinkerBase.java | 4 + .../action/SequenceNumberGeneratorAction.java | 2 +- .../ByPropertiesConditionAction.java | 43 ++++++ .../ByPropertiesConditionModel.java | 29 ++++ .../ByPropertiesConditionModelHandler.java | 79 +++++++++++ .../processor/conditional/IfModelHandler.java | 22 ++- 10 files changed, 375 insertions(+), 17 deletions(-) create mode 100644 logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluator.java create mode 100644 logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluatorBase.java create mode 100644 logback-core/src/main/java/ch/qos/logback/core/joran/conditional/ByPropertiesConditionAction.java create mode 100644 logback-core/src/main/java/ch/qos/logback/core/model/conditional/ByPropertiesConditionModel.java create mode 100644 logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/spi/Configurator.java b/logback-classic/src/main/java/ch/qos/logback/classic/spi/Configurator.java index bd2c2e145e..48073d60f7 100644 --- a/logback-classic/src/main/java/ch/qos/logback/classic/spi/Configurator.java +++ b/logback-classic/src/main/java/ch/qos/logback/classic/spi/Configurator.java @@ -14,22 +14,18 @@ package ch.qos.logback.classic.spi; import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.core.Context; import ch.qos.logback.core.spi.ContextAware; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; /** - * Allows programmatic initialization and configuration of Logback. The + *

Allows programmatic initialization and configuration of Logback. The * ServiceLoader is typically used to instantiate implementations and thus * implementations will need to follow the guidelines of the ServiceLoader, - * in particular the no-arg constructor requirement. + * in particular the no-arg constructor requirement.

* - * The return type of {@link #configure(LoggerContext) configure} was changed from 'void' to + *

The return type of {@link #configure(LoggerContext) configure} was changed from 'void' to * {@link ExecutionStatus) in logback version 1.3.0. + *

*/ public interface Configurator extends ContextAware { diff --git a/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluator.java b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluator.java new file mode 100644 index 0000000000..5563997609 --- /dev/null +++ b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluator.java @@ -0,0 +1,62 @@ +/** + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2025, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v1.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ + +package ch.qos.logback.core.boolex; + +import ch.qos.logback.core.Context; +import ch.qos.logback.core.joran.conditional.Condition; +import ch.qos.logback.core.joran.conditional.PropertyEvalScriptBuilder; +import ch.qos.logback.core.spi.ContextAware; +import ch.qos.logback.core.spi.LifeCycle; +import ch.qos.logback.core.spi.PropertyContainer; + +/** + * Interface for evaluating conditions based on properties during the conditional processing + * of Logback configuration files. This interface is intended to provide an + * alternative to legacy Janino-based evaluation. + *

+ * Implementations of this interface can access both global properties from the {@link Context} + * and local properties specific to the embedding configurator instance. This allows for fine-grained + * and context-aware evaluation of configuration conditions. + *

+ * + *

+ * Typical usage involves implementing this interface to provide custom logic for evaluating + * whether certain configuration blocks should be included or excluded based on property values. + *

+ * + * @since 1.5.20 + * @author Ceki Gülcü + */ +public interface PropertyEvaluator extends Condition, ContextAware, LifeCycle { + + /** + * Returns the local {@link PropertyContainer} used for property lookups specific to the embedding configurator. + * This is distinct from the global {@link Context} property container. + * + * @return the local property container, or null if not set + */ + public PropertyContainer getLocalPropertyContainer(); + + /** + * Sets a {@link PropertyContainer} specific to the embedding configurator, which is used for property lookups + * in addition to the global {@link Context} properties. This allows for overriding or supplementing global properties + * with local values during evaluation. + * + * @param aPropertyContainer the local property container to use for lookups + */ + public void setLocalPropertyContainer(PropertyContainer aPropertyContainer); + + +} \ No newline at end of file diff --git a/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluatorBase.java b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluatorBase.java new file mode 100644 index 0000000000..a66129876a --- /dev/null +++ b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluatorBase.java @@ -0,0 +1,134 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2025, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v1.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ + +package ch.qos.logback.core.boolex; + +import ch.qos.logback.core.spi.PropertyContainer; +import ch.qos.logback.core.util.OptionHelper; + +/** + *

Abstract base class provides some scaffolding. it is intended to ease migration + * from legacy conditional processing in configuration files + * (e.g. <if>, <then>, <else>) using the Janino library. + *

+ * + *

Nevertheless, it should also be useful in newly written code.

+ * + * @since 1.5.20 + * @author Ceki Gülcü + */ +abstract public class PropertyEvaluatorBase implements PropertyEvaluator { + + /** + * Indicates whether this evaluator has been started. + */ + boolean started; + /** + *

The local property container used for property lookups.

+ * + *

Local properties correspond to the properties in the embedding + * configurator.

+ */ + PropertyContainer localPropertyContainer; + + /** + * Returns the property container used by this evaluator. + * + * @return the property container + */ + @Override + public PropertyContainer getLocalPropertyContainer() { + return localPropertyContainer; + } + + /** + * Sets the property container for this evaluator. + * + * @param aPropertyContainer the property container to set + */ + @Override + public void setLocalPropertyContainer(PropertyContainer aPropertyContainer) { + this.localPropertyContainer = aPropertyContainer; + } + + /** + * Checks if the property with the given key is null. + * + * @param k the property key + * @return true if the property is null, false otherwise + */ + public boolean isNull(String k) { + String val = OptionHelper.propertyLookup(k, localPropertyContainer, getContext()); + return (val == null); + } + + /** + * Checks if the property with the given key is defined (not null). + * + * @param k the property key + * @return true if the property is defined, false otherwise + */ + public boolean isDefined(String k) { + String val = OptionHelper.propertyLookup(k, localPropertyContainer, getContext()); + return (val != null); + } + + /** + * Retrieves the property value for the given key, returning an empty string if null. + * This is a shorthand for {@link #property(String)}. + * + * @param k the property key + * @return the property value or an empty string + */ + public String p(String k) { + return property(k); + } + + /** + * Retrieves the property value for the given key, returning an empty string if null. + * + * @param k the property key + * @return the property value or an empty string + */ + public String property(String k) { + String val = OptionHelper.propertyLookup(k, localPropertyContainer, getContext()); + if (val != null) + return val; + else + return ""; + } + + /** + * Checks if this evaluator has been started. + * + * @return true if started, false otherwise + */ + public boolean isStarted() { + return started; + } + + /** + * Starts this evaluator. + */ + public void start() { + started = true; + } + + /** + * Stops this evaluator. + */ + public void stop() { + started = false; + } +} diff --git a/logback-core/src/main/java/ch/qos/logback/core/joran/JoranConfiguratorBase.java b/logback-core/src/main/java/ch/qos/logback/core/joran/JoranConfiguratorBase.java index 662a9161b3..973505c882 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/joran/JoranConfiguratorBase.java +++ b/logback-core/src/main/java/ch/qos/logback/core/joran/JoranConfiguratorBase.java @@ -14,9 +14,7 @@ package ch.qos.logback.core.joran; import ch.qos.logback.core.joran.action.*; -import ch.qos.logback.core.joran.conditional.ElseAction; -import ch.qos.logback.core.joran.conditional.IfAction; -import ch.qos.logback.core.joran.conditional.ThenAction; +import ch.qos.logback.core.joran.conditional.*; import ch.qos.logback.core.joran.sanity.AppenderWithinAppenderSanityChecker; import ch.qos.logback.core.joran.sanity.SanityChecker; import ch.qos.logback.core.joran.spi.ElementSelector; @@ -77,6 +75,7 @@ protected void addElementSelectorAndActionAssociations(RuleStore rs) { rs.addRule(new ElementSelector("*/param"), ParamAction::new); // add if-then-else support + rs.addRule(new ElementSelector("*/condition"), ByPropertiesConditionAction::new); rs.addRule(new ElementSelector("*/if"), IfAction::new); rs.addTransparentPathPart("if"); rs.addRule(new ElementSelector("*/if/then"), ThenAction::new); diff --git a/logback-core/src/main/java/ch/qos/logback/core/joran/ModelClassToModelHandlerLinkerBase.java b/logback-core/src/main/java/ch/qos/logback/core/joran/ModelClassToModelHandlerLinkerBase.java index 8c3ece450b..8ffb02f5c9 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/joran/ModelClassToModelHandlerLinkerBase.java +++ b/logback-core/src/main/java/ch/qos/logback/core/joran/ModelClassToModelHandlerLinkerBase.java @@ -16,10 +16,12 @@ import ch.qos.logback.core.Context; import ch.qos.logback.core.model.*; +import ch.qos.logback.core.model.conditional.ByPropertiesConditionModel; import ch.qos.logback.core.model.conditional.ElseModel; import ch.qos.logback.core.model.conditional.IfModel; import ch.qos.logback.core.model.conditional.ThenModel; import ch.qos.logback.core.model.processor.*; +import ch.qos.logback.core.model.processor.conditional.ByPropertiesConditionModelHandler; import ch.qos.logback.core.model.processor.conditional.ElseModelHandler; import ch.qos.logback.core.model.processor.conditional.IfModelHandler; import ch.qos.logback.core.model.processor.conditional.ThenModelHandler; @@ -62,6 +64,8 @@ public void link(DefaultProcessor defaultProcessor) { defaultProcessor.addHandler(StatusListenerModel.class, StatusListenerModelHandler::makeInstance); defaultProcessor.addHandler(ImplicitModel.class, ImplicitModelHandler::makeInstance); + + defaultProcessor.addHandler(ByPropertiesConditionModel.class, ByPropertiesConditionModelHandler::makeInstance); defaultProcessor.addHandler(IfModel.class, IfModelHandler::makeInstance); defaultProcessor.addHandler(ThenModel.class, ThenModelHandler::makeInstance); defaultProcessor.addHandler(ElseModel.class, ElseModelHandler::makeInstance); diff --git a/logback-core/src/main/java/ch/qos/logback/core/joran/action/SequenceNumberGeneratorAction.java b/logback-core/src/main/java/ch/qos/logback/core/joran/action/SequenceNumberGeneratorAction.java index 96ccc3ee46..9d57e54258 100755 --- a/logback-core/src/main/java/ch/qos/logback/core/joran/action/SequenceNumberGeneratorAction.java +++ b/logback-core/src/main/java/ch/qos/logback/core/joran/action/SequenceNumberGeneratorAction.java @@ -1,6 +1,6 @@ /** * Logback: the reliable, generic, fast and flexible logging framework. - * Copyright (C) 1999-2015, QOS.ch. All rights reserved. + * Copyright (C) 1999-2025, QOS.ch. All rights reserved. * * This program and the accompanying materials are dual-licensed under * either the terms of the Eclipse Public License v1.0 as published by diff --git a/logback-core/src/main/java/ch/qos/logback/core/joran/conditional/ByPropertiesConditionAction.java b/logback-core/src/main/java/ch/qos/logback/core/joran/conditional/ByPropertiesConditionAction.java new file mode 100644 index 0000000000..48ececdaaf --- /dev/null +++ b/logback-core/src/main/java/ch/qos/logback/core/joran/conditional/ByPropertiesConditionAction.java @@ -0,0 +1,43 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2025, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v1.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ +package ch.qos.logback.core.joran.conditional; + +import ch.qos.logback.core.joran.action.BaseModelAction; +import ch.qos.logback.core.joran.action.PreconditionValidator; +import ch.qos.logback.core.joran.spi.SaxEventInterpretationContext; +import ch.qos.logback.core.model.Model; +import ch.qos.logback.core.model.conditional.ByPropertiesConditionModel; +import org.xml.sax.Attributes; + +public class ByPropertiesConditionAction extends BaseModelAction { + + + @Override + protected Model buildCurrentModel(SaxEventInterpretationContext interpretationContext, String name, + Attributes attributes) { + ByPropertiesConditionModel sngm = new ByPropertiesConditionModel(); + sngm.setClassName(attributes.getValue(CLASS_ATTRIBUTE)); + return sngm; + } + + @Override + protected boolean validPreconditions(SaxEventInterpretationContext seic, String name, Attributes attributes) { + PreconditionValidator validator = new PreconditionValidator(this, seic, name, attributes); + validator.validateClassAttribute(); + return validator.isValid(); + } + +} + + diff --git a/logback-core/src/main/java/ch/qos/logback/core/model/conditional/ByPropertiesConditionModel.java b/logback-core/src/main/java/ch/qos/logback/core/model/conditional/ByPropertiesConditionModel.java new file mode 100644 index 0000000000..299580d827 --- /dev/null +++ b/logback-core/src/main/java/ch/qos/logback/core/model/conditional/ByPropertiesConditionModel.java @@ -0,0 +1,29 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2025, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v1.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ + +package ch.qos.logback.core.model.conditional; + +import ch.qos.logback.core.model.ComponentModel; + +public class ByPropertiesConditionModel extends ComponentModel { + + private static final long serialVersionUID = -1788292310734560420L; + + @Override + protected ByPropertiesConditionModel makeNewInstance() { + return new ByPropertiesConditionModel(); + } + + +} diff --git a/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java b/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java new file mode 100644 index 0000000000..a0ca57569f --- /dev/null +++ b/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java @@ -0,0 +1,79 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2025, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v1.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ + +package ch.qos.logback.core.model.processor.conditional; + +import ch.qos.logback.core.Context; +import ch.qos.logback.core.boolex.PropertyEvaluator; +import ch.qos.logback.core.model.Model; +import ch.qos.logback.core.model.conditional.IfModel; +import ch.qos.logback.core.model.conditional.ByPropertiesConditionModel; +import ch.qos.logback.core.model.processor.ModelHandlerBase; +import ch.qos.logback.core.model.processor.ModelHandlerException; +import ch.qos.logback.core.model.processor.ModelInterpretationContext; +import ch.qos.logback.core.util.OptionHelper; + +import static ch.qos.logback.core.model.conditional.IfModel.BranchState.ELSE_BRANCH; +import static ch.qos.logback.core.model.conditional.IfModel.BranchState.IF_BRANCH; + +public class ByPropertiesConditionModelHandler extends ModelHandlerBase { + + private boolean inError = false; + PropertyEvaluator propertyEvaluator; + + public ByPropertiesConditionModelHandler(Context context) { + super(context); + } + + @Override + protected Class getSupportedModelClass() { + return ByPropertiesConditionModel.class; + } + + static public ModelHandlerBase makeInstance(Context context, ModelInterpretationContext mic) { + return new ByPropertiesConditionModelHandler(context); + } + + + @Override + public void handle(ModelInterpretationContext mic, Model model) throws ModelHandlerException { + + ByPropertiesConditionModel byPropertiesConditionModel = (ByPropertiesConditionModel) model; + String className = byPropertiesConditionModel.getClassName(); + if (OptionHelper.isNullOrEmptyOrAllSpaces(className)) { + addWarn("Missing className. This should have been caught earlier."); + inError = true; + return; + } else { + className = mic.getImport(className); + } + try { + addInfo("About to instantiate PropertyEvaluator of type [" + className + "]"); + + propertyEvaluator = (PropertyEvaluator) OptionHelper.instantiateByClassName(className, + PropertyEvaluator.class, context); + propertyEvaluator.setContext(context); + propertyEvaluator.setLocalPropertyContainer(mic); + + boolean evaluationResult = propertyEvaluator.evaluate(); + IfModel.BranchState branchState = evaluationResult ? IF_BRANCH : ELSE_BRANCH; + mic.pushObject(branchState); + } catch (Exception e) { + inError = true; + mic.pushObject(IfModel.BranchState.IN_ERROR); + addError("Could not create a SequenceNumberGenerator of type [" + className + "].", e); + throw new ModelHandlerException(e); + } + } +} diff --git a/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/IfModelHandler.java b/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/IfModelHandler.java index 5bc101dbc7..26ca90506a 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/IfModelHandler.java +++ b/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/IfModelHandler.java @@ -57,25 +57,37 @@ protected Class getSupportedModelClass() { public void handle(ModelInterpretationContext mic, Model model) throws ModelHandlerException { ifModel = (IfModel) model; - + mic.pushModel(ifModel); + Object micTopObject = mic.peekObject(); + String conditionStr = ifModel.getCondition(); + + if(micTopObject instanceof BranchState) { + BranchState branchState = (BranchState) micTopObject; + ifModel.setBranchState(branchState); + // consume the BranchState at top of the object stack + mic.popObject(); + } else { + janinoFallback(mic, model, conditionStr); + } + } + + private void janinoFallback(ModelInterpretationContext mic, Model model, String conditionStr) { if (!EnvUtil.isJaninoAvailable()) { addError(MISSING_JANINO_MSG); addError(MISSING_JANINO_SEE); return; } - - mic.pushModel(ifModel); + Condition condition = null; int lineNum = model.getLineNumber(); - String conditionStr = ifModel.getCondition(); if (!OptionHelper.isNullOrEmptyOrAllSpaces(conditionStr)) { try { conditionStr = OptionHelper.substVars(conditionStr, mic, context); } catch (ScanException e) { addError("Failed to parse input [" + conditionStr + "] on line "+lineNum, e); ifModel.setBranchState(BranchState.IN_ERROR); - return; + return; } // do not allow 'new' operator From ee77a70217b5fc49e18de61176fa5de061b6074c Mon Sep 17 00:00:00 2001 From: ceki Date: Thu, 16 Oct 2025 17:38:43 +0200 Subject: [PATCH 14/36] provide an alternative to Janino based conditional configuration processing - Part 2 Signed-off-by: ceki --- .../joran/conditional/if0_NoJoran.xml | 30 ++++++++ .../conditional/ifWithoutElse_NoJoran.xml | 27 ++++++++ .../conditional/if_localProperty_NoJoran.xml | 31 +++++++++ .../joran/conditional/nestedIf_NoJoran.xml | 39 +++++++++++ .../blackbox/boolex/AlwaysFalseCondition.java | 25 +++++++ .../blackbox/boolex/AlwaysTrueCondition.java | 25 +++++++ .../joran/conditional/IfThenElseTest.java | 59 ++++++++++++++-- .../src/test/java/module-info.java | 3 + .../core/boolex/PropertyEqualsValue.java | 68 +++++++++++++++++++ .../core/boolex/PropertyEvaluatorBase.java | 3 +- .../core/joran/conditional/IfAction.java | 2 +- .../ByPropertiesConditionModelHandler.java | 22 ++++-- .../processor/conditional/IfModelHandler.java | 14 +++- 13 files changed, 337 insertions(+), 11 deletions(-) create mode 100644 logback-core-blackbox/src/test/blackboxInput/joran/conditional/if0_NoJoran.xml create mode 100644 logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifWithoutElse_NoJoran.xml create mode 100644 logback-core-blackbox/src/test/blackboxInput/joran/conditional/if_localProperty_NoJoran.xml create mode 100644 logback-core-blackbox/src/test/blackboxInput/joran/conditional/nestedIf_NoJoran.xml create mode 100644 logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/AlwaysFalseCondition.java create mode 100644 logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/AlwaysTrueCondition.java create mode 100644 logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEqualsValue.java diff --git a/logback-core-blackbox/src/test/blackboxInput/joran/conditional/if0_NoJoran.xml b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/if0_NoJoran.xml new file mode 100644 index 0000000000..6f1a863bc8 --- /dev/null +++ b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/if0_NoJoran.xml @@ -0,0 +1,30 @@ + + + + + + ki1 + val1 + + + + + + + + + + + diff --git a/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifWithoutElse_NoJoran.xml b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifWithoutElse_NoJoran.xml new file mode 100644 index 0000000000..4da818d79b --- /dev/null +++ b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifWithoutElse_NoJoran.xml @@ -0,0 +1,27 @@ + + + + + + ki1 + val1 + + + + + + + + diff --git a/logback-core-blackbox/src/test/blackboxInput/joran/conditional/if_localProperty_NoJoran.xml b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/if_localProperty_NoJoran.xml new file mode 100644 index 0000000000..3095b9f136 --- /dev/null +++ b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/if_localProperty_NoJoran.xml @@ -0,0 +1,31 @@ + + + + + + + Ki1 + Val1 + + + + + + + + + + + diff --git a/logback-core-blackbox/src/test/blackboxInput/joran/conditional/nestedIf_NoJoran.xml b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/nestedIf_NoJoran.xml new file mode 100644 index 0000000000..e8f91afd2e --- /dev/null +++ b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/nestedIf_NoJoran.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/AlwaysFalseCondition.java b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/AlwaysFalseCondition.java new file mode 100644 index 0000000000..90cb68cea9 --- /dev/null +++ b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/AlwaysFalseCondition.java @@ -0,0 +1,25 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2025, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v1.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ + +package ch.qos.logback.core.blackbox.boolex; + +import ch.qos.logback.core.boolex.PropertyEvaluatorBase; + +public class AlwaysFalseCondition extends PropertyEvaluatorBase { + + @Override + public boolean evaluate() { + return false; + } +} \ No newline at end of file diff --git a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/AlwaysTrueCondition.java b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/AlwaysTrueCondition.java new file mode 100644 index 0000000000..3de8659cc0 --- /dev/null +++ b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/AlwaysTrueCondition.java @@ -0,0 +1,25 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2025, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v1.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ + +package ch.qos.logback.core.blackbox.boolex; + +import ch.qos.logback.core.boolex.PropertyEvaluatorBase; + +public class AlwaysTrueCondition extends PropertyEvaluatorBase { + + @Override + public boolean evaluate() { + return true; + } +} \ No newline at end of file diff --git a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/joran/conditional/IfThenElseTest.java b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/joran/conditional/IfThenElseTest.java index 72e9d66cda..6ece86851d 100644 --- a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/joran/conditional/IfThenElseTest.java +++ b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/joran/conditional/IfThenElseTest.java @@ -24,6 +24,7 @@ import ch.qos.logback.core.blackbox.model.processor.BlackboxStackModelHandler; import ch.qos.logback.core.joran.action.Action; import ch.qos.logback.core.joran.action.PropertyAction; +import ch.qos.logback.core.joran.conditional.ByPropertiesConditionAction; import ch.qos.logback.core.joran.conditional.ElseAction; import ch.qos.logback.core.joran.conditional.IfAction; import ch.qos.logback.core.joran.conditional.ThenAction; @@ -32,6 +33,7 @@ import ch.qos.logback.core.joran.spi.RuleStore; import ch.qos.logback.core.model.ImplicitModel; import ch.qos.logback.core.model.PropertyModel; +import ch.qos.logback.core.model.conditional.ByPropertiesConditionModel; import ch.qos.logback.core.model.conditional.ElseModel; import ch.qos.logback.core.model.conditional.IfModel; import ch.qos.logback.core.model.conditional.ThenModel; @@ -39,6 +41,7 @@ import ch.qos.logback.core.model.processor.ImplicitModelHandler; import ch.qos.logback.core.model.processor.NOPModelHandler; import ch.qos.logback.core.model.processor.PropertyModelHandler; +import ch.qos.logback.core.model.processor.conditional.ByPropertiesConditionModelHandler; import ch.qos.logback.core.model.processor.conditional.ElseModelHandler; import ch.qos.logback.core.model.processor.conditional.IfModelHandler; import ch.qos.logback.core.model.processor.conditional.ThenModelHandler; @@ -46,6 +49,7 @@ import ch.qos.logback.core.status.StatusUtil; import ch.qos.logback.core.testUtil.RandomUtil; import ch.qos.logback.core.util.StatusPrinter; +import ch.qos.logback.core.util.StatusPrinter2; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -60,6 +64,7 @@ public class IfThenElseTest { Context context = new ContextBase(); StatusUtil checker = new StatusUtil(context); + StatusPrinter2 statusPrinter2 = new StatusPrinter2(); BlackboxSimpleConfigurator simpleConfigurator; int diff = RandomUtil.getPositiveInt(); static final String CONDITIONAL_DIR_PREFIX = BlackboxCoreTestConstants.JORAN_INPUT_PREFIX + "conditional/"; @@ -75,6 +80,7 @@ public void setUp() throws Exception { rulesMap.put(new ElementSelector("x"), BlackboxTopElementAction::new); rulesMap.put(new ElementSelector("x/stack"), BlackboxStackAction::new); rulesMap.put(new ElementSelector("x/property"), PropertyAction::new); + rulesMap.put(new ElementSelector("*/condition"), ByPropertiesConditionAction::new); rulesMap.put(new ElementSelector("*/if"), IfAction::new); rulesMap.put(new ElementSelector("*/if/then"), ThenAction::new); rulesMap.put(new ElementSelector("*/if/else"), ElseAction::new); @@ -98,6 +104,7 @@ protected void addModelHandlerAssociations(DefaultProcessor defaultProcessor) { defaultProcessor.addHandler(BlackboxStackModel.class, BlackboxStackModelHandler::makeInstance); defaultProcessor.addHandler(PropertyModel.class, PropertyModelHandler::makeInstance); defaultProcessor.addHandler(ImplicitModel.class, ImplicitModelHandler::makeInstance); + defaultProcessor.addHandler(ByPropertiesConditionModel.class, ByPropertiesConditionModelHandler::makeInstance); defaultProcessor.addHandler(IfModel.class, IfModelHandler::makeInstance); defaultProcessor.addHandler(ThenModel.class, ThenModelHandler::makeInstance); defaultProcessor.addHandler(ElseModel.class, ElseModelHandler::makeInstance); @@ -109,7 +116,7 @@ protected void addModelHandlerAssociations(DefaultProcessor defaultProcessor) { @AfterEach public void tearDown() throws Exception { - StatusPrinter.printIfErrorsOccured(context); + statusPrinter2.printIfErrorsOccured(context); System.clearProperty(sysKey); } @@ -120,7 +127,7 @@ public void ifWithExec() throws JoranException { checker.containsException(org.codehaus.commons.compiler.CompileException.class); checker.containsMatch(Status.ERROR, "Failed to parse condition"); } - + // ---------------------------------------------------------------------------------------------------- @Test public void whenContextPropertyIsSet_IfThenBranchIsEvaluated() throws JoranException { context.putProperty(ki1, val1); @@ -128,6 +135,14 @@ public void whenContextPropertyIsSet_IfThenBranchIsEvaluated() throws JoranExcep verifyConfig(new String[] { "BEGIN", "a", "END" }); } + @Test + public void whenContextPropertyIsSet_IfThenBranchIsEvaluated_WithoutJoran() throws JoranException { + context.putProperty(ki1, val1); + + simpleConfigurator.doConfigure(CONDITIONAL_DIR_PREFIX + "if0_NoJoran.xml"); + verifyConfig(new String[] { "BEGIN", "a", "END" }); + } + // ---------------------------------------------------------------------------------------------------- @Test public void ifWithNew() throws JoranException { context.putProperty(ki1, val1); @@ -137,19 +152,32 @@ public void ifWithNew() throws JoranException { verifyConfig(new String[] { "BEGIN", "END" }); } - + // ---------------------------------------------------------------------------------------------------- @Test public void whenLocalPropertyIsSet_IfThenBranchIsEvaluated() throws JoranException { simpleConfigurator.doConfigure(CONDITIONAL_DIR_PREFIX + "if_localProperty.xml"); verifyConfig(new String[] { "BEGIN", "a", "END" }); } + @Test + public void whenLocalPropertyIsSet_IfThenBranchIsEvaluated_NoJoran() throws JoranException { + simpleConfigurator.doConfigure(CONDITIONAL_DIR_PREFIX + "if_localProperty_NoJoran.xml"); + verifyConfig(new String[] { "BEGIN", "a", "END" }); + } + // ---------------------------------------------------------------------------------------------------- @Test public void whenNoPropertyIsDefined_ElseBranchIsEvaluated() throws JoranException { simpleConfigurator.doConfigure(CONDITIONAL_DIR_PREFIX + "if0.xml"); verifyConfig(new String[] { "BEGIN", "b", "END" }); } + @Test + public void whenNoPropertyIsDefined_ElseBranchIsEvaluated_NoJoran() throws JoranException { + simpleConfigurator.doConfigure(CONDITIONAL_DIR_PREFIX + "if0_NoJoran.xml"); + verifyConfig(new String[] { "BEGIN", "b", "END" }); + } + // ---------------------------------------------------------------------------------------------------- + @Test public void whenContextPropertyIsSet_IfThenBranchIsEvaluated_NO_ELSE_DEFINED() throws JoranException { context.putProperty(ki1, val1); @@ -157,6 +185,13 @@ public void whenContextPropertyIsSet_IfThenBranchIsEvaluated_NO_ELSE_DEFINED() t verifyConfig(new String[] { "BEGIN", "a", "END" }); } + @Test + public void whenContextPropertyIsSet_IfThenBranchIsEvaluated_NO_ELSE_DEFINED_NoJoran() throws JoranException { + context.putProperty(ki1, val1); + simpleConfigurator.doConfigure(CONDITIONAL_DIR_PREFIX + "ifWithoutElse_NoJoran.xml"); + verifyConfig(new String[] { "BEGIN", "a", "END" }); + } + // ---------------------------------------------------------------------------------------------------- @Test public void whenNoPropertyIsDefined_IfThenBranchIsNotEvaluated_NO_ELSE_DEFINED() throws JoranException { simpleConfigurator.doConfigure(CONDITIONAL_DIR_PREFIX + "ifWithoutElse.xml"); @@ -164,6 +199,13 @@ public void whenNoPropertyIsDefined_IfThenBranchIsNotEvaluated_NO_ELSE_DEFINED() Assertions.assertTrue(checker.isErrorFree(0)); } + @Test + public void whenNoPropertyIsDefined_IfThenBranchIsNotEvaluated_NO_ELSE_DEFINED_NoJoran() throws JoranException { + simpleConfigurator.doConfigure(CONDITIONAL_DIR_PREFIX + "ifWithoutElse_NoJoran.xml"); + verifyConfig(new String[] { "BEGIN", "END" }); + Assertions.assertTrue(checker.isErrorFree(0)); + } + // ---------------------------------------------------------------------------------------------------- @Test public void nestedIf() throws JoranException { simpleConfigurator.doConfigure(CONDITIONAL_DIR_PREFIX + "nestedIf.xml"); @@ -171,13 +213,22 @@ public void nestedIf() throws JoranException { verifyConfig(new String[] { "BEGIN", "a", "c", "END" }); Assertions.assertTrue(checker.isErrorFree(0)); } + @Test + public void nestedIf_NoJoran() throws JoranException { + simpleConfigurator.doConfigure(CONDITIONAL_DIR_PREFIX + "nestedIf_NoJoran.xml"); + //StatusPrinter.print(context); + verifyConfig(new String[] { "BEGIN", "a", "c", "END" }); + Assertions.assertTrue(checker.isErrorFree(0)); + } + + // ---------------------------------------------------------------------------------------------------- @Test public void useNonExistenceOfSystemPropertyToDefineAContextProperty() throws JoranException { Assertions.assertNull(System.getProperty(sysKey)); Assertions.assertNull(context.getProperty(dynaKey)); simpleConfigurator.doConfigure(CONDITIONAL_DIR_PREFIX + "ifSystem.xml"); - System.out.println(dynaKey + "=" + context.getProperty(dynaKey)); + //System.out.println(dynaKey + "=" + context.getProperty(dynaKey)); Assertions.assertNotNull(context.getProperty(dynaKey)); } diff --git a/logback-core-blackbox/src/test/java/module-info.java b/logback-core-blackbox/src/test/java/module-info.java index 4b472ea8d6..0eeb2efd31 100644 --- a/logback-core-blackbox/src/test/java/module-info.java +++ b/logback-core-blackbox/src/test/java/module-info.java @@ -6,10 +6,13 @@ requires org.junit.jupiter.engine; requires janino; + requires commons.compiler; + requires org.fusesource.jansi; requires org.tukaani.xz; + exports ch.qos.logback.core.blackbox.boolex; exports ch.qos.logback.core.blackbox.joran.conditional; exports ch.qos.logback.core.blackbox.joran; exports ch.qos.logback.core.blackbox.appender; diff --git a/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEqualsValue.java b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEqualsValue.java new file mode 100644 index 0000000000..72f779845a --- /dev/null +++ b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEqualsValue.java @@ -0,0 +1,68 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2025, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v1.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ + +package ch.qos.logback.core.boolex; + +public class PropertyEqualsValue extends PropertyEvaluatorBase { + + + String key; + String value; + + public void start() { + if (key == null) { + addError("In PropertyEqualsValue 'key' parameter cannot be null"); + return; + } + if (value == null) { + addError("In PropertyEqualsValue 'value' parameter cannot be null"); + return; + } + super.start(); + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + + @Override + public boolean evaluate() { + if (key == null) { + addError("key cannot be null"); + return false; + } + + String val = p(key); + if (val == null) + return false; + else { + return val.equals(value); + } + } + + +} diff --git a/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluatorBase.java b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluatorBase.java index a66129876a..66cdfb4f5d 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluatorBase.java +++ b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluatorBase.java @@ -14,6 +14,7 @@ package ch.qos.logback.core.boolex; +import ch.qos.logback.core.spi.ContextAwareBase; import ch.qos.logback.core.spi.PropertyContainer; import ch.qos.logback.core.util.OptionHelper; @@ -28,7 +29,7 @@ * @since 1.5.20 * @author Ceki Gülcü */ -abstract public class PropertyEvaluatorBase implements PropertyEvaluator { +abstract public class PropertyEvaluatorBase extends ContextAwareBase implements PropertyEvaluator { /** * Indicates whether this evaluator has been started. diff --git a/logback-core/src/main/java/ch/qos/logback/core/joran/conditional/IfAction.java b/logback-core/src/main/java/ch/qos/logback/core/joran/conditional/IfAction.java index be77e803fb..c328704d92 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/joran/conditional/IfAction.java +++ b/logback-core/src/main/java/ch/qos/logback/core/joran/conditional/IfAction.java @@ -29,7 +29,7 @@ public class IfAction extends BaseModelAction { @Override protected boolean validPreconditions(SaxEventInterpretationContext interpcont, String name, Attributes attributes) { PreconditionValidator pv = new PreconditionValidator(this, interpcont, name, attributes); - pv.validateGivenAttribute(CONDITION_ATTRIBUTE); + //pv.validateGivenAttribute(CONDITION_ATTRIBUTE); return pv.isValid(); } diff --git a/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java b/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java index a0ca57569f..d1fefb7e2f 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java +++ b/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java @@ -65,10 +65,7 @@ public void handle(ModelInterpretationContext mic, Model model) throws ModelHand PropertyEvaluator.class, context); propertyEvaluator.setContext(context); propertyEvaluator.setLocalPropertyContainer(mic); - - boolean evaluationResult = propertyEvaluator.evaluate(); - IfModel.BranchState branchState = evaluationResult ? IF_BRANCH : ELSE_BRANCH; - mic.pushObject(branchState); + mic.pushObject(propertyEvaluator); } catch (Exception e) { inError = true; mic.pushObject(IfModel.BranchState.IN_ERROR); @@ -76,4 +73,21 @@ public void handle(ModelInterpretationContext mic, Model model) throws ModelHand throw new ModelHandlerException(e); } } + + public void postHandle(ModelInterpretationContext mic, Model model) throws ModelHandlerException { + if (inError) { + return; + } + Object o = mic.peekObject(); + if (o != propertyEvaluator) { + addWarn("The object at the of the stack is not the propertyEvaluator instance pushed earlier."); + } else { + mic.popObject(); + } + + boolean evaluationResult = propertyEvaluator.evaluate(); + IfModel.BranchState branchState = evaluationResult ? IF_BRANCH : ELSE_BRANCH; + mic.pushObject(branchState); + + } } diff --git a/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/IfModelHandler.java b/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/IfModelHandler.java index 26ca90506a..71ba60ed6d 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/IfModelHandler.java +++ b/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/IfModelHandler.java @@ -36,6 +36,9 @@ public class IfModelHandler extends ModelHandlerBase { public static final String NEW_OPERATOR_DISALLOWED_MSG = "The 'condition' attribute may not contain the 'new' operator."; public static final String NEW_OPERATOR_DISALLOWED_SEE = "See also " + CoreConstants.CODES_URL + "#conditionNew"; + public static final String CONDITION_ATTR_DEPRECATED_MSG = "The 'condition' attribute in element is deprecated and slated for removal. Use element instead."; + public static final String CONDITION_ATTR_DEPRECATED_SEE = "See also " + CoreConstants.CODES_URL + "#conditionAttributeDeprecation"; + enum Branch {IF_BRANCH, ELSE_BRANCH; } IfModel ifModel = null; @@ -60,6 +63,8 @@ public void handle(ModelInterpretationContext mic, Model model) throws ModelHand mic.pushModel(ifModel); Object micTopObject = mic.peekObject(); String conditionStr = ifModel.getCondition(); + emitDeprecationWarningIfNecessary(conditionStr); + if(micTopObject instanceof BranchState) { BranchState branchState = (BranchState) micTopObject; @@ -119,11 +124,18 @@ private void janinoFallback(ModelInterpretationContext mic, Model model, String } } + private void emitDeprecationWarningIfNecessary(String conditionStr) { + if(!OptionHelper.isNullOrEmptyOrAllSpaces(conditionStr)) { + addWarn(CONDITION_ATTR_DEPRECATED_MSG); + addWarn(CONDITION_ATTR_DEPRECATED_SEE); + } + } + + private boolean hasNew(String conditionStr) { return conditionStr.contains("new "); } - @Override public void postHandle(ModelInterpretationContext mic, Model model) throws ModelHandlerException { From 258558f457089c786b6c36a51a8ff9a5a5c66b94 Mon Sep 17 00:00:00 2001 From: ceki Date: Thu, 16 Oct 2025 19:06:44 +0200 Subject: [PATCH 15/36] provide an alternative to Janino based conditional configuration processing - Part 3 Signed-off-by: ceki --- .../joran/conditional/ifSystem_NoJoran.xml | 29 ++++++++++++ .../boolex/IsPropertyNullCondition.java | 44 +++++++++++++++++++ .../joran/conditional/IfThenElseTest.java | 23 ++++++++-- .../ByPropertiesConditionModelHandler.java | 6 +++ 4 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifSystem_NoJoran.xml create mode 100644 logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/IsPropertyNullCondition.java diff --git a/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifSystem_NoJoran.xml b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifSystem_NoJoran.xml new file mode 100644 index 0000000000..6693a7ea6e --- /dev/null +++ b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifSystem_NoJoran.xml @@ -0,0 +1,29 @@ + + + + + + + + + sysKey + + + + + + + + diff --git a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/IsPropertyNullCondition.java b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/IsPropertyNullCondition.java new file mode 100644 index 0000000000..72723d6593 --- /dev/null +++ b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/IsPropertyNullCondition.java @@ -0,0 +1,44 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2025, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v1.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ + +package ch.qos.logback.core.blackbox.boolex; + +import ch.qos.logback.core.boolex.PropertyEvaluatorBase; + +public class IsPropertyNullCondition extends PropertyEvaluatorBase { + + String key; + + public void start() { + if (key == null) { + addError("In IsPropertyNullCondition 'key' parameter cannot be null"); + return; + } + super.start(); + } + + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @Override + public boolean evaluate() { + return isNull(key); + } +} \ No newline at end of file diff --git a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/joran/conditional/IfThenElseTest.java b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/joran/conditional/IfThenElseTest.java index 6ece86851d..7ade64eea2 100644 --- a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/joran/conditional/IfThenElseTest.java +++ b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/joran/conditional/IfThenElseTest.java @@ -220,9 +220,7 @@ public void nestedIf_NoJoran() throws JoranException { verifyConfig(new String[] { "BEGIN", "a", "c", "END" }); Assertions.assertTrue(checker.isErrorFree(0)); } - // ---------------------------------------------------------------------------------------------------- - @Test public void useNonExistenceOfSystemPropertyToDefineAContextProperty() throws JoranException { Assertions.assertNull(System.getProperty(sysKey)); @@ -231,7 +229,15 @@ public void useNonExistenceOfSystemPropertyToDefineAContextProperty() throws Jor //System.out.println(dynaKey + "=" + context.getProperty(dynaKey)); Assertions.assertNotNull(context.getProperty(dynaKey)); } - + @Test + public void useNonExistenceOfSystemPropertyToDefineAContextProperty_NoJoran() throws JoranException { + Assertions.assertNull(System.getProperty(sysKey)); + Assertions.assertNull(context.getProperty(dynaKey)); + simpleConfigurator.doConfigure(CONDITIONAL_DIR_PREFIX + "ifSystem_NoJoran.xml"); + //System.out.println(dynaKey + "=" + context.getProperty(dynaKey)); + Assertions.assertNotNull(context.getProperty(dynaKey)); + } + // ---------------------------------------------------------------------------------------------------- @Test public void noContextPropertyShouldBeDefinedIfSystemPropertyExists() throws JoranException { System.setProperty(sysKey, "a"); @@ -242,6 +248,17 @@ public void noContextPropertyShouldBeDefinedIfSystemPropertyExists() throws Jora Assertions.assertNull(context.getProperty(dynaKey)); } + @Test + public void noContextPropertyShouldBeDefinedIfSystemPropertyExists_NoJoran() throws JoranException { + System.setProperty(sysKey, "a"); + Assertions.assertNull(context.getProperty(dynaKey)); + System.out.println("before " + dynaKey + "=" + context.getProperty(dynaKey)); + simpleConfigurator.doConfigure(CONDITIONAL_DIR_PREFIX + "ifSystem_NoJoran.xml"); + System.out.println(dynaKey + "=" + context.getProperty(dynaKey)); + Assertions.assertNull(context.getProperty(dynaKey)); + } + // ---------------------------------------------------------------------------------------------------- + private void verifyConfig(String[] expected) { Stack witness = new Stack<>(); witness.addAll(Arrays.asList(expected)); diff --git a/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java b/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java index d1fefb7e2f..12c0119e9a 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java +++ b/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java @@ -85,6 +85,12 @@ public void postHandle(ModelInterpretationContext mic, Model model) throws Model mic.popObject(); } + propertyEvaluator.start(); + if(!propertyEvaluator.isStarted()) { + addError("PropertyEvaluator of type ["+propertyEvaluator.getClass().getName()+"] did not start successfully."); + mic.pushObject(IfModel.BranchState.IN_ERROR); + return; + } boolean evaluationResult = propertyEvaluator.evaluate(); IfModel.BranchState branchState = evaluationResult ? IF_BRANCH : ELSE_BRANCH; mic.pushObject(branchState); From 0b4432a31921df31e31bf9f4331f6e7e2888e893 Mon Sep 17 00:00:00 2001 From: ceki Date: Sat, 18 Oct 2025 22:20:59 +0200 Subject: [PATCH 16/36] provide an alternative to Janino based conditional configuration processing - Part 4 Signed-off-by: ceki --- .../joran/conditional/if0_NoJoran.xml | 2 +- .../joran/conditional/ifSystem_NoJoran.xml | 2 +- .../conditional/ifWithoutElse_NoJoran.xml | 2 +- .../conditional/if_localProperty_NoJoran.xml | 2 +- .../blackbox/boolex/AlwaysFalseCondition.java | 4 +- .../blackbox/boolex/AlwaysTrueCondition.java | 4 +- .../boolex/IsPropertyDefinedCondition.java | 77 ++++++++++++ .../core}/boolex/IsPropertyNullCondition.java | 6 +- ...yEvaluator.java => PropertyCondition.java} | 3 +- ...orBase.java => PropertyConditionBase.java} | 51 ++++++-- .../boolex/PropertyEqualityCondition.java | 117 ++++++++++++++++++ .../core/boolex/PropertyEqualsValue.java | 68 ---------- .../core/joran/conditional/Condition.java | 20 +++ .../ByPropertiesConditionModelHandler.java | 8 +- .../qos/logback/core/util/OptionHelper.java | 14 +++ 15 files changed, 282 insertions(+), 98 deletions(-) create mode 100644 logback-core/src/main/java/ch/qos/logback/core/boolex/IsPropertyDefinedCondition.java rename {logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox => logback-core/src/main/java/ch/qos/logback/core}/boolex/IsPropertyNullCondition.java (84%) rename logback-core/src/main/java/ch/qos/logback/core/boolex/{PropertyEvaluator.java => PropertyCondition.java} (94%) rename logback-core/src/main/java/ch/qos/logback/core/boolex/{PropertyEvaluatorBase.java => PropertyConditionBase.java} (59%) create mode 100644 logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEqualityCondition.java delete mode 100644 logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEqualsValue.java diff --git a/logback-core-blackbox/src/test/blackboxInput/joran/conditional/if0_NoJoran.xml b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/if0_NoJoran.xml index 6f1a863bc8..f0af770984 100644 --- a/logback-core-blackbox/src/test/blackboxInput/joran/conditional/if0_NoJoran.xml +++ b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/if0_NoJoran.xml @@ -14,7 +14,7 @@ - + ki1 val1 diff --git a/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifSystem_NoJoran.xml b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifSystem_NoJoran.xml index 6693a7ea6e..5d47fb2b78 100644 --- a/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifSystem_NoJoran.xml +++ b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifSystem_NoJoran.xml @@ -17,7 +17,7 @@ - + sysKey diff --git a/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifWithoutElse_NoJoran.xml b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifWithoutElse_NoJoran.xml index 4da818d79b..60299b5d07 100644 --- a/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifWithoutElse_NoJoran.xml +++ b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifWithoutElse_NoJoran.xml @@ -14,7 +14,7 @@ - + ki1 val1 diff --git a/logback-core-blackbox/src/test/blackboxInput/joran/conditional/if_localProperty_NoJoran.xml b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/if_localProperty_NoJoran.xml index 3095b9f136..28a92500e4 100644 --- a/logback-core-blackbox/src/test/blackboxInput/joran/conditional/if_localProperty_NoJoran.xml +++ b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/if_localProperty_NoJoran.xml @@ -15,7 +15,7 @@ - + Ki1 Val1 diff --git a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/AlwaysFalseCondition.java b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/AlwaysFalseCondition.java index 90cb68cea9..857de7c761 100644 --- a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/AlwaysFalseCondition.java +++ b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/AlwaysFalseCondition.java @@ -14,9 +14,9 @@ package ch.qos.logback.core.blackbox.boolex; -import ch.qos.logback.core.boolex.PropertyEvaluatorBase; +import ch.qos.logback.core.boolex.PropertyConditionBase; -public class AlwaysFalseCondition extends PropertyEvaluatorBase { +public class AlwaysFalseCondition extends PropertyConditionBase { @Override public boolean evaluate() { diff --git a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/AlwaysTrueCondition.java b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/AlwaysTrueCondition.java index 3de8659cc0..d264028df6 100644 --- a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/AlwaysTrueCondition.java +++ b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/AlwaysTrueCondition.java @@ -14,9 +14,9 @@ package ch.qos.logback.core.blackbox.boolex; -import ch.qos.logback.core.boolex.PropertyEvaluatorBase; +import ch.qos.logback.core.boolex.PropertyConditionBase; -public class AlwaysTrueCondition extends PropertyEvaluatorBase { +public class AlwaysTrueCondition extends PropertyConditionBase { @Override public boolean evaluate() { diff --git a/logback-core/src/main/java/ch/qos/logback/core/boolex/IsPropertyDefinedCondition.java b/logback-core/src/main/java/ch/qos/logback/core/boolex/IsPropertyDefinedCondition.java new file mode 100644 index 0000000000..ac507d06aa --- /dev/null +++ b/logback-core/src/main/java/ch/qos/logback/core/boolex/IsPropertyDefinedCondition.java @@ -0,0 +1,77 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2025, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v1.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ + +package ch.qos.logback.core.boolex; + +/** + * Checks whether a named property is defined in the + * context (e.g. system properties, environment, or the configured + * property map used by the surrounding framework). + * + *

This condition expects a property name to be provided via + * {@link #setKey(String)}. When {@link #evaluate()} is called it returns + * {@code true} if the named property is defined and {@code false} + * otherwise. + */ +public class IsPropertyDefinedCondition extends PropertyConditionBase { + + /** + * The property name to check for definition. Must be set before + * starting this evaluator. + */ + String key; + + /** + * Start the evaluator. If the required {@link #key} is not set an + * error is reported and startup is aborted. + */ + public void start() { + if (key == null) { + addError("In IsPropertyDefinedEvaluator 'key' parameter cannot be null"); + return; + } + super.start(); + } + + /** + * Return the configured property name (key) that this evaluator will + * test for definition. + * + * @return the property key, or {@code null} if not set + */ + public String getKey() { + return key; + } + + /** + * Set the property name (key) to be checked by this evaluator. + * + * @param key the property name to check; must not be {@code null} + */ + public void setKey(String key) { + this.key = key; + } + + + /** + * Evaluate whether the configured property is defined. + * + * @return {@code true} if the property named by {@link #key} is + * defined, {@code false} otherwise + */ + @Override + public boolean evaluate() { + return isDefined(key); + } +} diff --git a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/IsPropertyNullCondition.java b/logback-core/src/main/java/ch/qos/logback/core/boolex/IsPropertyNullCondition.java similarity index 84% rename from logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/IsPropertyNullCondition.java rename to logback-core/src/main/java/ch/qos/logback/core/boolex/IsPropertyNullCondition.java index 72723d6593..2bd024808a 100644 --- a/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/boolex/IsPropertyNullCondition.java +++ b/logback-core/src/main/java/ch/qos/logback/core/boolex/IsPropertyNullCondition.java @@ -12,11 +12,9 @@ * as published by the Free Software Foundation. */ -package ch.qos.logback.core.blackbox.boolex; +package ch.qos.logback.core.boolex; -import ch.qos.logback.core.boolex.PropertyEvaluatorBase; - -public class IsPropertyNullCondition extends PropertyEvaluatorBase { +public class IsPropertyNullCondition extends PropertyConditionBase { String key; diff --git a/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluator.java b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyCondition.java similarity index 94% rename from logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluator.java rename to logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyCondition.java index 5563997609..2a4e298026 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluator.java +++ b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyCondition.java @@ -16,7 +16,6 @@ import ch.qos.logback.core.Context; import ch.qos.logback.core.joran.conditional.Condition; -import ch.qos.logback.core.joran.conditional.PropertyEvalScriptBuilder; import ch.qos.logback.core.spi.ContextAware; import ch.qos.logback.core.spi.LifeCycle; import ch.qos.logback.core.spi.PropertyContainer; @@ -39,7 +38,7 @@ * @since 1.5.20 * @author Ceki Gülcü */ -public interface PropertyEvaluator extends Condition, ContextAware, LifeCycle { +public interface PropertyCondition extends Condition, ContextAware, LifeCycle { /** * Returns the local {@link PropertyContainer} used for property lookups specific to the embedding configurator. diff --git a/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluatorBase.java b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyConditionBase.java similarity index 59% rename from logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluatorBase.java rename to logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyConditionBase.java index 66cdfb4f5d..343e451315 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEvaluatorBase.java +++ b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyConditionBase.java @@ -14,22 +14,31 @@ package ch.qos.logback.core.boolex; +import ch.qos.logback.core.model.processor.ModelInterpretationContext; import ch.qos.logback.core.spi.ContextAwareBase; import ch.qos.logback.core.spi.PropertyContainer; import ch.qos.logback.core.util.OptionHelper; /** - *

Abstract base class provides some scaffolding. it is intended to ease migration + *

Abstract base class provides some scaffolding. It is intended to ease migration * from legacy conditional processing in configuration files - * (e.g. <if>, <then>, <else>) using the Janino library. - *

+ * (e.g. <if>, <then>, <else>) using the Janino library. Nevertheless, + * it should also be useful in newly written code.

* - *

Nevertheless, it should also be useful in newly written code.

+ *

Properties are looked up in the following order:

* + *
    + *
  1. In the local property container, usually the {@link ModelInterpretationContext}
  2. + *
  3. in the logger context
  4. + *
  5. system properties
  6. + *
  7. environment variables
  8. + *
+ * + * @see OptionHelper#propertyLookup(String, PropertyContainer, PropertyContainer) * @since 1.5.20 * @author Ceki Gülcü */ -abstract public class PropertyEvaluatorBase extends ContextAwareBase implements PropertyEvaluator { +abstract public class PropertyConditionBase extends ContextAwareBase implements PropertyCondition { /** * Indicates whether this evaluator has been started. @@ -39,14 +48,17 @@ abstract public class PropertyEvaluatorBase extends ContextAwareBase implements *

The local property container used for property lookups.

* *

Local properties correspond to the properties in the embedding - * configurator.

+ * configurator, i.e. usually the {@link ModelInterpretationContext} instance.

*/ PropertyContainer localPropertyContainer; /** - * Returns the property container used by this evaluator. + * Returns the local property container used by this evaluator. + * + *

Local properties correspond to the properties in the embedding + * configurator, i.e. usually the {@link ModelInterpretationContext} instance.

* - * @return the property container + * @return the local property container */ @Override public PropertyContainer getLocalPropertyContainer() { @@ -54,18 +66,25 @@ public PropertyContainer getLocalPropertyContainer() { } /** - * Sets the property container for this evaluator. + * Sets the local property container for this evaluator. + * + *

Local properties correspond to the properties in the embedding + * configurator, i.e. usually the {@link ModelInterpretationContext} instance.

* - * @param aPropertyContainer the property container to set + * @param aLocalPropertyContainer the local property container to set */ @Override - public void setLocalPropertyContainer(PropertyContainer aPropertyContainer) { - this.localPropertyContainer = aPropertyContainer; + public void setLocalPropertyContainer(PropertyContainer aLocalPropertyContainer) { + this.localPropertyContainer = aLocalPropertyContainer; } /** * Checks if the property with the given key is null. * + *

The property is looked up via the + * {@link OptionHelper#propertyLookup(String, PropertyContainer, PropertyContainer)} method. + * See above for the lookup order.

+ * * @param k the property key * @return true if the property is null, false otherwise */ @@ -77,6 +96,10 @@ public boolean isNull(String k) { /** * Checks if the property with the given key is defined (not null). * + *

The property is looked up via the + * {@link OptionHelper#propertyLookup(String, PropertyContainer, PropertyContainer)} method. + * See above for the lookup order.

+ * * @param k the property key * @return true if the property is defined, false otherwise */ @@ -99,6 +122,10 @@ public String p(String k) { /** * Retrieves the property value for the given key, returning an empty string if null. * + *

The property is looked up via the + * {@link OptionHelper#propertyLookup(String, PropertyContainer, PropertyContainer)} method. + * See above for the lookup order.

+ * * @param k the property key * @return the property value or an empty string */ diff --git a/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEqualityCondition.java b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEqualityCondition.java new file mode 100644 index 0000000000..e48fbcd5c4 --- /dev/null +++ b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEqualityCondition.java @@ -0,0 +1,117 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2025, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v1.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ + +package ch.qos.logback.core.boolex; + +/** + * Condition that evaluates to {@code true} when a property + * equals a specified expected value. + * + *

The property named by {@link #key} is resolved using the + * inherited property lookup mechanism (see {@code PropertyConditionBase}). + * If the resolved property value equals {@link #value} (using + * {@link String#equals(Object)}), this condition evaluates to {@code true}. + * + * @since 1.5.20 + */ +public class PropertyEqualityCondition extends PropertyConditionBase { + + /** + * The property name (key) to look up. Must be set before starting. + */ + String key; + + /** + * The expected value to compare the resolved property against. + */ + String value; + + /** + * Start the component and validate required parameters. + * If either {@link #key} or {@link #value} is {@code null}, an error + * is reported and the component does not start. + */ + public void start() { + if (key == null) { + addError("In PropertyEqualsValue 'key' parameter cannot be null"); + return; + } + if (value == null) { + addError("In PropertyEqualsValue 'value' parameter cannot be null"); + return; + } + super.start(); + } + + /** + * Return the configured expected value. + * + * @return the expected value, or {@code null} if not set + */ + public String getValue() { + return value; + } + + /** + * Set the expected value that the resolved property must equal for + * this condition to evaluate to {@code true}. + * + * @param value the expected value + */ + public void setValue(String value) { + this.value = value; + } + + /** + * Return the property key that will be looked up when evaluating the + * condition. + * + * @return the property key, or {@code null} if not set + */ + public String getKey() { + return key; + } + + /** + * Set the property key to resolve during evaluation. + * + * @param key the property key + */ + public void setKey(String key) { + this.key = key; + } + + /** + * Evaluate the condition: resolve the property named by {@link #key} + * and compare it to {@link #value}. + * + * @return {@code true} if the resolved property equals the expected + * value; {@code false} otherwise + */ + @Override + public boolean evaluate() { + if (key == null) { + addError("key cannot be null"); + return false; + } + + String val = p(key); + if (val == null) + return false; + else { + return val.equals(value); + } + } + +} diff --git a/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEqualsValue.java b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEqualsValue.java deleted file mode 100644 index 72f779845a..0000000000 --- a/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyEqualsValue.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Logback: the reliable, generic, fast and flexible logging framework. - * Copyright (C) 1999-2025, QOS.ch. All rights reserved. - * - * This program and the accompanying materials are dual-licensed under - * either the terms of the Eclipse Public License v1.0 as published by - * the Eclipse Foundation - * - * or (per the licensee's choosing) - * - * under the terms of the GNU Lesser General Public License version 2.1 - * as published by the Free Software Foundation. - */ - -package ch.qos.logback.core.boolex; - -public class PropertyEqualsValue extends PropertyEvaluatorBase { - - - String key; - String value; - - public void start() { - if (key == null) { - addError("In PropertyEqualsValue 'key' parameter cannot be null"); - return; - } - if (value == null) { - addError("In PropertyEqualsValue 'value' parameter cannot be null"); - return; - } - super.start(); - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - - @Override - public boolean evaluate() { - if (key == null) { - addError("key cannot be null"); - return false; - } - - String val = p(key); - if (val == null) - return false; - else { - return val.equals(value); - } - } - - -} diff --git a/logback-core/src/main/java/ch/qos/logback/core/joran/conditional/Condition.java b/logback-core/src/main/java/ch/qos/logback/core/joran/conditional/Condition.java index 250dbad672..8c6b417829 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/joran/conditional/Condition.java +++ b/logback-core/src/main/java/ch/qos/logback/core/joran/conditional/Condition.java @@ -13,6 +13,26 @@ */ package ch.qos.logback.core.joran.conditional; +/** + *

A condition evaluated during Joran conditional processing.

+ * + *

Implementations of this interface encapsulate a boolean test that + * determines whether a conditional block in a Joran configuration should + * be processed.

+ * + *

Typical implementations evaluate configuration state, environment + * variables, or other runtime properties.

+ * + * @since 0.9.20 + * @author Ceki Gülcü + */ public interface Condition { + + /** + * Evaluate the condition. + * + * @return {@code true} if the condition is satisfied and the associated + * conditional block should be activated; {@code false} otherwise + */ boolean evaluate(); } diff --git a/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java b/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java index 12c0119e9a..89bbb3d1be 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java +++ b/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java @@ -15,7 +15,7 @@ package ch.qos.logback.core.model.processor.conditional; import ch.qos.logback.core.Context; -import ch.qos.logback.core.boolex.PropertyEvaluator; +import ch.qos.logback.core.boolex.PropertyCondition; import ch.qos.logback.core.model.Model; import ch.qos.logback.core.model.conditional.IfModel; import ch.qos.logback.core.model.conditional.ByPropertiesConditionModel; @@ -30,7 +30,7 @@ public class ByPropertiesConditionModelHandler extends ModelHandlerBase { private boolean inError = false; - PropertyEvaluator propertyEvaluator; + PropertyCondition propertyEvaluator; public ByPropertiesConditionModelHandler(Context context) { super(context); @@ -61,8 +61,8 @@ public void handle(ModelInterpretationContext mic, Model model) throws ModelHand try { addInfo("About to instantiate PropertyEvaluator of type [" + className + "]"); - propertyEvaluator = (PropertyEvaluator) OptionHelper.instantiateByClassName(className, - PropertyEvaluator.class, context); + propertyEvaluator = (PropertyCondition) OptionHelper.instantiateByClassName(className, + PropertyCondition.class, context); propertyEvaluator.setContext(context); propertyEvaluator.setLocalPropertyContainer(mic); mic.pushObject(propertyEvaluator); diff --git a/logback-core/src/main/java/ch/qos/logback/core/util/OptionHelper.java b/logback-core/src/main/java/ch/qos/logback/core/util/OptionHelper.java index e38b8ebafe..c643a70892 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/util/OptionHelper.java +++ b/logback-core/src/main/java/ch/qos/logback/core/util/OptionHelper.java @@ -113,6 +113,20 @@ public static String substVars(String input, PropertyContainer pc0, PropertyCont } + /** + * Try to lookup the property in the following order: + *
    + *
  • pc1 (usually the local property container)
  • + *
  • pc2 (usually the {@link Context context})
  • + *
  • System properties
  • + *
  • Environment variables
  • + *
+ * + * @param key the property key + * @param pc1 the first property container to search + * @param pc2 the second property container to search + * @return the property value or null if not found + */ public static String propertyLookup(String key, PropertyContainer pc1, PropertyContainer pc2) { String value = null; // first try the props passed as parameter From 930fb15c993a4344bcecc6ba2225c12a2c38e676 Mon Sep 17 00:00:00 2001 From: ceki Date: Sat, 18 Oct 2025 22:54:23 +0200 Subject: [PATCH 17/36] prepare release 1.5.20 Signed-off-by: ceki --- logback-access/pom.xml | 2 +- logback-classic-blackbox/pom.xml | 2 +- logback-classic/pom.xml | 2 +- logback-core-blackbox/pom.xml | 2 +- logback-core/pom.xml | 2 +- logback-examples/pom.xml | 2 +- pom.xml | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/logback-access/pom.xml b/logback-access/pom.xml index e3eb32a060..47015e53aa 100644 --- a/logback-access/pom.xml +++ b/logback-access/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.20-SNAPSHOT + 1.5.20 logback-access diff --git a/logback-classic-blackbox/pom.xml b/logback-classic-blackbox/pom.xml index 654f8d560d..ff851ef0d2 100644 --- a/logback-classic-blackbox/pom.xml +++ b/logback-classic-blackbox/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.20-SNAPSHOT + 1.5.20 logback-classic-blackbox diff --git a/logback-classic/pom.xml b/logback-classic/pom.xml index d8a3ee457c..f6d1fe8638 100755 --- a/logback-classic/pom.xml +++ b/logback-classic/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.20-SNAPSHOT + 1.5.20 logback-classic diff --git a/logback-core-blackbox/pom.xml b/logback-core-blackbox/pom.xml index d6e7c61422..0f0de8b3e9 100644 --- a/logback-core-blackbox/pom.xml +++ b/logback-core-blackbox/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.20-SNAPSHOT + 1.5.20 logback-core-blackbox diff --git a/logback-core/pom.xml b/logback-core/pom.xml index f46203bf55..fd17f0ccc8 100755 --- a/logback-core/pom.xml +++ b/logback-core/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.20-SNAPSHOT + 1.5.20 logback-core diff --git a/logback-examples/pom.xml b/logback-examples/pom.xml index 1d0960e7a0..871f6c1766 100755 --- a/logback-examples/pom.xml +++ b/logback-examples/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.20-SNAPSHOT + 1.5.20 logback-examples diff --git a/pom.xml b/pom.xml index d2b71f12d8..d8fde11235 100755 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.20-SNAPSHOT + 1.5.20 pom Logback-Parent @@ -50,7 +50,7 @@ - 2025-10-01T09:35:18Z + 2025-10-18T20:53:00Z 11 From 6f8155ee43ad6d07f727e4ef3e70df61af164e63 Mon Sep 17 00:00:00 2001 From: ceki Date: Sun, 19 Oct 2025 20:26:35 +0200 Subject: [PATCH 18/36] start work on 1.5.21-SNAPSHOT Signed-off-by: ceki --- logback-access/pom.xml | 2 +- logback-classic-blackbox/pom.xml | 2 +- logback-classic/pom.xml | 2 +- logback-core-blackbox/pom.xml | 2 +- logback-core/pom.xml | 2 +- logback-examples/pom.xml | 2 +- pom.xml | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/logback-access/pom.xml b/logback-access/pom.xml index 47015e53aa..3e83462237 100644 --- a/logback-access/pom.xml +++ b/logback-access/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.20 + 1.5.21-SNAPSHOT logback-access diff --git a/logback-classic-blackbox/pom.xml b/logback-classic-blackbox/pom.xml index ff851ef0d2..1924d53f96 100644 --- a/logback-classic-blackbox/pom.xml +++ b/logback-classic-blackbox/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.20 + 1.5.21-SNAPSHOT logback-classic-blackbox diff --git a/logback-classic/pom.xml b/logback-classic/pom.xml index f6d1fe8638..af6462c176 100755 --- a/logback-classic/pom.xml +++ b/logback-classic/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.20 + 1.5.21-SNAPSHOT logback-classic diff --git a/logback-core-blackbox/pom.xml b/logback-core-blackbox/pom.xml index 0f0de8b3e9..69eae43d21 100644 --- a/logback-core-blackbox/pom.xml +++ b/logback-core-blackbox/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.20 + 1.5.21-SNAPSHOT logback-core-blackbox diff --git a/logback-core/pom.xml b/logback-core/pom.xml index fd17f0ccc8..dc03ec7578 100755 --- a/logback-core/pom.xml +++ b/logback-core/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.20 + 1.5.21-SNAPSHOT logback-core diff --git a/logback-examples/pom.xml b/logback-examples/pom.xml index 871f6c1766..2fcb33d325 100755 --- a/logback-examples/pom.xml +++ b/logback-examples/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.20 + 1.5.21-SNAPSHOT logback-examples diff --git a/pom.xml b/pom.xml index d8fde11235..5a5ec356e1 100755 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ ch.qos.logback logback-parent - 1.5.20 + 1.5.21-SNAPSHOT pom Logback-Parent @@ -50,7 +50,7 @@ - 2025-10-18T20:53:00Z + 2025-10-19T18:26:13Z 11 From 916b69141877d42029773ea99e3a0c99e88f453f Mon Sep 17 00:00:00 2001 From: ceki Date: Mon, 20 Oct 2025 15:15:34 +0200 Subject: [PATCH 19/36] make JsonEncoder extension friendly Signed-off-by: ceki --- .../logback/classic/encoder/JsonEncoder.java | 294 +++++++++++++----- 1 file changed, 219 insertions(+), 75 deletions(-) diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/encoder/JsonEncoder.java b/logback-classic/src/main/java/ch/qos/logback/classic/encoder/JsonEncoder.java index 18c61461c5..474294c33f 100644 --- a/logback-classic/src/main/java/ch/qos/logback/classic/encoder/JsonEncoder.java +++ b/logback-classic/src/main/java/ch/qos/logback/classic/encoder/JsonEncoder.java @@ -23,7 +23,6 @@ import org.slf4j.Marker; import org.slf4j.event.KeyValuePair; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; @@ -36,9 +35,45 @@ import static ch.qos.logback.core.model.ModelConstants.NULL_STR; /** + * JSON encoder that produces one JSON object per line in JSON Lines format, suitable for structured logging. + * Each {@link ILoggingEvent} is encoded into a JSON object containing fields such as timestamp, level, message, + * and optional elements like MDC properties, markers, and stack traces. * + *

This encoder supports extensive configuration through boolean flags to include or exclude specific fields + * in the output, allowing customization for different logging needs. For example, you can enable/disable + * sequence numbers, nanoseconds, thread names, logger context, markers, MDC, key-value pairs, arguments, + * and throwable information.

* - * https://jsonlines.org/ https://datatracker.ietf.org/doc/html/rfc8259 + *

The encoder is designed for extensibility: subclasses can override protected methods (e.g., + * {@link #appendLoggerContext}, {@link #appendThrowableProxy}, {@link #appendMarkers}) to customize + * how specific parts of the JSON are generated. Additionally, the {@link #appendCustomFields} hook + * allows appending custom top-level fields to the JSON object.

+ * + *

Configuration

+ *

Use the setter methods (e.g., {@link #setWithSequenceNumber}, {@link #setWithTimestamp}) to control + * which fields are included. By default, most fields are enabled except {@code withFormattedMessage}.

+ * + *

Example Usage

+ *
{@code
+ * 
+ *   
+ *     false
+ *     false
+ *     false
+ *   
+ * 
+ * }
+ * + *

This produces output similar to the following (on a single line): + *

+ * + *
{"timestamp":1640995200000,"level":"INFO","loggerName":"com.example.MyClass","context":{"name":"default","birthdate":1640995200000,"properties":{}},"message":"Hello World"}
+ * + * @see JSON Lines + * @see RFC 8259 (The JavaScript Object Notation (JSON) Data Interchange Format) + * @see ch.qos.logback.core.encoder.EncoderBase + * @since 1.3.8/1.4.8 + * @author Ceki Gülcü */ public class JsonEncoder extends EncoderBase { static final boolean DO_NOT_ADD_QUOTE_KEY = false; @@ -89,20 +124,20 @@ public class JsonEncoder extends EncoderBase { public static final String STEP_ARRAY_NAME_ATTRIBUTE = "stepArray"; - private static final char OPEN_OBJ = '{'; - private static final char CLOSE_OBJ = '}'; - private static final char OPEN_ARRAY = '['; - private static final char CLOSE_ARRAY = ']'; + protected static final char OPEN_OBJ = '{'; + protected static final char CLOSE_OBJ = '}'; + protected static final char OPEN_ARRAY = '['; + protected static final char CLOSE_ARRAY = ']'; - private static final char QUOTE = DOUBLE_QUOTE_CHAR; - private static final char SP = ' '; - private static final char ENTRY_SEPARATOR = COLON_CHAR; + protected static final char QUOTE = DOUBLE_QUOTE_CHAR; + protected static final char SP = ' '; + protected static final char ENTRY_SEPARATOR = COLON_CHAR; - private static final String COL_SP = ": "; + protected static final String COL_SP = ": "; - private static final String QUOTE_COL = "\":"; + protected static final String QUOTE_COL = "\":"; - private static final char VALUE_SEPARATOR = COMMA_CHAR; + protected static final char VALUE_SEPARATOR = COMMA_CHAR; private boolean withSequenceNumber = true; @@ -194,12 +229,34 @@ public byte[] encode(ILoggingEvent event) { if (withThrowable) appendThrowableProxy(sb, THROWABLE_ATTR_NAME, event.getThrowableProxy()); + // allow subclasses to append custom top-level fields; default implementation is a no-op + appendCustomFields(sb, event); + sb.append(CLOSE_OBJ); sb.append(CoreConstants.JSON_LINE_SEPARATOR); return sb.toString().getBytes(UTF_8_CHARSET); } - void appendValueSeparator(StringBuilder sb, boolean... subsequentConditionals) { + /** + * Append a JSON value separator (a comma) to the provided {@link StringBuilder} + * when any of the supplied boolean flags indicate that a subsequent element + * is present. + * + *

Callers pass a sequence of booleans that represent whether subsequent + * JSON members will be written. If at least one of those booleans is + * {@code true}, this method appends a single comma (',') to separate JSON + * fields.

+ * + *

This method is protected so subclasses that extend the encoder can + * reuse or override the logic for inserting separators between generated + * JSON members.

+ * + * @param sb the {@link StringBuilder} to append the separator to; must not be {@code null} + * @param subsequentConditionals one or more booleans indicating whether + * subsequent JSON elements will be written. + * If any value is {@code true}, a comma is appended. + */ + protected void appendValueSeparator(StringBuilder sb, boolean... subsequentConditionals) { boolean enabled = false; for (boolean subsequent : subsequentConditionals) { if (subsequent) { @@ -212,7 +269,7 @@ void appendValueSeparator(StringBuilder sb, boolean... subsequentConditionals) { sb.append(VALUE_SEPARATOR); } - private void appendLoggerContext(StringBuilder sb, LoggerContextVO loggerContextVO) { + protected void appendLoggerContext(StringBuilder sb, LoggerContextVO loggerContextVO) { sb.append(QUOTE).append(CONTEXT_ATTR_NAME).append(QUOTE_COL); if (loggerContextVO == null) { @@ -231,7 +288,7 @@ private void appendLoggerContext(StringBuilder sb, LoggerContextVO loggerContext } - private void appendMap(StringBuilder sb, String attrName, Map map) { + protected void appendMap(StringBuilder sb, String attrName, Map map) { sb.append(QUOTE).append(attrName).append(QUOTE_COL); if (map == null) { sb.append(NULL_STR); @@ -253,11 +310,11 @@ private void appendMap(StringBuilder sb, String attrName, Map ma sb.append(CLOSE_OBJ); } - private void appendThrowableProxy(StringBuilder sb, String attributeName, IThrowableProxy itp) { + protected void appendThrowableProxy(StringBuilder sb, String attributeName, IThrowableProxy itp) { appendThrowableProxy(sb, attributeName, itp, true); } - private void appendThrowableProxy(StringBuilder sb, String attributeName, IThrowableProxy itp, boolean appendValueSeparator) { + protected void appendThrowableProxy(StringBuilder sb, String attributeName, IThrowableProxy itp, boolean appendValueSeparator) { if (appendValueSeparator) sb.append(VALUE_SEPARATOR); @@ -316,10 +373,16 @@ private void appendThrowableProxy(StringBuilder sb, String attributeName, IThrow } - private void appendSTEPArray(StringBuilder sb, StackTraceElementProxy[] stepArray, int commonFrames) { + protected void appendSTEPArray(StringBuilder sb, StackTraceElementProxy[] stepArray, int commonFrames) { sb.append(QUOTE).append(STEP_ARRAY_NAME_ATTRIBUTE).append(QUOTE_COL).append(OPEN_ARRAY); - int len = stepArray != null ? stepArray.length : 0; + // If there are no stack trace elements, write an empty array and return early. + if (stepArray == null || stepArray.length == 0) { + sb.append(CLOSE_ARRAY); + return; + } + + int len = stepArray.length; if (commonFrames >= len) { commonFrames = 0; @@ -351,19 +414,36 @@ private void appendSTEPArray(StringBuilder sb, StackTraceElementProxy[] stepArra sb.append(CLOSE_ARRAY); } - private void appenderMember(StringBuilder sb, String key, String value) { + /** + * Hook allowing subclasses to append additional fields into the root JSON object. + * Default implementation is a no-op. + * + *

Subclasses may append additional top-level JSON members here. If a + * subclass writes additional members it should prepend them with + * {@link #VALUE_SEPARATOR} (a comma) if necessary to keep the JSON valid. + * Implementations must not close the root JSON object or write the final + * line separator; {@link JsonEncoder} handles those.

+ * + * @param sb the StringBuilder that accumulates the JSON output; never null + * @param event the logging event being encoded; never null + */ + protected void appendCustomFields(StringBuilder sb, ILoggingEvent event) { + // no-op by default; subclasses may append VALUE_SEPARATOR then their fields + } + + protected void appenderMember(StringBuilder sb, String key, String value) { sb.append(QUOTE).append(key).append(QUOTE_COL).append(QUOTE).append(value).append(QUOTE); } - private void appenderMemberWithIntValue(StringBuilder sb, String key, int value) { + protected void appenderMemberWithIntValue(StringBuilder sb, String key, int value) { sb.append(QUOTE).append(key).append(QUOTE_COL).append(value); } - private void appenderMemberWithLongValue(StringBuilder sb, String key, long value) { + protected void appenderMemberWithLongValue(StringBuilder sb, String key, long value) { sb.append(QUOTE).append(key).append(QUOTE_COL).append(value); } - private void appendKeyValuePairs(StringBuilder sb, ILoggingEvent event) { + protected void appendKeyValuePairs(StringBuilder sb, ILoggingEvent event) { List kvpList = event.getKeyValuePairs(); if (kvpList == null || kvpList.isEmpty()) return; @@ -382,7 +462,7 @@ private void appendKeyValuePairs(StringBuilder sb, ILoggingEvent event) { sb.append(CLOSE_ARRAY); } - private void appendArgumentArray(StringBuilder sb, ILoggingEvent event) { + protected void appendArgumentArray(StringBuilder sb, ILoggingEvent event) { Object[] argumentArray = event.getArgumentArray(); if (argumentArray == null) return; @@ -399,7 +479,7 @@ private void appendArgumentArray(StringBuilder sb, ILoggingEvent event) { sb.append(CLOSE_ARRAY); } - private void appendMarkers(StringBuilder sb, ILoggingEvent event) { + protected void appendMarkers(StringBuilder sb, ILoggingEvent event) { List markerList = event.getMarkerList(); if (markerList == null) return; @@ -434,7 +514,7 @@ private String jsonEscape(String s) { return jsonEscapeString(s); } - private void appendMDC(StringBuilder sb, ILoggingEvent event) { + protected void appendMDC(StringBuilder sb, ILoggingEvent event) { Map map = event.getMDCPropertyMap(); sb.append(VALUE_SEPARATOR); sb.append(QUOTE).append(MDC_ATTR_NAME).append(QUOTE_COL).append(SP).append(OPEN_OBJ); @@ -452,7 +532,13 @@ private void appendMDC(StringBuilder sb, ILoggingEvent event) { sb.append(CLOSE_OBJ); } - boolean isNotEmptyMap(Map map) { + /** + * Return {@code true} when the provided map is non-null and non-empty. + * + * @param map the map to check; may be null + * @return {@code true} if the map contains at least one entry + */ + boolean isNotEmptyMap(Map map) { if (map == null) return false; return !map.isEmpty(); @@ -464,7 +550,8 @@ public byte[] footerBytes() { } /** - * @param withSequenceNumber + * Set whether the sequence number is included in each encoded event. + * @param withSequenceNumber {@code true} to include the sequence number in the output * @since 1.5.0 */ public void setWithSequenceNumber(boolean withSequenceNumber) { @@ -472,7 +559,8 @@ public void setWithSequenceNumber(boolean withSequenceNumber) { } /** - * @param withTimestamp + * Set whether the event timestamp is included in each encoded event. + * @param withTimestamp {@code true} to include the event timestamp in the output * @since 1.5.0 */ public void setWithTimestamp(boolean withTimestamp) { @@ -480,55 +568,111 @@ public void setWithTimestamp(boolean withTimestamp) { } /** - * @param withNanoseconds + * Set whether nanoseconds will be included in the timestamp output. + * @param withNanoseconds {@code true} to include nanoseconds in the timestamp output * @since 1.5.0 */ public void setWithNanoseconds(boolean withNanoseconds) { this.withNanoseconds = withNanoseconds; } - public void setWithLevel(boolean withLevel) { - this.withLevel = withLevel; - } - - public void setWithThreadName(boolean withThreadName) { - this.withThreadName = withThreadName; - } - - public void setWithLoggerName(boolean withLoggerName) { - this.withLoggerName = withLoggerName; - } - - public void setWithContext(boolean withContext) { - this.withContext = withContext; - } - - public void setWithMarkers(boolean withMarkers) { - this.withMarkers = withMarkers; - } - - public void setWithMDC(boolean withMDC) { - this.withMDC = withMDC; - } - - public void setWithKVPList(boolean withKVPList) { - this.withKVPList = withKVPList; - } - - public void setWithMessage(boolean withMessage) { - this.withMessage = withMessage; - } - - public void setWithArguments(boolean withArguments) { - this.withArguments = withArguments; - } - - public void setWithThrowable(boolean withThrowable) { - this.withThrowable = withThrowable; - } - - public void setWithFormattedMessage(boolean withFormattedMessage) { - this.withFormattedMessage = withFormattedMessage; - } - -} + /** + * Enable or disable the inclusion of the log level in the encoded output. + * + * @param withLevel {@code true} to include the log level. Default is {@code true}. + */ + public void setWithLevel(boolean withLevel) { + this.withLevel = withLevel; + } + + /** + * Enable or disable the inclusion of the thread name in the encoded output. + * + * @param withThreadName {@code true} to include the thread name. Default is {@code true}. + */ + public void setWithThreadName(boolean withThreadName) { + this.withThreadName = withThreadName; + } + + /** + * Enable or disable the inclusion of the logger name in the encoded output. + * + * @param withLoggerName {@code true} to include the logger name. Default is {@code true}. + */ + public void setWithLoggerName(boolean withLoggerName) { + this.withLoggerName = withLoggerName; + } + + /** + * Enable or disable the inclusion of the logger context information. + * + * @param withContext {@code true} to include the logger context. Default is {@code true}. + */ + public void setWithContext(boolean withContext) { + this.withContext = withContext; + } + + /** + * Enable or disable the inclusion of markers in the encoded output. + * + * @param withMarkers {@code true} to include markers. Default is {@code true}. + */ + public void setWithMarkers(boolean withMarkers) { + this.withMarkers = withMarkers; + } + + /** + * Enable or disable the inclusion of MDC properties in the encoded output. + * + * @param withMDC {@code true} to include MDC properties. Default is {@code true}. + */ + public void setWithMDC(boolean withMDC) { + this.withMDC = withMDC; + } + + /** + * Enable or disable the inclusion of key-value pairs attached to the logging event. + * + * @param withKVPList {@code true} to include the key/value pairs list. Default is {@code true}. + */ + public void setWithKVPList(boolean withKVPList) { + this.withKVPList = withKVPList; + } + + /** + * Enable or disable the inclusion of the raw message text in the encoded output. + * + * @param withMessage {@code true} to include the message. Default is {@code true}. + */ + public void setWithMessage(boolean withMessage) { + this.withMessage = withMessage; + } + + /** + * Enable or disable the inclusion of the event argument array in the encoded output. + * + * @param withArguments {@code true} to include the argument array. Default is {@code true}. + */ + public void setWithArguments(boolean withArguments) { + this.withArguments = withArguments; + } + + /** + * Enable or disable the inclusion of throwable information in the encoded output. + * + * @param withThrowable {@code true} to include throwable/stacktrace information. Default is {@code true}. + */ + public void setWithThrowable(boolean withThrowable) { + this.withThrowable = withThrowable; + } + + /** + * Enable or disable the inclusion of the formatted message in the encoded output. + * + * @param withFormattedMessage {@code true} to include the formatted message. Default is {@code false}. + */ + public void setWithFormattedMessage(boolean withFormattedMessage) { + this.withFormattedMessage = withFormattedMessage; + } + + } From 0091c0c853c6736bf86a19da667028bf4550d6a4 Mon Sep 17 00:00:00 2001 From: ceki Date: Mon, 20 Oct 2025 19:51:17 +0200 Subject: [PATCH 20/36] fixes LOGBACK-427 Signed-off-by: ceki --- .../qos/logback/classic/log4j/XMLLayout.java | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/log4j/XMLLayout.java b/logback-classic/src/main/java/ch/qos/logback/classic/log4j/XMLLayout.java index 4bc04388a8..c4da9189e8 100644 --- a/logback-classic/src/main/java/ch/qos/logback/classic/log4j/XMLLayout.java +++ b/logback-classic/src/main/java/ch/qos/logback/classic/log4j/XMLLayout.java @@ -17,6 +17,7 @@ import java.util.Set; import java.util.Map.Entry; +import ch.qos.logback.classic.net.SMTPAppender; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.IThrowableProxy; import ch.qos.logback.classic.spi.StackTraceElementProxy; @@ -37,10 +38,8 @@ */ public class XMLLayout extends LayoutBase { - private final int DEFAULT_SIZE = 256; - private final int UPPER_LIMIT = 2048; + private static final int DEFAULT_SIZE = 256; - private StringBuilder buf = new StringBuilder(DEFAULT_SIZE); private boolean locationInfo = false; private boolean properties = false; @@ -57,8 +56,10 @@ public void start() { * *

* If you are embedding this layout within an - * {@link org.apache.log4j.net.SMTPAppender} then make sure to set the - * LocationInfo option of that appender as well. + * {@link SMTPAppender} then make sure to set the + * {@link SMTPAppender#setIncludeCallerData(boolean) includeCallerData} option + * as well. + *

*/ public void setLocationInfo(boolean flag) { locationInfo = flag; @@ -96,13 +97,7 @@ public boolean getProperties() { */ public String doLayout(ILoggingEvent event) { - // Reset working buffer. If the buffer is too large, then we need a new - // one in order to avoid the penalty of creating a large array. - if (buf.capacity() > UPPER_LIMIT) { - buf = new StringBuilder(DEFAULT_SIZE); - } else { - buf.setLength(0); - } + StringBuilder buf = new StringBuilder(DEFAULT_SIZE); // We yield to the \r\n heresy. From 46bf3763bea53c1849501f3ebbe809c7202b031f Mon Sep 17 00:00:00 2001 From: ceki Date: Mon, 20 Oct 2025 22:27:43 +0200 Subject: [PATCH 21/36] improved GZ compression Signed-off-by: ceki --- .../rolling/helper/CompressionStrategyBase.java | 2 +- .../core/rolling/helper/GZCompressionStrategy.java | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/CompressionStrategyBase.java b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/CompressionStrategyBase.java index f5ef07b3f6..b215564b52 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/CompressionStrategyBase.java +++ b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/CompressionStrategyBase.java @@ -22,7 +22,7 @@ abstract public class CompressionStrategyBase extends ContextAwareBase implements CompressionStrategy { - static final int BUFFER_SIZE = 8192; + static final int BUFFER_SIZE = 65536; void createMissingTargetDirsIfNecessary(File file) { boolean result = FileUtil.createMissingParentDirectories(file); diff --git a/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/GZCompressionStrategy.java b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/GZCompressionStrategy.java index d7b205ef4c..231b5d49a0 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/GZCompressionStrategy.java +++ b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/GZCompressionStrategy.java @@ -17,7 +17,6 @@ import ch.qos.logback.core.status.ErrorStatus; import ch.qos.logback.core.status.WarnStatus; -import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -50,16 +49,11 @@ public void compress(String originalFileName, String compressedFileName, String addInfo("GZ compressing [" + file2gz + "] as [" + gzedFile + "]"); createMissingTargetDirsIfNecessary(gzedFile); + System.out.println("GZ compressing [" + file2gz + "] as [" + gzedFile + "]"); + try (FileInputStream fis = new FileInputStream(originalFileName); + GZIPOutputStream gzos = new GZIPOutputStream(new FileOutputStream(compressedFileName), BUFFER_SIZE)) { - try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(originalFileName)); - GZIPOutputStream gzos = new GZIPOutputStream(new FileOutputStream(compressedFileName))) { - - byte[] inbuf = new byte[BUFFER_SIZE]; - int n; - - while ((n = bis.read(inbuf)) != -1) { - gzos.write(inbuf, 0, n); - } + fis.transferTo(gzos); addInfo("Done GZ compressing [" + file2gz + "] as [" + gzedFile + "]"); } catch (Exception e) { From f61f617b697d788451f5d21466172356b00f62d3 Mon Sep 17 00:00:00 2001 From: ceki Date: Tue, 21 Oct 2025 11:41:10 +0200 Subject: [PATCH 22/36] revert to while read loop Signed-off-by: ceki --- .../core/rolling/helper/GZCompressionStrategy.java | 10 +++++++--- .../core/rolling/helper/XZCompressionStrategy.java | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/GZCompressionStrategy.java b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/GZCompressionStrategy.java index 231b5d49a0..b5b04f240b 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/GZCompressionStrategy.java +++ b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/GZCompressionStrategy.java @@ -49,11 +49,15 @@ public void compress(String originalFileName, String compressedFileName, String addInfo("GZ compressing [" + file2gz + "] as [" + gzedFile + "]"); createMissingTargetDirsIfNecessary(gzedFile); - System.out.println("GZ compressing [" + file2gz + "] as [" + gzedFile + "]"); try (FileInputStream fis = new FileInputStream(originalFileName); - GZIPOutputStream gzos = new GZIPOutputStream(new FileOutputStream(compressedFileName), BUFFER_SIZE)) { + GZIPOutputStream gzos = new GZIPOutputStream(new FileOutputStream(compressedFileName), BUFFER_SIZE)) { - fis.transferTo(gzos); + byte[] inbuf = new byte[BUFFER_SIZE]; + int n; + + while ((n = fis.read(inbuf)) != -1) { + gzos.write(inbuf, 0, n); + } addInfo("Done GZ compressing [" + file2gz + "] as [" + gzedFile + "]"); } catch (Exception e) { diff --git a/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/XZCompressionStrategy.java b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/XZCompressionStrategy.java index 95a539dc6b..b5dd05591d 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/XZCompressionStrategy.java +++ b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/XZCompressionStrategy.java @@ -17,6 +17,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; + import org.tukaani.xz.LZMA2Options; import org.tukaani.xz.XZOutputStream; @@ -56,7 +57,7 @@ public void compress(String nameOfFile2xz, String nameOfxzedFile, String innerEn createMissingTargetDirsIfNecessary(xzedFile); try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(nameOfFile2xz)); - XZOutputStream xzos = new XZOutputStream(new FileOutputStream(nameOfxzedFile), new LZMA2Options())) { + XZOutputStream xzos = new XZOutputStream(new FileOutputStream(nameOfxzedFile), new LZMA2Options())) { byte[] inbuf = new byte[BUFFER_SIZE]; int n; From 8411d007cbd9a65228041e6c4724b8c2738320f6 Mon Sep 17 00:00:00 2001 From: ceki Date: Tue, 21 Oct 2025 12:06:47 +0200 Subject: [PATCH 23/36] minor improvements Signed-off-by: ceki --- .../main/java/ch/qos/logback/core/CoreConstants.java | 5 +++++ .../logback/core/rolling/helper/FileFilterUtil.java | 10 +++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/logback-core/src/main/java/ch/qos/logback/core/CoreConstants.java b/logback-core/src/main/java/ch/qos/logback/core/CoreConstants.java index 47b96f5bbf..dde2d07b17 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/CoreConstants.java +++ b/logback-core/src/main/java/ch/qos/logback/core/CoreConstants.java @@ -13,6 +13,7 @@ */ package ch.qos.logback.core; +import java.io.File; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -145,6 +146,10 @@ public class CoreConstants { * An empty Class array. */ public static final Class[] EMPTY_CLASS_ARRAY = new Class[] {}; + + + public static final File[] EMPTY_FILE_ARRAY = new File[0]; + public static final String CAUSED_BY = "Caused by: "; public static final String SUPPRESSED = "Suppressed: "; public static final String WRAPPED_BY = "Wrapped by: "; diff --git a/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/FileFilterUtil.java b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/FileFilterUtil.java index 1502e55188..68a59a831e 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/FileFilterUtil.java +++ b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/FileFilterUtil.java @@ -20,8 +20,12 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static ch.qos.logback.core.CoreConstants.EMPTY_FILE_ARRAY; + public class FileFilterUtil { + + public static void sortFileArrayByName(File[] fileArray) { Arrays.sort(fileArray, new Comparator() { public int compare(File o1, File o2) { @@ -74,10 +78,10 @@ static public boolean isEmptyDirectory(File dir) { public static File[] filesInFolderMatchingStemRegex(File file, final String stemRegex) { if (file == null) { - return new File[0]; + return EMPTY_FILE_ARRAY; } - if (!file.exists() || !file.isDirectory()) { - return new File[0]; + if (!file.isDirectory()) { + return EMPTY_FILE_ARRAY; } // better compile the regex. See also LOGBACK-1409 From a8e6fa4dc1118d011901e71c8fae8014043a604c Mon Sep 17 00:00:00 2001 From: ceki Date: Tue, 21 Oct 2025 17:54:18 +0200 Subject: [PATCH 24/36] drop BufferedInputStream to avoid axtra memory copies Signed-off-by: ceki --- .../logback/core/rolling/helper/XZCompressionStrategy.java | 4 ++-- .../core/rolling/helper/ZipCompressionStrategy.java | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/XZCompressionStrategy.java b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/XZCompressionStrategy.java index b5dd05591d..70904c4f47 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/XZCompressionStrategy.java +++ b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/XZCompressionStrategy.java @@ -56,13 +56,13 @@ public void compress(String nameOfFile2xz, String nameOfxzedFile, String innerEn addInfo("XZ compressing [" + file2xz + "] as [" + xzedFile + "]"); createMissingTargetDirsIfNecessary(xzedFile); - try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(nameOfFile2xz)); + try (FileInputStream fis = new FileInputStream(nameOfFile2xz); XZOutputStream xzos = new XZOutputStream(new FileOutputStream(nameOfxzedFile), new LZMA2Options())) { byte[] inbuf = new byte[BUFFER_SIZE]; int n; - while ((n = bis.read(inbuf)) != -1) { + while ((n = fis.read(inbuf)) != -1) { xzos.write(inbuf, 0, n); } } catch (Exception e) { diff --git a/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/ZipCompressionStrategy.java b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/ZipCompressionStrategy.java index 1ac2855ede..32730bc813 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/ZipCompressionStrategy.java +++ b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/ZipCompressionStrategy.java @@ -31,7 +31,6 @@ * @since 1.5.18 */ public class ZipCompressionStrategy extends CompressionStrategyBase { - static final int BUFFER_SIZE = 8192; @Override public void compress(String originalFileName, String compressedFileName, String innerEntryName) { @@ -64,8 +63,8 @@ public void compress(String originalFileName, String compressedFileName, String addInfo("ZIP compressing [" + file2zip + "] as [" + zippedFile + "]"); createMissingTargetDirsIfNecessary(zippedFile); - try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(originalFileName)); - ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(compressedFileName))) { + try (FileInputStream fis = new FileInputStream(originalFileName); + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(compressedFileName))) { ZipEntry zipEntry = computeZipEntry(innerEntryName); zos.putNextEntry(zipEntry); @@ -73,7 +72,7 @@ public void compress(String originalFileName, String compressedFileName, String byte[] inbuf = new byte[BUFFER_SIZE]; int n; - while ((n = bis.read(inbuf)) != -1) { + while ((n = fis.read(inbuf)) != -1) { zos.write(inbuf, 0, n); } From 26ea7ea6a0424956f185303c9f2ae32f2c43ae6a Mon Sep 17 00:00:00 2001 From: ceki Date: Thu, 23 Oct 2025 10:54:20 +0200 Subject: [PATCH 25/36] add support for parameterized unit tests Signed-off-by: ceki --- ...imeBasedFileNamingAndTriggeringPolicy.java | 5 +- ...asedFileNamingAndTriggeringPolicyBase.java | 8 ++- .../helper/TimeBasedArchiveRemover.java | 9 ++- .../ParentScaffoldingForRollingTests.java | 66 +++++++++++++++++++ pom.xml | 7 ++ 5 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 logback-core/src/test/java/ch/qos/logback/core/rolling/testUtil/ParentScaffoldingForRollingTests.java diff --git a/logback-core/src/main/java/ch/qos/logback/core/rolling/DefaultTimeBasedFileNamingAndTriggeringPolicy.java b/logback-core/src/main/java/ch/qos/logback/core/rolling/DefaultTimeBasedFileNamingAndTriggeringPolicy.java index 7b8612f042..8c05881328 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/rolling/DefaultTimeBasedFileNamingAndTriggeringPolicy.java +++ b/logback-core/src/main/java/ch/qos/logback/core/rolling/DefaultTimeBasedFileNamingAndTriggeringPolicy.java @@ -13,6 +13,8 @@ import java.io.File; import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Date; import ch.qos.logback.core.joran.spi.NoAutoStart; @@ -54,7 +56,8 @@ public boolean isTriggeringEvent(File activeFile, final E event) { long nextCheck = computeNextCheck(currentTime); atomicNextCheck.set(nextCheck); Instant instantOfElapsedPeriod = dateInCurrentPeriod; - addInfo("Elapsed period: " + instantOfElapsedPeriod.toString()); + ZonedDateTime ztd = instantOfElapsedPeriod.atZone(zoneId); + addInfo("Elapsed period: " + ztd.toString()); this.elapsedPeriodsFileName = tbrp.fileNamePatternWithoutCompSuffix.convert(instantOfElapsedPeriod); setDateInCurrentPeriod(currentTime); return true; diff --git a/logback-core/src/main/java/ch/qos/logback/core/rolling/TimeBasedFileNamingAndTriggeringPolicyBase.java b/logback-core/src/main/java/ch/qos/logback/core/rolling/TimeBasedFileNamingAndTriggeringPolicyBase.java index 7961399ccf..864636903e 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/rolling/TimeBasedFileNamingAndTriggeringPolicyBase.java +++ b/logback-core/src/main/java/ch/qos/logback/core/rolling/TimeBasedFileNamingAndTriggeringPolicyBase.java @@ -17,6 +17,7 @@ import java.io.File; import java.time.Instant; +import java.time.ZoneId; import java.util.Locale; import java.util.TimeZone; import java.util.concurrent.atomic.AtomicLong; @@ -56,6 +57,8 @@ abstract public class TimeBasedFileNamingAndTriggeringPolicyBase extends Cont protected boolean started = false; protected boolean errorFree = true; + protected ZoneId zoneId = ZoneId.systemDefault(); + public boolean isStarted() { return started; } @@ -68,7 +71,8 @@ public void start() { } if (dtc.getZoneId() != null) { - TimeZone tz = TimeZone.getTimeZone(dtc.getZoneId()); + this.zoneId = dtc.getZoneId(); + TimeZone tz = TimeZone.getTimeZone(zoneId); rc = new RollingCalendar(dtc.getDatePattern(), tz, Locale.getDefault()); } else { rc = new RollingCalendar(dtc.getDatePattern()); @@ -90,7 +94,7 @@ public void start() { if (tbrp.getParentsRawFileProperty() != null) { File currentFile = new File(tbrp.getParentsRawFileProperty()); - if (currentFile.exists() && currentFile.canRead()) { + if (currentFile.canRead()) { timestamp = currentFile.lastModified(); setDateInCurrentPeriod(timestamp); } diff --git a/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/TimeBasedArchiveRemover.java b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/TimeBasedArchiveRemover.java index d2ce45f8c1..9156ab416c 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/TimeBasedArchiveRemover.java +++ b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/TimeBasedArchiveRemover.java @@ -96,6 +96,7 @@ public void cleanPeriod(Instant instantOfPeriodToClean) { File[] matchingFileArray = getFilesInPeriod(instantOfPeriodToClean); for (File f : matchingFileArray) { + addInfo("deleting historically stale " + f); checkAndDeleteFile(f); } @@ -106,7 +107,7 @@ public void cleanPeriod(Instant instantOfPeriodToClean) { } private boolean checkAndDeleteFile(File f) { - addInfo("deleting historically stale " + f); + if (f == null) { addWarn("Cannot delete empty file"); return false; @@ -126,7 +127,7 @@ void capTotalSize(Instant now) { long totalSize = 0; long totalRemoved = 0; int successfulDeletions = 0; - int failedDeletions = 0; + int failedDeletions = 0; for (int offset = 0; offset < maxHistory; offset++) { Instant instant = rc.getEndOfNextNthPeriod(now, -offset); @@ -134,9 +135,11 @@ void capTotalSize(Instant now) { descendingSort(matchingFileArray, instant); for (File f : matchingFileArray) { long size = f.length(); + //System.out.println("File: " + f + " size=" + size); totalSize += size; if (totalSize > totalSizeCap) { - addInfo("Deleting [" + f + "]" + " of size " + new FileSize(size) + " on account of totalSizeCap " + totalSizeCap); + //addInfo("Deleting [" + f + "]" + " of size " + new FileSize(size) + " on account of totalSizeCap " + totalSizeCap); + addInfo("Deleting [" + f + "]" + " of size " + size + " on account of totalSizeCap " + totalSizeCap); boolean success = checkAndDeleteFile(f); diff --git a/logback-core/src/test/java/ch/qos/logback/core/rolling/testUtil/ParentScaffoldingForRollingTests.java b/logback-core/src/test/java/ch/qos/logback/core/rolling/testUtil/ParentScaffoldingForRollingTests.java new file mode 100644 index 0000000000..b50fc30c55 --- /dev/null +++ b/logback-core/src/test/java/ch/qos/logback/core/rolling/testUtil/ParentScaffoldingForRollingTests.java @@ -0,0 +1,66 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2025, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v1.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ + +package ch.qos.logback.core.rolling.testUtil; + +import ch.qos.logback.core.Context; +import ch.qos.logback.core.ContextBase; +import ch.qos.logback.core.encoder.EchoEncoder; +import ch.qos.logback.core.testUtil.CoreTestConstants; +import ch.qos.logback.core.testUtil.RandomUtil; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +public class ParentScaffoldingForRollingTest { + + protected EchoEncoder encoder = new EchoEncoder(); + protected int diff = RandomUtil.getPositiveInt(); + protected String randomOutputDir = CoreTestConstants.OUTPUT_DIR_PREFIX + diff + "/"; + + Calendar calendar = Calendar.getInstance(); + protected Context context = new ContextBase(); + + protected long currentTime; // initialized in setUp() + protected List> futureList = new ArrayList>(); + + public void setUp() { + context.setName("test"); + calendar.set(Calendar.MILLISECOND, 333); + currentTime = 1760822446333L; //calendar.getTimeInMillis(); + + } + + protected void add(Future future) { + if (future == null) + return; + if (!futureList.contains(future)) { + futureList.add(future); + } + } + + protected void waitForJobsToComplete() { + for (Future future : futureList) { + try { + future.get(10, TimeUnit.SECONDS); + } catch (Exception e) { + new RuntimeException("unexpected exception while testing", e); + } + } + futureList.clear(); + } +} diff --git a/pom.xml b/pom.xml index 5a5ec356e1..c7e211d6e1 100755 --- a/pom.xml +++ b/pom.xml @@ -146,6 +146,13 @@ test + + org.junit.jupiter + junit-jupiter-params + ${junit-jupiter-api.version} + test + + org.hamcrest hamcrest-library From 0e3ab572919177605327c3e52b8b3cf8d8c7c200 Mon Sep 17 00:00:00 2001 From: ceki Date: Thu, 23 Oct 2025 10:55:52 +0200 Subject: [PATCH 26/36] refactoring rolling appender tests Signed-off-by: ceki --- .../TimeBasedRollingWithConfigFileTest.java | 1 + .../JVMExitBeforeCompressionISDoneTest.java | 1 + .../rolling/SizeAndTimeBasedFNATP_Test.java | 2 +- .../core/rolling/SizeBasedRollingTest.java | 12 +- .../core/rolling/TimeBasedRollingTest.java | 4 +- ...meBasedRollingWithArchiveRemoval_Test.java | 75 +++++++++--- .../ParentScaffoldingForRollingTests.java | 69 ++++++++++- .../testUtil/ScaffoldingForRollingTests.java | 114 ++---------------- 8 files changed, 150 insertions(+), 128 deletions(-) diff --git a/logback-classic/src/test/java/ch/qos/logback/classic/rolling/TimeBasedRollingWithConfigFileTest.java b/logback-classic/src/test/java/ch/qos/logback/classic/rolling/TimeBasedRollingWithConfigFileTest.java index 58fd8b8865..b5972d2336 100755 --- a/logback-classic/src/test/java/ch/qos/logback/classic/rolling/TimeBasedRollingWithConfigFileTest.java +++ b/logback-classic/src/test/java/ch/qos/logback/classic/rolling/TimeBasedRollingWithConfigFileTest.java @@ -24,6 +24,7 @@ import ch.qos.logback.core.rolling.RollingFileAppender; import ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicy; import ch.qos.logback.core.rolling.TimeBasedRollingPolicy; +import ch.qos.logback.core.rolling.testUtil.ParentScaffoldingForRollingTests; import ch.qos.logback.core.rolling.testUtil.ScaffoldingForRollingTests; import ch.qos.logback.core.status.Status; import ch.qos.logback.core.status.testUtil.StatusChecker; diff --git a/logback-core/src/test/java/ch/qos/logback/core/rolling/JVMExitBeforeCompressionISDoneTest.java b/logback-core/src/test/java/ch/qos/logback/core/rolling/JVMExitBeforeCompressionISDoneTest.java index cc80c5c372..fba700acff 100755 --- a/logback-core/src/test/java/ch/qos/logback/core/rolling/JVMExitBeforeCompressionISDoneTest.java +++ b/logback-core/src/test/java/ch/qos/logback/core/rolling/JVMExitBeforeCompressionISDoneTest.java @@ -2,6 +2,7 @@ import java.util.Date; +import ch.qos.logback.core.rolling.testUtil.ParentScaffoldingForRollingTests; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; diff --git a/logback-core/src/test/java/ch/qos/logback/core/rolling/SizeAndTimeBasedFNATP_Test.java b/logback-core/src/test/java/ch/qos/logback/core/rolling/SizeAndTimeBasedFNATP_Test.java index a9e486502b..a76397f0ec 100755 --- a/logback-core/src/test/java/ch/qos/logback/core/rolling/SizeAndTimeBasedFNATP_Test.java +++ b/logback-core/src/test/java/ch/qos/logback/core/rolling/SizeAndTimeBasedFNATP_Test.java @@ -17,13 +17,13 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; -import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.function.UnaryOperator; import ch.qos.logback.core.CoreConstants; +import ch.qos.logback.core.rolling.testUtil.ParentScaffoldingForRollingTests; import ch.qos.logback.core.util.CachingDateFormatter; import ch.qos.logback.core.util.Duration; import ch.qos.logback.core.util.StatusPrinter; diff --git a/logback-core/src/test/java/ch/qos/logback/core/rolling/SizeBasedRollingTest.java b/logback-core/src/test/java/ch/qos/logback/core/rolling/SizeBasedRollingTest.java index 94e5e87f51..c4add4249e 100755 --- a/logback-core/src/test/java/ch/qos/logback/core/rolling/SizeBasedRollingTest.java +++ b/logback-core/src/test/java/ch/qos/logback/core/rolling/SizeBasedRollingTest.java @@ -16,23 +16,23 @@ import java.io.IOException; import java.util.List; +import ch.qos.logback.core.rolling.testUtil.ParentScaffoldingForRollingTests; import ch.qos.logback.core.util.Duration; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import ch.qos.logback.core.encoder.EchoEncoder; -import ch.qos.logback.core.rolling.testUtil.ScaffoldingForRollingTests; import ch.qos.logback.core.testUtil.CoreTestConstants; import ch.qos.logback.core.util.FileSize; import ch.qos.logback.core.util.StatusPrinter; +public class SizeBasedRollingTest extends ParentScaffoldingForRollingTests { -public class SizeBasedRollingTest extends ScaffoldingForRollingTests { - - RollingFileAppender rfa = new RollingFileAppender(); + static public final String DATE_PATTERN_WITH_SECONDS = "yyyy-MM-dd_HH_mm_ss"; + RollingFileAppender rfa = new RollingFileAppender<>(); FixedWindowRollingPolicy fwrp = new FixedWindowRollingPolicy(); - SizeBasedTriggeringPolicy sizeBasedTriggeringPolicy = new SizeBasedTriggeringPolicy(); - EchoEncoder encoder = new EchoEncoder(); + SizeBasedTriggeringPolicy sizeBasedTriggeringPolicy = new SizeBasedTriggeringPolicy<>(); + EchoEncoder encoder = new EchoEncoder<>(); @BeforeEach public void setUp() { diff --git a/logback-core/src/test/java/ch/qos/logback/core/rolling/TimeBasedRollingTest.java b/logback-core/src/test/java/ch/qos/logback/core/rolling/TimeBasedRollingTest.java index 1f636be770..f272ed246b 100755 --- a/logback-core/src/test/java/ch/qos/logback/core/rolling/TimeBasedRollingTest.java +++ b/logback-core/src/test/java/ch/qos/logback/core/rolling/TimeBasedRollingTest.java @@ -18,12 +18,13 @@ import java.io.IOException; import java.util.function.UnaryOperator; +import ch.qos.logback.core.rolling.testUtil.ParentScaffoldingForRollingTests; +import ch.qos.logback.core.rolling.testUtil.ScaffoldingForRollingTests; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import ch.qos.logback.core.encoder.EchoEncoder; -import ch.qos.logback.core.rolling.testUtil.ScaffoldingForRollingTests; import ch.qos.logback.core.testUtil.EnvUtilForTests; import ch.qos.logback.core.util.StatusPrinter; @@ -51,6 +52,7 @@ */ public class TimeBasedRollingTest extends ScaffoldingForRollingTests { + static public final String DATE_PATTERN_WITH_SECONDS = "yyyy-MM-dd_HH_mm_ss"; static final int NO_RESTART = 0; static final int WITH_RESTART = 1; static final int WITH_RESTART_AND_LONG_WAIT = 2000; diff --git a/logback-core/src/test/java/ch/qos/logback/core/rolling/TimeBasedRollingWithArchiveRemoval_Test.java b/logback-core/src/test/java/ch/qos/logback/core/rolling/TimeBasedRollingWithArchiveRemoval_Test.java index 5d34768e1c..d503c66c2c 100755 --- a/logback-core/src/test/java/ch/qos/logback/core/rolling/TimeBasedRollingWithArchiveRemoval_Test.java +++ b/logback-core/src/test/java/ch/qos/logback/core/rolling/TimeBasedRollingWithArchiveRemoval_Test.java @@ -17,7 +17,6 @@ import static ch.qos.logback.core.CoreConstants.STRICT_ISO8601_PATTERN; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; - import java.io.File; import java.io.FileFilter; import java.time.Instant; @@ -33,15 +32,21 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Stream; + import java.util.concurrent.atomic.LongAdder; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.time.temporal.ChronoUnit; +import ch.qos.logback.core.rolling.testUtil.ParentScaffoldingForRollingTests; +import ch.qos.logback.core.status.OnConsoleStatusListener; import ch.qos.logback.core.util.StatusPrinter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.ParameterizedTest; import ch.qos.logback.core.CoreConstants; import ch.qos.logback.core.pattern.SpacePadder; @@ -51,7 +56,7 @@ import ch.qos.logback.core.util.FileSize; import ch.qos.logback.core.util.FixedRateInvocationGate; -public class TimeBasedRollingWithArchiveRemoval_Test extends ScaffoldingForRollingTests { +public class TimeBasedRollingWithArchiveRemoval_Test extends ParentScaffoldingForRollingTests { String MONTHLY_DATE_PATTERN = "yyyy-MM"; String MONTHLY_CRONOLOG_DATE_PATTERN = "yyyy/MM"; final String DAILY_CRONOLOG_DATE_PATTERN = "yyyy/MM/dd"; @@ -61,7 +66,8 @@ public class TimeBasedRollingWithArchiveRemoval_Test extends ScaffoldingForRolli DateTimeFormatter STRICT_DATE_PARSER = DateTimeFormatter.ofPattern(STRICT_ISO8601_PATTERN); - // by default tbfnatp is an instance of + + // by default tbfnatp is an instance of // DefaultTimeBasedFileNamingAndTriggeringPolicy TimeBasedFileNamingAndTriggeringPolicy tbfnatp = new DefaultTimeBasedFileNamingAndTriggeringPolicy(); @@ -187,23 +193,38 @@ public void checkCleanupForBasicDailyRollover() { generateDailyRolloverAndCheckFileCount(cp); } - @Test - public void checkCleanupForBasicDailyRolloverWithSizeCap() { - long bytesOutputPerPeriod = 15984; + @ParameterizedTest + @MethodSource + public void checkCleanupForBasicDailyRolloverWithSizeCap(Long injectedCurrentTime) { + this.currentTime = injectedCurrentTime; + // the size cap is based on observations made during test runs + long bytesOutputPerPeriod = 16500; int sizeInUnitsOfBytesPerPeriod = 2; + // 1000 is to give some leeway + long sizeCap = sizeInUnitsOfBytesPerPeriod * bytesOutputPerPeriod + 1000; + + cp.maxHistory(5).simulatedNumberOfPeriods(10) - .sizeCap(sizeInUnitsOfBytesPerPeriod * bytesOutputPerPeriod + 1000); + .sizeCap(sizeCap); generateDailyRollover(cp); + // expect two archive for sizeInUnitsOfBytesPerPeriod =2 plus the latest period to remain checkFileCount(sizeInUnitsOfBytesPerPeriod + 1); } + static Stream checkCleanupForBasicDailyRolloverWithSizeCap() { + // currentTime = 1760822446333 + // Sat Oct 18 2025 21:20:46.333 UTC + return Stream.of(1760822446333L, System.currentTimeMillis()); + } @Test public void checkThatSmallTotalSizeCapLeavesAtLeastOneArhcive() { long WED_2016_03_23_T_131345_CET = WED_2016_03_23_T_230705_CET - 10 * CoreConstants.MILLIS_IN_ONE_HOUR; // long bytesOutputPerPeriod = 15984; + + cp = new ConfigParameters(WED_2016_03_23_T_131345_CET); final int verySmallCapSize = 1; cp.maxHistory(5).simulatedNumberOfPeriods(3).sizeCap(verySmallCapSize); @@ -306,8 +327,8 @@ public void dailySizeBasedRolloverWithSizeCap() { List foundFiles = findFilesByPattern("\\d{4}-\\d{2}-\\d{2}-clean(\\.\\d)"); Collections.sort(foundFiles, new Comparator() { public int compare(File f0, File f1) { - String s0 = f0.getName().toString(); - String s1 = f1.getName().toString(); + String s0 = f0.getName(); + String s1 = f1.getName(); return s0.compareTo(s1); } }); @@ -370,7 +391,7 @@ public void dailyChronologSizeBasedRolloverWithSecondPhase() { void logTwiceAndStop(long currentTime, String fileNamePattern, int maxHistory, long durationInMillis) { ConfigParameters params = new ConfigParameters(currentTime).fileNamePattern(fileNamePattern) .maxHistory(maxHistory); - buildRollingFileAppender(params, DO_CLEAN_HISTORY_ON_START); + configureRollingFileAppender(params, DO_CLEAN_HISTORY_ON_START); rfa.doAppend("Hello ----------------------------------------------------------" + new Date(currentTime)); currentTime += durationInMillis / 2; add(tbrp.compressionFuture); @@ -451,7 +472,7 @@ int expectedCountWithFolders(int maxHistory, boolean withExtraFolder) { return result; } - void buildRollingFileAppender(ConfigParameters cp, boolean cleanHistoryOnStart) { + void configureRollingFileAppender(ConfigParameters cp, boolean cleanHistoryOnStart) { rfa.setContext(context); rfa.setEncoder(encoder); tbrp.setContext(context); @@ -460,6 +481,7 @@ void buildRollingFileAppender(ConfigParameters cp, boolean cleanHistoryOnStart) tbrp.setTotalSizeCap(new FileSize(cp.sizeCap)); tbrp.setParent(rfa); tbrp.setCleanHistoryOnStart(cleanHistoryOnStart); + tbfnatp.setContext(context); tbrp.timeBasedFileNamingAndTriggeringPolicy = tbfnatp; tbrp.timeBasedFileNamingAndTriggeringPolicy.setCurrentTime(cp.simulatedTime); tbrp.start(); @@ -471,22 +493,38 @@ void buildRollingFileAppender(ConfigParameters cp, boolean cleanHistoryOnStart) boolean DO_NOT_CLEAN_HISTORY_ON_START = false; long logOverMultiplePeriods(ConfigParameters cp) { + //addOnConsoleStatusListenerForDebugging(); + + configureRollingFileAppender(cp, DO_NOT_CLEAN_HISTORY_ON_START); - buildRollingFileAppender(cp, DO_NOT_CLEAN_HISTORY_ON_START); int runLength = cp.simulatedNumberOfPeriods * ticksPerPeriod; int startInactivityIndex = cp.startInactivity * ticksPerPeriod; int endInactivityIndex = startInactivityIndex + cp.numInactivityPeriods * ticksPerPeriod; long tickDuration = cp.periodDurationInMillis / ticksPerPeriod; + System.out.println("ticksPerPeriod=" + ticksPerPeriod); + System.out.println("cp.startInactivity="+cp.startInactivity); + System.out.println("cp.simulatedNumberOfPeriods="+cp.simulatedNumberOfPeriods); + System.out.println("cp.periodDurationInMillis="+cp.periodDurationInMillis); + + System.out.println("runLength=" + runLength); + + System.out.println("startInactivityIndex=" + startInactivityIndex); + System.out.println("endInactivityIndex=" + endInactivityIndex); + System.out.println("tickDuration=" + tickDuration); + System.out.println(" "); + for (int i = 0; i <= runLength; i++) { - Date currentDate = new Date(tbrp.timeBasedFileNamingAndTriggeringPolicy.getCurrentTime()); + long timeInMillis = tbrp.timeBasedFileNamingAndTriggeringPolicy.getCurrentTime(); + + Date currentDate = new Date(timeInMillis); + //System.out.println("i=" + i + ", currentDate=" + currentDate); if (i < startInactivityIndex || i > endInactivityIndex) { rfa.doAppend(buildMessageString(currentDate, i)); } - tbrp.timeBasedFileNamingAndTriggeringPolicy.setCurrentTime( - addTime(tbrp.timeBasedFileNamingAndTriggeringPolicy.getCurrentTime(), tickDuration)); + tbrp.timeBasedFileNamingAndTriggeringPolicy.setCurrentTime(addTime(timeInMillis, tickDuration)); add(tbrp.compressionFuture); add(tbrp.cleanUpFuture); @@ -506,6 +544,13 @@ long logOverMultiplePeriods(ConfigParameters cp) { return tbrp.timeBasedFileNamingAndTriggeringPolicy.getCurrentTime(); } + private void addOnConsoleStatusListenerForDebugging() { + OnConsoleStatusListener onConsoleStatusListener = new OnConsoleStatusListener(); + onConsoleStatusListener.setContext(context); + onConsoleStatusListener.start(); + context.getStatusManager().add(onConsoleStatusListener); + } + private static String buildMessageString(Date currentDate, int i) { StringBuilder sb = new StringBuilder("Hello"); String currentDateStr = currentDate.toString(); diff --git a/logback-core/src/test/java/ch/qos/logback/core/rolling/testUtil/ParentScaffoldingForRollingTests.java b/logback-core/src/test/java/ch/qos/logback/core/rolling/testUtil/ParentScaffoldingForRollingTests.java index b50fc30c55..5430bfefdd 100644 --- a/logback-core/src/test/java/ch/qos/logback/core/rolling/testUtil/ParentScaffoldingForRollingTests.java +++ b/logback-core/src/test/java/ch/qos/logback/core/rolling/testUtil/ParentScaffoldingForRollingTests.java @@ -17,20 +17,31 @@ import ch.qos.logback.core.Context; import ch.qos.logback.core.ContextBase; import ch.qos.logback.core.encoder.EchoEncoder; +import ch.qos.logback.core.rolling.helper.FileFilterUtil; +import ch.qos.logback.core.rolling.helper.FileNamePattern; import ch.qos.logback.core.testUtil.CoreTestConstants; import ch.qos.logback.core.testUtil.RandomUtil; +import java.io.File; +import java.io.IOException; +import java.sql.Date; import java.util.ArrayList; import java.util.Calendar; +import java.util.Enumeration; import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; -public class ParentScaffoldingForRollingTest { +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ParentScaffoldingForRollingTests { protected EchoEncoder encoder = new EchoEncoder(); protected int diff = RandomUtil.getPositiveInt(); protected String randomOutputDir = CoreTestConstants.OUTPUT_DIR_PREFIX + diff + "/"; + protected List expectedFilenameList = new ArrayList(); Calendar calendar = Calendar.getInstance(); protected Context context = new ContextBase(); @@ -38,6 +49,39 @@ public class ParentScaffoldingForRollingTest { protected long currentTime; // initialized in setUp() protected List> futureList = new ArrayList>(); + public static void existenceCheck(List filenameList) { + for (String filename : filenameList) { + assertTrue(new File(filename).exists(), "File " + filename + " does not exist"); + } + } + + public static void reverseSortedContentCheck(String outputDirStr, int runLength, String prefix) throws IOException { + File[] fileArray = ScaffoldingForRollingTests.getFilesInDirectory(outputDirStr); + FileFilterUtil.reverseSortFileArrayByName(fileArray); + ScaffoldingForRollingTests.fileContentCheck(fileArray, runLength, prefix); + } + + static protected void checkZipEntryName(String filepath, String pattern) throws IOException { + ZipFile zf = new ZipFile(filepath); + + try { + Enumeration entries = zf.entries(); + assert ((entries.hasMoreElements())); + ZipEntry firstZipEntry = entries.nextElement(); + assert ((!entries.hasMoreElements())); + assertTrue(firstZipEntry.getName().matches(pattern)); + } finally { + if (zf != null) + zf.close(); + } + } + + static protected void zipEntryNameCheck(List expectedFilenameList, String pattern) throws IOException { + for (String filepath : expectedFilenameList) { + checkZipEntryName(filepath, pattern); + } + } + public void setUp() { context.setName("test"); calendar.set(Calendar.MILLISECOND, 333); @@ -63,4 +107,27 @@ protected void waitForJobsToComplete() { } futureList.clear(); } + + protected List filterElementsInListBySuffix(String suffix) { + List zipFiles = new ArrayList(); + for (String filename : expectedFilenameList) { + if (filename.endsWith(suffix)) + zipFiles.add(filename); + } + return zipFiles; + } + + protected void addExpectedFileName_ByDate(String patternStr, long millis) { + FileNamePattern fileNamePattern = new FileNamePattern(patternStr, context); + String fn = fileNamePattern.convert(new Date(millis)); + expectedFilenameList.add(fn); + } + + protected String testId2FileName(String testId) { + return randomOutputDir + testId + ".log"; + } + + protected void incCurrentTime(long increment) { + currentTime += increment; + } } diff --git a/logback-core/src/test/java/ch/qos/logback/core/rolling/testUtil/ScaffoldingForRollingTests.java b/logback-core/src/test/java/ch/qos/logback/core/rolling/testUtil/ScaffoldingForRollingTests.java index aee7326341..7bfa102e3c 100755 --- a/logback-core/src/test/java/ch/qos/logback/core/rolling/testUtil/ScaffoldingForRollingTests.java +++ b/logback-core/src/test/java/ch/qos/logback/core/rolling/testUtil/ScaffoldingForRollingTests.java @@ -13,28 +13,16 @@ */ package ch.qos.logback.core.rolling.testUtil; -import ch.qos.logback.core.Context; -import ch.qos.logback.core.ContextBase; -import ch.qos.logback.core.encoder.EchoEncoder; import ch.qos.logback.core.rolling.helper.FileFilterUtil; -import ch.qos.logback.core.rolling.helper.FileNamePattern; -import ch.qos.logback.core.testUtil.CoreTestConstants; import ch.qos.logback.core.testUtil.FileToBufferUtil; -import ch.qos.logback.core.testUtil.RandomUtil; import java.io.File; import java.io.IOException; import java.sql.Date; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Calendar; -import java.util.Enumeration; import java.util.List; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; import java.util.function.UnaryOperator; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -45,27 +33,18 @@ * * @author Ceki Gülcü */ -public class ScaffoldingForRollingTests { +public class ScaffoldingForRollingTests extends ParentScaffoldingForRollingTests { static public final String DATE_PATTERN_WITH_SECONDS = "yyyy-MM-dd_HH_mm_ss"; static public final String DATE_PATTERN_BY_DAY = "yyyy-MM-dd"; static public final SimpleDateFormat SDF = new SimpleDateFormat(DATE_PATTERN_WITH_SECONDS); + public static final int MILLIS_IN_ONE_SECOND = 1000; - int diff = RandomUtil.getPositiveInt(); - protected String randomOutputDir = CoreTestConstants.OUTPUT_DIR_PREFIX + diff + "/"; - protected EchoEncoder encoder = new EchoEncoder(); - protected Context context = new ContextBase(); - protected List expectedFilenameList = new ArrayList(); - protected long nextRolloverThreshold; // initialized in setUp() - protected long currentTime; // initialized in setUp() - protected List> futureList = new ArrayList>(); - Calendar calendar = Calendar.getInstance(); + protected long nextRolloverThreshold; // initialized in setUp() public void setUp() { - context.setName("test"); - calendar.set(Calendar.MILLISECOND, 333); - currentTime = calendar.getTimeInMillis(); + super.setUp(); recomputeRolloverThreshold(currentTime); } @@ -108,18 +87,6 @@ public static void sortedContentCheck(String outputDirStr, int runLength, String fileContentCheck(fileArray, runLength, prefix, runStart); } - public static void reverseSortedContentCheck(String outputDirStr, int runLength, String prefix) throws IOException { - File[] fileArray = getFilesInDirectory(outputDirStr); - FileFilterUtil.reverseSortFileArrayByName(fileArray); - fileContentCheck(fileArray, runLength, prefix); - } - - public static void existenceCheck(List filenameList) { - for (String filename : filenameList) { - assertTrue(new File(filename).exists(), "File " + filename + " does not exist"); - } - } - public static int existenceCount(List filenameList) { int existenceCounter = 0; for (String filename : filenameList) { @@ -138,45 +105,31 @@ protected String impossibleFileName(String testId) { throw new RuntimeException("implement"); } - protected String testId2FileName(String testId) { - return randomOutputDir + testId + ".log"; - } - // assuming rollover every second protected void recomputeRolloverThreshold(long ct) { - long delta = ct % 1000; - nextRolloverThreshold = (ct - delta) + 1000; + long delta = ct % MILLIS_IN_ONE_SECOND; + nextRolloverThreshold = (ct - delta) + MILLIS_IN_ONE_SECOND; } protected boolean passThresholdTime(long nextRolloverThreshold) { return currentTime >= nextRolloverThreshold; } - protected void incCurrentTime(long increment) { - currentTime += increment; - } - protected Date getDateOfCurrentPeriodsStart() { - long delta = currentTime % 1000; + long delta = currentTime % MILLIS_IN_ONE_SECOND; return new Date(currentTime - delta); } protected Date getDateOfPreviousPeriodsStart() { - long delta = currentTime % 1000; - return new Date(currentTime - delta - 1000); + long delta = currentTime % MILLIS_IN_ONE_SECOND; + return new Date(currentTime - delta - MILLIS_IN_ONE_SECOND); } protected long getMillisOfCurrentPeriodsStart() { - long delta = currentTime % 1000; + long delta = currentTime % MILLIS_IN_ONE_SECOND; return (currentTime - delta); } - protected void addExpectedFileName_ByDate(String patternStr, long millis) { - FileNamePattern fileNamePattern = new FileNamePattern(patternStr, context); - String fn = fileNamePattern.convert(new Date(millis)); - expectedFilenameList.add(fn); - } - protected void addExpectedFileNamedIfItsTime_ByDate(String fileNamePatternStr) { if (passThresholdTime(nextRolloverThreshold)) { addExpectedFileName_ByDate(fileNamePatternStr, getMillisOfCurrentPeriodsStart()); @@ -200,15 +153,6 @@ protected void addExpectedFileName_ByFileIndexCounter(String randomOutputDir, St expectedFilenameList.add(fn); } - protected List filterElementsInListBySuffix(String suffix) { - List zipFiles = new ArrayList(); - for (String filename : expectedFilenameList) { - if (filename.endsWith(suffix)) - zipFiles.add(filename); - } - return zipFiles; - } - protected void addExpectedFileNamedIfItsTime_ByDate(String outputDir, String testId, boolean gzExtension) { if (passThresholdTime(nextRolloverThreshold)) { addExpectedFileName_ByDate(outputDir, testId, getDateOfCurrentPeriodsStart(), gzExtension); @@ -240,12 +184,6 @@ String addGZIfNotLast(int i) { } } - protected void zipEntryNameCheck(List expectedFilenameList, String pattern) throws IOException { - for (String filepath : expectedFilenameList) { - checkZipEntryName(filepath, pattern); - } - } - protected void checkZipEntryMatchesZipFilename(List expectedFilenameList) throws IOException { for (String filepath : expectedFilenameList) { String stripped = stripStemFromZipFilename(filepath); @@ -261,37 +199,5 @@ String stripStemFromZipFilename(String filepath) { } - void checkZipEntryName(String filepath, String pattern) throws IOException { - ZipFile zf = new ZipFile(filepath); - - try { - Enumeration entries = zf.entries(); - assert ((entries.hasMoreElements())); - ZipEntry firstZipEntry = entries.nextElement(); - assert ((!entries.hasMoreElements())); - assertTrue(firstZipEntry.getName().matches(pattern)); - } finally { - if (zf != null) - zf.close(); - } - } - - protected void add(Future future) { - if (future == null) - return; - if (!futureList.contains(future)) { - futureList.add(future); - } - } - protected void waitForJobsToComplete() { - for (Future future : futureList) { - try { - future.get(10, TimeUnit.SECONDS); - } catch (Exception e) { - new RuntimeException("unexpected exception while testing", e); - } - } - futureList.clear(); - } } From cba802446031949a97e5f17df02713746252c16d Mon Sep 17 00:00:00 2001 From: ceki Date: Thu, 23 Oct 2025 16:44:44 +0200 Subject: [PATCH 27/36] minor javadoc changes Signed-off-by: ceki --- .../core/hook/DefaultShutdownHook.java | 5 ++++ .../JVMExitBeforeCompressionISDoneTest.java | 24 ++++++++++++++----- ...meBasedRollingWithArchiveRemoval_Test.java | 9 +++---- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/logback-core/src/main/java/ch/qos/logback/core/hook/DefaultShutdownHook.java b/logback-core/src/main/java/ch/qos/logback/core/hook/DefaultShutdownHook.java index c67a391a4c..30380d3ac6 100755 --- a/logback-core/src/main/java/ch/qos/logback/core/hook/DefaultShutdownHook.java +++ b/logback-core/src/main/java/ch/qos/logback/core/hook/DefaultShutdownHook.java @@ -35,9 +35,14 @@ public class DefaultShutdownHook extends ShutdownHookBase { */ private Duration delay = DEFAULT_DELAY; + + /** + * Creates a DefaultShutdownHook using the default delay ({@link #DEFAULT_DELAY}). + */ public DefaultShutdownHook() { } + public Duration getDelay() { return delay; } diff --git a/logback-core/src/test/java/ch/qos/logback/core/rolling/JVMExitBeforeCompressionISDoneTest.java b/logback-core/src/test/java/ch/qos/logback/core/rolling/JVMExitBeforeCompressionISDoneTest.java index fba700acff..ba3d2f2a61 100755 --- a/logback-core/src/test/java/ch/qos/logback/core/rolling/JVMExitBeforeCompressionISDoneTest.java +++ b/logback-core/src/test/java/ch/qos/logback/core/rolling/JVMExitBeforeCompressionISDoneTest.java @@ -1,8 +1,13 @@ package ch.qos.logback.core.rolling; +import java.net.URL; +import java.net.URLClassLoader; import java.util.Date; -import ch.qos.logback.core.rolling.testUtil.ParentScaffoldingForRollingTests; +import ch.qos.logback.core.Context; +import ch.qos.logback.core.hook.ShutdownHook; +import ch.qos.logback.core.hook.ShutdownHookBase; +import ch.qos.logback.core.status.Status; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -18,11 +23,18 @@ import org.junit.jupiter.api.Test; @Disabled +/** + * This test is disabled because it is intended to be run manually as it is difficult + * to unit test shutdown hooks. + * + * To run this test, enable it and execute it as a JUnit test. Observe the + * console output to see if the compression completes before the JVM exits. + */ public class JVMExitBeforeCompressionISDoneTest extends ScaffoldingForRollingTests { RollingFileAppender rfa = new RollingFileAppender(); TimeBasedRollingPolicy tbrp = new TimeBasedRollingPolicy(); - DefaultShutdownHook delayingShutdownHook = new DefaultShutdownHook(); + ShutdownHook shutdownHook = new DefaultShutdownHook(); static final long FRI_2016_05_13_T_170415_GMT = 1463159055630L; @@ -33,7 +45,7 @@ public class JVMExitBeforeCompressionISDoneTest extends ScaffoldingForRollingTes public void setUp() { super.setUp(); StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener()); - delayingShutdownHook.setContext(context); + shutdownHook.setContext(context); initRFA(rfa); } @@ -43,7 +55,7 @@ void initRFA(RollingFileAppender rfa) { } void initTRBP(RollingFileAppender rfa, TimeBasedRollingPolicy tbrp, String filenamePattern, - long givenTime) { + long givenTime) { tbrp.setContext(context); tbrp.setFileNamePattern(filenamePattern); tbrp.setParent(rfa); @@ -56,13 +68,13 @@ void initTRBP(RollingFileAppender rfa, TimeBasedRollingPolicy tb @AfterEach public void tearDown() throws Exception { - StatusPrinter.print(context); + //StatusPrinter.print(context); } @Disabled @Test public void test1() { - Thread shutdownThread = new Thread(delayingShutdownHook); + Thread shutdownThread = new Thread(shutdownHook); Runtime.getRuntime().addShutdownHook(shutdownThread); String patternPrefix = "test1"; diff --git a/logback-core/src/test/java/ch/qos/logback/core/rolling/TimeBasedRollingWithArchiveRemoval_Test.java b/logback-core/src/test/java/ch/qos/logback/core/rolling/TimeBasedRollingWithArchiveRemoval_Test.java index d503c66c2c..56ffeebcc7 100755 --- a/logback-core/src/test/java/ch/qos/logback/core/rolling/TimeBasedRollingWithArchiveRemoval_Test.java +++ b/logback-core/src/test/java/ch/qos/logback/core/rolling/TimeBasedRollingWithArchiveRemoval_Test.java @@ -51,7 +51,6 @@ import ch.qos.logback.core.CoreConstants; import ch.qos.logback.core.pattern.SpacePadder; import ch.qos.logback.core.rolling.helper.RollingCalendar; -import ch.qos.logback.core.rolling.testUtil.ScaffoldingForRollingTests; import ch.qos.logback.core.status.testUtil.StatusChecker; import ch.qos.logback.core.util.FileSize; import ch.qos.logback.core.util.FixedRateInvocationGate; @@ -418,11 +417,9 @@ public void cleanHistoryOnStartWithHourPattern() { @Disabled @Test // this test assumes a high degree of collisions in the archived files. Every 24 - // hours, the archive - // belonging to the previous day will be overwritten. Given that logback goes 14 - // days (336 hours) in history - // to clean files on start up, it is bound to delete more recent files. It is - // not logback's responsibility + // hours, the archive belonging to the previous day will be overwritten. Given that + // logback goes 14 days (336 hours) in history to clean files on start up, it is + // bound to delete more recent files. It is not logback's responsibility // to cater for such degenerate cases. public void cleanHistoryOnStartWithHourPatternWithCollisions() { long now = this.currentTime; From 083475bee5b032a903a02d7461282dba310e5042 Mon Sep 17 00:00:00 2001 From: ceki Date: Fri, 24 Oct 2025 11:56:52 +0200 Subject: [PATCH 28/36] change support tier Signed-off-by: ceki --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c1d13079f3..93745d6971 100755 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ who can quickly answer your questions. # Urgent issues For urgent issues do not hesitate to [champion a -release](https://github.com/sponsors/qos-ch/sponsorships?tier_id=77436). +release](https://github.com/sponsors/qos-ch/sponsorships?tier_id=543501). In principle, most championed issues are solved within 3 business days followed up by a release. From 2ca8c527524870b46a4a7c195cbf8e0ee263ca28 Mon Sep 17 00:00:00 2001 From: ceki Date: Wed, 29 Oct 2025 16:01:11 +0100 Subject: [PATCH 29/36] update funding info Signed-off-by: ceki --- FUNDING.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/FUNDING.yml b/FUNDING.yml index 18dbb1166a..6e93655bd8 100644 --- a/FUNDING.yml +++ b/FUNDING.yml @@ -1 +1,18 @@ -github: qos-ch \ No newline at end of file +github: qos-ch +tidelift: maven/ch.qos.logback:logback-core +thanks-dev: gh/ceki + + +#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +#patreon: # Replace with a single Patreon username +#open_collective: # Replace with a single Open Collective username +#ko_fi: # Replace with a single Ko-fi username +#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +#liberapay: # Replace with a single Liberapay username +#issuehunt: # Replace with a single IssueHunt username +#lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +#polar: # Replace with a single Polar username +#buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +#thanks_dev: # Replace with a single thanks.dev username +#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] \ No newline at end of file From 2bf5557a76b7c292253d4aa962da762134796431 Mon Sep 17 00:00:00 2001 From: ceki Date: Wed, 29 Oct 2025 18:42:04 +0100 Subject: [PATCH 30/36] fix failed LegacyPatternLayoutTest#subPattern test due to TZ discrepancies, use LevelConverter instead of DateConverter Signed-off-by: ceki --- .../logback/classic/pattern/LegacyPatternLayoutTest.java | 7 ++++++- .../ch/qos/logback/classic/pattern/SubPatternLayout.java | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/logback-classic/src/test/java/ch/qos/logback/classic/pattern/LegacyPatternLayoutTest.java b/logback-classic/src/test/java/ch/qos/logback/classic/pattern/LegacyPatternLayoutTest.java index e04c7402c4..e60d84db68 100644 --- a/logback-classic/src/test/java/ch/qos/logback/classic/pattern/LegacyPatternLayoutTest.java +++ b/logback-classic/src/test/java/ch/qos/logback/classic/pattern/LegacyPatternLayoutTest.java @@ -31,6 +31,10 @@ public class LegacyPatternLayoutTest { LoggerContext context = new LoggerContext(); + /** + * Test backward compatibility for classes derived from + * PatternLayout that add custom conversion words. + */ @Test public void subPattern() { SubPatternLayout layout = new SubPatternLayout(); layout.setPattern("%"+SubPatternLayout.DOOO); @@ -38,9 +42,10 @@ public class LegacyPatternLayoutTest { layout.start(); LoggingEvent event = new LoggingEvent(); event.setTimeStamp(0); + event.setLevel(Level.INFO); String result = layout.doLayout(event); - assertEquals("1970-01-01 01:00:00,000", result); + assertEquals("INFO", result); } @Test diff --git a/logback-classic/src/test/java/ch/qos/logback/classic/pattern/SubPatternLayout.java b/logback-classic/src/test/java/ch/qos/logback/classic/pattern/SubPatternLayout.java index 6d1694f390..899024a59e 100644 --- a/logback-classic/src/test/java/ch/qos/logback/classic/pattern/SubPatternLayout.java +++ b/logback-classic/src/test/java/ch/qos/logback/classic/pattern/SubPatternLayout.java @@ -29,7 +29,7 @@ public class SubPatternLayout extends PatternLayout { SubPatternLayout() { Map defaultConverterMap = getDefaultConverterMap(); - defaultConverterMap.put(DOOO, DateConverter.class.getName()); + defaultConverterMap.put(DOOO, LevelConverter.class.getName()); } } From ab6a006ad08c328a190de76d71d91f9bbac06364 Mon Sep 17 00:00:00 2001 From: ceki Date: Wed, 29 Oct 2025 18:52:59 +0100 Subject: [PATCH 31/36] add maven cache to github CI, update .github/FUNDING.yml Signed-off-by: ceki --- .github/FUNDING.yml | 20 ++++++++++++++++++-- .github/workflows/main.yml | 1 + FUNDING.yml | 18 ------------------ 3 files changed, 19 insertions(+), 20 deletions(-) delete mode 100644 FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c6e1d4be36..6e93655bd8 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,18 @@ -tidelift: maven/ch.qos.logback:logback-classic -github: qos-ch \ No newline at end of file +github: qos-ch +tidelift: maven/ch.qos.logback:logback-core +thanks-dev: gh/ceki + + +#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +#patreon: # Replace with a single Patreon username +#open_collective: # Replace with a single Open Collective username +#ko_fi: # Replace with a single Ko-fi username +#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +#liberapay: # Replace with a single Liberapay username +#issuehunt: # Replace with a single IssueHunt username +#lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +#polar: # Replace with a single Polar username +#buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +#thanks_dev: # Replace with a single thanks.dev username +#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7b75aea8d4..303add2fd6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,6 +39,7 @@ jobs: with: distribution: 'temurin' java-version: ${{ matrix.jdk }} + cache: maven # This enables Maven dependency caching - name: Install # download dependencies, etc, so test log looks better run: mvn -B install diff --git a/FUNDING.yml b/FUNDING.yml deleted file mode 100644 index 6e93655bd8..0000000000 --- a/FUNDING.yml +++ /dev/null @@ -1,18 +0,0 @@ -github: qos-ch -tidelift: maven/ch.qos.logback:logback-core -thanks-dev: gh/ceki - - -#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -#patreon: # Replace with a single Patreon username -#open_collective: # Replace with a single Open Collective username -#ko_fi: # Replace with a single Ko-fi username -#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -#liberapay: # Replace with a single Liberapay username -#issuehunt: # Replace with a single IssueHunt username -#lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -#polar: # Replace with a single Polar username -#buy_me_a_coffee: # Replace with a single Buy Me a Coffee username -#thanks_dev: # Replace with a single thanks.dev username -#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] \ No newline at end of file From 04a7ba558c393070c2eb2c78a1a999ba19d482ee Mon Sep 17 00:00:00 2001 From: ceki Date: Thu, 30 Oct 2025 22:27:54 +0100 Subject: [PATCH 32/36] most subclasses of UnsynchronizedAppenderBase do not need a reentry guard Signed-off-by: ceki --- .../java/ch/qos/logback/core/Appender.java | 42 +++++- .../ch/qos/logback/core/ConsoleAppender.java | 10 ++ .../core/UnsynchronizedAppenderBase.java | 60 +++++--- .../qos/logback/core/util/ReentryGuard.java | 137 ++++++++++++++++++ .../core/util/ReentryGuardFactory.java | 69 +++++++++ 5 files changed, 293 insertions(+), 25 deletions(-) create mode 100644 logback-core/src/main/java/ch/qos/logback/core/util/ReentryGuard.java create mode 100644 logback-core/src/main/java/ch/qos/logback/core/util/ReentryGuardFactory.java diff --git a/logback-core/src/main/java/ch/qos/logback/core/Appender.java b/logback-core/src/main/java/ch/qos/logback/core/Appender.java index 3d4831620a..95057ef6a5 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/Appender.java +++ b/logback-core/src/main/java/ch/qos/logback/core/Appender.java @@ -17,25 +17,53 @@ import ch.qos.logback.core.spi.FilterAttachable; import ch.qos.logback.core.spi.LifeCycle; +/** + * Contract for components responsible for delivering logging events to their + * final destination (console, file, remote server, etc.). + * + *

Implementations are typically configured and managed by a LoggerContext. + * The type parameter E represents the event type the appender consumes (for + * example a log event object). Implementations should honor lifecycle methods + * from {@link LifeCycle} and may be {@link ContextAware} and + * {@link FilterAttachable} to support contextual information and filtering.

+ * + *

Concurrency: appenders are generally invoked by multiple threads. Implementations + * must ensure thread-safety where applicable (for example when writing to shared + * resources). The {@link #doAppend(Object)} method may be called concurrently.

+ * + * @param the event type accepted by this appender + */ public interface Appender extends LifeCycle, ContextAware, FilterAttachable { /** - * Get the name of this appender. The name uniquely identifies the appender. + * Get the name of this appender. The name uniquely identifies the appender + * within its context and is used for configuration and lookup. + * + * @return the appender name, or {@code null} if not set */ String getName(); /** - * This is where an appender accomplishes its work. Note that the argument is of - * type Object. - * - * @param event + * This is where an appender accomplishes its work: format and deliver the + * provided event to the appender's destination. + * + *

Implementations should apply any configured filters before outputting + * the event. Implementations should avoid throwing runtime exceptions; + * if an error occurs that cannot be handled internally, a {@link LogbackException} + * (or a subtype) may be thrown to indicate a failure during append.

+ * + * @param event the event to append; may not be {@code null} + * @throws LogbackException if the append fails in a way that needs to be + * propagated to the caller */ void doAppend(E event) throws LogbackException; /** * Set the name of this appender. The name is used by other components to - * identify this appender. - * + * identify and reference this appender (for example in configuration or for + * status messages). + * + * @param name the new name for this appender; may be {@code null} to unset */ void setName(String name); diff --git a/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java b/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java index eec96b5240..7ba10addb2 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java +++ b/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java @@ -25,6 +25,8 @@ import ch.qos.logback.core.status.Status; import ch.qos.logback.core.status.WarnStatus; import ch.qos.logback.core.util.Loader; +import ch.qos.logback.core.util.ReentryGuard; +import ch.qos.logback.core.util.ReentryGuardFactory; /** * ConsoleAppender appends log events to System.out or @@ -100,6 +102,14 @@ public void start() { super.start(); } + /** + * Create a ThreadLocal ReentryGuard to prevent recursive appender invocations. + * @return a ReentryGuard instance of type {@link ReentryGuardFactory.GuardType#THREAD_LOCAL THREAD_LOCAL}. + */ + protected ReentryGuard buildReentryGuard() { + return ReentryGuardFactory.makeGuard(ReentryGuardFactory.GuardType.THREAD_LOCAL); + } + private OutputStream wrapWithJansi(OutputStream targetStream) { try { addInfo("Enabling JANSI AnsiPrintStream for the console."); diff --git a/logback-core/src/main/java/ch/qos/logback/core/UnsynchronizedAppenderBase.java b/logback-core/src/main/java/ch/qos/logback/core/UnsynchronizedAppenderBase.java index 79ea30e18b..668863478f 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/UnsynchronizedAppenderBase.java +++ b/logback-core/src/main/java/ch/qos/logback/core/UnsynchronizedAppenderBase.java @@ -20,6 +20,8 @@ import ch.qos.logback.core.spi.FilterAttachableImpl; import ch.qos.logback.core.spi.FilterReply; import ch.qos.logback.core.status.WarnStatus; +import ch.qos.logback.core.util.ReentryGuard; +import ch.qos.logback.core.util.ReentryGuardFactory; /** * Similar to {@link AppenderBase} except that derived appenders need to handle thread @@ -31,16 +33,13 @@ abstract public class UnsynchronizedAppenderBase extends ContextAwareBase implements Appender { protected volatile boolean started = false; - - // using a ThreadLocal instead of a boolean add 75 nanoseconds per - // doAppend invocation. This is tolerable as doAppend takes at least a few - // microseconds - // on a real appender /** * The guard prevents an appender from repeatedly calling its own doAppend * method. + * + * @since 1.5.21 */ - private ThreadLocal guard = new ThreadLocal(); + private ReentryGuard reentryGuard; /** * Appenders are named. @@ -59,23 +58,20 @@ public String getName() { static final int ALLOWED_REPEATS = 3; public void doAppend(E eventObject) { - // WARNING: The guard check MUST be the first statement in the - // doAppend() method. + if (!this.started) { + if (statusRepeatCount++ < ALLOWED_REPEATS) { + addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this)); + } + return; + } // prevent re-entry. - if (Boolean.TRUE.equals(guard.get())) { + if (reentryGuard.isLocked()) { return; } try { - guard.set(Boolean.TRUE); - - if (!this.started) { - if (statusRepeatCount++ < ALLOWED_REPEATS) { - addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this)); - } - return; - } + reentryGuard.lock(); if (getFilterChainDecision(eventObject) == FilterReply.DENY) { return; @@ -89,7 +85,7 @@ public void doAppend(E eventObject) { addError("Appender [" + name + "] failed to append.", e); } } finally { - guard.set(Boolean.FALSE); + reentryGuard.unlock(); } } @@ -103,9 +99,37 @@ public void setName(String name) { } public void start() { + this.reentryGuard = buildReentryGuard(); started = true; } + /** + * Create a {@link ReentryGuard} instance used by this appender to prevent + * recursive/re-entrant calls to {@link #doAppend(Object)}. + * + *

The default implementation returns a no-op guard produced by + * {@link ReentryGuardFactory#makeGuard(ch.qos.logback.core.util.ReentryGuardFactory.GuardType)} + * using {@code GuardType.NOP}. Subclasses that require actual re-entry + * protection (for example using a thread-local or lock-based guard) should + * override this method to return an appropriate {@link ReentryGuard} + * implementation.

+ * + *

Contract/expectations: + *

    + *
  • Called from {@link #start()} to initialize the appender's guard.
  • + *
  • Implementations should be lightweight and thread-safe.
  • + *
  • Return value must not be {@code null}.
  • + *
+ *

+ * + * @return a non-null {@link ReentryGuard} used to detect and prevent + * re-entrant appends. By default this is a no-op guard. + * @since 1.5.21 + */ + protected ReentryGuard buildReentryGuard() { + return ReentryGuardFactory.makeGuard(ReentryGuardFactory.GuardType.NOP); + } + public void stop() { started = false; } diff --git a/logback-core/src/main/java/ch/qos/logback/core/util/ReentryGuard.java b/logback-core/src/main/java/ch/qos/logback/core/util/ReentryGuard.java new file mode 100644 index 0000000000..91372b43ec --- /dev/null +++ b/logback-core/src/main/java/ch/qos/logback/core/util/ReentryGuard.java @@ -0,0 +1,137 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2025, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v1.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ + +package ch.qos.logback.core.util; + +/** + * Guard used to prevent re-entrant (recursive) appender invocations on a per-thread basis. + * + *

Implementations are used by appenders and other components that must avoid + * recursively calling back into themselves (for example when an error causes + * logging while handling a logging event). Typical usage: check {@link #isLocked()} + * before proceeding and call {@link #lock()} / {@link #unlock()} around the + * guarded region.

+ * + *

Concurrency: guards operate on a per-thread basis; callers should treat the + * guard as thread-local state. Implementations must document their semantics; + * the provided {@link ReentryGuardImpl} uses a {@link ThreadLocal} to track the + * locked state for the current thread.

+* + * @since 1.5.21 + */ +public interface ReentryGuard { + + /** + * Return true if the current thread holds the guard (i.e. is inside a guarded region). + * + *

Implementations typically return {@code false} if the current thread has not + * previously called {@link #lock()} or if the stored value is {@code null}.

+ * + * @return {@code true} if the guard is locked for the current thread, {@code false} otherwise + */ + boolean isLocked(); + + /** + * Mark the guard as locked for the current thread. + * + *

Callers must ensure {@link #unlock()} is invoked in a finally block to + * avoid leaving the guard permanently locked for the thread.

+ */ + void lock(); + + /** + * Release the guard for the current thread. + * + *

After calling {@code unlock()} the {@link #isLocked()} should return + * {@code false} for the current thread (unless {@code lock()} is called again).

+ */ + void unlock(); + + + /** + * Default per-thread implementation backed by a {@link ThreadLocal}. + * + *

Semantics: a value of {@link Boolean#TRUE} indicates the current thread + * is inside a guarded region. If the ThreadLocal has no value ({@code null}), + * {@link #isLocked()} treats this as unlocked (returns {@code false}).

+ * + *

Note: this implementation intentionally uses {@code ThreadLocal} + * to avoid global synchronization. The initial state is unlocked.

+ * + * Typical usage: + *
+     * if (!guard.isLocked()) {
+     *   guard.lock();
+     *   try {
+     *     // guarded work
+     *   } finally {
+     *     guard.unlock();
+     *   }
+     * }
+     * 
+ * + */ + class ReentryGuardImpl implements ReentryGuard { + + private ThreadLocal guard = new ThreadLocal(); + + + @Override + public boolean isLocked() { + // the guard is considered locked if the ThreadLocal contains Boolean.TRUE + // note that initially the ThreadLocal contains null + return (Boolean.TRUE.equals(guard.get())); + } + + @Override + public void lock() { + guard.set(Boolean.TRUE); + } + + @Override + public void unlock() { + guard.set(Boolean.FALSE); + } + } + + /** + * No-op implementation that never locks. Useful in contexts where re-entrancy + * protection is not required. + * + *

{@link #isLocked()} always returns {@code false}. {@link #lock()} and + * {@link #unlock()} are no-ops.

+ * + *

Use this implementation when the caller explicitly wants to disable + * reentrancy protection (for example in tests or in environments where the + * cost of thread-local checks is undesirable and re-entrancy cannot occur).

+ * + */ + class NOPRentryGuard implements ReentryGuard { + @Override + public boolean isLocked() { + return false; + } + + @Override + public void lock() { + // NOP + } + + @Override + public void unlock() { + // NOP + } + } + +} diff --git a/logback-core/src/main/java/ch/qos/logback/core/util/ReentryGuardFactory.java b/logback-core/src/main/java/ch/qos/logback/core/util/ReentryGuardFactory.java new file mode 100644 index 0000000000..aa10f266f9 --- /dev/null +++ b/logback-core/src/main/java/ch/qos/logback/core/util/ReentryGuardFactory.java @@ -0,0 +1,69 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2025, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v1.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ + +package ch.qos.logback.core.util; + +import java.util.Objects; + +/** + * Factory that creates {@link ReentryGuard} instances according to a requested type. + * + *

This class centralizes creation of the built-in guard implementations. + * Consumers can use the factory to obtain either a per-thread guard or a no-op + * guard depending on their needs.

+ * + * @since 1.5.21 + */ +public class ReentryGuardFactory { + + /** + * Types of guards that can be produced by this factory. + * + * THREAD_LOCAL - returns a {@link ReentryGuard.ReentryGuardImpl} backed by a ThreadLocal. + * NOP - returns a {@link ReentryGuard.NOPRentryGuard} which never locks. + */ + public enum GuardType { + THREAD_LOCAL, + NOP + } + + + /** + * Create a {@link ReentryGuard} for the given {@link GuardType}. + * + *

Returns a fresh instance of the requested guard implementation. The + * factory does not cache instances; callers may obtain separate instances + * as required.

+ * + *

Thread-safety: this method is stateless and may be called concurrently + * from multiple threads.

+ * + * @param guardType the type of guard to create; must not be {@code null} + * @return a new {@link ReentryGuard} instance implementing the requested semantics + * @throws NullPointerException if {@code guardType} is {@code null} + * @throws IllegalArgumentException if an unknown guard type is provided + * @since 1.5.21 + */ + public static ReentryGuard makeGuard(GuardType guardType) { + Objects.requireNonNull(guardType, "guardType must not be null"); + switch (guardType) { + case THREAD_LOCAL: + return new ReentryGuard.ReentryGuardImpl(); + case NOP: + return new ReentryGuard.NOPRentryGuard(); + default: + throw new IllegalArgumentException("Unknown GuardType: " + guardType); + } + } +} From 149714232d81fa7844a4518de4b17ae3b77ce648 Mon Sep 17 00:00:00 2001 From: ceki Date: Fri, 31 Oct 2025 22:17:14 +0100 Subject: [PATCH 33/36] improve performance for 2 or more turbo filters Signed-off-by: ceki --- .../logback/classic/spi/TurboFilterList.java | 29 ++++++++++--------- .../logback/classic/turbo/TurboFilter.java | 2 +- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/spi/TurboFilterList.java b/logback-classic/src/main/java/ch/qos/logback/classic/spi/TurboFilterList.java index 698354dca3..ac651a4474 100644 --- a/logback-classic/src/main/java/ch/qos/logback/classic/spi/TurboFilterList.java +++ b/logback-classic/src/main/java/ch/qos/logback/classic/spi/TurboFilterList.java @@ -1,13 +1,13 @@ /** * Logback: the reliable, generic, fast and flexible logging framework. * Copyright (C) 1999-2015, QOS.ch. All rights reserved. - * + *

* This program and the accompanying materials are dual-licensed under * either the terms of the Eclipse Public License v1.0 as published by * the Eclipse Foundation - * - * or (per the licensee's choosing) - * + *

+ * or (per the licensee's choosing) + *

* under the terms of the GNU Lesser General Public License version 2.1 * as published by the Free Software Foundation. */ @@ -24,7 +24,7 @@ /** * Implementation of TurboFilterAttachable. - * + * * @author Ceki Gülcü */ final public class TurboFilterList extends CopyOnWriteArrayList { @@ -37,31 +37,32 @@ final public class TurboFilterList extends CopyOnWriteArrayList { * then NEUTRAL is returned. */ public FilterReply getTurboFilterChainDecision(final Marker marker, final Logger logger, final Level level, - final String format, final Object[] params, final Throwable t) { + final String format, final Object[] params, final Throwable t) { final int size = size(); - // if (size == 0) { - // return FilterReply.NEUTRAL; - // } + // caller may have already performed this check, but we do it here as well to be sure + if (size == 0) { + return FilterReply.NEUTRAL; + } + if (size == 1) { try { TurboFilter tf = get(0); return tf.decide(marker, logger, level, format, params, t); } catch (IndexOutOfBoundsException iobe) { + // concurrent modification detected, fall through to the general case return FilterReply.NEUTRAL; } } - Object[] tfa = toArray(); - final int len = tfa.length; - for (int i = 0; i < len; i++) { - // for (TurboFilter tf : this) { - final TurboFilter tf = (TurboFilter) tfa[i]; + + for (TurboFilter tf : this) { final FilterReply r = tf.decide(marker, logger, level, format, params, t); if (r == FilterReply.DENY || r == FilterReply.ACCEPT) { return r; } } + return FilterReply.NEUTRAL; } diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/turbo/TurboFilter.java b/logback-classic/src/main/java/ch/qos/logback/classic/turbo/TurboFilter.java index 35bc7be649..0a50a4abdc 100644 --- a/logback-classic/src/main/java/ch/qos/logback/classic/turbo/TurboFilter.java +++ b/logback-classic/src/main/java/ch/qos/logback/classic/turbo/TurboFilter.java @@ -27,7 +27,7 @@ * first is much more performant. *

* For more information about turbo filters, please refer to the online manual - * at http://logback.qos.ch/manual/filters.html#TurboFilter + * at https://logback.qos.ch/manual/filters.html#TurboFilter * * @author Ceki Gulcu */ From 3cecf2983c6a86d3f183b5808e19abf636bc63ad Mon Sep 17 00:00:00 2001 From: ceki Date: Fri, 31 Oct 2025 22:26:03 +0100 Subject: [PATCH 34/36] add comment for the TurboFilter list ACCEPT case Signed-off-by: ceki --- .../src/main/java/ch/qos/logback/classic/Logger.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/Logger.java b/logback-classic/src/main/java/ch/qos/logback/classic/Logger.java index 7793cc39cb..5819b7fccf 100644 --- a/logback-classic/src/main/java/ch/qos/logback/classic/Logger.java +++ b/logback-classic/src/main/java/ch/qos/logback/classic/Logger.java @@ -375,6 +375,7 @@ private void filterAndLog_0_Or3Plus(final String localFQCN, final Marker marker, final FilterReply decision = loggerContext.getTurboFilterChainDecision_0_3OrMore(marker, this, level, msg, params, t); + // the ACCEPT case falls through if (decision == FilterReply.NEUTRAL) { if (effectiveLevelInt > level.levelInt) { return; @@ -391,6 +392,7 @@ private void filterAndLog_1(final String localFQCN, final Marker marker, final L final FilterReply decision = loggerContext.getTurboFilterChainDecision_1(marker, this, level, msg, param, t); + // the ACCEPT case falls through if (decision == FilterReply.NEUTRAL) { if (effectiveLevelInt > level.levelInt) { return; @@ -408,6 +410,7 @@ private void filterAndLog_2(final String localFQCN, final Marker marker, final L final FilterReply decision = loggerContext.getTurboFilterChainDecision_2(marker, this, level, msg, param1, param2, t); + // the ACCEPT case falls through if (decision == FilterReply.NEUTRAL) { if (effectiveLevelInt > level.levelInt) { return; From dea5b956f327236d0872249b9fa12562287167ac Mon Sep 17 00:00:00 2001 From: ceki Date: Sat, 1 Nov 2025 20:15:00 +0100 Subject: [PATCH 35/36] minor - remove superflous call to Objects.requireNonNull Signed-off-by: ceki --- .../java/ch/qos/logback/core/util/ReentryGuardFactory.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/logback-core/src/main/java/ch/qos/logback/core/util/ReentryGuardFactory.java b/logback-core/src/main/java/ch/qos/logback/core/util/ReentryGuardFactory.java index aa10f266f9..1689b5437d 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/util/ReentryGuardFactory.java +++ b/logback-core/src/main/java/ch/qos/logback/core/util/ReentryGuardFactory.java @@ -14,8 +14,6 @@ package ch.qos.logback.core.util; -import java.util.Objects; - /** * Factory that creates {@link ReentryGuard} instances according to a requested type. * @@ -56,7 +54,6 @@ public enum GuardType { * @since 1.5.21 */ public static ReentryGuard makeGuard(GuardType guardType) { - Objects.requireNonNull(guardType, "guardType must not be null"); switch (guardType) { case THREAD_LOCAL: return new ReentryGuard.ReentryGuardImpl(); From 1cd2df4be866ba48ec410ecd2e33855324b62476 Mon Sep 17 00:00:00 2001 From: ceki Date: Sat, 1 Nov 2025 20:31:04 +0100 Subject: [PATCH 36/36] fix issues/871 See https://github.com/qos-ch/logback/issues/871 Signed-off-by: ceki --- .../java/ch/qos/logback/classic/Logger.java | 48 ++++++++++++--- .../ch/qos/logback/classic/LoggerContext.java | 36 ++++++----- .../logback/classic/spi/TurboFilterList.java | 52 ++++++++++++---- .../logback/classic/turbo/TurboFilter.java | 50 +++++++++++++++- .../classic/TurboFilteringInLoggerTest.java | 60 +++++++++++++------ 5 files changed, 187 insertions(+), 59 deletions(-) diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/Logger.java b/logback-classic/src/main/java/ch/qos/logback/classic/Logger.java index 5819b7fccf..a73fea34c0 100644 --- a/logback-classic/src/main/java/ch/qos/logback/classic/Logger.java +++ b/logback-classic/src/main/java/ch/qos/logback/classic/Logger.java @@ -369,6 +369,7 @@ Logger createChildByName(final String childName) { * by about 20 nanoseconds. */ + // for 0 or 3 or more parameters private void filterAndLog_0_Or3Plus(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params, final Throwable t) { @@ -387,6 +388,7 @@ private void filterAndLog_0_Or3Plus(final String localFQCN, final Marker marker, buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t); } + private void filterAndLog_1(final String localFQCN, final Marker marker, final Level level, final String msg, final Object param, final Throwable t) { @@ -748,10 +750,12 @@ public String toString() { * Method that calls the attached TurboFilter objects based on the logger and * the level. * - * It is used by isYYYEnabled() methods. - * - * It returns the typical FilterReply values: ACCEPT, NEUTRAL or DENY. - * + *

It is used by isXYZEnabled() methods such as {@link #isDebugEnabled()}, + * {@link #isInfoEnabled()} etc. + *

+ * + *

It returns the typical FilterReply values: ACCEPT, NEUTRAL or DENY. + *

* @param level * @return the reply given by the TurboFilters */ @@ -759,6 +763,24 @@ private FilterReply callTurboFilters(Marker marker, Level level) { return loggerContext.getTurboFilterChainDecision_0_3OrMore(marker, this, level, null, null, null); } + /** + * Method that calls the attached TurboFilter objects based on this logger and + * {@link org.slf4j.event.LoggingEvent LoggingEvent}. + * + *

This method is typically called by + * {@link #log(org.slf4j.event.LoggingEvent) log(LoggingEvent)} method.

+ * + *

It returns {@link FilterReply} values: ACCEPT, NEUTRAL or DENY. + *

+ * + * @param slf4jEvent the SLF4J LoggingEvent + * @return the reply given by the TurboFilters + */ + private FilterReply callTurboFilters(org.slf4j.event.LoggingEvent slf4jEvent) { + return loggerContext.getTurboFilterChainDecision(this, slf4jEvent); + } + + /** * Return the context for this logger. * @@ -785,7 +807,9 @@ public void log(Marker marker, String fqcn, int levelInt, String message, Object /** * Support SLF4J interception during initialization as introduced in SLF4J - * version 1.7.15 + * version 1.7.15. Alternatively, this method can be called by SLF4J's fluent API, i.e. by + * {@link LoggingEventBuilder}. + * * * @since 1.1.4 * @param slf4jEvent @@ -794,10 +818,16 @@ public void log(org.slf4j.event.LoggingEvent slf4jEvent) { org.slf4j.event.Level slf4jLevel = slf4jEvent.getLevel(); Level logbackLevel = Level.convertAnSLF4JLevel(slf4jLevel); - // By default, assume this class was the caller. This happens during - // initialization. - // However, it is possible that the caller is some other library, e.g. - // slf4j-jdk-platform-logging + // invoke turbo filters. See also https://github.com/qos-ch/logback/issues/871 + final FilterReply decision = loggerContext.getTurboFilterChainDecision(this, slf4jEvent); + // the ACCEPT and NEUTRAL cases falls through as there are no further level checks to be done + if (decision == FilterReply.DENY) { + return; + } + + // By default, assume this class was the caller. In some cases, {@link SubstituteLogger} can also be a caller. + // + // It is possible that the caller is some other library, e.g. slf4j-jdk-platform-logging String callerBoundary = slf4jEvent.getCallerBoundary(); if (callerBoundary == null) { diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/LoggerContext.java b/logback-classic/src/main/java/ch/qos/logback/classic/LoggerContext.java index 8faed3ed3c..be8e7870fb 100755 --- a/logback-classic/src/main/java/ch/qos/logback/classic/LoggerContext.java +++ b/logback-classic/src/main/java/ch/qos/logback/classic/LoggerContext.java @@ -13,24 +13,6 @@ */ package ch.qos.logback.classic; -import static ch.qos.logback.core.CoreConstants.EVALUATOR_MAP; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.locks.ReentrantLock; - -import ch.qos.logback.classic.util.LogbackMDCAdapter; -import ch.qos.logback.core.status.ErrorStatus; -import ch.qos.logback.core.status.InfoStatus; -import org.slf4j.ILoggerFactory; -import org.slf4j.Marker; - import ch.qos.logback.classic.spi.LoggerComparator; import ch.qos.logback.classic.spi.LoggerContextListener; import ch.qos.logback.classic.spi.LoggerContextVO; @@ -45,8 +27,16 @@ import ch.qos.logback.core.status.StatusListener; import ch.qos.logback.core.status.StatusManager; import ch.qos.logback.core.status.WarnStatus; +import org.slf4j.ILoggerFactory; +import org.slf4j.Marker; import org.slf4j.spi.MDCAdapter; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; + +import static ch.qos.logback.core.CoreConstants.EVALUATOR_MAP; + /** * LoggerContext glues many of the logback-classic components together. In * principle, every logback-classic component instance is attached either @@ -281,12 +271,20 @@ final FilterReply getTurboFilterChainDecision_1(final Marker marker, final Logge final FilterReply getTurboFilterChainDecision_2(final Marker marker, final Logger logger, final Level level, final String format, final Object param1, final Object param2, final Throwable t) { - if (turboFilterList.size() == 0) { + if (turboFilterList.isEmpty()) { return FilterReply.NEUTRAL; } return turboFilterList.getTurboFilterChainDecision(marker, logger, level, format, new Object[] { param1, param2 }, t); } + final FilterReply getTurboFilterChainDecision(final Logger logger, final org.slf4j.event.LoggingEvent slf4jEvent) { + if (turboFilterList.isEmpty()) { + return FilterReply.NEUTRAL; + } + return turboFilterList.getTurboFilterChainDecision(logger, slf4jEvent); + } + + // === start listeners ============================================== public void addListener(LoggerContextListener listener) { loggerContextListenerList.add(listener); diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/spi/TurboFilterList.java b/logback-classic/src/main/java/ch/qos/logback/classic/spi/TurboFilterList.java index ac651a4474..70264a98dc 100644 --- a/logback-classic/src/main/java/ch/qos/logback/classic/spi/TurboFilterList.java +++ b/logback-classic/src/main/java/ch/qos/logback/classic/spi/TurboFilterList.java @@ -33,7 +33,7 @@ final public class TurboFilterList extends CopyOnWriteArrayList { /** * Loop through the filters in the chain. As soon as a filter decides on ACCEPT - * or DENY, then that value is returned. If all of the filters return NEUTRAL, + * or DENY, then that value is returned. If all turbo filters return NEUTRAL, * then NEUTRAL is returned. */ public FilterReply getTurboFilterChainDecision(final Marker marker, final Logger logger, final Level level, @@ -66,16 +66,44 @@ public FilterReply getTurboFilterChainDecision(final Marker marker, final Logger return FilterReply.NEUTRAL; } - // public boolean remove(TurboFilter turboFilter) { - // return tfList.remove(turboFilter); - // } - // - // public TurboFilter remove(int index) { - // return tfList.remove(index); - // } - // - // final public int size() { - // return tfList.size(); - // } + + /** + * Loop through the filters in the chain. As soon as a filter decides on ACCEPT + * or DENY, then that value is returned. If all turbo filters return NEUTRAL, + * then NEUTRAL is returned. + * + * @param logger the logger requesting a decision + * @param slf4jEvent the SLF4J logging event + * @return the decision of the turbo filter chain + * @since 1.5.21 + */ + public FilterReply getTurboFilterChainDecision(Logger logger, org.slf4j.event.LoggingEvent slf4jEvent) { + + final int size = size(); + // caller may have already performed this check, but we do it here as well to be sure + if (size == 0) { + return FilterReply.NEUTRAL; + } + + if (size == 1) { + try { + TurboFilter tf = get(0); + return tf.decide(logger, slf4jEvent); + } catch (IndexOutOfBoundsException iobe) { + // concurrent modification detected, fall through to the general case + return FilterReply.NEUTRAL; + } + } + + + for (TurboFilter tf : this) { + final FilterReply r = tf.decide(logger, slf4jEvent); + if (r == FilterReply.DENY || r == FilterReply.ACCEPT) { + return r; + } + } + + return FilterReply.NEUTRAL; + } } diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/turbo/TurboFilter.java b/logback-classic/src/main/java/ch/qos/logback/classic/turbo/TurboFilter.java index 0a50a4abdc..e761fa78a5 100644 --- a/logback-classic/src/main/java/ch/qos/logback/classic/turbo/TurboFilter.java +++ b/logback-classic/src/main/java/ch/qos/logback/classic/turbo/TurboFilter.java @@ -13,6 +13,8 @@ */ package ch.qos.logback.classic.turbo; +import ch.qos.logback.classic.LoggerContext; +import org.slf4j.LoggerFactory; import org.slf4j.Marker; import ch.qos.logback.classic.Level; @@ -21,6 +23,8 @@ import ch.qos.logback.core.spi.FilterReply; import ch.qos.logback.core.spi.LifeCycle; +import java.util.List; + /** * TurboFilter is a specialized filter with a decide method that takes a bunch * of parameters instead of a single event object. The latter is cleaner but the @@ -28,7 +32,7 @@ *

* For more information about turbo filters, please refer to the online manual * at https://logback.qos.ch/manual/filters.html#TurboFilter - * + * * @author Ceki Gulcu */ public abstract class TurboFilter extends ContextAwareBase implements LifeCycle { @@ -53,6 +57,50 @@ public abstract class TurboFilter extends ContextAwareBase implements LifeCycle public abstract FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t); + + /** + *

This method is intended to be called via SLF4J's fluent API and more specifically by + * {@link Logger#log(org.slf4j.event.LoggingEvent slf4jEvent)}. Derived classes are strongly + * encouraged to override this method with a better suited and more specialized + * implementation. + *

+ * + *

The present default implementation translates the given SLF4J {@code LoggingEvent} into the + * set of parameters required by {@link #decide(Marker, Logger, Level, String, Object[], Throwable)} + * and delegate the decision to that method. + *

+ * + *

Concretely, this method: + *

    + *
  • extracts the first marker (if any) from the event's marker list,
  • + *
  • maps the SLF4J level to Logback's {@link Level},
  • + *
  • and forwards the event message, arguments and throwable.
  • + *
+ * + *

Returns the {@link ch.qos.logback.core.spi.FilterReply} produced by + * {@code decide(...)}, which should be one of DENY, NEUTRAL or ACCEPT. + * + *

Derived classes are strongly encouraged to override this method with a + * better suited and more specialized implementation.

+ * + * @param logger the Logger that is logging the event; non-null + * @param slf4jEvent the SLF4J logging event to translate and evaluate; may be non-null + * @return the filter decision ({@code DENY}, {@code NEUTRAL} or {@code ACCEPT}) + * + * @since 1.5.21 + */ + public FilterReply decide(Logger logger, org.slf4j.event.LoggingEvent slf4jEvent) { + List markers = slf4jEvent.getMarkers(); + Marker firstMarker = (markers != null && !markers.isEmpty()) ? markers.get(0) : null; + + Level logbackLevel = Level.convertAnSLF4JLevel(slf4jEvent.getLevel()); + String format = slf4jEvent.getMessage(); + Object[] params = slf4jEvent.getArgumentArray(); + Throwable t = slf4jEvent.getThrowable(); + + return decide(firstMarker, logger, logbackLevel, format, params, t); + } + public void start() { this.start = true; } diff --git a/logback-classic/src/test/java/ch/qos/logback/classic/TurboFilteringInLoggerTest.java b/logback-classic/src/test/java/ch/qos/logback/classic/TurboFilteringInLoggerTest.java index 3d7132e314..f0be09ccab 100644 --- a/logback-classic/src/test/java/ch/qos/logback/classic/TurboFilteringInLoggerTest.java +++ b/logback-classic/src/test/java/ch/qos/logback/classic/TurboFilteringInLoggerTest.java @@ -44,7 +44,7 @@ public class TurboFilteringInLoggerTest { ListAppender listAppender = new ListAppender<>(); - MDCFilter mdcFilter = new MDCFilter(); + @BeforeEach public void setUp() throws Exception { loggerContext = new LoggerContext(); @@ -59,25 +59,28 @@ public void setUp() throws Exception { } - private void addMDCFilter() { - - mdcFilter.setOnMatch("ACCEPT"); - mdcFilter.setOnMismatch("DENY"); - mdcFilter.setMDCKey(key); - mdcFilter.setValue(value); - mdcFilter.start(); - loggerContext.addTurboFilter(mdcFilter); + private CountingMDCFilter addMDCFilter() { + CountingMDCFilter countingMDCFilter = new CountingMDCFilter(); + countingMDCFilter.setOnMatch("ACCEPT"); + countingMDCFilter.setOnMismatch("DENY"); + countingMDCFilter.setMDCKey(key); + countingMDCFilter.setValue(value); + countingMDCFilter.start(); + loggerContext.addTurboFilter(countingMDCFilter); + return countingMDCFilter; } - private void addYesFilter() { + private YesFilter addYesFilter() { YesFilter filter = new YesFilter(); filter.start(); loggerContext.addTurboFilter(filter); + return filter; } - private void addNoFilter() { + private NoFilter addNoFilter() { NoFilter filter = new NoFilter(); filter.start(); loggerContext.addTurboFilter(filter); + return filter; } private void addAcceptBLUEFilter() { @@ -105,16 +108,22 @@ public void testIsDebugEnabledWithYesFilter() { @Test public void testIsInfoEnabledWithYesFilter() { - addYesFilter(); + YesFilter filter = addYesFilter(); logger.setLevel(Level.WARN); - assertTrue(logger.isInfoEnabled()); + assertTrue(logger.isInfoEnabled()); // count+=1 + logger.info("testIsInfoEnabledWithYesFilter1"); // count+=1 + logger.atInfo().log("testIsInfoEnabledWithYesFilter2"); // count+=2 + assertEquals(2, listAppender.list.size()); + assertEquals(4, filter.count); } @Test public void testIsWarnEnabledWithYesFilter() { - addYesFilter(); + YesFilter filter = addYesFilter(); logger.setLevel(Level.ERROR); - assertTrue(logger.isWarnEnabled()); + assertTrue(logger.isWarnEnabled()); // count+=1 + assertEquals(1, filter.count); + } @Test @@ -190,26 +199,41 @@ public void testLoggingContextReset() { @Test public void fluentAPI() { - addMDCFilter(); + CountingMDCFilter countingMDCFilter = addMDCFilter(); Logger logger = loggerContext.getLogger(this.getClass()); - logger.atDebug().log("hello 1"); + logger.atDebug().log("hello 1"); // count+=1 assertEquals(0, listAppender.list.size()); MDC.put(key, value); - logger.atDebug().log("hello 2"); + logger.atDebug().log("hello 2"); // count+=2 assertEquals(1, listAppender.list.size()); + assertEquals(3, countingMDCFilter.count); } } class YesFilter extends TurboFilter { + int count = 0; @Override public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { + count++; return FilterReply.ACCEPT; } } class NoFilter extends TurboFilter { + int count = 0; @Override public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { + count++; return FilterReply.DENY; } +} + + +class CountingMDCFilter extends MDCFilter { + int count = 0; + @Override + public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { + count++; + return super.decide(marker, logger, level, format, params, t); + } } \ No newline at end of file