blob: 8472bab5bb403d7f720357c89403d1fdbf733006 [file] [log] [blame]
/*
* 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;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileOwnerAttributeView;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.util.Constants;
import org.apache.logging.log4j.core.util.FileUtils;
/**
* Manages actual File I/O for File Appenders.
*/
public class FileManager extends OutputStreamManager {
private static final FileManagerFactory FACTORY = new FileManagerFactory();
private final boolean isAppend;
private final boolean createOnDemand;
private final boolean isLocking;
private final String advertiseURI;
private final int bufferSize;
private final Set<PosixFilePermission> filePermissions;
private final String fileOwner;
private final String fileGroup;
private final boolean attributeViewEnabled;
/**
* @since 2.9
*/
protected FileManager(final LoggerContext loggerContext, final String fileName, final OutputStream os, final boolean append, final boolean locking,
final boolean createOnDemand, final String advertiseURI, final Layout<? extends Serializable> layout,
final String filePermissions, final String fileOwner, final String fileGroup, final boolean writeHeader,
final ByteBuffer buffer) {
super(loggerContext, os, fileName, createOnDemand, layout, writeHeader, buffer);
this.isAppend = append;
this.createOnDemand = createOnDemand;
this.isLocking = locking;
this.advertiseURI = advertiseURI;
this.bufferSize = buffer.capacity();
final Set<String> views = FileSystems.getDefault().supportedFileAttributeViews();
if (views.contains("posix")) {
this.filePermissions = filePermissions != null ? PosixFilePermissions.fromString(filePermissions) : null;
this.fileGroup = fileGroup;
} else {
this.filePermissions = null;
this.fileGroup = null;
if (filePermissions != null) {
LOGGER.warn("Posix file attribute permissions defined but it is not supported by this files system.");
}
if (fileGroup != null) {
LOGGER.warn("Posix file attribute group defined but it is not supported by this files system.");
}
}
if (views.contains("owner")) {
this.fileOwner = fileOwner;
} else {
this.fileOwner = null;
if (fileOwner != null) {
LOGGER.warn("Owner file attribute defined but it is not supported by this files system.");
}
}
// Supported and defined
this.attributeViewEnabled = this.filePermissions != null || this.fileOwner != null || this.fileGroup != null;
}
/**
* Returns the FileManager.
* @param fileName The name of the file to manage.
* @param append true if the file should be appended to, false if it should be overwritten.
* @param locking true if the file should be locked while writing, false otherwise.
* @param bufferedIo true if the contents should be buffered as they are written.
* @param createOnDemand true if you want to lazy-create the file (a.k.a. on-demand.)
* @param advertiseUri the URI to use when advertising the file
* @param layout The layout
* @param bufferSize buffer size for buffered IO
* @param filePermissions File permissions
* @param fileOwner File owner
* @param fileGroup File group
* @param configuration The configuration.
* @return A FileManager for the File.
*/
public static FileManager getFileManager(final String fileName, final boolean append, boolean locking,
final boolean bufferedIo, final boolean createOnDemand, final String advertiseUri,
final Layout<? extends Serializable> layout,
final int bufferSize, final String filePermissions, final String fileOwner, final String fileGroup,
final Configuration configuration) {
if (locking && bufferedIo) {
locking = false;
}
return narrow(FileManager.class, getManager(fileName, new FactoryData(append, locking, bufferedIo, bufferSize,
createOnDemand, advertiseUri, layout, filePermissions, fileOwner, fileGroup, configuration), FACTORY));
}
@Override
protected OutputStream createOutputStream() throws IOException {
final String filename = getFileName();
LOGGER.debug("Now writing to {} at {}", filename, new Date());
final File file = new File(filename);
createParentDir(file);
final FileOutputStream fos = new FileOutputStream(file, isAppend);
if (file.exists() && file.length() == 0) {
try {
FileTime now = FileTime.fromMillis(System.currentTimeMillis());
Files.setAttribute(file.toPath(), "creationTime", now);
} catch (Exception ex) {
LOGGER.warn("Unable to set current file time for {}", filename);
}
writeHeader(fos);
}
defineAttributeView(Paths.get(filename));
return fos;
}
protected void createParentDir(File file) {
}
protected void defineAttributeView(final Path path) {
if (attributeViewEnabled) {
try {
// FileOutputStream may not create new file on all jvm
path.toFile().createNewFile();
FileUtils.defineFilePosixAttributeView(path, filePermissions, fileOwner, fileGroup);
} catch (final Exception e) {
LOGGER.error("Could not define attribute view on path \"{}\" got {}", path, e.getMessage(), e);
}
}
}
@Override
protected synchronized void write(final byte[] bytes, final int offset, final int length,
final boolean immediateFlush) {
if (isLocking) {
try {
@SuppressWarnings("resource")
final FileChannel channel = ((FileOutputStream) getOutputStream()).getChannel();
/*
* Lock the whole file. This could be optimized to only lock from the current file position. Note that
* locking may be advisory on some systems and mandatory on others, so locking just from the current
* position would allow reading on systems where locking is mandatory. Also, Java 6 will throw an
* exception if the region of the file is already locked by another FileChannel in the same JVM.
* Hopefully, that will be avoided since every file should have a single file manager - unless two
* different files strings are configured that somehow map to the same file.
*/
try (final FileLock lock = channel.lock(0, Long.MAX_VALUE, false)) {
super.write(bytes, offset, length, immediateFlush);
}
} catch (final IOException ex) {
throw new AppenderLoggingException("Unable to obtain lock on " + getName(), ex);
}
} else {
super.write(bytes, offset, length, immediateFlush);
}
}
/**
* Overrides {@link OutputStreamManager#writeToDestination(byte[], int, int)} to add support for file locking.
*
* @param bytes the array containing data
* @param offset from where to write
* @param length how many bytes to write
* @since 2.8
*/
@Override
protected synchronized void writeToDestination(final byte[] bytes, final int offset, final int length) {
if (isLocking) {
try {
@SuppressWarnings("resource")
final FileChannel channel = ((FileOutputStream) getOutputStream()).getChannel();
/*
* Lock the whole file. This could be optimized to only lock from the current file position. Note that
* locking may be advisory on some systems and mandatory on others, so locking just from the current
* position would allow reading on systems where locking is mandatory. Also, Java 6 will throw an
* exception if the region of the file is already locked by another FileChannel in the same JVM.
* Hopefully, that will be avoided since every file should have a single file manager - unless two
* different files strings are configured that somehow map to the same file.
*/
try (final FileLock lock = channel.lock(0, Long.MAX_VALUE, false)) {
super.writeToDestination(bytes, offset, length);
}
} catch (final IOException ex) {
throw new AppenderLoggingException("Unable to obtain lock on " + getName(), ex);
}
} else {
super.writeToDestination(bytes, offset, length);
}
}
/**
* Returns the name of the File being managed.
* @return The name of the File being managed.
*/
public String getFileName() {
return getName();
}
/**
* Returns the append status.
* @return true if the file will be appended to, false if it is overwritten.
*/
public boolean isAppend() {
return isAppend;
}
/**
* Returns the lazy-create.
* @return true if the file will be lazy-created.
*/
public boolean isCreateOnDemand() {
return createOnDemand;
}
/**
* Returns the lock status.
* @return true if the file will be locked when writing, false otherwise.
*/
public boolean isLocking() {
return isLocking;
}
/**
* Returns the buffer size to use if the appender was configured with BufferedIO=true, otherwise returns a negative
* number.
* @return the buffer size, or a negative number if the output stream is not buffered
*/
public int getBufferSize() {
return bufferSize;
}
/**
* Returns posix file permissions if defined and the OS supports posix file attribute,
* null otherwise.
* @return File posix permissions
* @see PosixFileAttributeView
*/
public Set<PosixFilePermission> getFilePermissions() {
return filePermissions;
}
/**
* Returns file owner if defined and the OS supports owner file attribute view,
* null otherwise.
* @return File owner
* @see FileOwnerAttributeView
*/
public String getFileOwner() {
return fileOwner;
}
/**
* Returns file group if defined and the OS supports posix/group file attribute view,
* null otherwise.
* @return File group
* @see PosixFileAttributeView
*/
public String getFileGroup() {
return fileGroup;
}
/**
* Returns true if file attribute view enabled for this file manager.
*
* @return True if posix or owner supported and defined false otherwise.
*/
public boolean isAttributeViewEnabled() {
return attributeViewEnabled;
}
/**
* FileManager's content format is specified by: <code>Key: "fileURI" Value: provided "advertiseURI" param</code>.
*
* @return Map of content format keys supporting FileManager
*/
@Override
public Map<String, String> getContentFormat() {
final Map<String, String> result = new HashMap<>(super.getContentFormat());
result.put("fileURI", advertiseURI);
return result;
}
/**
* Factory Data.
*/
private static class FactoryData extends ConfigurationFactoryData {
private final boolean append;
private final boolean locking;
private final boolean bufferedIo;
private final int bufferSize;
private final boolean createOnDemand;
private final String advertiseURI;
private final Layout<? extends Serializable> layout;
private final String filePermissions;
private final String fileOwner;
private final String fileGroup;
/**
* Constructor.
* @param append Append status.
* @param locking Locking status.
* @param bufferedIo Buffering flag.
* @param bufferSize Buffer size.
* @param createOnDemand if you want to lazy-create the file (a.k.a. on-demand.)
* @param advertiseURI the URI to use when advertising the file
* @param layout The layout
* @param filePermissions File permissions
* @param fileOwner File owner
* @param fileGroup File group
* @param configuration the configuration
*/
public FactoryData(final boolean append, final boolean locking, final boolean bufferedIo, final int bufferSize,
final boolean createOnDemand, final String advertiseURI, final Layout<? extends Serializable> layout,
final String filePermissions, final String fileOwner, final String fileGroup,
final Configuration configuration) {
super(configuration);
this.append = append;
this.locking = locking;
this.bufferedIo = bufferedIo;
this.bufferSize = bufferSize;
this.createOnDemand = createOnDemand;
this.advertiseURI = advertiseURI;
this.layout = layout;
this.filePermissions = filePermissions;
this.fileOwner = fileOwner;
this.fileGroup = fileGroup;
}
}
/**
* Factory to create a FileManager.
*/
private static class FileManagerFactory implements ManagerFactory<FileManager, FactoryData> {
/**
* Creates a FileManager.
* @param name The name of the File.
* @param data The FactoryData
* @return The FileManager for the File.
*/
@Override
public FileManager createManager(final String name, final FactoryData data) {
final File file = new File(name);
try {
FileUtils.makeParentDirs(file);
final int actualSize = data.bufferedIo ? data.bufferSize : Constants.ENCODER_BYTE_BUFFER_SIZE;
final ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[actualSize]);
final FileOutputStream fos = data.createOnDemand ? null : new FileOutputStream(file, data.append);
final boolean writeHeader = file.exists() && file.length() == 0;
final FileManager fm = new FileManager(data.getLoggerContext(), name, fos, data.append, data.locking,
data.createOnDemand, data.advertiseURI, data.layout,
data.filePermissions, data.fileOwner, data.fileGroup, writeHeader, byteBuffer);
if (fos != null && fm.attributeViewEnabled) {
fm.defineAttributeView(file.toPath());
}
return fm;
} catch (final IOException ex) {
LOGGER.error("FileManager (" + name + ") " + ex, ex);
}
return null;
}
}
}