diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java index ea1bae76696..042e114cd9f 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java @@ -30,6 +30,7 @@ import org.apache.logging.log4j.core.Core; import org.apache.logging.log4j.core.appender.rolling.action.Action; import org.apache.logging.log4j.core.appender.rolling.action.CompositeAction; +import org.apache.logging.log4j.core.appender.rolling.action.DelayedCompressionAction; import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction; import org.apache.logging.log4j.core.appender.rolling.action.PathCondition; import org.apache.logging.log4j.core.appender.rolling.action.PosixViewAttributeAction; @@ -109,6 +110,11 @@ public static class Builder implements org.apache.logging.log4j.core.util.Builde @PluginBuilderAttribute(value = "tempCompressedFilePattern") private String tempCompressedFilePattern; + @PluginBuilderAttribute("delayedCompressionSeconds") + private String delayedCompressionSecondsStr; + + private long delayedCompressionSeconds; + @PluginConfiguration private Configuration config; @@ -145,6 +151,7 @@ public DefaultRolloverStrategy build() { final String trimmedCompressionLevelStr = compressionLevelStr != null ? compressionLevelStr.trim() : compressionLevelStr; final int compressionLevel = Integers.parseInt(trimmedCompressionLevelStr, Deflater.DEFAULT_COMPRESSION); + final int delayedCompressionSeconds = Integer.parseInt(delayedCompressionSecondsStr, 0); // The config object can be null when this object is built programmatically. final StrSubstitutor nonNullStrSubstitutor = config != null ? config.getStrSubstitutor() : new StrSubstitutor(); @@ -156,7 +163,8 @@ public DefaultRolloverStrategy build() { nonNullStrSubstitutor, customActions, stopCustomActionsOnError, - tempCompressedFilePattern); + tempCompressedFilePattern, + delayedCompressionSeconds); } public String getMax() { @@ -272,6 +280,18 @@ public Builder setTempCompressedFilePattern(final String tempCompressedFilePatte return this; } + /** + * Defines the maximum delay in seconds for compression. + * + * @param delayedCompressionSeconds the maximum delay in seconds (0 means immediate) + * @return This builder for chaining convenience + * @since 2.26.0 + */ + public Builder setDelayedCompressionSeconds(final String delayedCompressionSeconds) { + this.delayedCompressionSecondsStr = delayedCompressionSeconds; + return this; + } + public Configuration getConfig() { return config; } @@ -419,6 +439,7 @@ public static DefaultRolloverStrategy createStrategy( private final List customActions; private final boolean stopCustomActionsOnError; private final PatternProcessor tempCompressedFilePattern; + private final int delayedCompressionSeconds; /** * Constructs a new instance. @@ -446,7 +467,8 @@ protected DefaultRolloverStrategy( strSubstitutor, customActions, stopCustomActionsOnError, - null); + null, + 0L); } /** @@ -467,7 +489,8 @@ protected DefaultRolloverStrategy( final StrSubstitutor strSubstitutor, final Action[] customActions, final boolean stopCustomActionsOnError, - final String tempCompressedFilePatternString) { + final String tempCompressedFilePatternString, + final int delayedCompressionSeconds) { super(strSubstitutor); this.minIndex = minIndex; this.maxIndex = maxIndex; @@ -477,6 +500,7 @@ protected DefaultRolloverStrategy( this.customActions = customActions == null ? Collections.emptyList() : Arrays.asList(customActions); this.tempCompressedFilePattern = tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null; + this.delayedCompressionSeconds = delayedCompressionSeconds; } public int getCompressionLevel() { @@ -713,7 +737,14 @@ public RolloverDescription rollover(final RollingFileManager manager) throws Sec final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo), manager.isRenameEmptyFiles()); - final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError); + Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError); + + if (asyncAction != null && delayedCompressionSeconds > 0) { + // Wrap the entire async action with delay - DelayedCompressionAction will provide + // delay configuration for RollingFileManager to handle scheduling + asyncAction = new DelayedCompressionAction(asyncAction, delayedCompressionSeconds); + } + return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction); } @@ -721,4 +752,14 @@ public RolloverDescription rollover(final RollingFileManager manager) throws Sec public String toString() { return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ", useMax=" + useMax + ")"; } + + /** + * Gets the maximum delay in seconds for compression. + * + * @return the maximum delay in seconds + */ + public int getDelayedCompressionSeconds() { + return delayedCompressionSeconds; + } + } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingFileManager.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingFileManager.java index afdb7416585..4b9965843ab 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingFileManager.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingFileManager.java @@ -30,9 +30,9 @@ import java.util.Date; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.Semaphore; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import org.apache.logging.log4j.core.Layout; @@ -44,6 +44,8 @@ import org.apache.logging.log4j.core.appender.FileManager; import org.apache.logging.log4j.core.appender.rolling.action.AbstractAction; import org.apache.logging.log4j.core.appender.rolling.action.Action; +import org.apache.logging.log4j.core.appender.rolling.action.CompositeAction; +import org.apache.logging.log4j.core.appender.rolling.action.Schedulable; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.internal.annotation.SuppressFBWarnings; import org.apache.logging.log4j.core.util.Constants; @@ -72,10 +74,18 @@ public class RollingFileManager extends FileManager { private final boolean directWrite; private final CopyOnWriteArrayList rolloverListeners = new CopyOnWriteArrayList<>(); - /* This executor pool will create a new Thread for every work async action to be performed. Using it allows - us to make sure all the Threads are completed when the Manager is stopped. */ - private final ExecutorService asyncExecutor = - new ThreadPoolExecutor(0, Integer.MAX_VALUE, 0, TimeUnit.MILLISECONDS, new EmptyQueue(), threadFactory); + /* This scheduled executor pool handles both immediate async actions and delayed compression actions. + * For immediate tasks, it creates new threads like the original ThreadPoolExecutor. + * For delayed tasks, it uses the scheduling capabilities. + * Using it allows us to make sure all the Threads are completed when the Manager is stopped. */ + private final ScheduledExecutorService asyncExecutor = new ScheduledThreadPoolExecutor(0, threadFactory) { + @Override + public void execute(Runnable command) { + // For immediate execution, create a new thread to maintain original behavior + Thread thread = getThreadFactory().newThread(command); + thread.start(); + } + }; private static final AtomicReferenceFieldUpdater triggeringPolicyUpdater = AtomicReferenceFieldUpdater.newUpdater( @@ -638,6 +648,14 @@ public RolloverStrategy getRolloverStrategy() { return this.rolloverStrategy; } + /** + * Returns the async executor service for both immediate and delayed actions. + * @return The ScheduledExecutorService + */ + public ScheduledExecutorService getAsyncExecutor() { + return this.asyncExecutor; + } + private boolean rollover(final RolloverStrategy strategy) { boolean outputStreamClosed = false; @@ -670,7 +688,7 @@ private boolean rollover(final RolloverStrategy strategy) { if (syncActionSuccess && descriptor.getAsynchronous() != null) { LOGGER.debug("RollingFileManager executing async {}", descriptor.getAsynchronous()); - asyncExecutor.execute(new AsyncAction(descriptor.getAsynchronous(), this)); + scheduleActionsWithDelay(descriptor.getAsynchronous()); asyncActionStarted = false; } } @@ -875,4 +893,75 @@ public boolean addAll(final Collection collection) { return false; } } + + /** + * Schedules actions with delays based on their Schedulable interface implementation. + * Actions implementing Schedulable with positive delays are scheduled individually, + * while others are executed immediately. + * + * @param action the action to schedule + */ + private void scheduleActionsWithDelay(Action action) { + if (action instanceof Schedulable) { + Schedulable schedulableAction = (Schedulable) action; + int delaySeconds = schedulableAction.getDelaySeconds(); + + if (delaySeconds > 0) { + // Schedule the action with its specific delay + LOGGER.debug("Scheduling action with {} seconds delay", delaySeconds); + asyncExecutor.schedule( + new AsyncAction(action, this), + delaySeconds, + TimeUnit.SECONDS + ); + return; + } + } + + if (action instanceof CompositeAction) { + // Handle each action in the composite separately + scheduleCompositeAction((CompositeAction) action); + } else { + // Execute immediately if no delay + asyncExecutor.execute(new AsyncAction(action, this)); + } + } + + /** + * Schedules actions within a CompositeAction based on their individual delays. + * + * @param compositeAction the CompositeAction to schedule + */ + private void scheduleCompositeAction(CompositeAction compositeAction) { + Action[] actions = compositeAction.getActions(); + for (Action action : actions) { + scheduleIndividualAction(action); + } + } + + /** + * Schedules an individual action based on its Schedulable interface. + * + * @param action the action to schedule + */ + private void scheduleIndividualAction(Action action) { + if (action instanceof Schedulable) { + Schedulable schedulableAction = (Schedulable) action; + int delaySeconds = schedulableAction.getDelaySeconds(); + + if (delaySeconds > 0) { + // Schedule the action with its specific delay + LOGGER.debug("Scheduling individual action with {} seconds delay", delaySeconds); + asyncExecutor.schedule( + new AsyncAction(action, this), + delaySeconds, + TimeUnit.SECONDS + ); + return; + } + } + + // Execute immediately if no delay + asyncExecutor.execute(new AsyncAction(action, this)); + } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java new file mode 100644 index 00000000000..5a61dda472d --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/DelayedCompressionAction.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you 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 + * + * http://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.apache.logging.log4j.core.appender.rolling.action; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.status.StatusLogger; + +import java.io.IOException; + +import static java.util.Objects.requireNonNull; + +/** + * Wrapper action that provides delay configuration for compression actions. + * This action wraps another action and specifies a delay time, allowing + * the scheduling to be handled externally by RollingFileManager. + */ +public class DelayedCompressionAction extends AbstractAction implements Schedulable { + + private static final Logger LOGGER = StatusLogger.getLogger(); + + private final Action originalAction; + private final int delaySeconds; + + /** + * Creates a new DelayedCompressionAction. + * + * @param originalAction the action to be executed with delay + * @param delaySeconds the delay in seconds (0 means immediate execution) + */ + public DelayedCompressionAction(Action originalAction, int delaySeconds) { + this.originalAction = requireNonNull(originalAction, "originalAction"); + this.delaySeconds = delaySeconds; + } + + /** + * Executes the wrapped action immediately. + * The delay is handled externally by RollingFileManager based on the + * getDelaySeconds() method. + * + * @return true if the action was successfully executed + * @throws IOException if an error occurs during execution + */ + @Override + public boolean execute() throws IOException { + String sourceFile = extractSourceFileName(originalAction); + + try { + LOGGER.debug("Starting delayed compression of {}", sourceFile); + boolean success = originalAction.execute(); + if (success) { + LOGGER.debug("Successfully completed delayed compression of {}", sourceFile); + } else { + LOGGER.warn("Failed to execute delayed compression of {}", sourceFile); + } + return success; + } catch (Exception e) { + LOGGER.warn("Exception during delayed compression of {}", sourceFile, e); + if (e instanceof IOException) { + throw (IOException) e; + } + throw new IOException(e); + } + } + + /** + * Attempts to extract the source file name from the action for logging purposes. + * + * @param action the action to extract file name from + * @return the source file name or action toString if cannot be determined + */ + private String extractSourceFileName(Action action) { + // This is a best-effort attempt to get the source file name + // The actual implementation may need to be enhanced based on the action type + return action.toString(); + } + + /** + * Gets the original action being wrapped. + * + * @return the original action + */ + public Action getOriginalAction() { + return originalAction; + } + + /** + * Gets the delay in seconds for compression. + * + * @return the delay in seconds + */ + public int getDelaySeconds() { + return delaySeconds; + } + + @Override + public boolean isComplete() { + // For simplicity, we consider this action complete after execution + // In a more sophisticated implementation, we might track the wrapped action's completion + return true; + } + + @Override + public String toString() { + return "DelayedCompressionAction[originalAction=" + originalAction + ", delaySeconds=" + delaySeconds + "]"; + } +} + diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/Schedulable.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/Schedulable.java new file mode 100644 index 00000000000..eba4fbde87d --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/Schedulable.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you 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 + * + * http://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.apache.logging.log4j.core.appender.rolling.action; + +/** + * Interface for objects that can be scheduled with a delay. + * This provides a generic way to define delayed execution for any type of object, + * not just actions. The delay time is specified in seconds. + */ +public interface Schedulable { + + /** + * Gets the delay time in seconds before this object should be executed or processed. + * A return value of 0 or negative means the object should be executed immediately. + * + * @return the delay in seconds (0 means execute immediately) + */ + int getDelaySeconds(); + + /** + * Checks if this object should be delayed. + * + * @return true if the object has a positive delay + */ + default boolean isDelayed() { + return getDelaySeconds() > 0; + } +} +