TimeBasedRollingPolicy.java

/*
 * Copyright 2017 Providence Authors
 *
 * 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 net.morimekta.providence.logging.rolling;

import net.morimekta.providence.logging.RollingFileMessageWriter;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.time.Clock;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A rolling policy that writes messages into files for certain time
 * periods based on a date based file pattern and a time resolution.
 * The time pattern must at least have the accuracy of the selected
 * resolution.
 */
public class TimeBasedRollingPolicy implements RollingFileMessageWriter.RollingPolicy {
    public TimeBasedRollingPolicy(TimeUnit resolution,
                                  String rollingFilePattern) {
        this(1, resolution, rollingFilePattern);
    }

    public TimeBasedRollingPolicy(int units,
                                  TimeUnit resolution,
                                  String rollingFilePattern) {
        this(units, resolution, rollingFilePattern, Clock.systemDefaultZone());
    }

    public TimeBasedRollingPolicy(TimeUnit resolution,
                                  String rollingFilePattern,
                                  Clock clock) {
        this(1, resolution, rollingFilePattern, clock);
    }

    public TimeBasedRollingPolicy(int units,
                                  TimeUnit resolution,
                                  String rollingFilePattern,
                                  Clock clock) {
        Matcher matcher = FILE_PATTERN.matcher(rollingFilePattern);
        if (!matcher.matches()) {
            throw new IllegalArgumentException("No timestamp input in rolling file pattern");
        }
        if (units < 1) {
            throw new IllegalArgumentException("Invalid duration: " + units);
        }

        this.filePrefix = matcher.group(1);
        this.fileSuffix = matcher.group(3);

        this.units = units;
        this.resolution = resolution;
        this.formatter = getFormatter(matcher.group(2), resolution);
        this.clock = clock;
    }

    @Override
    public void maybeUpdateCurrentFile(@Nonnull RollingFileMessageWriter.CurrentFileUpdater onRollFile,
                                       boolean initialCall) throws IOException {
        // This will normalize the timestamp to the first millisecond with the
        // requested resolution. Note that if the units are not really divisible
        // in the time unit, you may get some wacky timestamps, e.g. having a resolution
        // of 11 minutes.
        long ts = resolution.toMillis(
                units * (resolution.convert(clock.millis(), TimeUnit.MILLISECONDS) / units));
        if (initialCall || ts != lastUpdateTs) {
            // This is the actual roll over.
            onRollFile.updateCurrentFile(filePrefix + fileTimestamp(ts) + fileSuffix);
            lastUpdateTs = ts;
        }
    }

    static final Pattern FILE_PATTERN = Pattern.compile(
            "(.*)[%]d[{]([^{}]*)[}](.*)");

    private final String   filePrefix;
    private final String   fileSuffix;
    private final int      units;
    private final TimeUnit resolution;
    private final DateTimeFormatter formatter;
    private final Clock    clock;

    private long lastUpdateTs;

    private String fileTimestamp(long ts) {
        ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(ts), Clock.systemUTC().getZone())
                                         .withZoneSameLocal(clock.getZone());
        return formatter.format(zdt);
    }

    protected static DateTimeFormatter getFormatter(String timestampPattern,
                                                    TimeUnit resolution) {
        if (timestampPattern.length() > 0) {
            return DateTimeFormatter.ofPattern(timestampPattern);
        }

        switch (resolution) {
            case DAYS: return DateTimeFormatter.ofPattern("yyyy-MM-dd");
            case HOURS: return DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH");
            case MINUTES: return DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm");
            default: throw new IllegalArgumentException("Not a valid log rotation resolution: " + resolution.toString() + ", must be days, hours or minutes");
        }
    }
}