package io.dropwizard.logging;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.encoder.LayoutWrappingEncoder;
import ch.qos.logback.core.rolling.DefaultTimeBasedFileNamingAndTriggeringPolicy;
import ch.qos.logback.core.rolling.FixedWindowRollingPolicy;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy;
import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy;
import ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicy;
import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
import ch.qos.logback.core.spi.DeferredProcessingAware;
import ch.qos.logback.core.util.FileSize;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.dropwizard.logging.async.AsyncAppenderFactory;
import io.dropwizard.logging.filter.LevelFilterFactory;
import io.dropwizard.logging.layout.LayoutFactory;
import io.dropwizard.util.Size;
import io.dropwizard.validation.MinSize;
import io.dropwizard.validation.ValidationMethod;
import javax.validation.constraints.Min;
/**
* An {@link AppenderFactory} implementation which provides an appender that writes events to a file, archiving older
* files as it goes.
* <p/>
* <b>Configuration Parameters:</b>
* <table>
* <tr>
* <td>Name</td>
* <td>Default</td>
* <td>Description</td>
* </tr>
* <tr>
* <td>{@code type}</td>
* <td><b>REQUIRED</b></td>
* <td>The appender type. Must be {@code file}.</td>
* </tr>
* <tr>
* <td>{@code threshold}</td>
* <td>{@code ALL}</td>
* <td>The lowest level of events to write to the file.</td>
* </tr>
* <tr>
* <td>{@code currentLogFilename}</td>
* <td><b>REQUIRED</b></td>
* <td>The filename where current events are logged.</td>
* </tr>
* <tr>
* <td>{@code archive}</td>
* <td>{@code true}</td>
* <td>Whether or not to archive old events in separate files.</td>
* </tr>
* <tr>
* <td>{@code archivedLogFilenamePattern}</td>
* <td><b>REQUIRED</b> if {@code archive} is {@code true}.</td>
* <td>
* The filename pattern for archived files.
* If {@code maxFileSize} is specified, rollover is size-based, and the pattern must contain {@code %i} for
* an integer index of the archived file.
* Otherwise rollover is date-based, and the pattern must contain {@code %d}, which is replaced with the
* date in {@code yyyy-MM-dd} form.
* If the pattern ends with {@code .gz} or {@code .zip}, files will be compressed as they are archived.
* </td>
* </tr>
* <tr>
* <td>{@code archivedFileCount}</td>
* <td>{@code 5}</td>
* <td>
* The number of archived files to keep. Must be greater than or equal to {@code 0}. Zero is a
* special value signifying to keep infinite logs (use with caution)
* </td>
* </tr>
* <tr>
* <td>{@code maxFileSize}</td>
* <td>(unlimited)</td>
* <td>
* The maximum size of the currently active file before a rollover is triggered. The value can be expressed
* in bytes, kilobytes, megabytes, gigabytes, and terabytes by appending B, K, MB, GB, or TB to the
* numeric value. Examples include 100MB, 1GB, 1TB. Sizes can also be spelled out, such as 100 megabytes,
* 1 gigabyte, 1 terabyte.
* </td>
* </tr>
* <tr>
* <td>{@code timeZone}</td>
* <td>{@code UTC}</td>
* <td>The time zone to which event timestamps will be converted.</td>
* </tr>
* <tr>
* <td>{@code logFormat}</td>
* <td>the default format</td>
* <td>
* The Logback pattern with which events will be formatted. See
* <a href="http://logback.qos.ch/manual/layouts.html#conversionWord">the Logback documentation</a>
* for details.
* </td>
* </tr>
* <tr>
* <td>{@code bufferSize}</td>
* <td>8KB</td>
* <td>
* The buffer size of the underlying FileAppender (setting added in logback 1.1.10). Increasing this from
* the default of 8KB to 256KB is reported to significantly reduce thread contention.
* </td>
* </tr>
* </table>
*
* @see AbstractAppenderFactory
*/
@JsonTypeName("file")
public class FileAppenderFactory<E extends DeferredProcessingAware> extends AbstractAppenderFactory<E> {
private String currentLogFilename;
private boolean archive = true;
private String archivedLogFilenamePattern;
@Min(0)
private int archivedFileCount = 5;
private Size maxFileSize;
@MinSize(1)
private Size bufferSize = Size.bytes(FileAppender.DEFAULT_BUFFER_SIZE);
@JsonProperty
public String getCurrentLogFilename() {
return currentLogFilename;
}
@JsonProperty
public void setCurrentLogFilename(String currentLogFilename) {
this.currentLogFilename = currentLogFilename;
}
@JsonProperty
public boolean isArchive() {
return archive;
}
@JsonProperty
public void setArchive(boolean archive) {
this.archive = archive;
}
@JsonProperty
public String getArchivedLogFilenamePattern() {
return archivedLogFilenamePattern;
}
@JsonProperty
public void setArchivedLogFilenamePattern(String archivedLogFilenamePattern) {
this.archivedLogFilenamePattern = archivedLogFilenamePattern;
}
@JsonProperty
public int getArchivedFileCount() {
return archivedFileCount;
}
@JsonProperty
public void setArchivedFileCount(int archivedFileCount) {
this.archivedFileCount = archivedFileCount;
}
@JsonProperty
public Size getMaxFileSize() {
return maxFileSize;
}
@JsonProperty
public void setMaxFileSize(Size maxFileSize) {
this.maxFileSize = maxFileSize;
}
@JsonProperty
public Size getBufferSize() {
return bufferSize;
}
@JsonProperty
public void setBufferSize(Size bufferSize) {
this.bufferSize = bufferSize;
}
@JsonIgnore
@ValidationMethod(message = "must have archivedLogFilenamePattern if archive is true")
public boolean isValidArchiveConfiguration() {
return !archive || (archivedLogFilenamePattern != null);
}
@JsonIgnore
@ValidationMethod(message = "when specifying maxFileSize, archivedLogFilenamePattern must contain %i")
public boolean isValidForMaxFileSizeSetting() {
return !archive || maxFileSize == null ||
(archivedLogFilenamePattern != null && archivedLogFilenamePattern.contains("%i"));
}
@JsonIgnore
@ValidationMethod(message = "when archivedLogFilenamePattern contains %i, maxFileSize must be specified")
public boolean isMaxFileSizeSettingSpecified() {
return !archive || !(archivedLogFilenamePattern != null && archivedLogFilenamePattern.contains("%i")) ||
maxFileSize != null;
}
@JsonIgnore
@ValidationMethod(message = "currentLogFilename can only be null when archiving is enabled")
public boolean isValidFileConfiguration() {
return archive || currentLogFilename != null;
}
@Override
public Appender<E> build(LoggerContext context, String applicationName, LayoutFactory<E> layoutFactory,
LevelFilterFactory<E> levelFilterFactory, AsyncAppenderFactory<E> asyncAppenderFactory) {
final FileAppender<E> appender = buildAppender(context);
appender.setName("file-appender");
appender.setAppend(true);
appender.setContext(context);
final LayoutWrappingEncoder<E> layoutEncoder = new LayoutWrappingEncoder<>();
layoutEncoder.setLayout(buildLayout(context, layoutFactory));
appender.setEncoder(layoutEncoder);
appender.setPrudent(false);
appender.addFilter(levelFilterFactory.build(threshold));
getFilterFactories().forEach(f -> appender.addFilter(f.build()));
appender.start();
return wrapAsync(appender, asyncAppenderFactory);
}
protected FileAppender<E> buildAppender(LoggerContext context) {
if (archive) {
final RollingFileAppender<E> appender = new RollingFileAppender<>();
appender.setContext(context);
appender.setFile(currentLogFilename);
appender.setBufferSize(new FileSize(bufferSize.toBytes()));
if (maxFileSize != null && !archivedLogFilenamePattern.contains("%d")) {
final FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy();
rollingPolicy.setContext(context);
rollingPolicy.setMaxIndex(getArchivedFileCount());
rollingPolicy.setFileNamePattern(getArchivedLogFilenamePattern());
rollingPolicy.setParent(appender);
rollingPolicy.start();
appender.setRollingPolicy(rollingPolicy);
final SizeBasedTriggeringPolicy<E> triggeringPolicy = new SizeBasedTriggeringPolicy<>();
triggeringPolicy.setMaxFileSize(new FileSize(maxFileSize.toBytes()));
triggeringPolicy.setContext(context);
triggeringPolicy.start();
appender.setTriggeringPolicy(triggeringPolicy);
return appender;
} else {
final TimeBasedRollingPolicy<E> rollingPolicy;
if (maxFileSize == null) {
rollingPolicy = new TimeBasedRollingPolicy<>();
} else {
final SizeAndTimeBasedRollingPolicy<E> sizeAndTimeBasedRollingPolicy = new SizeAndTimeBasedRollingPolicy<>();
sizeAndTimeBasedRollingPolicy.setMaxFileSize(new FileSize(maxFileSize.toBytes()));
rollingPolicy = sizeAndTimeBasedRollingPolicy;
}
final TimeBasedFileNamingAndTriggeringPolicy<E> triggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy<>();
triggeringPolicy.setContext(context);
rollingPolicy.setContext(context);
rollingPolicy.setFileNamePattern(archivedLogFilenamePattern);
rollingPolicy.setTimeBasedFileNamingAndTriggeringPolicy(triggeringPolicy);
triggeringPolicy.setTimeBasedRollingPolicy(rollingPolicy);
rollingPolicy.setMaxHistory(archivedFileCount);
appender.setRollingPolicy(rollingPolicy);
appender.setTriggeringPolicy(triggeringPolicy);
rollingPolicy.setParent(appender);
rollingPolicy.start();
return appender;
}
}
final FileAppender<E> appender = new FileAppender<>();
appender.setContext(context);
appender.setFile(currentLogFilename);
appender.setBufferSize(new FileSize(bufferSize.toBytes()));
return appender;
}
}