diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/AbstractLoggingSystem.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/AbstractLoggingSystem.java index 1bc91f4bd8d7..52d822475df3 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/AbstractLoggingSystem.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/AbstractLoggingSystem.java @@ -26,6 +26,7 @@ import org.jspecify.annotations.Nullable; +import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.io.ClassPathResource; import org.springframework.util.Assert; @@ -180,7 +181,10 @@ protected final String getPackagedConfigFile(String fileName) { } protected final void applySystemProperties(Environment environment, @Nullable LogFile logFile) { - new LoggingSystemProperties(environment, getDefaultValueResolver(environment), null).apply(logFile); + LoggingSystemProperties systemProperties = (environment instanceof ConfigurableEnvironment configurableEnvironment) + ? getSystemProperties(configurableEnvironment) + : new LoggingSystemProperties(environment, getDefaultValueResolver(environment), null); + systemProperties.apply(logFile); } /** 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 cea74711abc3..e1aebc202366 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 @@ -58,8 +58,10 @@ import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.logging.LoggingSystemFactory; +import org.springframework.boot.logging.LoggingSystemProperties; import org.springframework.core.Conventions; import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; @@ -128,6 +130,11 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { this.loggerContext = loggerContext; } + @Override + public LoggingSystemProperties getSystemProperties(ConfigurableEnvironment environment) { + return new Log4j2LoggingSystemProperties(environment, getDefaultValueResolver(environment), null); + } + @Override protected String[] getStandardConfigLocations() { // With Log4J2 we use the ConfigurationFactory 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..577496820e89 --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2LoggingSystemProperties.java @@ -0,0 +1,127 @@ +/* + * 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 HoJoo Moon + * @since 4.0.0 + * @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; + } + } + +} 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..7db88024b17d --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2RollingPolicySystemProperty.java @@ -0,0 +1,105 @@ +/* + * 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 HoJoo Moon + * @since 4.0.0 + * @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; + } + +} 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..35383b1ae340 --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringBootTriggeringPolicy.java @@ -0,0 +1,182 @@ +/* + * 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.apache.logging.log4j.core.LogEvent; +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.util.Builder; +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; + +/** + * Factory for creating a standard Log4j2 {@link TriggeringPolicy} based on configuration + * attributes. The supported strategies are {@code size}, {@code time}, + * {@code size-and-time}, and {@code cron}. + * + * @author HoJoo Moon + * @since 4.0.0 + */ +@Plugin(name = "SpringBootTriggeringPolicy", category = Node.CATEGORY, elementType = "TriggeringPolicy", + deferChildren = true, printObject = true) +public abstract class SpringBootTriggeringPolicy implements TriggeringPolicy { + + private SpringBootTriggeringPolicy() { + } + + @Override + public void initialize(RollingFileManager manager) { + throw new UnsupportedOperationException("This class should not be instantiated"); + } + + @Override + public boolean isTriggeringEvent(LogEvent logEvent) { + throw new UnsupportedOperationException("This class should not be instantiated"); + } + + @PluginBuilderFactory + public static SpringBootTriggeringPolicyBuilder newBuilder() { + return new SpringBootTriggeringPolicyBuilder(); + } + + /** + * Builder for creating a {@link TriggeringPolicy}. + */ + 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 TriggeringPolicy 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; + } + return 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)); + }; + } + + 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() { + Assert.notNull(this.configuration, "configuration must not be null"); + Configuration configuration = this.configuration; + + // 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 3afaffa0cddc..97d7cb4d31a9 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 @@ -139,6 +139,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.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.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.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..73e2b2240484 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 @@ - +