diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java index a3fdea3e978f..3c9d71f73a3f 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java @@ -324,10 +324,15 @@ private void load(LoggingInitializationContext initializationContext, String loc List overrides = getOverrides(initializationContext); Environment environment = initializationContext.getEnvironment(); Assert.state(environment != null, "'environment' must not be null"); - applySystemProperties(environment, logFile); + applyLog4j2SystemProperties(environment, logFile); loadConfiguration(location, logFile, overrides); } + + private void applyLog4j2SystemProperties(Environment environment, @Nullable LogFile logFile) { + new Log4j2LoggingSystemProperties(environment, getDefaultValueResolver(environment), null).apply(logFile); + } + private List getOverrides(LoggingInitializationContext initializationContext) { Environment environment = initializationContext.getEnvironment(); Assert.state(environment != null, "'environment' must not be null"); diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2LoggingSystemProperties.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2LoggingSystemProperties.java new file mode 100644 index 000000000000..e2a1251687d7 --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2LoggingSystemProperties.java @@ -0,0 +1,125 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging.log4j2; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.logging.LogFile; +import org.springframework.boot.logging.LoggingSystemProperties; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertyResolver; +import org.springframework.util.unit.DataSize; + +/** + * {@link LoggingSystemProperties} for Log4j2. + * + * @author hojooo + * @see Log4j2RollingPolicySystemProperty + */ +public class Log4j2LoggingSystemProperties extends LoggingSystemProperties { + + public Log4j2LoggingSystemProperties(Environment environment) { + super(environment); + } + + /** + * Create a new {@link Log4j2LoggingSystemProperties} instance. + * @param environment the source environment + * @param setter setter used to apply the property + */ + public Log4j2LoggingSystemProperties(Environment environment, + @Nullable BiConsumer setter) { + super(environment, setter); + } + + /** + * Create a new {@link Log4j2LoggingSystemProperties} instance. + * @param environment the source environment + * @param defaultValueResolver function used to resolve default values or {@code null} + * @param setter setter used to apply the property or {@code null} for system + * properties + */ + public Log4j2LoggingSystemProperties(Environment environment, + Function<@Nullable String, @Nullable String> defaultValueResolver, + @Nullable BiConsumer setter) { + super(environment, defaultValueResolver, setter); + } + + @Override + protected void apply(@Nullable LogFile logFile, PropertyResolver resolver) { + super.apply(logFile, resolver); + applyRollingPolicyProperties(resolver); + } + + private void applyRollingPolicyProperties(PropertyResolver resolver) { + applyRollingPolicy(Log4j2RollingPolicySystemProperty.STRATEGY, resolver); + applyRollingPolicy(Log4j2RollingPolicySystemProperty.TIME_INTERVAL, resolver, Integer.class); + applyRollingPolicy(Log4j2RollingPolicySystemProperty.TIME_MODULATE, resolver, Boolean.class); + applyRollingPolicy(Log4j2RollingPolicySystemProperty.CRON_SCHEDULE, resolver); + applyRollingPolicy(Log4j2RollingPolicySystemProperty.FILE_NAME_PATTERN, resolver); + applyRollingPolicy(Log4j2RollingPolicySystemProperty.CLEAN_HISTORY_ON_START, resolver); + applyRollingPolicy(Log4j2RollingPolicySystemProperty.MAX_FILE_SIZE, resolver, DataSize.class); + applyRollingPolicy(Log4j2RollingPolicySystemProperty.TOTAL_SIZE_CAP, resolver, DataSize.class); + applyRollingPolicy(Log4j2RollingPolicySystemProperty.MAX_HISTORY, resolver); + } + + private void applyRollingPolicy(Log4j2RollingPolicySystemProperty property, PropertyResolver resolver) { + applyRollingPolicy(property, resolver, String.class); + } + + private void applyRollingPolicy(Log4j2RollingPolicySystemProperty property, PropertyResolver resolver, + Class type) { + T value = getProperty(resolver, property.getApplicationPropertyName(), type); + if (value == null && property.getDeprecatedApplicationPropertyName() != null) { + value = getProperty(resolver, property.getDeprecatedApplicationPropertyName(), type); + } + if (value != null) { + String stringValue = String.valueOf((value instanceof DataSize dataSize) ? dataSize.toBytes() : value); + setSystemProperty(property.getEnvironmentVariableName(), stringValue); + } + } + + @SuppressWarnings("unchecked") + private @Nullable T getProperty(PropertyResolver resolver, String key, Class type) { + try { + return resolver.getProperty(key, type); + } + catch (ConversionFailedException | ConverterNotFoundException ex) { + if (type != DataSize.class) { + throw ex; + } + // Fallback for Log4j2 compatibility - try parsing as string if DataSize conversion fails + String value = resolver.getProperty(key); + if (value != null) { + try { + return (T) DataSize.parse(value); + } + catch (Exception parseEx) { + ex.addSuppressed(parseEx); + throw ex; + } + } + return null; + } + } + +} \ No newline at end of file diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2RollingPolicySystemProperty.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2RollingPolicySystemProperty.java new file mode 100644 index 000000000000..552b99d5621a --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2RollingPolicySystemProperty.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging.log4j2; + +import org.jspecify.annotations.Nullable; + +/** + * Log4j2 rolling policy system properties that can later be used by log configuration + * files. + * + * @author hojooo + * @see Log4j2LoggingSystemProperties + */ +public enum Log4j2RollingPolicySystemProperty { + + /** + * Logging system property for the rolled-over log file name pattern. + */ + FILE_NAME_PATTERN("file-name-pattern", "logging.pattern.rolling-file-name"), + + /** + * Logging system property for the clean history on start flag. + */ + CLEAN_HISTORY_ON_START("clean-history-on-start", "logging.file.clean-history-on-start"), + + /** + * Logging system property for the file log max size. + */ + MAX_FILE_SIZE("max-file-size", "logging.file.max-size"), + + /** + * Logging system property for the file total size cap. + */ + TOTAL_SIZE_CAP("total-size-cap", "logging.file.total-size-cap"), + + /** + * Logging system property for the file log max history. + */ + MAX_HISTORY("max-history", "logging.file.max-history"), + + /** + * Logging system property for the rolling policy strategy. + */ + STRATEGY("strategy", null), + + /** + * Logging system property for the rolling policy time interval. + */ + TIME_INTERVAL("time-based.interval", null), + + /** + * Logging system property for the rolling policy time modulate flag. + */ + TIME_MODULATE("time-based.modulate", null), + + /** + * Logging system property for the cron based schedule. + */ + CRON_SCHEDULE("cron.schedule", null); + + private final String environmentVariableName; + + private final String applicationPropertyName; + + private final @Nullable String deprecatedApplicationPropertyName; + + Log4j2RollingPolicySystemProperty(String applicationPropertyName, @Nullable String deprecatedApplicationPropertyName) { + this.environmentVariableName = "LOG4J2_ROLLINGPOLICY_" + name(); + this.applicationPropertyName = "logging.log4j2.rollingpolicy." + applicationPropertyName; + this.deprecatedApplicationPropertyName = deprecatedApplicationPropertyName; + } + + /** + * Return the name of environment variable that can be used to access this property. + * @return the environment variable name + */ + public String getEnvironmentVariableName() { + return this.environmentVariableName; + } + + String getApplicationPropertyName() { + return this.applicationPropertyName; + } + + @Nullable String getDeprecatedApplicationPropertyName() { + return this.deprecatedApplicationPropertyName; + } + +} \ No newline at end of file diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringBootTriggeringPolicy.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringBootTriggeringPolicy.java new file mode 100644 index 000000000000..006da416d7c1 --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringBootTriggeringPolicy.java @@ -0,0 +1,246 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging.log4j2; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import org.apache.logging.log4j.core.LifeCycle; +import org.apache.logging.log4j.core.LifeCycle2; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.rolling.AbstractTriggeringPolicy; +import org.apache.logging.log4j.core.appender.rolling.CompositeTriggeringPolicy; +import org.apache.logging.log4j.core.appender.rolling.CronTriggeringPolicy; +import org.apache.logging.log4j.core.appender.rolling.RollingFileManager; +import org.apache.logging.log4j.core.appender.rolling.SizeBasedTriggeringPolicy; +import org.apache.logging.log4j.core.appender.rolling.TimeBasedTriggeringPolicy; +import org.apache.logging.log4j.core.appender.rolling.TriggeringPolicy; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; +import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.util.Builder; +import org.jspecify.annotations.Nullable; + +/** + * {@link TriggeringPolicy} that selects one of several standard Log4j2 + * {@link TriggeringPolicy TriggeringPolicies} based on configuration attributes. + * The supported strategies are {@code size}, {@code time}, {@code size-and-time}, and {@code cron}. + * + * @author hojooo + */ +@Plugin(name = "SpringBootTriggeringPolicy", category = Node.CATEGORY, elementType = "TriggeringPolicy", + deferChildren = true, printObject = true) +public final class SpringBootTriggeringPolicy extends AbstractTriggeringPolicy { + + private final TriggeringPolicy delegate; + + private SpringBootTriggeringPolicy(TriggeringPolicy delegate) { + this.delegate = delegate; + } + + TriggeringPolicy getDelegate() { + return this.delegate; + } + + @Override + public void initialize(RollingFileManager manager) { + this.delegate.initialize(manager); + } + + @Override + public boolean isTriggeringEvent(LogEvent event) { + return this.delegate.isTriggeringEvent(event); + } + + @Override + public void start() { + super.start(); + if (this.delegate instanceof LifeCycle lifecycle) { + lifecycle.start(); + } + } + + @Override + public boolean stop(long timeout, TimeUnit timeUnit) { + setStopping(); + boolean result = true; + if (this.delegate instanceof LifeCycle2 lifecycle2) { + result = lifecycle2.stop(timeout, timeUnit); + } + else if (this.delegate instanceof LifeCycle lifecycle) { + lifecycle.stop(); + } + setStopped(); + return result; + } + + @Override + public boolean isStarted() { + if (this.delegate instanceof LifeCycle lifecycle) { + return lifecycle.isStarted(); + } + return super.isStarted(); + } + + @Override + public String toString() { + return "SpringBootTriggeringPolicy{" + this.delegate + "}"; + } + + @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) { + return newBuilder().setStrategy(strategy) + .setMaxFileSize(maxFileSize) + .setTimeInterval(timeInterval) + .setTimeModulate(timeModulate) + .setCronExpression(cronExpression) + .setConfiguration(configuration) + .build(); + } + + /** + * Builder for {@link SpringBootTriggeringPolicy}. + */ + public static class SpringBootTriggeringPolicyBuilder + implements Builder { + + private static final String DEFAULT_STRATEGY = "size"; + + private static final String DEFAULT_MAX_FILE_SIZE = "10MB"; + + private static final int DEFAULT_TIME_INTERVAL = 1; + + private static final String DEFAULT_CRON_EXPRESSION = "0 0 0 * * ?"; + + @PluginAttribute("strategy") + private @Nullable String strategy; + + @PluginAttribute("maxFileSize") + private @Nullable String maxFileSize; + + @PluginAttribute("timeInterval") + private @Nullable Integer timeInterval; + + @PluginAttribute("timeModulate") + private @Nullable Boolean timeModulate; + + @PluginAttribute("cronExpression") + private @Nullable String cronExpression; + + @PluginConfiguration + private @Nullable Configuration configuration; + + @Override + public SpringBootTriggeringPolicy build() { + // Read strategy from system properties first, then from attributes + String resolvedStrategy = System.getProperty("LOG4J2_ROLLINGPOLICY_STRATEGY"); + if (resolvedStrategy == null) { + resolvedStrategy = (this.strategy != null) ? this.strategy : DEFAULT_STRATEGY; + } + TriggeringPolicy policy = switch (resolvedStrategy) { + case "time" -> createTimePolicy(); + case "size-and-time" -> CompositeTriggeringPolicy.createPolicy(createSizePolicy(), createTimePolicy()); + case "cron" -> createCronPolicy(); + case "size" -> createSizePolicy(); + default -> throw new IllegalArgumentException( + "Unsupported rolling policy strategy '%s'".formatted(resolvedStrategy)); + }; + return new SpringBootTriggeringPolicy(policy); + } + + private TriggeringPolicy createSizePolicy() { + // Read from system properties first, then from attributes + String size = System.getProperty("LOG4J2_ROLLINGPOLICY_MAX_FILE_SIZE"); + if (size == null) { + size = (this.maxFileSize != null) ? this.maxFileSize : DEFAULT_MAX_FILE_SIZE; + } + return SizeBasedTriggeringPolicy.createPolicy(size); + } + + private TriggeringPolicy createTimePolicy() { + // Read from system properties first, then from attributes + String intervalStr = System.getProperty("LOG4J2_ROLLINGPOLICY_TIME_INTERVAL"); + int interval = (intervalStr != null) ? Integer.parseInt(intervalStr) + : (this.timeInterval != null) ? this.timeInterval : DEFAULT_TIME_INTERVAL; + + String modulateStr = System.getProperty("LOG4J2_ROLLINGPOLICY_TIME_MODULATE"); + boolean modulate = (modulateStr != null) ? Boolean.parseBoolean(modulateStr) + : (this.timeModulate != null) ? this.timeModulate : false; + + return TimeBasedTriggeringPolicy.newBuilder().withInterval(interval).withModulate(modulate).build(); + } + + private TriggeringPolicy createCronPolicy() { + Configuration configuration = Objects.requireNonNull(this.configuration, "configuration must not be null"); + + // Read from system properties first, then from attributes + String schedule = System.getProperty("LOG4J2_ROLLINGPOLICY_CRON_SCHEDULE"); + if (schedule == null) { + schedule = (this.cronExpression != null) ? this.cronExpression : DEFAULT_CRON_EXPRESSION; + } + + return CronTriggeringPolicy.createPolicy(configuration, null, schedule); + } + + SpringBootTriggeringPolicyBuilder setStrategy(@Nullable String strategy) { + this.strategy = strategy; + return this; + } + + SpringBootTriggeringPolicyBuilder setMaxFileSize(@Nullable String maxFileSize) { + this.maxFileSize = maxFileSize; + return this; + } + + SpringBootTriggeringPolicyBuilder setTimeInterval(@Nullable Integer timeInterval) { + this.timeInterval = timeInterval; + return this; + } + + SpringBootTriggeringPolicyBuilder setTimeModulate(@Nullable Boolean timeModulate) { + this.timeModulate = timeModulate; + return this; + } + + SpringBootTriggeringPolicyBuilder setCronExpression(@Nullable String cronExpression) { + this.cronExpression = cronExpression; + return this; + } + + SpringBootTriggeringPolicyBuilder setConfiguration(Configuration configuration) { + this.configuration = configuration; + return this; + } + + } + +} diff --git a/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 26d63493f927..4ddc7b09d1bd 100644 --- a/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -134,6 +134,68 @@ "type": "java.util.List", "description": "Overriding configuration files used to create a composite configuration. Can be prefixed with 'optional:' to only load the override if it exists." }, + { + "name": "logging.log4j2.rollingpolicy.clean-history-on-start", + "type": "java.lang.Boolean", + "description": "Whether to clean the archive log files on startup.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": false + }, + { + "name": "logging.log4j2.rollingpolicy.strategy", + "type": "java.lang.String", + "description": "Rolling policy strategy. Supported values are 'size', 'time', 'size-and-time', and 'cron'.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": "size" + }, + { + "name": "logging.log4j2.rollingpolicy.time-based.interval", + "type": "java.lang.Integer", + "description": "Time based triggering interval when the strategy is 'time' or 'size-and-time'.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": 1 + }, + { + "name": "logging.log4j2.rollingpolicy.time-based.modulate", + "type": "java.lang.Boolean", + "description": "Whether to align the next rollover time to occur at the top of the interval when the strategy is time based.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": false + }, + { + "name": "logging.log4j2.rollingpolicy.cron.schedule", + "type": "java.lang.String", + "description": "Cron expression used when the strategy is 'cron'.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener" + }, + { + "name": "logging.log4j2.rollingpolicy.file-name-pattern", + "type": "java.lang.String", + "description": "Pattern for rolled-over log file names.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": "${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz" + }, + { + "name": "logging.log4j2.rollingpolicy.max-file-size", + "type": "org.springframework.util.unit.DataSize", + "description": "Maximum log file size.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": "10MB" + }, + { + "name": "logging.log4j2.rollingpolicy.max-history", + "type": "java.lang.Integer", + "description": "Maximum number of archive log files to keep.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": 7 + }, + { + "name": "logging.log4j2.rollingpolicy.total-size-cap", + "type": "org.springframework.util.unit.DataSize", + "description": "Total size of log backups to be kept.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": "0B" + }, { "name": "logging.logback.rollingpolicy.clean-history-on-start", "type": "java.lang.Boolean", diff --git a/core/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml b/core/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml index c81eca81ddd0..a9cbcd0cbc77 100644 --- a/core/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml +++ b/core/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml @@ -21,7 +21,8 @@ - +