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 18dbb1166a..0000000000
--- a/FUNDING.yml
+++ /dev/null
@@ -1 +0,0 @@
-github: qos-ch
\ No newline at end of file
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.
diff --git a/logback-access/pom.xml b/logback-access/pom.xml
index fa36882592..f3648ef6ca 100644
--- a/logback-access/pom.xml
+++ b/logback-access/pom.xml
@@ -8,7 +8,7 @@
ch.qos.logbacklogback-parent
- 1.5.18
+ 1.5.22logback-access
diff --git a/logback-classic-blackbox/pom.xml b/logback-classic-blackbox/pom.xml
index 4762eeaac4..6a3aef924b 100644
--- a/logback-classic-blackbox/pom.xml
+++ b/logback-classic-blackbox/pom.xml
@@ -8,7 +8,7 @@
ch.qos.logbacklogback-parent
- 1.5.18
+ 1.5.22logback-classic-blackbox
@@ -130,14 +130,6 @@
-
- org.apache.maven.plugins
- maven-deploy-plugin
-
- true
-
-
-
diff --git a/logback-classic-blackbox/src/test/java/ch/qos/logback/classic/blackbox/net/SMTPAppender_GreenTest.java b/logback-classic-blackbox/src/test/java/ch/qos/logback/classic/blackbox/net/SMTPAppender_GreenTest.java
index 8cb0fd7e1b..6fd4f8a884 100644
--- a/logback-classic-blackbox/src/test/java/ch/qos/logback/classic/blackbox/net/SMTPAppender_GreenTest.java
+++ b/logback-classic-blackbox/src/test/java/ch/qos/logback/classic/blackbox/net/SMTPAppender_GreenTest.java
@@ -20,23 +20,21 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
+import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.blackbox.BlackboxClassicTestConstants;
import ch.qos.logback.classic.net.SMTPAppender;
import ch.qos.logback.classic.util.LogbackMDCAdapter;
import ch.qos.logback.core.util.EnvUtil;
+import com.icegreen.greenmail.util.*;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
-import com.icegreen.greenmail.util.DummySSLSocketFactory;
-import com.icegreen.greenmail.util.GreenMail;
-import com.icegreen.greenmail.util.GreenMailUtil;
-import com.icegreen.greenmail.util.ServerSetup;
-
//import ch.qos.logback.classic.ClassicTestConstants;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
@@ -66,6 +64,11 @@ public class SMTPAppender_GreenTest {
static final String FOOTER = "FOOTER\n";
static final String DEFAULT_PATTERN = "%-4relative %mdc [%thread] %-5level %class - %msg%n";
+ static final String JAVAX_NET_DEBUG_KEY = "javax.net.debug";
+ static final String SSH_HANDSHAKE_DEBUG_VAL = "ssl:handshake";
+ // for use in authenticated SSL tests only (to avoid SSL handshake failures)
+ static final String CHECK_SERVER_IDENTITY_KEY = "mail.smtp.ssl.checkserveridentity";
+
static final boolean SYNCHRONOUS = false;
static final boolean ASYNCHRONOUS = true;
static int TIMEOUT = 3000;
@@ -82,12 +85,27 @@ public class SMTPAppender_GreenTest {
static String REQUIRED_USERNAME = "alice";
static String REQUIRED_PASSWORD = "alicepass";
+
@BeforeEach
public void setUp() throws Exception {
loggerContext.setMDCAdapter(logbackMDCAdapter);
StatusListenerConfigHelper.addOnConsoleListenerInstance(loggerContext, new OnConsoleStatusListener());
+ setGlobalLogbackLoggerForGreenmail(Level.INFO);
+ }
+
+
+ @AfterEach
+ public void tearDown() throws Exception {
+ greenMailServer.stop();
+ setGlobalLogbackLoggerForGreenmail(null);
}
+ private static void setGlobalLogbackLoggerForGreenmail(Level level) {
+ Logger logbackLogger = (Logger) LoggerFactory.getLogger("com.icegreen.greenmail");
+ logbackLogger.setLevel(level);
+ }
+
+
void startSMTPServer(boolean withSSL) {
ServerSetup serverSetup;
@@ -107,10 +125,6 @@ void startSMTPServer(boolean withSSL) {
}
}
- @AfterEach
- public void tearDown() throws Exception {
- greenMailServer.stop();
- }
void buildSMTPAppender(String subject, boolean synchronicity) throws Exception {
smtpAppender = new SMTPAppender();
@@ -456,9 +470,12 @@ void unsetSystemPropertiesForStartTLS() {
@Test
public void authenticatedSSL() throws Exception {
+
try {
+ // without setting this property, the SSL handshake fails with newer icegreen, angus versions
+ System.setProperty(CHECK_SERVER_IDENTITY_KEY, "false");
setSystemPropertiesForStartTLS();
-
+ //System.setProperty("greenmail.smtps.host", "127.0.0.1");
startSMTPServer(WITH_SSL);
buildSMTPAppender("testMultipleTo", SYNCHRONOUS);
smtpAppender.setUsername(REQUIRED_USERNAME);
@@ -478,6 +495,7 @@ public void authenticatedSSL() throws Exception {
assertNotNull(mma);
assertTrue(mma.length == 1, "body should not be empty");
} finally {
+ System.clearProperty(CHECK_SERVER_IDENTITY_KEY);
unsetSystemPropertiesForStartTLS();
}
}
diff --git a/logback-classic/pom.xml b/logback-classic/pom.xml
index c7d58872d8..38e22ea81f 100755
--- a/logback-classic/pom.xml
+++ b/logback-classic/pom.xml
@@ -8,7 +8,7 @@
ch.qos.logbacklogback-parent
- 1.5.18
+ 1.5.22logback-classic
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..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,12 +369,14 @@ 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) {
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;
@@ -386,11 +388,13 @@ 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) {
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 +412,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;
@@ -745,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
*/
@@ -756,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.
*
@@ -782,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
@@ -791,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/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-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.
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}.
+ *
+ * @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;
+ }
+
+ }
diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/joran/PropertiesConfigurator.java b/logback-classic/src/main/java/ch/qos/logback/classic/joran/PropertiesConfigurator.java
index db2e7410fe..e3cad12e38 100644
--- a/logback-classic/src/main/java/ch/qos/logback/classic/joran/PropertiesConfigurator.java
+++ b/logback-classic/src/main/java/ch/qos/logback/classic/joran/PropertiesConfigurator.java
@@ -187,7 +187,8 @@ public String subst(String ref) {
String substituted = variableSubstitutionsHelper.subst(ref);
if (ref != null && !ref.equals(substituted)) {
- addInfo("value \"" + substituted + "\" substituted for \"" + ref + "\"");
+ String sanitized = variableSubstitutionsHelper.sanitizeIfConfidential(ref, substituted);
+ addInfo("value \"" + sanitized + "\" substituted for \"" + ref + "\"");
}
return substituted;
}
diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/joran/ReconfigureOnChangeTask.java b/logback-classic/src/main/java/ch/qos/logback/classic/joran/ReconfigureOnChangeTask.java
index a59e8e1121..58bf9104c6 100755
--- a/logback-classic/src/main/java/ch/qos/logback/classic/joran/ReconfigureOnChangeTask.java
+++ b/logback-classic/src/main/java/ch/qos/logback/classic/joran/ReconfigureOnChangeTask.java
@@ -183,8 +183,22 @@ public String toString() {
return "ReconfigureOnChangeTask(born:" + birthdate + ")";
}
-
+ /**
+ * Contains typo. Replaced by {@link #setScheduledFuture(ScheduledFuture)}.
+ * @param aScheduledFuture
+ * @deprecated
+ */
+ @Deprecated
public void setScheduredFuture(ScheduledFuture> aScheduledFuture) {
+ setScheduledFuture(aScheduledFuture);
+ }
+
+ /**
+ * Replaces {@link #setScheduredFuture(ScheduledFuture)}
+ * @param aScheduledFuture
+ * @since 1.5.19
+ */
+ public void setScheduledFuture(ScheduledFuture> aScheduledFuture) {
this.scheduledFuture = aScheduledFuture;
}
}
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.
diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/model/processor/ConfigurationModelHandlerFull.java b/logback-classic/src/main/java/ch/qos/logback/classic/model/processor/ConfigurationModelHandlerFull.java
index 749c92b872..83f5db8254 100755
--- a/logback-classic/src/main/java/ch/qos/logback/classic/model/processor/ConfigurationModelHandlerFull.java
+++ b/logback-classic/src/main/java/ch/qos/logback/classic/model/processor/ConfigurationModelHandlerFull.java
@@ -16,6 +16,7 @@
import ch.qos.logback.classic.joran.ReconfigureOnChangeTask;
import ch.qos.logback.classic.model.ConfigurationModel;
import ch.qos.logback.core.Context;
+import ch.qos.logback.core.joran.spi.ConfigurationWatchList;
import ch.qos.logback.core.joran.util.ConfigurationWatchListUtil;
import ch.qos.logback.core.model.Model;
import ch.qos.logback.core.model.processor.ModelHandlerBase;
@@ -25,7 +26,6 @@
import ch.qos.logback.core.util.Duration;
import ch.qos.logback.core.util.OptionHelper;
-import java.net.URL;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -58,6 +58,18 @@ public void postHandle(ModelInterpretationContext mic, Model model) throws Model
// post handling of scan attribute works even we need to watch for included files because the main url is
// set in GenericXMLConfigurator very early in the configuration process
postProcessScanAttrib(mic, configurationModel);
+
+ ConfigurationWatchList cwl = ConfigurationWatchListUtil.getConfigurationWatchList(getContext());
+ if (cwl != null) {
+ try {
+ addInfo("Main configuration file URL: " + cwl.getMainURL());
+ addInfo("FileWatchList= {" + cwl.getFileWatchListAsStr()+"}");
+ addInfo("URLWatchList= {" + cwl.getUrlWatchListAsStr()+"}");
+ } catch(NoSuchMethodError e) {
+ addWarn("It looks like the version of logback-classic is more recent than");
+ addWarn("the version of logback-core. Please align the two versions.");
+ }
+ }
}
protected void postProcessScanAttrib(ModelInterpretationContext mic, ConfigurationModel configurationModel) {
@@ -103,7 +115,7 @@ public void detachedPostProcess(String scanStr, String scanPeriodStr) {
ScheduledFuture> scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(rocTask, duration.getMilliseconds(), duration.getMilliseconds(),
TimeUnit.MILLISECONDS);
- rocTask.setScheduredFuture(scheduledFuture);
+ rocTask.setScheduledFuture(scheduledFuture);
context.addScheduledFuture(scheduledFuture);
}
diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/pattern/ThrowableProxyConverter.java b/logback-classic/src/main/java/ch/qos/logback/classic/pattern/ThrowableProxyConverter.java
index d12cbd8247..b7be004373 100644
--- a/logback-classic/src/main/java/ch/qos/logback/classic/pattern/ThrowableProxyConverter.java
+++ b/logback-classic/src/main/java/ch/qos/logback/classic/pattern/ThrowableProxyConverter.java
@@ -177,17 +177,9 @@ private void subjoinFirstLine(StringBuilder buf, String prefix, int indent, IThr
if (prefix != null) {
buf.append(prefix);
}
- subjoinExceptionMessage(buf, tp);
+ ThrowableProxyUtil.subjoinExceptionMessage(buf, tp);
}
- private void subjoinExceptionMessage(StringBuilder buf, IThrowableProxy tp) {
- if (tp.isCyclic()) {
- buf.append("[CIRCULAR REFERENCE: ").append(tp.getClassName()).append(": ").append(tp.getMessage())
- .append(']');
- } else {
- buf.append(tp.getClassName()).append(": ").append(tp.getMessage());
- }
- }
protected void subjoinSTEPArray(StringBuilder buf, int indent, IThrowableProxy tp) {
StackTraceElementProxy[] stepArray = tp.getStackTraceElementProxyArray();
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-classic/src/main/java/ch/qos/logback/classic/spi/IThrowableProxy.java b/logback-classic/src/main/java/ch/qos/logback/classic/spi/IThrowableProxy.java
index acf63b16a1..3129706ee0 100644
--- a/logback-classic/src/main/java/ch/qos/logback/classic/spi/IThrowableProxy.java
+++ b/logback-classic/src/main/java/ch/qos/logback/classic/spi/IThrowableProxy.java
@@ -1,19 +1,33 @@
/**
* 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.
*/
package ch.qos.logback.classic.spi;
public interface IThrowableProxy {
+
+ /**
+ * Return the overriding message if any. This method returns null
+ * if there is no overriding message.
+ *
+ *
Overriding message exists only if the original throwable implementation overrides the toString() method.
+ *
+ * @return the overriding message or null
+ * @since 1.5.22
+ */
+ default String getOverridingMessage() {
+ return null;
+ }
+
String getMessage();
String getClassName();
@@ -28,7 +42,7 @@ public interface IThrowableProxy {
/**
* Is this instance the result of a cyclic exception?
- *
+ *
* @return true if cyclic, false otherwise
* @sine 1.3.0
*/
diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/spi/LoggerContextVO.java b/logback-classic/src/main/java/ch/qos/logback/classic/spi/LoggerContextVO.java
index efc22c65f2..18ca822f67 100644
--- a/logback-classic/src/main/java/ch/qos/logback/classic/spi/LoggerContextVO.java
+++ b/logback-classic/src/main/java/ch/qos/logback/classic/spi/LoggerContextVO.java
@@ -41,6 +41,13 @@ public class LoggerContextVO implements Serializable {
protected Map propertyMap;
protected long birthTime;
+ /**
+ * No-arg constructor for serialization frameworks only.
+ *
+ * @since 1.5.21
+ */
+ public LoggerContextVO() {}
+
public LoggerContextVO(LoggerContext lc) {
this.name = lc.getName();
this.propertyMap = lc.getCopyOfPropertyMap();
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);
diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/spi/LoggingEventVO.java b/logback-classic/src/main/java/ch/qos/logback/classic/spi/LoggingEventVO.java
index 26f4f5cc13..fc3f8ad072 100644
--- a/logback-classic/src/main/java/ch/qos/logback/classic/spi/LoggingEventVO.java
+++ b/logback-classic/src/main/java/ch/qos/logback/classic/spi/LoggingEventVO.java
@@ -86,7 +86,7 @@ public static LoggingEventVO build(ILoggingEvent le) {
ledo.sequenceNumber = le.getSequenceNumber();
ledo.throwableProxy = ThrowableProxyVO.build(le.getThrowableProxy());
// add caller data only if it is there already
- // fixes http://jira.qos.ch/browse/LBCLASSIC-145
+ // See also https://jira.qos.ch/browse/LOGBACK-480
if (le.hasCallerData()) {
ledo.callerDataArray = le.getCallerData();
}
diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/spi/ThrowableProxy.java b/logback-classic/src/main/java/ch/qos/logback/classic/spi/ThrowableProxy.java
index b4b816c16e..a18cf72bc8 100644
--- a/logback-classic/src/main/java/ch/qos/logback/classic/spi/ThrowableProxy.java
+++ b/logback-classic/src/main/java/ch/qos/logback/classic/spi/ThrowableProxy.java
@@ -28,6 +28,7 @@ public class ThrowableProxy implements IThrowableProxy {
private Throwable throwable;
private String className;
+ private String overridingMessage;
private String message;
// package-private because of ThrowableProxyUtil
StackTraceElementProxy[] stackTraceElementProxyArray;
@@ -65,6 +66,7 @@ public ThrowableProxy(Throwable throwable, Set alreadyProcessedSet) {
this.throwable = throwable;
this.className = throwable.getClass().getName();
this.message = throwable.getMessage();
+ this.overridingMessage = buildOverridingMessage(throwable);
this.stackTraceElementProxyArray = ThrowableProxyUtil.steArrayToStepArray(throwable.getStackTrace());
this.cyclic = false;
@@ -101,6 +103,18 @@ public ThrowableProxy(Throwable throwable, Set alreadyProcessedSet) {
}
}
+ private String buildOverridingMessage(Throwable throwable) {
+ StringBuilder sb = new StringBuilder();
+ ThrowableProxyUtil.appendNominalFirstLine(sb, throwable.getClass().getName(), throwable.getMessage());
+ String messageFromToString = throwable.toString();
+ String nominalMessage = sb.toString();
+ if (!nominalMessage.equals(messageFromToString)) {
+ return messageFromToString;
+ } else {
+ return null;
+ }
+ }
+
public Throwable getThrowable() {
return throwable;
}
@@ -109,6 +123,16 @@ public String getMessage() {
return message;
}
+ /*
+ * (non-Javadoc)
+ *
+ * @see ch.qos.logback.classic.spi.IThrowableProxy#getOverridingMessage()
+ */
+ @Override
+ public String getOverridingMessage() {
+ return overridingMessage;
+ }
+
/*
* (non-Javadoc)
*
diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/spi/ThrowableProxyUtil.java b/logback-classic/src/main/java/ch/qos/logback/classic/spi/ThrowableProxyUtil.java
index b24af02ce8..ccefc7763a 100644
--- a/logback-classic/src/main/java/ch/qos/logback/classic/spi/ThrowableProxyUtil.java
+++ b/logback-classic/src/main/java/ch/qos/logback/classic/spi/ThrowableProxyUtil.java
@@ -73,6 +73,10 @@ static int findNumberOfCommonFrames(StackTraceElement[] steArray, StackTraceElem
return count;
}
+ public static void appendNominalFirstLine(StringBuilder buf, String classname, String message) {
+ buf.append(classname).append(": ").append(message);
+ }
+
public static String asString(IThrowableProxy tp) {
StringBuilder sb = new StringBuilder(BUILDER_CAPACITY);
@@ -181,12 +185,21 @@ public static void subjoinFirstLineRootCauseFirst(StringBuilder buf, IThrowableP
subjoinExceptionMessage(buf, tp);
}
- private static void subjoinExceptionMessage(StringBuilder buf, IThrowableProxy tp) {
+ public static void subjoinExceptionMessage(StringBuilder stringBuilder, IThrowableProxy tp) {
if (tp.isCyclic()) {
- buf.append("[CIRCULAR REFERENCE: ").append(tp.getClassName()).append(": ").append(tp.getMessage())
- .append(']');
+ stringBuilder.append("[CIRCULAR REFERENCE: ");
+ appendNominalOrOverridingMessage(stringBuilder, tp);
+ stringBuilder.append(']');
+ } else {
+ appendNominalOrOverridingMessage(stringBuilder, tp);
+ }
+ }
+
+ private static void appendNominalOrOverridingMessage(StringBuilder stringBuilder, IThrowableProxy tp) {
+ if(tp.getOverridingMessage() == null) {
+ appendNominalFirstLine(stringBuilder, tp.getClassName(), tp.getMessage());
} else {
- buf.append(tp.getClassName()).append(": ").append(tp.getMessage());
+ stringBuilder.append(tp.getOverridingMessage());
}
}
}
diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/spi/ThrowableProxyVO.java b/logback-classic/src/main/java/ch/qos/logback/classic/spi/ThrowableProxyVO.java
index ea1a99d765..1c20e64af1 100644
--- a/logback-classic/src/main/java/ch/qos/logback/classic/spi/ThrowableProxyVO.java
+++ b/logback-classic/src/main/java/ch/qos/logback/classic/spi/ThrowableProxyVO.java
@@ -21,6 +21,7 @@ public class ThrowableProxyVO implements IThrowableProxy, Serializable {
private static final long serialVersionUID = -773438177285807139L;
private String className;
+ private String overridingMessage;
private String message;
private int commonFramesCount;
private StackTraceElementProxy[] stackTraceElementProxyArray;
@@ -31,6 +32,17 @@ public class ThrowableProxyVO implements IThrowableProxy, Serializable {
public String getMessage() {
return message;
}
+ /**
+ * Return the overriding message if any. This method returns null
+ * if there is no overriding message.
+ *
+ *
Overriding message exists only if the original throwable implementation overrides the toString() method.
+ *
+ * @return the overriding message or null
+ * @since 1.5.22
+ */
+ @Override
+ public String getOverridingMessage() { return overridingMessage;}
public String getClassName() {
return className;
@@ -102,6 +114,7 @@ public static ThrowableProxyVO build(IThrowableProxy throwableProxy) {
ThrowableProxyVO tpvo = new ThrowableProxyVO();
tpvo.className = throwableProxy.getClassName();
tpvo.message = throwableProxy.getMessage();
+ tpvo.overridingMessage = throwableProxy.getOverridingMessage();
tpvo.commonFramesCount = throwableProxy.getCommonFrames();
tpvo.stackTraceElementProxyArray = throwableProxy.getStackTraceElementProxyArray();
tpvo.cyclic = throwableProxy.isCyclic();
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..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
@@ -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 {
@@ -33,48 +33,77 @@ 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,
- 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;
}
- // 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 35bc7be649..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,14 +23,16 @@
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
* 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
*/
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/main/java/ch/qos/logback/classic/util/ContextInitializer.java b/logback-classic/src/main/java/ch/qos/logback/classic/util/ContextInitializer.java
index 0e1876b904..f1addd5150 100644
--- a/logback-classic/src/main/java/ch/qos/logback/classic/util/ContextInitializer.java
+++ b/logback-classic/src/main/java/ch/qos/logback/classic/util/ContextInitializer.java
@@ -51,8 +51,7 @@ public class ContextInitializer {
*/
final public static String CONFIG_FILE_PROPERTY = ClassicConstants.CONFIG_FILE_PROPERTY;
- String[] INTERNAL_CONFIGURATOR_CLASSNAME_LIST = {"ch.qos.logback.classic.joran.SerializedModelConfigurator",
- "ch.qos.logback.classic.util.DefaultJoranConfigurator", "ch.qos.logback.classic.BasicConfigurator"};
+ String[] INTERNAL_CONFIGURATOR_CLASSNAME_LIST = {"ch.qos.logback.classic.util.DefaultJoranConfigurator", "ch.qos.logback.classic.BasicConfigurator"};
final LoggerContext loggerContext;
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
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());
}
}
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-classic/src/test/java/ch/qos/logback/classic/spi/PubThrowableProxy.java b/logback-classic/src/test/java/ch/qos/logback/classic/spi/PubThrowableProxy.java
index 2f6c17ff31..43b018aef3 100644
--- a/logback-classic/src/test/java/ch/qos/logback/classic/spi/PubThrowableProxy.java
+++ b/logback-classic/src/test/java/ch/qos/logback/classic/spi/PubThrowableProxy.java
@@ -19,6 +19,9 @@
import java.util.Arrays;
+/**
+ * Used in tests to represent a ThrowableProxy in a JSON-friendly way.
+ */
public class PubThrowableProxy implements IThrowableProxy {
private String className;
diff --git a/logback-classic/src/test/java/ch/qos/logback/classic/spi/ThrowableProxyTest.java b/logback-classic/src/test/java/ch/qos/logback/classic/spi/ThrowableProxyTest.java
index 100d6b744b..4b8a4728b5 100644
--- a/logback-classic/src/test/java/ch/qos/logback/classic/spi/ThrowableProxyTest.java
+++ b/logback-classic/src/test/java/ch/qos/logback/classic/spi/ThrowableProxyTest.java
@@ -47,11 +47,11 @@ public void verify(Throwable t) {
result = result.replace("common frames omitted", "more");
String expected = sw.toString();
- // System.out.println("========expected");
- // System.out.println(expected);
-
- // System.out.println("========result");
- // System.out.println(result);
+// System.out.println("========expected");
+// System.out.println(expected);
+//
+// System.out.println("========result");
+// System.out.println(result);
assertEquals(expected, result);
}
@@ -180,6 +180,16 @@ public void cyclicSuppressed() {
verify(e);
}
+ @Test
+ public void overriddenToString() {
+ Exception e = new Exception() {
+ public String toString() {
+ return getClass().getName() + " [extra]";
+ }
+ };
+ verify(e);
+ }
+
void someMethod() throws Exception {
throw new Exception("someMethod");
}
@@ -202,4 +212,6 @@ void someOtherMethod() throws Exception {
throw new Exception("someOtherMethod", e);
}
}
+
+
}
diff --git a/logback-core-blackbox/pom.xml b/logback-core-blackbox/pom.xml
index 2a361d0814..588a7bb412 100644
--- a/logback-core-blackbox/pom.xml
+++ b/logback-core-blackbox/pom.xml
@@ -8,7 +8,7 @@
ch.qos.logbacklogback-parent
- 1.5.18
+ 1.5.22logback-core-blackbox
@@ -70,14 +70,6 @@
-
- org.apache.maven.plugins
- maven-deploy-plugin
-
- true
-
-
-
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..f0af770984
--- /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/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/blackboxInput/joran/conditional/ifSystem_NoJoran.xml b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifSystem_NoJoran.xml
new file mode 100644
index 0000000000..5d47fb2b78
--- /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/blackboxInput/joran/conditional/ifWithoutElse_NoJoran.xml b/logback-core-blackbox/src/test/blackboxInput/joran/conditional/ifWithoutElse_NoJoran.xml
new file mode 100644
index 0000000000..60299b5d07
--- /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..28a92500e4
--- /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/COWArrayListConcurrencyTest.java b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/COWArrayListConcurrencyTest.java
new file mode 100644
index 0000000000..683cd67be7
--- /dev/null
+++ b/logback-core-blackbox/src/test/java/ch/qos/logback/core/blackbox/COWArrayListConcurrencyTest.java
@@ -0,0 +1,188 @@
+/*
+ * Logback: the reliable, generic, fast and flexible logging framework.
+ * Copyright (C) 1999-2024, 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;
+
+//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;
+
+@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);
+//// }
+// 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();
+// }
+// }
+// }
+
+
+}
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..857de7c761
--- /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.PropertyConditionBase;
+
+public class AlwaysFalseCondition extends PropertyConditionBase {
+
+ @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..d264028df6
--- /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.PropertyConditionBase;
+
+public class AlwaysTrueCondition extends PropertyConditionBase {
+
+ @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 d993418962..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
@@ -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,12 +49,12 @@
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;
import org.junit.jupiter.api.Test;
-import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Stack;
@@ -61,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/";
@@ -76,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);
@@ -99,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);
@@ -110,7 +116,7 @@ protected void addModelHandlerAssociations(DefaultProcessor defaultProcessor) {
@AfterEach
public void tearDown() throws Exception {
- StatusPrinter.printIfErrorsOccured(context);
+ statusPrinter2.printIfErrorsOccured(context);
System.clearProperty(sysKey);
}
@@ -121,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);
@@ -129,18 +135,49 @@ 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);
+ 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");
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);
@@ -148,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");
@@ -155,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");
@@ -162,16 +213,31 @@ 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));
}
-
+ @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");
@@ -182,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-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/pom.xml b/logback-core/pom.xml
index eac72ea2cb..01e123bf30 100755
--- a/logback-core/pom.xml
+++ b/logback-core/pom.xml
@@ -8,7 +8,7 @@
ch.qos.logbacklogback-parent
- 1.5.18
+ 1.5.22logback-core
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/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/ConsoleAppender.java b/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java
index 8200b05015..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
@@ -88,7 +90,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();
@@ -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/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/UnsynchronizedAppenderBase.java b/logback-core/src/main/java/ch/qos/logback/core/UnsynchronizedAppenderBase.java
index 79ea30e18b..fe1d80de82 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,9 @@
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;
+import ch.qos.logback.core.util.SimpleTimeBasedGuard;
/**
* Similar to {@link AppenderBase} except that derived appenders need to handle thread
@@ -31,16 +34,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.
@@ -53,29 +53,26 @@ public String getName() {
return name;
}
- private int statusRepeatCount = 0;
- private int exceptionCount = 0;
+ private SimpleTimeBasedGuard notStartedGuard = new SimpleTimeBasedGuard();
+ private SimpleTimeBasedGuard exceptionGuard = new SimpleTimeBasedGuard();
- 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 (notStartedGuard.allow()) {
+ 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;
@@ -85,11 +82,11 @@ public void doAppend(E eventObject) {
this.append(eventObject);
} catch (Exception e) {
- if (exceptionCount++ < ALLOWED_REPEATS) {
+ if (exceptionGuard.allow()) {
addError("Appender [" + name + "] failed to append.", e);
}
} finally {
- guard.set(Boolean.FALSE);
+ reentryGuard.unlock();
}
}
@@ -103,9 +100,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/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/src/main/java/ch/qos/logback/core/boolex/IsPropertyNullCondition.java b/logback-core/src/main/java/ch/qos/logback/core/boolex/IsPropertyNullCondition.java
new file mode 100644
index 0000000000..2bd024808a
--- /dev/null
+++ b/logback-core/src/main/java/ch/qos/logback/core/boolex/IsPropertyNullCondition.java
@@ -0,0 +1,42 @@
+/*
+ * 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 IsPropertyNullCondition extends PropertyConditionBase {
+
+ 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/src/main/java/ch/qos/logback/core/boolex/PropertyCondition.java b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyCondition.java
new file mode 100644
index 0000000000..2a4e298026
--- /dev/null
+++ b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyCondition.java
@@ -0,0 +1,61 @@
+/**
+ * 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.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 PropertyCondition 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/PropertyConditionBase.java b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyConditionBase.java
new file mode 100644
index 0000000000..343e451315
--- /dev/null
+++ b/logback-core/src/main/java/ch/qos/logback/core/boolex/PropertyConditionBase.java
@@ -0,0 +1,162 @@
+/*
+ * 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.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
+ * 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.
+ *
+ *
Properties are looked up in the following order:
+ *
+ *
+ *
In the local property container, usually the {@link ModelInterpretationContext}
The local property container used for property lookups.
+ *
+ *
Local properties correspond to the properties in the embedding
+ * configurator, i.e. usually the {@link ModelInterpretationContext} instance.
+ */
+ PropertyContainer localPropertyContainer;
+
+ /**
+ * 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 local property container
+ */
+ @Override
+ public PropertyContainer getLocalPropertyContainer() {
+ return localPropertyContainer;
+ }
+
+ /**
+ * 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 aLocalPropertyContainer the local property container to set
+ */
+ @Override
+ 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
+ */
+ 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).
+ *
+ *
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
+ */
+ 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.
+ *
+ *
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
+ */
+ 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/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/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/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/ParamModelHandler.java b/logback-core/src/main/java/ch/qos/logback/core/joran/ParamModelHandler.java
index dae5167c78..77ffebc8cb 100755
--- a/logback-core/src/main/java/ch/qos/logback/core/joran/ParamModelHandler.java
+++ b/logback-core/src/main/java/ch/qos/logback/core/joran/ParamModelHandler.java
@@ -28,19 +28,19 @@ protected Class getSupportedModelClass() {
}
@Override
- public void handle(ModelInterpretationContext intercon, Model model) throws ModelHandlerException {
+ public void handle(ModelInterpretationContext mic, Model model) throws ModelHandlerException {
ParamModel paramModel = (ParamModel) model;
- String valueStr = intercon.subst(paramModel.getValue());
+ String valueStr = mic.subst(paramModel.getValue());
- Object o = intercon.peekObject();
+ Object o = mic.peekObject();
PropertySetter propSetter = new PropertySetter(beanDescriptionCache, o);
propSetter.setContext(context);
// allow for variable substitution for name as well
- String finalName = intercon.subst(paramModel.getName());
+ String finalName = mic.subst(paramModel.getName());
propSetter.setProperty(finalName, valueStr);
}
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/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/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/joran/spi/ConfigurationWatchList.java b/logback-core/src/main/java/ch/qos/logback/core/joran/spi/ConfigurationWatchList.java
index 9bc6270f90..9c11b1eb4b 100755
--- a/logback-core/src/main/java/ch/qos/logback/core/joran/spi/ConfigurationWatchList.java
+++ b/logback-core/src/main/java/ch/qos/logback/core/joran/spi/ConfigurationWatchList.java
@@ -18,7 +18,6 @@
import java.io.File;
import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.security.NoSuchAlgorithmException;
@@ -30,6 +29,8 @@
import static ch.qos.logback.core.CoreConstants.PROPERTIES_FILE_EXTENSION;
/**
+ * This class manages the list of files and/or urls that are watched for changes.
+ *
* @author Ceki Gülcü
*/
public class ConfigurationWatchList extends ContextAwareBase {
@@ -130,18 +131,16 @@ private void addAsHTTP_or_HTTPS_URLToWatch(URL url) {
}
/**
- * Add the url but only if it is file://.
- * @param url should be a file
+ * Add the url but only if it is file:// or http(s)://
+ * @param url should be a file or http(s)
*/
-
public void addToWatchList(URL url) {
+ // assume that the caller has checked that the protocol is one of {file, https, http}.
String protocolStr = url.getProtocol();
if (protocolStr.equals(FILE_PROTOCOL_STR)) {
addAsFileToWatch(url);
} else if (isHTTP_Or_HTTPS(protocolStr)) {
addAsHTTP_or_HTTPS_URLToWatch(url);
- } else {
- addInfo("Cannot watch ["+url + "] as its protocol is not one of file, http or https.");
}
}
@@ -269,7 +268,7 @@ static public boolean isWatchableProtocol(URL url) {
return false;
}
String protocolStr = url.getProtocol();
- return Arrays.stream(WATCHABLE_PROTOCOLS).anyMatch(protocol -> protocol.equalsIgnoreCase(protocolStr));
+ return isWatchableProtocol(protocolStr);
}
/**
@@ -283,12 +282,23 @@ static public boolean isWatchableProtocol(String protocolStr) {
return Arrays.stream(WATCHABLE_PROTOCOLS).anyMatch(protocol -> protocol.equalsIgnoreCase(protocolStr));
}
- @Override
- public String toString() {
-
- String fileWatchListStr = fileWatchList.stream().map(File::getPath).collect(Collectors.joining(", "));
+ /**
+ * Returns the urlWatchList field as a String
+ * @return the urlWatchList field as a String
+ * @since 1.5.19
+ */
+ public String getUrlWatchListAsStr() {
String urlWatchListStr = urlWatchList.stream().map(URL::toString).collect(Collectors.joining(", "));
+ return urlWatchListStr;
+ }
- return "ConfigurationWatchList(" + "mainURL=" + mainURL + ", fileWatchList={" + fileWatchListStr + "}, urlWatchList=[" + urlWatchListStr + "})";
+ /**
+ * Returns the fileWatchList field as a String
+ * @return the fileWatchList field as a String
+ * @since 1.5.19
+ */
+ public String getFileWatchListAsStr() {
+ return fileWatchList.stream().map(File::getPath).collect(Collectors.joining(", "));
}
+
}
diff --git a/logback-core/src/main/java/ch/qos/logback/core/joran/util/ConfigurationWatchListUtil.java b/logback-core/src/main/java/ch/qos/logback/core/joran/util/ConfigurationWatchListUtil.java
index 51a451ad5e..25ebaf2e7c 100644
--- a/logback-core/src/main/java/ch/qos/logback/core/joran/util/ConfigurationWatchListUtil.java
+++ b/logback-core/src/main/java/ch/qos/logback/core/joran/util/ConfigurationWatchListUtil.java
@@ -24,6 +24,8 @@
import java.net.URL;
/**
+ * A thin layer on top of {@link ConfigurationWatchList}.
+ *
* @author Ceki Gülcü
*/
public class ConfigurationWatchListUtil {
@@ -85,9 +87,14 @@ public static void addToWatchList(Context context, URL url, boolean createCWL) {
}
}
- addInfo(context, "Adding [" + url + "] to configuration watch list.");
- cwl.addToWatchList(url);
-
+ String protocol = url.getProtocol();
+ if(cwl.isWatchableProtocol(protocol)) {
+ addInfo(context, "Will add [" + url + "] to configuration watch list.");
+ cwl.addToWatchList(url);
+ } else {
+ addInfo(context, "Will not add configuration file ["+url + "] to watch list, because '"+protocol+"' protocol is not watchable.");
+ addInfo(context, "Only the protocols 'file', 'http' and 'https' are watchable.");
+ }
}
private static ConfigurationWatchList registerNewConfigurationWatchListWithContext(Context context) {
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/ModelInterpretationContext.java b/logback-core/src/main/java/ch/qos/logback/core/model/processor/ModelInterpretationContext.java
index 4ddb6abeee..918159d81c 100644
--- a/logback-core/src/main/java/ch/qos/logback/core/model/processor/ModelInterpretationContext.java
+++ b/logback-core/src/main/java/ch/qos/logback/core/model/processor/ModelInterpretationContext.java
@@ -18,14 +18,12 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Properties;
import java.util.Stack;
import java.util.function.Supplier;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.Context;
import ch.qos.logback.core.joran.GenericXMLConfigurator;
-import ch.qos.logback.core.joran.JoranConfiguratorBase;
import ch.qos.logback.core.joran.JoranConstants;
import ch.qos.logback.core.joran.spi.DefaultNestedComponentRegistry;
import ch.qos.logback.core.joran.util.beans.BeanDescriptionCache;
@@ -34,7 +32,6 @@
import ch.qos.logback.core.spi.AppenderAttachable;
import ch.qos.logback.core.spi.ContextAwareBase;
import ch.qos.logback.core.spi.ContextAwarePropertyContainer;
-import ch.qos.logback.core.spi.PropertyContainer;
public class ModelInterpretationContext extends ContextAwareBase implements ContextAwarePropertyContainer {
@@ -165,13 +162,13 @@ public BeanDescriptionCache getBeanDescriptionCache() {
public String subst(String ref) {
String substituted = variableSubstitutionsHelper.subst(ref);
- if(ref != null && !ref.equals(substituted)) {
- addInfo("value \""+substituted+"\" substituted for \""+ref+"\"");
+ if(ref != null && !ref.equals(substituted) ) {
+ String sanitized = variableSubstitutionsHelper.sanitizeIfConfidential(ref, substituted);
+ addInfo("value \""+sanitized+"\" substituted for \""+ref+"\"");
}
return substituted;
}
-
public DefaultNestedComponentRegistry getDefaultNestedComponentRegistry() {
return defaultNestedComponentRegistry;
}
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..89bbb3d1be
--- /dev/null
+++ b/logback-core/src/main/java/ch/qos/logback/core/model/processor/conditional/ByPropertiesConditionModelHandler.java
@@ -0,0 +1,99 @@
+/*
+ * 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.PropertyCondition;
+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;
+ PropertyCondition 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 = (PropertyCondition) OptionHelper.instantiateByClassName(className,
+ PropertyCondition.class, context);
+ propertyEvaluator.setContext(context);
+ propertyEvaluator.setLocalPropertyContainer(mic);
+ mic.pushObject(propertyEvaluator);
+ } 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);
+ }
+ }
+
+ 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();
+ }
+
+ 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);
+
+ }
+}
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..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
@@ -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,12 @@ 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";
+
+ 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;
@@ -54,25 +60,46 @@ 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();
+ emitDeprecationWarningIfNecessary(conditionStr);
+
+
+ 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
+ if(hasNew(conditionStr)) {
+ addError(NEW_OPERATOR_DISALLOWED_MSG);
+ addError(NEW_OPERATOR_DISALLOWED_SEE);
+ return;
}
try {
@@ -96,8 +123,19 @@ public void handle(ModelInterpretationContext mic, Model model) throws ModelHand
}
}
}
-
-
+
+ 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 {
diff --git a/logback-core/src/main/java/ch/qos/logback/core/model/util/VariableSubstitutionsHelper.java b/logback-core/src/main/java/ch/qos/logback/core/model/util/VariableSubstitutionsHelper.java
index b0fd93a79c..f197af0af6 100644
--- a/logback-core/src/main/java/ch/qos/logback/core/model/util/VariableSubstitutionsHelper.java
+++ b/logback-core/src/main/java/ch/qos/logback/core/model/util/VariableSubstitutionsHelper.java
@@ -26,22 +26,55 @@
/**
* Helper methods to deal with properties.
*
+ *
This class acts as a small container for substitution properties and
+ * delegates actual variable substitution to {@link OptionHelper#substVars}.
+ * It also offers a convenience method to mask confidential property values
+ * (for example passwords) by returning a blurred placeholder.
+ *
* @since 1.5.1
*/
public class VariableSubstitutionsHelper extends ContextAwareBase implements ContextAwarePropertyContainer {
+ static final String PASSWORD = "password";
+ static final String SECRET = "secret";
+ static final String CONFIDENTIAL = "confidential";
+
+ static final String BLURRED_STR = "******";
+
protected Map propertiesMap;
+ /**
+ * Create a helper backed by an empty property map.
+ *
+ * @param context the logback context to associate with this helper; may be null
+ */
public VariableSubstitutionsHelper(Context context) {
this.setContext(context);
this.propertiesMap = new HashMap<>();
}
+ /**
+ * Create a helper pre-populated with the contents of {@code otherMap}.
+ * The provided map is copied and further modifications do not affect the
+ * original map.
+ *
+ * @param context the logback context to associate with this helper; may be null
+ * @param otherMap initial properties to copy; if null an empty map is created
+ */
public VariableSubstitutionsHelper(Context context, Map otherMap) {
this.setContext(context);
this.propertiesMap = new HashMap<>(otherMap);
}
+ /**
+ * Perform variable substitution on the provided reference string.
+ *
+ *
Returns {@code null} if {@code ref} is {@code null}. On parse errors
+ * the original input string is returned and an error is logged.
+ *
+ * @param ref the string possibly containing variables to substitute
+ * @return the string with substitutions applied, or {@code null} if {@code ref} was {@code null}
+ */
@Override
public String subst(String ref) {
if (ref == null) {
@@ -54,12 +87,42 @@ public String subst(String ref) {
addError("Problem while parsing [" + ref + "]", e);
return ref;
}
+ }
+ /**
+ * Return a blurred placeholder for confidential properties.
+ *
+ *
If the property name {@code ref} contains any of the case-insensitive
+ * substrings {@code "password"}, {@code "secret"} or {@code "confidential"}
+ * this method returns a fixed blurred string ("******"). Otherwise, the
+ * supplied {@code substituted} value is returned unchanged.
+ *
+ * @param ref the property name to inspect; must not be {@code null}
+ * @param substituted the substituted value to return when the property is not confidential
+ * @return a blurred placeholder when the property appears confidential, otherwise {@code substituted}
+ * @throws IllegalArgumentException when {@code ref} is {@code null}
+ */
+ public String sanitizeIfConfidential(String ref, String substituted) {
+ if(ref == null) {
+ throw new IllegalArgumentException("ref cannot be null");
+ }
+
+ String lowerCaseRef = ref.toLowerCase();
+
+ if(lowerCaseRef.contains(PASSWORD) || lowerCaseRef.contains(SECRET) || lowerCaseRef.contains(CONFIDENTIAL)) {
+ return BLURRED_STR;
+ } else
+ return substituted;
}
/**
- * Add a property to the properties of this execution context. If the property
- * exists already, it is overwritten.
+ * Add or overwrite a substitution property.
+ *
+ *
Null keys or values are ignored. Values are trimmed before storing
+ * to avoid surprises caused by leading or trailing whitespace.
+ *
+ * @param key the property name; ignored if {@code null}
+ * @param value the property value; ignored if {@code null}
*/
@Override
public void addSubstitutionProperty(String key, String value) {
@@ -71,13 +134,27 @@ public void addSubstitutionProperty(String key, String value) {
propertiesMap.put(key, value);
}
+ /**
+ * Retrieve a property value by name.
+ *
+ * @param key the property name
+ * @return the property value or {@code null} if not present
+ */
@Override
public String getProperty(String key) {
return propertiesMap.get(key);
}
+ /**
+ * Return a shallow copy of the internal property map.
+ *
+ *
The returned map is a copy and modifications to it do not affect the
+ * internal state of this helper.
+ *
+ * @return a copy of the property map
+ */
@Override
public Map getCopyOfPropertyMap() {
- return new HashMap(propertiesMap);
+ return new HashMap<>(propertiesMap);
}
}
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;
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/CompressionStrategyBase.java b/logback-core/src/main/java/ch/qos/logback/core/rolling/helper/CompressionStrategyBase.java
index f5ef07b3f6..01774e0786 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
@@ -14,7 +14,6 @@
package ch.qos.logback.core.rolling.helper;
-import ch.qos.logback.core.ContextBase;
import ch.qos.logback.core.spi.ContextAwareBase;
import ch.qos.logback.core.util.FileUtil;
@@ -22,7 +21,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/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
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..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
@@ -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,14 +49,13 @@ public void compress(String originalFileName, String compressedFileName, String
addInfo("GZ compressing [" + file2gz + "] as [" + gzedFile + "]");
createMissingTargetDirsIfNecessary(gzedFile);
-
- try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(originalFileName));
- GZIPOutputStream gzos = new GZIPOutputStream(new FileOutputStream(compressedFileName))) {
+ try (FileInputStream fis = new FileInputStream(originalFileName);
+ GZIPOutputStream gzos = new GZIPOutputStream(new FileOutputStream(compressedFileName), BUFFER_SIZE)) {
byte[] inbuf = new byte[BUFFER_SIZE];
int n;
- while ((n = bis.read(inbuf)) != -1) {
+ while ((n = fis.read(inbuf)) != -1) {
gzos.write(inbuf, 0, n);
}
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/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..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
@@ -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;
@@ -55,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));
- XZOutputStream xzos = new XZOutputStream(new FileOutputStream(nameOfxzedFile), new LZMA2Options())) {
+ 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);
}
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));
}
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() + "]");
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
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).
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.
+ *
+ */
+ 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..1689b5437d
--- /dev/null
+++ b/logback-core/src/main/java/ch/qos/logback/core/util/ReentryGuardFactory.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.util;
+
+/**
+ * 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) {
+ switch (guardType) {
+ case THREAD_LOCAL:
+ return new ReentryGuard.ReentryGuardImpl();
+ case NOP:
+ return new ReentryGuard.NOPRentryGuard();
+ default:
+ throw new IllegalArgumentException("Unknown GuardType: " + guardType);
+ }
+ }
+}
diff --git a/logback-core/src/main/java/ch/qos/logback/core/util/SimpleTimeBasedGuard.java b/logback-core/src/main/java/ch/qos/logback/core/util/SimpleTimeBasedGuard.java
new file mode 100644
index 0000000000..2bb8f684e5
--- /dev/null
+++ b/logback-core/src/main/java/ch/qos/logback/core/util/SimpleTimeBasedGuard.java
@@ -0,0 +1,180 @@
+/*
+ * 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.concurrent.atomic.AtomicLong;
+
+/**
+ * A simple time-based guard that limits the number of allowed operations within a sliding time window.
+ * This class is useful for rate limiting or preventing excessive actions over time periods.
+ * It supports time injection for testing purposes.
+ *
+ * @author Ceki Gülcü
+ * @since 1.5.22
+ */
+public class SimpleTimeBasedGuard {
+
+ private final long windowDurationMs;
+ private final int maxAllows;
+
+ /**
+ * Default window duration in milliseconds: 30 minutes.
+ */
+ public static final long DEFAULT_WINDOW_MS = 30*60_000L; // 30 minutes
+
+ /**
+ * Default maximum number of allows per window: 2.
+ */
+ public static final int DEFAULT_MAX_ALLOWS = 2;
+
+ // Injectable time
+ private final AtomicLong artificialTime = new AtomicLong(-1L);
+
+ // Current window state
+ private volatile long windowStartMs = 0;
+ private volatile int allowsUsed = 0;
+
+ /**
+ * Creates a guard with custom limits.
+ *
+ * @param windowDurationMs how many millis per window (e.g. 30_000 for 30 minutes)
+ * @param maxAllows how many allows per window (e.g. 2)
+ */
+ public SimpleTimeBasedGuard(long windowDurationMs, int maxAllows) {
+ if (windowDurationMs <= 0) throw new IllegalArgumentException("windowDurationMs must be > 0");
+ if (maxAllows < 1) throw new IllegalArgumentException("maxAllows must be >= 1");
+
+ this.windowDurationMs = windowDurationMs;
+ this.maxAllows = maxAllows;
+ }
+
+ /**
+ * Convenience: uses defaults — 2 allows every 30 minutes
+ */
+ public SimpleTimeBasedGuard() {
+ this(DEFAULT_WINDOW_MS, DEFAULT_MAX_ALLOWS);
+ }
+
+ /**
+ * Checks if an operation is allowed based on the current time window.
+ * If allowed, increments the usage count for the current window.
+ * If the window has expired, resets the window and allows the operation.
+ *
+ * @return true if the operation is allowed, false otherwise
+ */
+ public synchronized boolean allow() {
+ long now = currentTimeMillis();
+
+ // First call ever
+ if (windowStartMs == 0) {
+ windowStartMs = now;
+ allowsUsed = 1;
+ return true;
+ }
+
+ // Still in current window?
+ if (now < windowStartMs + windowDurationMs) {
+ if (allowsUsed < maxAllows) {
+ allowsUsed++;
+ return true;
+ }
+ return false;
+ }
+
+ // New window → reset
+ windowStartMs = now;
+ allowsUsed = 1;
+ return true;
+ }
+
+ // --- Time injection for testing ---
+
+ /**
+ * Sets the artificial current time for testing purposes.
+ * When set, {@link #currentTimeMillis()} will return this value instead of {@link System#currentTimeMillis()}.
+ *
+ * @param timestamp the artificial timestamp in milliseconds
+ */
+ public void setCurrentTimeMillis(long timestamp) {
+ this.artificialTime.set(timestamp);
+ }
+
+ /**
+ * Clears the artificial time, reverting to using {@link System#currentTimeMillis()}.
+ */
+ public void clearCurrentTime() {
+ this.artificialTime.set(-1L);
+ }
+
+ private long currentTimeMillis() {
+ long t = artificialTime.get();
+ return t >= 0 ? t : System.currentTimeMillis();
+ }
+
+ void incCurrentTimeMillis(long increment) {
+ artificialTime.getAndAdd(increment);
+ }
+
+ // --- Helpful getters ---
+
+ /**
+ * Returns the number of allows used in the current window.
+ *
+ * @return the number of allows used
+ */
+ public int getAllowsUsed() {
+ return allowsUsed;
+ }
+
+ /**
+ * Returns the number of allows remaining in the current window.
+ *
+ * @return the number of allows remaining
+ */
+ public int getAllowsRemaining() {
+ return Math.max(0, maxAllows - allowsUsed);
+ }
+
+ /**
+ * Returns the window duration in milliseconds.
+ *
+ * @return the window duration in milliseconds
+ */
+ public long getWindowDuration() {
+ return windowDurationMs;
+ }
+
+ /**
+ * Returns the maximum number of allows per window.
+ *
+ * @return the maximum number of allows
+ */
+ public int getMaxAllows() {
+ return maxAllows;
+ }
+
+ /**
+ * Returns the number of milliseconds until the next window starts.
+ * If no window has started yet, returns the full window duration.
+ *
+ * @return milliseconds until next window
+ */
+ public long getMillisUntilNextWindow() {
+ if (windowStartMs == 0) return windowDurationMs;
+ long nextWindowStart = windowStartMs + windowDurationMs;
+ long now = currentTimeMillis();
+ return Math.max(0, nextWindowStart - now);
+ }
+}
diff --git a/logback-core/src/test/input/joran/implicitAction/nestedComplexWithPassword.xml b/logback-core/src/test/input/joran/implicitAction/nestedComplexWithPassword.xml
new file mode 100644
index 0000000000..a262f6d7b8
--- /dev/null
+++ b/logback-core/src/test/input/joran/implicitAction/nestedComplexWithPassword.xml
@@ -0,0 +1,21 @@
+
+
+
+
+ ${A_PASSWORD}
+ hello
+ world
+
+
\ No newline at end of file
diff --git a/logback-core/src/test/java/ch/qos/logback/core/joran/implicitAction/ImplicitActionTest.java b/logback-core/src/test/java/ch/qos/logback/core/joran/implicitAction/ImplicitActionTest.java
index 45e1f7b722..16e8f61e13 100755
--- a/logback-core/src/test/java/ch/qos/logback/core/joran/implicitAction/ImplicitActionTest.java
+++ b/logback-core/src/test/java/ch/qos/logback/core/joran/implicitAction/ImplicitActionTest.java
@@ -91,6 +91,24 @@ public void nestedComplex() throws Exception {
}
}
+ @Test
+ public void nestedComplexWithPassword() throws Exception {
+ try {
+ fruitContext.addSubstitutionProperty("A_PASSWORD", "blue");
+
+ simpleConfigurator.doConfigure(IMPLCIT_DIR + "nestedComplexWithPassword.xml");
+ StatusPrinter.print(fruitContext);
+ StatusChecker checker = new StatusChecker(fruitContext);
+
+ checker.assertContainsMatch("value \"\\*{6}\" substituted for \"\\$\\{A_PASSWORD\\}\"");
+ verifyFruit();
+
+ } catch (Exception je) {
+ StatusPrinter.print(fruitContext);
+ throw je;
+ }
+ }
+
@Test
public void nestedComplexWithoutClassAtrribute() throws Exception {
try {
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..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,7 +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.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;
@@ -17,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