org.wildfly.security.audit.PeriodicRotatingFileAuditEndpoint Maven / Gradle / Ivy
/*
* JBoss, Home of Professional Open Source.
* Copyright 2017 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed 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.wildfly.security.audit;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.WeekFields;
import java.util.Locale;
import static org.wildfly.common.Assert.checkNotNullParam;
import static org.wildfly.security.audit.ElytronMessages.audit;
/**
* An audit endpoint which rotates the log at a preset time interval.
* Depending on set suffix, moves old log records into files tagged by timestamp.
*
* Based on {@link org.jboss.logmanager.handlers.PeriodicSizeRotatingFileHandler}.
*
* @author Jan Kalina
* @author James R. Perkins
* @author Yeray Borges
*/
public class PeriodicRotatingFileAuditEndpoint extends FileAuditEndpoint {
private final DateTimeFormatter format;
private final Period period;
private final ZoneId timeZone;
private long nextRollover = Long.MAX_VALUE;
private String nextSuffix;
PeriodicRotatingFileAuditEndpoint(Builder builder) throws IOException {
super(builder);
this.format = builder.format;
this.period = builder.period;
this.timeZone = builder.timeZone;
final File file = getFile();
calcNextRollover(file != null && file.lastModified() > 0 ? file.lastModified() : clock.millis());
}
/**
* Checks whether time-based log rotation should be done and if so, it moves current log file
* into time-tagged file and exchange target file to continue logging into new, non-time-tagged file.
*
* @param instant time of the message acceptance
*/
@Override
protected void preWrite(Instant instant) {
final long recordMillis = instant.toEpochMilli();
if (recordMillis >= nextRollover) {
try {
final File file = getFile();
if (file == null) {
// no file is set; a direct output stream or writer was specified
return;
}
closeStreams(); // close the original file (some OSes won't let you move/rename a file that is open)
final Path target = Paths.get(file.toPath() + nextSuffix);
Files.move(file.toPath(), target, StandardCopyOption.REPLACE_EXISTING);
setFile(file);
} catch (IOException e) {
audit.unableToRotateLogFile(e);
}
calcNextRollover(recordMillis);
}
}
/**
* For given time and period obtains time when should be new log file started
*/
private void calcNextRollover(final long fromTime) {
if (period == Period.NEVER || format == null) {
nextRollover = Long.MAX_VALUE;
return;
}
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(fromTime), timeZone);
nextSuffix = format.format(zonedDateTime);
switch (period) {
case YEAR:
zonedDateTime = zonedDateTime.truncatedTo(ChronoUnit.DAYS)
.withDayOfYear(1)
.plus(1, ChronoUnit.YEARS);
break;
case MONTH:
zonedDateTime = zonedDateTime.truncatedTo(ChronoUnit.DAYS)
.withDayOfMonth(1)
.plus(1,ChronoUnit.MONTHS);
break;
case WEEK:
zonedDateTime = zonedDateTime.truncatedTo(ChronoUnit.DAYS)
.with(TemporalAdjusters.next(WeekFields.of(Locale.getDefault()).getFirstDayOfWeek()));
break;
case DAY:
zonedDateTime = zonedDateTime.truncatedTo(ChronoUnit.DAYS)
.plus(1, ChronoUnit.DAYS);
break;
case HALF_DAY:
ZonedDateTime halfDay = ZonedDateTime.from(zonedDateTime).truncatedTo(ChronoUnit.DAYS)
.plus(1, ChronoUnit.HALF_DAYS);
if ( zonedDateTime.isBefore(halfDay) ) {
zonedDateTime = halfDay;
}else{
zonedDateTime = halfDay.plus(1, ChronoUnit.HALF_DAYS);
}
break;
case HOUR:
zonedDateTime = zonedDateTime.truncatedTo(ChronoUnit.HOURS)
.plus(1, ChronoUnit.HOURS);
break;
case MINUTE:
zonedDateTime = zonedDateTime.truncatedTo(ChronoUnit.MINUTES)
.plus(1, ChronoUnit.MINUTES);
}
nextRollover = zonedDateTime.toInstant().toEpochMilli();
}
/**
* Possible period values. Keep in strictly ascending order of magnitude.
*/
enum Period {
MINUTE,
HOUR,
HALF_DAY,
DAY,
WEEK,
MONTH,
YEAR,
NEVER,
}
/**
* Obtain a new {@link Builder} capable of building a {@link PeriodicRotatingFileAuditEndpoint}.
*
* @return a new {@link Builder} capable of building a {@link PeriodicRotatingFileAuditEndpoint}.
*/
public static Builder builder() {
return new Builder();
}
/**
* A builder for periodic rotating file audit endpoints.
*/
public static class Builder extends FileAuditEndpoint.Builder {
DateTimeFormatter format;
Period period = Period.NEVER;
ZoneId timeZone = ZoneId.systemDefault();
Builder() {
super();
}
/**
* Set the configured time zone for this handler.
*
* @param timeZone the configured time zone
* @return this builder
*/
public Builder setTimeZone(ZoneId timeZone) {
this.timeZone = checkNotNullParam("timeZone", timeZone);
return this;
}
/**
* Set the suffix string. The string is in a format which can be understood by {@link java.time.format.DateTimeFormatter}.
* The period of the rotation is automatically calculated based on the suffix.
*
* @param suffix the suffix
* @return this builder
* @throws IllegalArgumentException if the suffix is not valid
*/
public Builder setSuffix(String suffix) throws IllegalArgumentException {
format = DateTimeFormatter.ofPattern(suffix).withZone(timeZone);
final int len = suffix.length();
period = Period.NEVER;
for (int i = 0; i < len; i ++) {
switch (suffix.charAt(i)) {
case 'y': period = min(period, Period.YEAR); break;
case 'M': period = min(period, Period.MONTH); break;
case 'w':
case 'W': period = min(period, Period.WEEK); break;
case 'D':
case 'd':
case 'F':
case 'E': period = min(period, Period.DAY); break;
case 'a': period = min(period, Period.HALF_DAY); break;
case 'H':
case 'k':
case 'K':
case 'h': period = min(period, Period.HOUR); break;
case 'm': period = min(period, Period.MINUTE); break;
case '\'': while (suffix.charAt(++i) != '\''){} break;
case 's':
case 'S': throw audit.rotatingBySecondUnsupported(suffix);
}
}
return this;
}
/**
* Construct a new instance.
*
* @return the built audit endpoint.
* @throws IOException if an I/O error occurs.
*/
@Override
public AuditEndpoint build() throws IOException {
return new PeriodicRotatingFileAuditEndpoint(this);
}
private static > T min(T a, T b) {
return a.compareTo(b) <= 0 ? a : b;
}
}
}