598 lines
20 KiB
Java
598 lines
20 KiB
Java
/*
|
|
* 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.juli;
|
|
|
|
import java.io.BufferedOutputStream;
|
|
import java.io.File;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.OutputStream;
|
|
import java.io.OutputStreamWriter;
|
|
import java.io.PrintWriter;
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.nio.file.DirectoryStream;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.security.AccessController;
|
|
import java.security.PrivilegedAction;
|
|
import java.sql.Timestamp;
|
|
import java.text.ParseException;
|
|
import java.text.SimpleDateFormat;
|
|
import java.util.Calendar;
|
|
import java.util.Date;
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.ThreadFactory;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import java.util.concurrent.locks.ReadWriteLock;
|
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
|
import java.util.logging.ErrorManager;
|
|
import java.util.logging.Filter;
|
|
import java.util.logging.Formatter;
|
|
import java.util.logging.Handler;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.LogManager;
|
|
import java.util.logging.LogRecord;
|
|
import java.util.regex.Pattern;
|
|
|
|
/**
|
|
* Implementation of <b>Handler</b> that appends log messages to a file
|
|
* named {prefix}{date}{suffix} in a configured directory.
|
|
*
|
|
* <p>The following configuration properties are available:</p>
|
|
*
|
|
* <ul>
|
|
* <li><code>directory</code> - The directory where to create the log file.
|
|
* If the path is not absolute, it is relative to the current working
|
|
* directory of the application. The Apache Tomcat configuration files usually
|
|
* specify an absolute path for this property,
|
|
* <code>${catalina.base}/logs</code>
|
|
* Default value: <code>logs</code></li>
|
|
* <li><code>rotatable</code> - If <code>true</code>, the log file will be
|
|
* rotated on the first write past midnight and the filename will be
|
|
* <code>{prefix}{date}{suffix}</code>, where date is yyyy-MM-dd. If <code>false</code>,
|
|
* the file will not be rotated and the filename will be <code>{prefix}{suffix}</code>.
|
|
* Default value: <code>true</code></li>
|
|
* <li><code>prefix</code> - The leading part of the log file name.
|
|
* Default value: <code>juli.</code></li>
|
|
* <li><code>suffix</code> - The trailing part of the log file name. Default value: <code>.log</code></li>
|
|
* <li><code>bufferSize</code> - Configures buffering. The value of <code>0</code>
|
|
* uses system default buffering (typically an 8K buffer will be used). A
|
|
* value of <code><0</code> forces a writer flush upon each log write. A
|
|
* value <code>>0</code> uses a BufferedOutputStream with the defined
|
|
* value but note that the system default buffering will also be
|
|
* applied. Default value: <code>-1</code></li>
|
|
* <li><code>encoding</code> - Character set used by the log file. Default value:
|
|
* empty string, which means to use the system default character set.</li>
|
|
* <li><code>level</code> - The level threshold for this Handler. See the
|
|
* <code>java.util.logging.Level</code> class for the possible levels.
|
|
* Default value: <code>ALL</code></li>
|
|
* <li><code>filter</code> - The <code>java.util.logging.Filter</code>
|
|
* implementation class name for this Handler. Default value: unset</li>
|
|
* <li><code>formatter</code> - The <code>java.util.logging.Formatter</code>
|
|
* implementation class name for this Handler. Default value:
|
|
* <code>java.util.logging.SimpleFormatter</code></li>
|
|
* <li><code>maxDays</code> - The maximum number of days to keep the log
|
|
* files. If the specified value is <code><=0</code> then the log files
|
|
* will be kept on the file system forever, otherwise they will be kept the
|
|
* specified maximum days. Default value: <code>-1</code>.</li>
|
|
* </ul>
|
|
*/
|
|
public class FileHandler extends Handler {
|
|
|
|
public static final int DEFAULT_MAX_DAYS = -1;
|
|
|
|
private static final ExecutorService DELETE_FILES_SERVICE =
|
|
Executors.newSingleThreadExecutor(new ThreadFactory() {
|
|
private static final String NAME_PREFIX = "FileHandlerLogFilesCleaner-";
|
|
private final boolean isSecurityEnabled;
|
|
private final ThreadGroup group;
|
|
private final AtomicInteger threadNumber = new AtomicInteger(1);
|
|
|
|
{
|
|
SecurityManager s = System.getSecurityManager();
|
|
if (s == null) {
|
|
this.isSecurityEnabled = false;
|
|
this.group = Thread.currentThread().getThreadGroup();
|
|
} else {
|
|
this.isSecurityEnabled = true;
|
|
this.group = s.getThreadGroup();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Thread newThread(Runnable r) {
|
|
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
|
|
try {
|
|
// Threads should not be created by the webapp classloader
|
|
if (isSecurityEnabled) {
|
|
AccessController.doPrivileged(new PrivilegedAction<Void>() {
|
|
|
|
@Override
|
|
public Void run() {
|
|
Thread.currentThread()
|
|
.setContextClassLoader(getClass().getClassLoader());
|
|
return null;
|
|
}
|
|
});
|
|
} else {
|
|
Thread.currentThread()
|
|
.setContextClassLoader(getClass().getClassLoader());
|
|
}
|
|
Thread t = new Thread(group, r,
|
|
NAME_PREFIX + threadNumber.getAndIncrement());
|
|
t.setDaemon(true);
|
|
return t;
|
|
} finally {
|
|
if (isSecurityEnabled) {
|
|
AccessController.doPrivileged(new PrivilegedAction<Void>() {
|
|
|
|
@Override
|
|
public Void run() {
|
|
Thread.currentThread().setContextClassLoader(loader);
|
|
return null;
|
|
}
|
|
});
|
|
} else {
|
|
Thread.currentThread().setContextClassLoader(loader);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// ------------------------------------------------------------ Constructor
|
|
|
|
|
|
public FileHandler() {
|
|
this(null, null, null, DEFAULT_MAX_DAYS);
|
|
}
|
|
|
|
|
|
public FileHandler(String directory, String prefix, String suffix) {
|
|
this(directory, prefix, suffix, DEFAULT_MAX_DAYS);
|
|
}
|
|
|
|
public FileHandler(String directory, String prefix, String suffix, int maxDays) {
|
|
this.directory = directory;
|
|
this.prefix = prefix;
|
|
this.suffix = suffix;
|
|
this.maxDays = maxDays;
|
|
configure();
|
|
openWriter();
|
|
clean();
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------- Instance Variables
|
|
|
|
|
|
/**
|
|
* The as-of date for the currently open log file, or a zero-length
|
|
* string if there is no open log file.
|
|
*/
|
|
private volatile String date = "";
|
|
|
|
|
|
/**
|
|
* The directory in which log files are created.
|
|
*/
|
|
private String directory = null;
|
|
|
|
|
|
/**
|
|
* The prefix that is added to log file filenames.
|
|
*/
|
|
private String prefix = null;
|
|
|
|
|
|
/**
|
|
* The suffix that is added to log file filenames.
|
|
*/
|
|
private String suffix = null;
|
|
|
|
|
|
/**
|
|
* Determines whether the log file is rotatable
|
|
*/
|
|
private boolean rotatable = true;
|
|
|
|
|
|
/**
|
|
* Maximum number of days to keep the log files
|
|
*/
|
|
private int maxDays = DEFAULT_MAX_DAYS;
|
|
|
|
|
|
/**
|
|
* The PrintWriter to which we are currently logging, if any.
|
|
*/
|
|
private volatile PrintWriter writer = null;
|
|
|
|
|
|
/**
|
|
* Lock used to control access to the writer.
|
|
*/
|
|
protected final ReadWriteLock writerLock = new ReentrantReadWriteLock();
|
|
|
|
|
|
/**
|
|
* Log buffer size.
|
|
*/
|
|
private int bufferSize = -1;
|
|
|
|
|
|
/**
|
|
* Represents a file name pattern of type {prefix}{date}{suffix}.
|
|
* The date is YYYY-MM-DD
|
|
*/
|
|
private Pattern pattern;
|
|
|
|
|
|
// --------------------------------------------------------- Public Methods
|
|
|
|
|
|
/**
|
|
* Format and publish a <code>LogRecord</code>.
|
|
*
|
|
* @param record description of the log event
|
|
*/
|
|
@Override
|
|
public void publish(LogRecord record) {
|
|
|
|
if (!isLoggable(record)) {
|
|
return;
|
|
}
|
|
|
|
// Construct the timestamp we will use, if requested
|
|
Timestamp ts = new Timestamp(System.currentTimeMillis());
|
|
String tsDate = ts.toString().substring(0, 10);
|
|
|
|
writerLock.readLock().lock();
|
|
try {
|
|
// If the date has changed, switch log files
|
|
if (rotatable && !date.equals(tsDate)) {
|
|
// Upgrade to writeLock before we switch
|
|
writerLock.readLock().unlock();
|
|
writerLock.writeLock().lock();
|
|
try {
|
|
// Make sure another thread hasn't already done this
|
|
if (!date.equals(tsDate)) {
|
|
closeWriter();
|
|
date = tsDate;
|
|
openWriter();
|
|
clean();
|
|
}
|
|
} finally {
|
|
// Downgrade to read-lock. This ensures the writer remains valid
|
|
// until the log message is written
|
|
writerLock.readLock().lock();
|
|
writerLock.writeLock().unlock();
|
|
}
|
|
}
|
|
|
|
String result = null;
|
|
try {
|
|
result = getFormatter().format(record);
|
|
} catch (Exception e) {
|
|
reportError(null, e, ErrorManager.FORMAT_FAILURE);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (writer != null) {
|
|
writer.write(result);
|
|
if (bufferSize < 0) {
|
|
writer.flush();
|
|
}
|
|
} else {
|
|
reportError("FileHandler is closed or not yet initialized, unable to log ["
|
|
+ result + "]", null, ErrorManager.WRITE_FAILURE);
|
|
}
|
|
} catch (Exception e) {
|
|
reportError(null, e, ErrorManager.WRITE_FAILURE);
|
|
}
|
|
} finally {
|
|
writerLock.readLock().unlock();
|
|
}
|
|
}
|
|
|
|
|
|
// -------------------------------------------------------- Private Methods
|
|
|
|
|
|
/**
|
|
* Close the currently open log file (if any).
|
|
*/
|
|
@Override
|
|
public void close() {
|
|
closeWriter();
|
|
}
|
|
|
|
protected void closeWriter() {
|
|
|
|
writerLock.writeLock().lock();
|
|
try {
|
|
if (writer == null) {
|
|
return;
|
|
}
|
|
writer.write(getFormatter().getTail(this));
|
|
writer.flush();
|
|
writer.close();
|
|
writer = null;
|
|
date = "";
|
|
} catch (Exception e) {
|
|
reportError(null, e, ErrorManager.CLOSE_FAILURE);
|
|
} finally {
|
|
writerLock.writeLock().unlock();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Flush the writer.
|
|
*/
|
|
@Override
|
|
public void flush() {
|
|
|
|
writerLock.readLock().lock();
|
|
try {
|
|
if (writer == null) {
|
|
return;
|
|
}
|
|
writer.flush();
|
|
} catch (Exception e) {
|
|
reportError(null, e, ErrorManager.FLUSH_FAILURE);
|
|
} finally {
|
|
writerLock.readLock().unlock();
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Configure from <code>LogManager</code> properties.
|
|
*/
|
|
private void configure() {
|
|
|
|
Timestamp ts = new Timestamp(System.currentTimeMillis());
|
|
date = ts.toString().substring(0, 10);
|
|
|
|
String className = this.getClass().getName(); //allow classes to override
|
|
|
|
ClassLoader cl = Thread.currentThread().getContextClassLoader();
|
|
|
|
// Retrieve configuration of logging file name
|
|
rotatable = Boolean.parseBoolean(getProperty(className + ".rotatable", "true"));
|
|
if (directory == null) {
|
|
directory = getProperty(className + ".directory", "logs");
|
|
}
|
|
if (prefix == null) {
|
|
prefix = getProperty(className + ".prefix", "juli.");
|
|
}
|
|
if (suffix == null) {
|
|
suffix = getProperty(className + ".suffix", ".log");
|
|
}
|
|
|
|
// https://bz.apache.org/bugzilla/show_bug.cgi?id=61232
|
|
boolean shouldCheckForRedundantSeparator = !rotatable && !prefix.isEmpty()
|
|
&& !suffix.isEmpty();
|
|
// assuming separator is just one char, if there are use cases with
|
|
// more, the notion of separator might be introduced
|
|
if (shouldCheckForRedundantSeparator
|
|
&& (prefix.charAt(prefix.length() - 1) == suffix.charAt(0))) {
|
|
suffix = suffix.substring(1);
|
|
}
|
|
|
|
pattern = Pattern.compile("^(" + Pattern.quote(prefix) + ")\\d{4}-\\d{1,2}-\\d{1,2}("
|
|
+ Pattern.quote(suffix) + ")$");
|
|
String sMaxDays = getProperty(className + ".maxDays", String.valueOf(DEFAULT_MAX_DAYS));
|
|
if (maxDays <= 0) {
|
|
try {
|
|
maxDays = Integer.parseInt(sMaxDays);
|
|
} catch (NumberFormatException ignore) {
|
|
// no-op
|
|
}
|
|
}
|
|
String sBufferSize = getProperty(className + ".bufferSize", String.valueOf(bufferSize));
|
|
try {
|
|
bufferSize = Integer.parseInt(sBufferSize);
|
|
} catch (NumberFormatException ignore) {
|
|
//no op
|
|
}
|
|
|
|
// Get encoding for the logging file
|
|
String encoding = getProperty(className + ".encoding", null);
|
|
if (encoding != null && encoding.length() > 0) {
|
|
try {
|
|
setEncoding(encoding);
|
|
} catch (UnsupportedEncodingException ex) {
|
|
// Ignore
|
|
}
|
|
}
|
|
|
|
// Get logging level for the handler
|
|
setLevel(Level.parse(getProperty(className + ".level", "" + Level.ALL)));
|
|
|
|
// Get filter configuration
|
|
String filterName = getProperty(className + ".filter", null);
|
|
if (filterName != null) {
|
|
try {
|
|
setFilter((Filter) cl.loadClass(filterName).getConstructor().newInstance());
|
|
} catch (Exception e) {
|
|
// Ignore
|
|
}
|
|
}
|
|
|
|
// Set formatter
|
|
String formatterName = getProperty(className + ".formatter", null);
|
|
if (formatterName != null) {
|
|
try {
|
|
setFormatter((Formatter) cl.loadClass(
|
|
formatterName).getConstructor().newInstance());
|
|
} catch (Exception e) {
|
|
// Ignore and fallback to defaults
|
|
setFormatter(new OneLineFormatter());
|
|
}
|
|
} else {
|
|
setFormatter(new OneLineFormatter());
|
|
}
|
|
|
|
// Set error manager
|
|
setErrorManager(new ErrorManager());
|
|
}
|
|
|
|
|
|
private String getProperty(String name, String defaultValue) {
|
|
String value = LogManager.getLogManager().getProperty(name);
|
|
if (value == null) {
|
|
value = defaultValue;
|
|
} else {
|
|
value = value.trim();
|
|
}
|
|
return value;
|
|
}
|
|
|
|
|
|
/**
|
|
* Open the new log file for the date specified by <code>date</code>.
|
|
*/
|
|
protected void open() {
|
|
openWriter();
|
|
}
|
|
|
|
protected void openWriter() {
|
|
|
|
// Create the directory if necessary
|
|
File dir = new File(directory);
|
|
if (!dir.mkdirs() && !dir.isDirectory()) {
|
|
reportError("Unable to create [" + dir + "]", null, ErrorManager.OPEN_FAILURE);
|
|
writer = null;
|
|
return;
|
|
}
|
|
|
|
// Open the current log file
|
|
writerLock.writeLock().lock();
|
|
FileOutputStream fos = null;
|
|
OutputStream os = null;
|
|
try {
|
|
File pathname = new File(dir.getAbsoluteFile(), prefix
|
|
+ (rotatable ? date : "") + suffix);
|
|
File parent = pathname.getParentFile();
|
|
if (!parent.mkdirs() && !parent.isDirectory()) {
|
|
reportError("Unable to create [" + parent + "]", null, ErrorManager.OPEN_FAILURE);
|
|
writer = null;
|
|
return;
|
|
}
|
|
String encoding = getEncoding();
|
|
fos = new FileOutputStream(pathname, true);
|
|
os = bufferSize > 0 ? new BufferedOutputStream(fos, bufferSize) : fos;
|
|
writer = new PrintWriter(
|
|
(encoding != null) ? new OutputStreamWriter(os, encoding)
|
|
: new OutputStreamWriter(os), false);
|
|
writer.write(getFormatter().getHead(this));
|
|
} catch (Exception e) {
|
|
reportError(null, e, ErrorManager.OPEN_FAILURE);
|
|
writer = null;
|
|
if (fos != null) {
|
|
try {
|
|
fos.close();
|
|
} catch (IOException e1) {
|
|
// Ignore
|
|
}
|
|
}
|
|
if (os != null) {
|
|
try {
|
|
os.close();
|
|
} catch (IOException e1) {
|
|
// Ignore
|
|
}
|
|
}
|
|
} finally {
|
|
writerLock.writeLock().unlock();
|
|
}
|
|
}
|
|
|
|
private void clean() {
|
|
if (maxDays <= 0) {
|
|
return;
|
|
}
|
|
DELETE_FILES_SERVICE.submit(new Runnable() {
|
|
|
|
@Override
|
|
public void run() {
|
|
try (DirectoryStream<Path> files = streamFilesForDelete()) {
|
|
for (Path file : files) {
|
|
Files.delete(file);
|
|
}
|
|
} catch (IOException e) {
|
|
reportError("Unable to delete log files older than [" + maxDays + "] days",
|
|
null, ErrorManager.GENERIC_FAILURE);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private DirectoryStream<Path> streamFilesForDelete() throws IOException {
|
|
final Date maxDaysOffset = getMaxDaysOffset();
|
|
final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
|
|
return Files.newDirectoryStream(new File(directory).toPath(),
|
|
new DirectoryStream.Filter<Path>() {
|
|
|
|
@Override
|
|
public boolean accept(Path path) throws IOException {
|
|
boolean result = false;
|
|
String date = obtainDateFromPath(path);
|
|
if (date != null) {
|
|
try {
|
|
Date dateFromFile = formatter.parse(date);
|
|
result = dateFromFile.before(maxDaysOffset);
|
|
} catch (ParseException e) {
|
|
// no-op
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
});
|
|
}
|
|
|
|
private String obtainDateFromPath(Path path) {
|
|
Path fileName = path.getFileName();
|
|
if (fileName == null) {
|
|
return null;
|
|
}
|
|
String date = fileName.toString();
|
|
if (pattern.matcher(date).matches()) {
|
|
date = date.substring(prefix.length());
|
|
return date.substring(0, date.length() - suffix.length());
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private Date getMaxDaysOffset() {
|
|
Calendar cal = Calendar.getInstance();
|
|
cal.set(Calendar.HOUR_OF_DAY, 0);
|
|
cal.set(Calendar.MINUTE, 0);
|
|
cal.set(Calendar.SECOND, 0);
|
|
cal.set(Calendar.MILLISECOND, 0);
|
|
cal.add(Calendar.DATE, -maxDays);
|
|
return cal.getTime();
|
|
}
|
|
}
|