FileBasedEventHandlerConfiguration.java

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2015-2016 ForgeRock AS.
 */
package org.forgerock.audit.events.handlers;

import static org.forgerock.util.Reject.checkNotNull;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.forgerock.audit.retention.DiskSpaceUsedRetentionPolicy;
import org.forgerock.audit.retention.FreeDiskSpaceRetentionPolicy;
import org.forgerock.audit.retention.RetentionPolicy;
import org.forgerock.audit.retention.SizeBasedRetentionPolicy;
import org.forgerock.audit.retention.TimeStampFileNamingPolicy;
import org.forgerock.audit.rotation.FixedTimeRotationPolicy;
import org.forgerock.audit.rotation.RotationPolicy;
import org.forgerock.audit.rotation.SizeBasedRotationPolicy;
import org.forgerock.audit.rotation.TimeLimitRotationPolicy;
import org.forgerock.util.Reject;
import org.forgerock.util.time.Duration;

import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Configures time based or size based log file rotation.
 */
public abstract class FileBasedEventHandlerConfiguration extends EventHandlerConfiguration {

    private static final Logger logger = LoggerFactory.getLogger(FileBasedEventHandlerConfiguration.class);

    @JsonPropertyDescription("audit.handlers.file.fileRotation")
    private FileRotation fileRotation = new FileRotation();
    @JsonPropertyDescription("audit.handlers.file.fileRetention")
    private FileRetention fileRetention = new FileRetention();
    @JsonPropertyDescription("audit.handlers.file.rotationRetentionCheckInterval")
    private String rotationRetentionCheckInterval = "5s";

    /**
     * Gets the {@link FileRotation}.
     * @return Not-null, The {@link FileRotation}.
     */
    public FileRotation getFileRotation() {
        return fileRotation;
    }

    /**
     * Sets the {@link FileRotation}.
     *
     * @param fileRotation Not-null, The {@link FileRotation}.
     */
    public void setFileRotation(final FileRotation fileRotation) {
        Reject.ifNull(fileRotation);
        this.fileRotation = fileRotation;
    }

    /**
     * Gets the {@link FileRetention}.
     * @return Not-null, The {@link FileRetention}.
     */
    public FileRetention getFileRetention() {
        return fileRetention;
    }

    /**
     * Sets the {@link FileRetention}.
     *
     * @param fileRetention Not-null, The {@link FileRetention}.
     */
    public void setFileRetention(final FileRetention fileRetention) {
        Reject.ifNull(fileRetention);
        this.fileRetention = fileRetention;
    }

    /**
     * Gets the interval to check time-based file rotation policies. The interval should be set as a {@link Duration}.
     * <p/>
     * Examples of valid durations are:
     * <pre>
     *      5 seconds
     *      5 minutes
     *      5 hours
     * </pre>
     * <p/>
     * Value of "zero" or "disabled" are not acceptable.
     *
     * @return The interval duration.
     */
    public String getRotationRetentionCheckInterval() {
        return rotationRetentionCheckInterval;
    }

    /**
     * Sets the interval to check time-based file rotation policies. The interval should be set as a {@link Duration}.
     * <p/>
     * Examples of valid durations are:
     * <pre>
     *      5 seconds
     *      5 minutes
     *      5 hours
     * </pre>
     * <p/>
     * Value of "zero" or "disabled" are not acceptable.
     *
     * @param rotationRetentionCheckInterval The interval duration.
     */
    public void setRotationRetentionCheckInterval(String rotationRetentionCheckInterval) {
        this.rotationRetentionCheckInterval = rotationRetentionCheckInterval;
    }

    /**
     * Groups the file rotation config parameters.
     */
    public static class FileRotation {

        /** The file size value to use when no maximum is set. */
        public static final long NO_MAX_FILE_SIZE = -1;
        /** The default file rotation suffix format. */
        public static final String DEFAULT_ROTATION_FILE_SUFFIX = "-yyyy.MM.dd-HH.mm.ss";

        @JsonPropertyDescription("audit.handlers.file.rotationEnabled")
        private boolean rotationEnabled = false;

        // size based rotation config parameters
        @JsonPropertyDescription("audit.handlers.file.maxFileSize")
        private long maxFileSize = NO_MAX_FILE_SIZE;

        // time Based Rotation config parameters
        @JsonPropertyDescription("audit.handlers.file.rotationFilePrefix")
        private String rotationFilePrefix = null;

        // fixed time based rotation config parameters
        @JsonPropertyDescription("audit.handlers.file.rotationTimes")
        private final List<String> rotationTimes = new LinkedList<>();

        @JsonPropertyDescription("audit.handlers.file.rotationFileSuffix")
        private String rotationFileSuffix = DEFAULT_ROTATION_FILE_SUFFIX;

        @JsonPropertyDescription("audit.handlers.file.rotationInterval")
        private String rotationInterval = "disabled";

        /**
         * Gets log rotation enabled state. By default log rotation is disabled.
         * @return True - If log rotation is enabled.
         *         False - If log rotation is disabled.
         */
        public boolean isRotationEnabled() {
            return rotationEnabled;
        }

        /**
         * Sets log rotation enabled state. By default log rotation is disabled.
         * @param rotationEnabled True - Enabled log rotation.
         *                        False - Disables log rotation.
         */
        public void setRotationEnabled(boolean rotationEnabled) {
            this.rotationEnabled = rotationEnabled;
        }

        /**
         * Gets the maximum file size of an audit log file in bytes.
         * @return The maximum file size in bytes.
         */
        public long getMaxFileSize() {
            return maxFileSize;
        }

        /**
         * Sets the maximum file size of an audit log file in bytes.
         * @param maxFileSize The maximum file size in bytes.
         */
        public void setMaxFileSize(long maxFileSize) {
            this.maxFileSize = maxFileSize;
        }

        /**
         * Gets the prefix to add to a log file on rotation. This is only used when time based rotation is enabled.
         * @return The prefix to add to the file.
         */
        public String getRotationFilePrefix() {
            return rotationFilePrefix;
        }

        /**
         * Sets the prefix to add to a log file on rotation. This is only used when time based rotation is enabled.
         * @param rotationFilePrefix The prefix to add to the file.
         */
        public void setRotationFilePrefix(String rotationFilePrefix) {
            this.rotationFilePrefix = rotationFilePrefix;
        }

        /**
         * Gets the suffix to add to a log file on rotation. This is only used when time based rotation is enabled.
         * The suffix allows use of Date and Time patterns defined in {@link SimpleDateFormat}. The default suffix is
         * "-yyyy.MM.dd-HH.mm.ss".
         * @return The suffix to add to the file.
         */
        public String getRotationFileSuffix() {
            return rotationFileSuffix;
        }

        /**
         * Sets the suffix to add to a log file on rotation. This is only used when time based rotation is enabled.
         * The suffix allows use of Date and Time patterns defined in {@link SimpleDateFormat}. The default suffix is
         * "-yyyy.MM.dd-HH.mm.ss".
         * @param rotationFileSuffix The suffix to add to the file.
         */
        public void setRotationFileSuffix(String rotationFileSuffix) {
            this.rotationFileSuffix = rotationFileSuffix;
        }

        /**
         * Gets the interval to trigger a file rotation. The interval should be set as a {@link Duration}.
         * <p/>
         * Examples of valid durations are:
         * <pre>
         *      5 seconds
         *      5 minutes
         *      5 hours
         *      disabled
         * </pre>
         * <p/>
         * A value of "zero" or "disabled" means that time based file rotation is disabled.
         *
         * @return The interval duration.
         */
        public String getRotationInterval() {
            return rotationInterval;
        }

        /**
         * Sets the interval to trigger a file rotation. The interval should be set as a {@link Duration}.
         * <p/>
         * Examples of valid durations are:
         * <pre>
         *      5 seconds
         *      5 minutes
         *      5 hours
         *      disabled
         * </pre>
         * <p/>
         * A value of "zero" or "disabled" disables time based file rotation.
         *
         * @param rotationInterval A String that can be parsed as a {@link Duration}, specifying rotation interval.
         */
        public void setRotationInterval(String rotationInterval) {
            this.rotationInterval = rotationInterval;
        }

        /**
         * Gets a list of times at which file rotation should be triggered; times should be provided as Strings that can
         * be parsed by {@link Duration} that each specify an offset from midnight.
         * <p/>
         * For example the list of [10 milliseconds, 20 milliseconds, 30 milliseconds] will
         * cause a file rotation to happen 10 milliseconds, 20 milliseconds and 30 milliseconds after midnight.
         *
         * @return The list of durations after midnight that rotation should happen.
         */
        public List<String> getRotationTimes() {
            return rotationTimes;
        }

        /**
         * Sets a list of times at which file rotation should be triggered; times should be provided as Strings that can
         * be parsed by {@link Duration} that each specify an offset from midnight.
         * <p/>
         * For example the list of [10 milliseconds, 20 milliseconds, 30 milliseconds] will
         * cause a file rotation to happen 10 milliseconds, 20 milliseconds and 30 milliseconds after midnight.
         *
         * @param rotationTimes The list of durations after midnight that rotation should happen.
         */
        public void setRotationTimes(List<String> rotationTimes) {
            this.rotationTimes.addAll(rotationTimes);
        }

        /**
         * Builds a {@link TimeStampFileNamingPolicy} instance from configuration options.
         *
         * @param file Initial log file
         * @return {@link TimeStampFileNamingPolicy} instance
         */
        @JsonIgnore
        public TimeStampFileNamingPolicy buildTimeStampFileNamingPolicy(final File file) {
            return new TimeStampFileNamingPolicy(checkNotNull(file), getRotationFileSuffix(), getRotationFilePrefix());
        }

        /**
         * Builds {@link RotationPolicy} instances from configuration options.
         *
         * @return {@link RotationPolicy} instances
         */
        @JsonIgnore
        public List<RotationPolicy> buildRotationPolicies() {
            final List<RotationPolicy> rotationPolicies = new ArrayList<>();

            // add SizeBasedRotationPolicy if a non zero size is supplied
            final long maxFileSize = getMaxFileSize();
            if (maxFileSize > 0) {
                rotationPolicies.add(new SizeBasedRotationPolicy(maxFileSize));
            }

            // add FixedTimeRotationPolicy
            final List<Duration> dailyRotationTimes = new LinkedList<>();
            for (final String rotationTime : getRotationTimes()) {
                Duration duration = parseDuration("rotation time", rotationTime, null);
                if (duration != null && !duration.isUnlimited()) {
                    dailyRotationTimes.add(duration);
                }
            }
            if (!dailyRotationTimes.isEmpty()) {
                rotationPolicies.add(new FixedTimeRotationPolicy(dailyRotationTimes));
            }

            // add TimeLimitRotationPolicy if enabled
            final Duration rotationInterval = parseDuration("rotation interval", getRotationInterval(), Duration.ZERO);
            if (!(rotationInterval.isZero() || rotationInterval.isUnlimited())) {
                rotationPolicies.add(new TimeLimitRotationPolicy(rotationInterval));
            }
            return rotationPolicies;
        }
    }

    /**
     * Groups the file retention config parameters.
     */
    public static class FileRetention {

        /** The value of number of history files to use when the value is unlimited. */
        public static final int UNLIMITED_HISTORY_FILES = -1;
        /** The disk space value when disk space is unrestricted. */
        public static final long ANY_DISK_SPACE = -1;

        @JsonPropertyDescription("audit.handlers.file.maxNumberOfHistoryFiles")
        private int maxNumberOfHistoryFiles = UNLIMITED_HISTORY_FILES;

        @JsonPropertyDescription("audit.handlers.file.maxDiskSpaceToUse")
        private long maxDiskSpaceToUse = ANY_DISK_SPACE;

        @JsonPropertyDescription("audit.handlers.file.minFreeSpaceRequired")
        private long minFreeSpaceRequired = ANY_DISK_SPACE;

        /**
         * Gets the maximum number of historical log files to retain. -1 disables pruning of old history files.
         * @return The maximum number of log files. -1 disables pruning of old history files.
         */
        public int getMaxNumberOfHistoryFiles() {
            return maxNumberOfHistoryFiles;
        }

        /**
         * Sets the maximum number of historical log files to retain. -1 disables pruning of old history files.
         * @param maxNumberOfHistoryFiles The maximum number of log files. -1 disables pruning of old history files.
         */
        public void setMaxNumberOfHistoryFiles(int maxNumberOfHistoryFiles) {
            this.maxNumberOfHistoryFiles = maxNumberOfHistoryFiles;
        }

        /**
         * Gets the maximum disk space the audit logs can occupy. A negative or zero value indicates this
         * policy is disabled.
         * @return The maximum disk space the audit logs can occupy.
         */
        public long getMaxDiskSpaceToUse() {
            return maxDiskSpaceToUse;
        }

        /**
         * Sets the maximum disk space the audit logs can occupy. A negative or zero value indicates this
         * policy is disabled.
         * @param maxDiskSpaceToUse The maximum disk space the audit logs can occupy.
         */
        public void setMaxDiskSpaceToUse(final long maxDiskSpaceToUse) {
            this.maxDiskSpaceToUse = maxDiskSpaceToUse;
        }

        /**
         * Gets the minimum free space the system must contain. A negative or zero value indicates this
         * policy is disabled.
         * @return The minimum free space the system must contain.
         */
        public long getMinFreeSpaceRequired() {
            return minFreeSpaceRequired;
        }

        /**
         * Sets the minimum free space the system must contain. A negative or zero value indicates this
         * policy is disabled.
         * @param minFreeSpaceRequired The minimum free space the system must contain.
         */
        public void setMinFreeSpaceRequired(final long minFreeSpaceRequired) {
            this.minFreeSpaceRequired = minFreeSpaceRequired;
        }

        /**
         * Builds {@link RetentionPolicy} instances from configuration options.
         *
         * @return {@link RetentionPolicy} instances
         */
        @JsonIgnore
        public List<RetentionPolicy> buildRetentionPolicies() {
            final List<RetentionPolicy> retentionPolicies = new ArrayList<>();

            // Add SizeBasedRetentionPolicy if the max number of files config value is more than 0
            final int maxNumberOfHistoryFiles = getMaxNumberOfHistoryFiles();
            if (maxNumberOfHistoryFiles > 0) {
                retentionPolicies.add(new SizeBasedRetentionPolicy(maxNumberOfHistoryFiles));
            }

            // Add DiskSpaceUsedRetentionPolicy if config value > 0
            final long maxDiskSpaceToUse = getMaxDiskSpaceToUse();
            if (maxDiskSpaceToUse > 0) {
                retentionPolicies.add(new DiskSpaceUsedRetentionPolicy(maxDiskSpaceToUse));
            }

            // Add FreeDiskSpaceRetentionPolicy if config value > 0
            final long minimumFreeDiskSpace = getMinFreeSpaceRequired();
            if (minimumFreeDiskSpace > 0) {
                retentionPolicies.add(new FreeDiskSpaceRetentionPolicy(minimumFreeDiskSpace));
            }
            return retentionPolicies;
        }
    }

    private static Duration parseDuration(final String description, final String duration,
            final Duration defaultValue) {
        try {
            return Duration.duration(duration);
        } catch (IllegalArgumentException e) {
            logger.info("Invalid {} value: '{}'", description, duration);
            return defaultValue;
        }
    }
}