Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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 @@ -16,34 +16,21 @@
*/
package org.apache.logging.log4j.core.appender.rolling;

import org.apache.logging.log4j.core.Core;
import org.apache.logging.log4j.core.appender.rolling.action.*;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.plugins.*;
import org.apache.logging.log4j.core.internal.annotation.SuppressFBWarnings;
import org.apache.logging.log4j.core.lookup.StrSubstitutor;
import org.apache.logging.log4j.core.util.Integers;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.zip.Deflater;
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.FileRenameAction;
import org.apache.logging.log4j.core.appender.rolling.action.PathCondition;
import org.apache.logging.log4j.core.appender.rolling.action.PosixViewAttributeAction;
import org.apache.logging.log4j.core.config.Configuration;
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.PluginBuilderAttribute;
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.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.internal.annotation.SuppressFBWarnings;
import org.apache.logging.log4j.core.lookup.StrSubstitutor;
import org.apache.logging.log4j.core.util.Integers;

/**
* When rolling over, <code>DefaultRolloverStrategy</code> renames files according to an algorithm as described below.
Expand Down Expand Up @@ -109,6 +96,11 @@ public static class Builder implements org.apache.logging.log4j.core.util.Builde
@PluginBuilderAttribute(value = "tempCompressedFilePattern")
private String tempCompressedFilePattern;

@PluginBuilderAttribute("deferredCompressionSeconds")
private String deferredCompressionSecondsStr;

private long deferredCompressionSeconds;

@PluginConfiguration
private Configuration config;

Expand Down Expand Up @@ -145,6 +137,7 @@ public DefaultRolloverStrategy build() {
final String trimmedCompressionLevelStr =
compressionLevelStr != null ? compressionLevelStr.trim() : compressionLevelStr;
final int compressionLevel = Integers.parseInt(trimmedCompressionLevelStr, Deflater.DEFAULT_COMPRESSION);
final int deferredCompressionSeconds = Integer.parseInt(deferredCompressionSecondsStr, 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 +149,8 @@ public DefaultRolloverStrategy build() {
nonNullStrSubstitutor,
customActions,
stopCustomActionsOnError,
tempCompressedFilePattern);
tempCompressedFilePattern,
deferredCompressionSeconds);
}

public String getMax() {
Expand Down Expand Up @@ -272,6 +266,18 @@ public Builder setTempCompressedFilePattern(final String tempCompressedFilePatte
return this;
}

/**
* Defines the maximum defer in seconds for compression.
*
* @param deferredCompressionSeconds the maximum defer in seconds (0 means immediate)
* @return This builder for chaining convenience
* @since 2.26.0
*/
public Builder setDeferredCompressionSeconds(final String deferredCompressionSeconds) {
this.deferredCompressionSecondsStr = deferredCompressionSeconds;
return this;
}

public Configuration getConfig() {
return config;
}
Expand Down Expand Up @@ -419,6 +425,7 @@ public static DefaultRolloverStrategy createStrategy(
private final List<Action> customActions;
private final boolean stopCustomActionsOnError;
private final PatternProcessor tempCompressedFilePattern;
private final int deferredCompressionSeconds;

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

/**
Expand All @@ -467,7 +475,8 @@ protected DefaultRolloverStrategy(
final StrSubstitutor strSubstitutor,
final Action[] customActions,
final boolean stopCustomActionsOnError,
final String tempCompressedFilePatternString) {
final String tempCompressedFilePatternString,
final int deferredCompressionSeconds) {
super(strSubstitutor);
this.minIndex = minIndex;
this.maxIndex = maxIndex;
Expand All @@ -477,6 +486,7 @@ protected DefaultRolloverStrategy(
this.customActions = customActions == null ? Collections.<Action>emptyList() : Arrays.asList(customActions);
this.tempCompressedFilePattern =
tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null;
this.deferredCompressionSeconds = deferredCompressionSeconds;
}

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

// Apply deferred compression if configured
if (asyncAction != null && deferredCompressionSeconds > 0) {
asyncAction = new DeferredCompressionAction(asyncAction, deferredCompressionSeconds);
}

return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction);
}

@Override
public String toString() {
return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ", useMax=" + useMax + ")";
}

/**
* Gets the maximum defer in seconds for compression.
*
* @return the maximum defer in seconds
*/
public int getDeferredCompressionSeconds() {
return deferredCompressionSeconds;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
* 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 java.util.concurrent.ThreadLocalRandom;

import static java.util.Objects.requireNonNull;

/**
* Wrapper action that schedules compression for defferred execution.
* This action wraps another action and schedules it to be executed
* after a random defer within a specified time window.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Wrapper action that schedules compression for defferred execution.
* This action wraps another action and schedules it to be executed
* after a random defer within a specified time window.
* Decorates an {@link Action} to execute it after a certain amount of delay.

*/
public class DeferredCompressionAction implements Action {
Copy link
Member

Choose a reason for hiding this comment

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

I think your earlier attempt, DelayedCompressionAction, fits the bill better. AFAICT, deferred waits for a condition, which does not necessarily needs to be a time, whereas delay makes it clear the involvement of time.

Once you implement this change, please update all the usages of [Dd]efer in this PR.


private static final Logger LOGGER = StatusLogger.getLogger();

private final Action originalAction;
private final int maxDeferSeconds;
private boolean complete = false;
private boolean interrupted = false;

/**
* Creates a new DelayedCompressionAction.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Creates a new DelayedCompressionAction.
* Creates a new instance.

*
* @param originalAction the action to be executed with defer
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* @param originalAction the action to be executed with defer
* @param originalAction the action to be executed with delay

* @param maxDeferSeconds the maximum defer in seconds (0 means immediate execution)
*/
public DeferredCompressionAction(Action originalAction, int maxDeferSeconds) {
Copy link
Member

Choose a reason for hiding this comment

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

Question: Shouldn't we be receiving a range instead of an upper bound?

this.originalAction = requireNonNull(originalAction, "originalAction");
this.maxDeferSeconds = maxDeferSeconds;
}

/**
* Executes the action with a defer using sleep.
*
* @return true if the action was successfully executed
* @throws IOException if an error occurs during execution
*/
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/**
* Executes the action with a defer using sleep.
*
* @return true if the action was successfully executed
* @throws IOException if an error occurs during execution
*/

@Override
public boolean execute() throws IOException {
if (interrupted) {
Copy link
Member

Choose a reason for hiding this comment

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

What about complete == true?

return false;
}

try {
// Extract source file name for logging (if possible)
String sourceFile = extractSourceFileName(originalAction);

// Calculate defer
int deferredSeconds = 0;
if (maxDeferSeconds > 0) {
deferredSeconds = ThreadLocalRandom.current().nextInt(maxDeferSeconds + 1);
LOGGER.debug("Scheduling compression of {} with defer of {} seconds", sourceFile, deferredSeconds);
}

// Sleep for the defer period
if (deferredSeconds > 0) {
try {
Thread.sleep(deferredSeconds * 1000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
interrupted = true;
LOGGER.warn("Delayed compression interrupted for {}", sourceFile);
return false;
}
}

// Execute the original action after defer
if (!interrupted) {
executeAction(originalAction, sourceFile);
}

return !interrupted;
} catch (Exception e) {
complete = true;
if (e instanceof IOException) {
throw (IOException) e;
}
throw new IOException(e);
}
}

/**
* Runs the action. This method is called when the action is executed
* as a Runnable.
*/
@Override
public void run() {
if (!interrupted) {
try {
execute();
} catch (final RuntimeException ex) {
LOGGER.warn("Exception during deferred compression execution", ex);
} catch (final IOException ex) {
LOGGER.warn("IOException during deferred compression execution", ex);
} catch (final Error e) {
LOGGER.warn("Error during deferred compression execution", e);
}
complete = true;
}
}

/**
* Cancels the compression task by interrupting the current thread.
*/
@Override
public void close() {
interrupted = true;
// Interrupt the current thread if it's sleeping
Thread.currentThread().interrupt();
}

/**
* Executes the compression action.
*
* @param action the action to execute
* @param sourceFile the source file name (for logging)
*/
private void executeAction(Action action, String sourceFile) {
try {
LOGGER.debug("Starting deferred compression of {}", sourceFile);
boolean success = action.execute();
if (success) {
LOGGER.debug("Successfully completed deferred compression of {}", sourceFile);
} else {
LOGGER.warn("Failed to execute deferred compression of {}", sourceFile);
}
} catch (Exception e) {
LOGGER.warn("Exception during deferred compression of {}", sourceFile, e);
} finally {
// 在finally块中设置complete状态
complete = true;
}
}

/**
* Checks if the action has completed.
*
* @return true if the action is complete
*/
@Override
public boolean isComplete() {
return complete;
}

/**
* 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 maximum defer in seconds for compression.
*
* @return the maximum defer in seconds
*/
public int getMaxDeferSeconds() {
return maxDeferSeconds;
}

@Override
public String toString() {
return "DeferredCompressionAction[originalAction=" + originalAction + ", maxDeferSeconds=" + maxDeferSeconds + "]";
}
}

Loading