Skip to content

Conversation

@hojooo
Copy link
Contributor

@hojooo hojooo commented Sep 17, 2025

Hello! Log4j2 does not support rolling policies. So this PR adds basic rolling policy configuration property support for Log4j2. It introduces Log4j2 specific properties equivalent to the existing logging.logback.rollingpolicy.* properties, enabling consistent logging configuration through application.properties across different logging implementations. 
And advanced rolling strategy support to the Log4j2 rolling policy configuration. It introduces the SpringBootTriggeringPolicy plugin to enable various rolling strategies and strategy-specific detailed configuration through application.properties.

1. Standardized Log4j2 Rolling Policy Properties

We've added standard properties for Log4j2, similar to the existing logging.logback.rollingpolicy.* properties. Users can now easily control the rolling policy using the following attributes in application.properties:

# Basic rolling policy configuration (file size, history, etc.)
logging.log4j2.rollingpolicy.max-file-size=10MB
logging.log4j2.rollingpolicy.max-history=7

# Rolled file name pattern configuration
logging.log4j2.rollingpolicy.file-name-pattern=${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz

2. Advanced Rolling Strategy

A custom Log4j2 plugin, SpringBootTriggeringPolicy, has been introduced to support various advanced rolling strategies beyond simple size-based rolling.

  • size (default): Rolls files based on their size.
  • time: Rolls files based on a time interval.
  • size-and-time: Rolls when both size and time conditions are met.
  • cron: Rolls based on a cron expression schedule.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Sep 17, 2025
@philwebb philwebb added the for: team-meeting An issue we'd like to discuss as a team to make progress label Sep 17, 2025
@hojooo hojooo force-pushed the log4j2-rolling-policy branch from 66fcaf7 to b5d3b84 Compare September 18, 2025 03:03
@hojooo hojooo force-pushed the log4j2-rolling-policy branch from 936ec72 to 05f8bb4 Compare October 7, 2025 07:18
@snicoll
Copy link
Member

snicoll commented Dec 1, 2025

@hojooo can you please rebase this PR to main and squash everything in one commit? Once you're done, running the build to make sure it passes would be appreciated.

@ppkarwasz would you have a min to review this one please? I am wondering, in particular, about SpringBootTriggeringPolicy that has to do quite a bit that looks unrelated to what a plugin should do (lifecycle management, etc).

@snicoll snicoll added the status: waiting-for-feedback We need additional information before we can continue label Dec 1, 2025
@hojooo
Copy link
Contributor Author

hojooo commented Dec 2, 2025

@snicoll Got it, thanks. I’ll update the PR shortly.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Dec 2, 2025
Signed-off-by: hojooo <ghwn5833@gmail.com>

Log4j2: Introduce SpringBootTriggeringPolicy and wire plugin discovery
      - Add SpringBootTriggeringPolicy plugin supporting size, time, size-and-time, and cron strategies
      - Read rolling strategy parameters from LOG4J2_ROLLINGPOLICY_* system properties (fallback to attributes)
      - Register Boot plugin package in Log4J2LoggingSystem to ensure plugin discovery
      - Use SpringBootTriggeringPolicy under a top-level Policies wrapper in log4j2-file.xml

Signed-off-by: hojooo <ghwn5833@gmail.com>

Log4j2: Add property-driven rolling strategy support and metadata
      - Introduce rolling policy properties: strategy, time-based.interval, time-based.modulate, cron.schedule
      - Extend Log4j2RollingPolicySystemProperty with STRATEGY, TIME_INTERVAL, TIME_MODULATE, CRON_SCHEDULE
      - Propagate new properties in Log4j2LoggingSystemProperties (with deprecated fallback guarded for null)
      - Document new properties in configuration metadata
      - Update Log4j2LoggingSystemPropertiesTests to assert new system properties mappingAdd Log4j2 rolling policy configuration support

Signed-off-by: hojooo <ghwn5833@gmail.com>

Tests: Initialize with packaged file config and unwrap composite policy
      - Initialize with classpath:org/springframework/boot/logging/log4j2/log4j2-file.xml to validate file-based rolling
      - Unwrap CompositeTriggeringPolicy to locate nested SpringBootTriggeringPolicy and assert its delegate
      - Keep assertions for time, size-and-time, and cron strategies

Signed-off-by: hojooo <ghwn5833@gmail.com>

Refactor Builder import path

Signed-off-by: hojooo <ghwn5833@gmail.com>

add clean-history-on-start and total-size-cap property in log4j2-file.xml

Signed-off-by: hojooo <ghwn5833@gmail.com>

refactor indent

Signed-off-by: hojooo <ghwn5833@gmail.com>

Refactor Log4j2LoggingSystemPropertiesTests to verify System.setProperty path

Signed-off-by: hojooo <ghwn5833@gmail.com>

Add Rolling Policy

Signed-off-by: hojooo <ghwn5833@gmail.com>
@hojooo hojooo force-pushed the log4j2-rolling-policy branch from 2e0bb46 to ec926ad Compare December 2, 2025 04:01
Signed-off-by: hojooo <ghwn5833@gmail.com>
Signed-off-by: hojooo <ghwn5833@gmail.com>
Signed-off-by: hojooo <ghwn5833@gmail.com>
Signed-off-by: hojooo <ghwn5833@gmail.com>
Signed-off-by: hojooo <ghwn5833@gmail.com>
Signed-off-by: hojooo <ghwn5833@gmail.com>
@snicoll snicoll added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Dec 2, 2025
Signed-off-by: hojooo <ghwn5833@gmail.com>
Signed-off-by: hojooo <ghwn5833@gmail.com>
Signed-off-by: hojooo <ghwn5833@gmail.com>
Signed-off-by: hojooo <ghwn5833@gmail.com>
@snicoll
Copy link
Member

snicoll commented Dec 2, 2025

@hojooo can you please stop pushing one commit at a time like that? Every time you do, it sends a notification to everyone watching this repo, which adds quite a bit of noise. Please build locally and push once you're ready.

@hojooo
Copy link
Contributor Author

hojooo commented Dec 2, 2025

@snicoll Thanks for the heads-up, and sorry for the noise I caused. I didn’t realize each small commit was sending notifications to everyone. I’ll make sure to build and test locally and then push in larger batches once things are ready.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Dec 2, 2025
Copy link
Contributor

@ppkarwasz ppkarwasz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@snicoll,

From a Log4j point of view, this PR looks overall correct, although it could be simplified and there are some smaller issues.

Comment on lines 56 to 60
private final TriggeringPolicy delegate;

private SpringBootTriggeringPolicy(TriggeringPolicy delegate) {
this.delegate = delegate;
}
Copy link
Contributor

@ppkarwasz ppkarwasz Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to implement a wrapper for a TriggeringPolicy. You can return the appropriate triggering policy directly from your factory method:

public abstract class SpringBootTriggeringPolicy implements TriggeringPolicy {

    private SpringBootTriggeringPolicy() {
      // no instantiation
    }

	public static TriggeringPolicy createPolicy(@PluginAttribute("strategy") @Nullable String strategy,
			@PluginAttribute("maxFileSize") @Nullable String maxFileSize,
			@PluginAttribute("timeInterval") @Nullable Integer timeInterval,
			@PluginAttribute("timeModulate") @Nullable Boolean timeModulate,
			@PluginAttribute("cronExpression") @Nullable String cronExpression,
			@PluginConfiguration Configuration configuration) {
    ...
    }
}

Factory methods can return whatever they want (see MongoDbProvider), although there is a small limitation in the injector that assumes they return the same type as the plugin type, which is why SpringBootTriggeringPolicy needs to implement TriggeringPolicy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the feedback! I agree this is a much cleaner approach. The wrapper pattern was adding unnecessary complexity when the factory can simply return the standard Log4j2 policies directly

Comment on lines 111 to 122
@PluginBuilderFactory
public static SpringBootTriggeringPolicyBuilder newBuilder() {
return new SpringBootTriggeringPolicyBuilder();
}

@PluginFactory
public static SpringBootTriggeringPolicy createPolicy(@PluginAttribute("strategy") @Nullable String strategy,
@PluginAttribute("maxFileSize") @Nullable String maxFileSize,
@PluginAttribute("timeInterval") @Nullable Integer timeInterval,
@PluginAttribute("timeModulate") @Nullable Boolean timeModulate,
@PluginAttribute("cronExpression") @Nullable String cronExpression,
@PluginConfiguration Configuration configuration) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need for both a factory method and a builder. Since the builder is easier to maintain than the factory method, you should leave just the builder.

Comment on lines 45 to 46
cleanHistoryOnStart="${sys:LOG4J2_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}"
totalSizeCap="${sys:LOG4J2_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}"/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The attributes cleanHistoryOnStart and totalSizeCap do not exist, see the DefaultRolloverStrategy documentation and the automatically generated list of configuration attributes. You would need to implement them in a custom rollover strategy:

  • If you want to rollover at each JVM startup, you can use OnStartupTriggeringPolicy,
  • If you want a limit on the total size of archived files, you need to configure the appropriate Delete actions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the clarification. I've removed these non-existent attributes from DefaultRolloverStrategy.

I’ve removed those non-existent attributes from DefaultRolloverStrategy, so the configuration is now:

<DefaultRolloverStrategy max="${sys:LOG4J2_ROLLINGPOLICY_MAX_HISTORY:-7}"/>

To keep this change focused, I’m not wiring in OnStartupTriggeringPolicy or Delete actions yet.
If we need startup-based rollover or a total size cap in the future, we can follow up with a custom rollover strategy and Delete actions in a separate change.

Signed-off-by: hojooo <ghwn5833@gmail.com>
Signed-off-by: hojooo <ghwn5833@gmail.com>
@snicoll snicoll self-assigned this Dec 10, 2025
snicoll pushed a commit to snicoll/spring-boot that referenced this pull request Dec 10, 2025
snicoll added a commit to snicoll/spring-boot that referenced this pull request Dec 10, 2025
@snicoll snicoll added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged status: feedback-provided Feedback has been provided labels Dec 10, 2025
@snicoll snicoll added this to the 4.1.x milestone Dec 10, 2025
@snicoll
Copy link
Member

snicoll commented Dec 12, 2025

I am trying things a bit in a sample and with a size-based policy I am getting:

2025-12-12T14:23:34.834545Z scheduling-1 ERROR An exception occurred processing Appender File
java.lang.IndexOutOfBoundsException: No group 1
	at java.base/java.util.regex.Matcher.checkGroup(Matcher.java:1845)
	at java.base/java.util.regex.Matcher.group(Matcher.java:686)
	at org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy.getEligibleFiles(AbstractRolloverStrategy.java:144)
	at org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy.getEligibleFiles(AbstractRolloverStrategy.java:93)
	at org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy.getEligibleFiles(AbstractRolloverStrategy.java:85)
	at org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy.purgeAscending(DefaultRolloverStrategy.java:447)
	at org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy.purge(DefaultRolloverStrategy.java:431)
	at org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy.rollover(DefaultRolloverStrategy.java:571)
	at org.apache.logging.log4j.core.appender.rolling.RollingFileManager.rollover(RollingFileManager.java:619)
	at org.apache.logging.log4j.core.appender.rolling.RollingFileManager.rollover(RollingFileManager.java:507)
	at org.apache.logging.log4j.core.appender.rolling.RollingFileManager.checkRollover(RollingFileManager.java:418)
	at org.apache.logging.log4j.core.appender.RollingFileAppender.append(RollingFileAppender.java:336)
	at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:160)
	at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:133)
	at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:124)
	at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:88)
	at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:711)
	at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:669)
	at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:645)
	at org.apache.logging.log4j.core.config.LoggerConfig.logParent(LoggerConfig.java:702)
	at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:671)
	at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:645)
	at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:589)
	at org.apache.logging.log4j.core.config.DefaultReliabilityStrategy.log(DefaultReliabilityStrategy.java:73)
	at org.apache.logging.log4j.core.Logger.log(Logger.java:187)
	at org.apache.logging.log4j.spi.AbstractLogger.tryLogMessage(AbstractLogger.java:2970)
	at org.apache.logging.log4j.spi.AbstractLogger.logMessageTrackRecursion(AbstractLogger.java:2922)
	at org.apache.logging.log4j.spi.AbstractLogger.logMessageSafely(AbstractLogger.java:2904)
	at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:2653)
	at org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:2393)
	at org.apache.logging.slf4j.Log4jLogger.debug(Log4jLogger.java:113)
	at smoketest.log4j2.SampleLog4j2Application.lambda$logSomething$0(SampleLog4j2Application.java:37)
	at java.base/java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:104)
	at java.base/java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:619)
	at smoketest.log4j2.SampleLog4j2Application.logSomething(SampleLog4j2Application.java:37)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	at java.base/java.lang.reflect.Method.invoke(Method.java:565)
	at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:128)
	at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$1(ScheduledMethodRunnable.java:122)
	at io.micrometer.observation.Observation.observe(Observation.java:498)
	at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:122)
	at org.springframework.scheduling.config.Task$OutcomeTrackingRunnable.run(Task.java:88)
	at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:545)
	at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:369)
	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:310)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
	at java.base/java.lang.Thread.run(Thread.java:1474)

2025-12-12T15:23:34.838+01:00 DEBUG 47648 --- [   scheduling-1] s.l.SampleLog4j2Application              : Sample Debug Message 91
2025-12-12T14:23:34.838801Z scheduling-1 ERROR An exception occurred processing Appender File
java.lang.IndexOutOfBoundsException: No group 1
	at java.base/java.util.regex.Matcher.checkGroup(Matcher.java:1845)
	at java.base/java.util.regex.Matcher.group(Matcher.java:686)
	at org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy.getEligibleFiles(AbstractRolloverStrategy.java:144)
	at org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy.getEligibleFiles(AbstractRolloverStrategy.java:93)
	at org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy.getEligibleFiles(AbstractRolloverStrategy.java:85)
	at org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy.purgeAscending(DefaultRolloverStrategy.java:447)
	at org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy.purge(DefaultRolloverStrategy.java:431)
	at org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy.rollover(DefaultRolloverStrategy.java:571)
	at org.apache.logging.log4j.core.appender.rolling.RollingFileManager.rollover(RollingFileManager.java:619)
	at org.apache.logging.log4j.core.appender.rolling.RollingFileManager.rollover(RollingFileManager.java:507)
	at org.apache.logging.log4j.core.appender.rolling.RollingFileManager.checkRollover(RollingFileManager.java:418)
	at org.apache.logging.log4j.core.appender.RollingFileAppender.append(RollingFileAppender.java:336)
	at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:160)
	at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:133)
	at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:124)
	at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:88)
	at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:711)
	at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:669)
	at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:645)
	at org.apache.logging.log4j.core.config.LoggerConfig.logParent(LoggerConfig.java:702)
	at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:671)
	at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:645)
	at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:589)
	at org.apache.logging.log4j.core.config.DefaultReliabilityStrategy.log(DefaultReliabilityStrategy.java:73)
	at org.apache.logging.log4j.core.Logger.log(Logger.java:187)
	at org.apache.logging.log4j.spi.AbstractLogger.tryLogMessage(AbstractLogger.java:2970)
	at org.apache.logging.log4j.spi.AbstractLogger.logMessageTrackRecursion(AbstractLogger.java:2922)
	at org.apache.logging.log4j.spi.AbstractLogger.logMessageSafely(AbstractLogger.java:2904)
	at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:2653)
	at org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:2393)
	at org.apache.logging.slf4j.Log4jLogger.debug(Log4jLogger.java:113)
	at smoketest.log4j2.SampleLog4j2Application.lambda$logSomething$0(SampleLog4j2Application.java:37)
	at java.base/java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:104)
	at java.base/java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:619)
	at smoketest.log4j2.SampleLog4j2Application.logSomething(SampleLog4j2Application.java:37)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	at java.base/java.lang.reflect.Method.invoke(Method.java:565)
	at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:128)
	at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$1(ScheduledMethodRunnable.java:122)
	at io.micrometer.observation.Observation.observe(Observation.java:498)
	at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:122)
	at org.springframework.scheduling.config.Task$OutcomeTrackingRunnable.run(Task.java:88)
	at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:545)
	at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:369)
	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:310)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
	at java.base/java.lang.Thread.run(Thread.java:1474)

2025-12-12T15:23:34.839+01:00 DEBUG 47648 --- [   scheduling-1] s.l.SampleLog4j2Application              : Sample Debug Message 92
2025-12-12T14:23:34.839548Z scheduling-1 ERROR An exception occurred processing Appender File
java.lang.IndexOutOfBoundsException: No group 1
	at java.base/java.util.regex.Matcher.checkGroup(Matcher.java:1845)
	at java.base/java.util.regex.Matcher.group(Matcher.java:686)
	at org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy.getEligibleFiles(AbstractRolloverStrategy.java:144)
	at org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy.getEligibleFiles(AbstractRolloverStrategy.java:93)
	at org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy.getEligibleFiles(AbstractRolloverStrategy.java:85)
	at org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy.purgeAscending(DefaultRolloverStrategy.java:447)
	at org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy.purge(DefaultRolloverStrategy.java:431)
	at org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy.rollover(DefaultRolloverStrategy.java:571)
	at org.apache.logging.log4j.core.appender.rolling.RollingFileManager.rollover(RollingFileManager.java:619)
	at org.apache.logging.log4j.core.appender.rolling.RollingFileManager.rollover(RollingFileManager.java:507)
	at org.apache.logging.log4j.core.appender.rolling.RollingFileManager.checkRollover(RollingFileManager.java:418)
	at org.apache.logging.log4j.core.appender.RollingFileAppender.append(RollingFileAppender.java:336)
	at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:160)
	at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:133)
	at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:124)
	at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:88)
	at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:711)
	at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:669)
	at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:645)
	at org.apache.logging.log4j.core.config.LoggerConfig.logParent(LoggerConfig.java:702)
	at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:671)
	at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:645)
	at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:589)
	at org.apache.logging.log4j.core.config.DefaultReliabilityStrategy.log(DefaultReliabilityStrategy.java:73)
	at org.apache.logging.log4j.core.Logger.log(Logger.java:187)
	at org.apache.logging.log4j.spi.AbstractLogger.tryLogMessage(AbstractLogger.java:2970)
	at org.apache.logging.log4j.spi.AbstractLogger.logMessageTrackRecursion(AbstractLogger.java:2922)
	at org.apache.logging.log4j.spi.AbstractLogger.logMessageSafely(AbstractLogger.java:2904)
	at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:2653)
	at org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:2393)
	at org.apache.logging.slf4j.Log4jLogger.debug(Log4jLogger.java:113)
	at smoketest.log4j2.SampleLog4j2Application.lambda$logSomething$0(SampleLog4j2Application.java:37)
	at java.base/java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:104)
	at java.base/java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:619)
	at smoketest.log4j2.SampleLog4j2Application.logSomething(SampleLog4j2Application.java:37)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	at java.base/java.lang.reflect.Method.invoke(Method.java:565)
	at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:128)
	at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$1(ScheduledMethodRunnable.java:122)
	at io.micrometer.observation.Observation.observe(Observation.java:498)
	at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:122)
	at org.springframework.scheduling.config.Task$OutcomeTrackingRunnable.run(Task.java:88)
	at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:545)
	at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:369)
	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:310)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
	at java.base/java.lang.Thread.run(Thread.java:1474)

2025-12-12T15:23:34.839+01:00 DEBUG 47648 --- [   scheduling-1] s.l.SampleLog4j2Application              : Sample Debug Message 93
2025-12-12T14:23:34.840190Z scheduling-1 ERROR An exception occurred processing Appender File
java.lang.IndexOutOfBoundsException: No group 1
	at java.base/java.util.regex.Matcher.checkGroup(Matcher.java:1845)
	at java.base/java.util.regex.Matcher.group(Matcher.java:686)
	at org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy.getEligibleFiles(AbstractRolloverStrategy.java:144)
	at org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy.getEligibleFiles(AbstractRolloverStrategy.java:93)
	at org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy.getEligibleFiles(AbstractRolloverStrategy.java:85)
	at org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy.purgeAscending(DefaultRolloverStrategy.java:447)
	at org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy.purge(DefaultRolloverStrategy.java:431)
	at org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy.rollover(DefaultRolloverStrategy.java:571)
	at org.apache.logging.log4j.core.appender.rolling.RollingFileManager.rollover(RollingFileManager.java:619)
	at org.apache.logging.log4j.core.appender.rolling.RollingFileManager.rollover(RollingFileManager.java:507)
	at org.apache.logging.log4j.core.appender.rolling.RollingFileManager.checkRollover(RollingFileManager.java:418)
	at org.apache.logging.log4j.core.appender.RollingFileAppender.append(RollingFileAppender.java:336)
	at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:160)
	at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:133)
	at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:124)
	at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:88)
	at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:711)
	at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:669)
	at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:645)
	at org.apache.logging.log4j.core.config.LoggerConfig.logParent(LoggerConfig.java:702)
	at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:671)
	at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:645)
	at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:589)
	at org.apache.logging.log4j.core.config.DefaultReliabilityStrategy.log(DefaultReliabilityStrategy.java:73)
	at org.apache.logging.log4j.core.Logger.log(Logger.java:187)
	at org.apache.logging.log4j.spi.AbstractLogger.tryLogMessage(AbstractLogger.java:2970)
	at org.apache.logging.log4j.spi.AbstractLogger.logMessageTrackRecursion(AbstractLogger.java:2922)
	at org.apache.logging.log4j.spi.AbstractLogger.logMessageSafely(AbstractLogger.java:2904)
	at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:2653)
	at org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:2393)
	at org.apache.logging.slf4j.Log4jLogger.debug(Log4jLogger.java:113)
	at smoketest.log4j2.SampleLog4j2Application.lambda$logSomething$0(SampleLog4j2Application.java:37)
	at java.base/java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:104)
	at java.base/java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:619)
	at smoketest.log4j2.SampleLog4j2Application.logSomething(SampleLog4j2Application.java:37)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	at java.base/java.lang.reflect.Method.invoke(Method.java:565)
	at org.springframework.scheduling.support.ScheduledMethodRunnable.runInternal(ScheduledMethodRunnable.java:128)
	at org.springframework.scheduling.support.ScheduledMethodRunnable.lambda$run$1(ScheduledMethodRunnable.java:122)
	at io.micrometer.observation.Observation.observe(Observation.java:498)
	at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:122)
	at org.springframework.scheduling.config.Task$OutcomeTrackingRunnable.run(Task.java:88)
	at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:545)
	at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:369)
	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:310)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
	at java.base/java.lang.Thread.run(Thread.java:1474)

@ppkarwasz is that a known issue? Can you please review SpringBootRetryPolicy on more time as those setter in the builder looks unnecessary to me but I can't remove them.

@ppkarwasz
Copy link
Contributor

I am trying things a bit in a sample and with a size-based policy I am getting:

2025-12-12T14:23:34.834545Z scheduling-1 ERROR An exception occurred processing Appender File
java.lang.IndexOutOfBoundsException: No group 1
	at java.base/java.util.regex.Matcher.checkGroup(Matcher.java:1845)
	at java.base/java.util.regex.Matcher.group(Matcher.java:686)
	at org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy.getEligibleFiles(AbstractRolloverStrategy.java:144)
	at org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy.getEligibleFiles(AbstractRolloverStrategy.java:93)
	at org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy.getEligibleFiles(AbstractRolloverStrategy.java:85)

@ppkarwasz is that a known issue?

It wasn’t a known issue until now 😉.

It looks like this happens when the filePattern contains something that resembles an %i pattern specifier but isn’t actually one. I was able to reproduce the problem reliably using a %%i pattern.

Would you mind reporting this to our issue tracker?

Can you please review SpringBootRetryPolicy on more time as those setter in the builder looks unnecessary to me but I can't remove them.

Sure

@snicoll
Copy link
Member

snicoll commented Dec 12, 2025

Thanks very much, I've created apache/logging-log4j2#4001

It looks like this happens when the filePattern contains something that resembles an %i pattern specifier but isn’t actually one.

This is our config

<RollingFile name="File" fileName="${sys:LOG_FILE}" filePattern="${sys:LOG_PATH}/$${date:yyyy-MM}/app-%d{yyyy-MM-dd-HH}-%i.log.gz">

It's the default that's applied in Spring Boot so if you have any suggestion to improve it, it is most welcome.

</Policies>
<Policies>
<SpringBootTriggeringPolicy strategy="${sys:LOG4J2_ROLLINGPOLICY_STRATEGY:-size}"
maxFileSize="${sys:LOG4J2_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log4j2 already supports configurable lookups, so using a Java system property (sys:) here seems unnecessary.

The Log4j2 Spring lookup reads values directly from the Spring Environment, avoiding additional logic.

This could be simplified to:

${spring:logging.log4j2.rollingpolicy.max-file-size}

A default can still be provided inline:

${spring:logging.log4j2.rollingpolicy.max-file-size:-10MB}

or via a <Property> element in the configuration file:

<Properties>
  <Property name="logging.log4j2.rollingpolicy.max-file-size" value="10MB"/>
</Properties>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to read that value from the Environment. Our integration sets system properties using a configurable logic, so I'll keep that as it is

Comment on lines +150 to +153
SpringBootTriggeringPolicyBuilder setStrategy(@Nullable String strategy) {
this.strategy = strategy;
return this;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As @snicoll remarked, a package-private setter is not very useful.

Log4j Core injects configuration attributes into the element with the @PluginAttribute annotation: in this case the private fields.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. How do I test this? I am a bit confused where this is used as this code maps those values from the environment and, for that, we don't need the attributes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is worth testing the final effect that Environment variables have on the Log4j Core context.

It should be rather straightforward to retrieve the TriggeringPolicy from the LoggerContext returned by Log4J2LoggingSystem:

LoggerContext context = ...
Appender appender = context.getConfiguration().getAppender("File");
assertThat(appender).isInstanceOf(RollingFileAppender.class);
RollingFileAppender rollingFileAppender = (RollingFileAppender) appender;
TriggeringPolicy triggeringPolicy = rollingFileAppender.getTriggeringPolicy();

If everything is wired right:

  1. Environment properties will set up Java System properties,
  2. Java System properties will be expanded in the log4j2-file.xml configuration file,
  3. Log4j Core will use them to create the right TriggeringPolicy.

Copy link
Member

@snicoll snicoll Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not my question. How do I test something that would be set the PluginAttribute. Looking at how it's being used with our system properties-based arrangement, I don't see how/where the plugin attribute can be set. I've actually remove the @PluginAttribute-fields and it works fine but I am getting an error on startup:

ERROR SpringBootTriggeringPolicy contains invalid attributes "cronExpression", "timeModulate", "timeInterval", "maxFileSize", "strategy"

@snicoll
Copy link
Member

snicoll commented Dec 12, 2025

@hojooo please don't proceed with the feedback as I have already modified the PR quite a bit. I'll take over.

@ppkarwasz
Copy link
Contributor

This is our config

<RollingFile name="File" fileName="${sys:LOG_FILE}" filePattern="${sys:LOG_PATH}/$${date:yyyy-MM}/app-%d{yyyy-MM-dd-HH}-%i.log.gz">

I think this could be improved a bit: lookups $${date:...} use the current time, while pattern converters %d{...} should use the time of the previous rollover. By using both, a couple of files per month will end up in the wrong folder.

This configuration should be better:

<RollingFile name="File"
             fileName="${sys:LOG_FILE}"
             filePattern="${sys:LOG_PATH}/%d{yyyy-MM}/app-%d{yyyy-MM-dd-HH}-%i.log.gz">

@snicoll
Copy link
Member

snicoll commented Dec 15, 2025

As part of this PR, we'd like to harmonize the default format to ${sys:LOG_FILE}.%d{yyyy-MM-dd}.%i.gz. I'll take care of that as part of this issue.

snicoll pushed a commit to snicoll/spring-boot that referenced this pull request Dec 16, 2025
snicoll added a commit to snicoll/spring-boot that referenced this pull request Dec 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

for: team-meeting An issue we'd like to discuss as a team to make progress type: enhancement A general enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants