Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand All @@ -156,7 +163,8 @@ public DefaultRolloverStrategy build() {
nonNullStrSubstitutor,
customActions,
stopCustomActionsOnError,
tempCompressedFilePattern);
tempCompressedFilePattern,
delayedCompressionSeconds);
}

public String getMax() {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -419,6 +439,7 @@ public static DefaultRolloverStrategy createStrategy(
private final List<Action> customActions;
private final boolean stopCustomActionsOnError;
private final PatternProcessor tempCompressedFilePattern;
private final int delayedCompressionSeconds;

/**
* Constructs a new instance.
Expand Down Expand Up @@ -446,7 +467,8 @@ protected DefaultRolloverStrategy(
strSubstitutor,
customActions,
stopCustomActionsOnError,
null);
null,
0L);
}

/**
Expand All @@ -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;
Expand All @@ -477,6 +500,7 @@ protected DefaultRolloverStrategy(
this.customActions = customActions == null ? Collections.<Action>emptyList() : Arrays.asList(customActions);
this.tempCompressedFilePattern =
tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null;
this.delayedCompressionSeconds = delayedCompressionSeconds;
}

public int getCompressionLevel() {
Expand Down Expand Up @@ -713,12 +737,29 @@ 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);
}

@Override
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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -72,10 +74,18 @@ public class RollingFileManager extends FileManager {
private final boolean directWrite;
private final CopyOnWriteArrayList<RolloverListener> 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<RollingFileManager, TriggeringPolicy> triggeringPolicyUpdater =
AtomicReferenceFieldUpdater.newUpdater(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -875,4 +893,75 @@ public boolean addAll(final Collection<? extends Runnable> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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 + "]";
}
}

Loading