/*
* 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.catalina.valves;
import java.io.BufferedWriter;
import java.io.CharArrayWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import org.apache.catalina.LifecycleException;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.buf.B2CConverter;
This is a concrete implementation of AbstractAccessLogValve
that outputs the access log to a file. The features of this implementation include:
- Automatic date-based rollover of log files
- Optional log file rotation
For UNIX users, another field called checkExists
is also
available. If set to true, the log file's existence will be checked before
each logging. This way an external log rotator can move the file
somewhere and Tomcat will start with a new file.
For JMX junkies, a public method called rotate
has
been made available to allow you to tell this instance to move
the existing log file to somewhere else and start writing a new log file.
/**
* This is a concrete implementation of {@link AbstractAccessLogValve} that
* outputs the access log to a file. The features of this implementation
* include:
* <ul>
* <li>Automatic date-based rollover of log files</li>
* <li>Optional log file rotation</li>
* </ul>
* <p>
* For UNIX users, another field called <code>checkExists</code> is also
* available. If set to true, the log file's existence will be checked before
* each logging. This way an external log rotator can move the file
* somewhere and Tomcat will start with a new file.
* </p>
*
* <p>
* For JMX junkies, a public method called <code>rotate</code> has
* been made available to allow you to tell this instance to move
* the existing log file to somewhere else and start writing a new log file.
* </p>
*/
public class AccessLogValve extends AbstractAccessLogValve {
private static final Log log = LogFactory.getLog(AccessLogValve.class);
//------------------------------------------------------ Constructor
public AccessLogValve() {
super();
}
// ----------------------------------------------------- Instance Variables
The as-of date for the currently open log file, or a zero-length
string if there is no open log file.
/**
* 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 dateStamp = "";
The directory in which log files are created.
/**
* The directory in which log files are created.
*/
private String directory = "logs";
The prefix that is added to log file filenames.
/**
* The prefix that is added to log file filenames.
*/
protected volatile String prefix = "access_log";
Should we rotate our log file? Default is true (like old behavior)
/**
* Should we rotate our log file? Default is true (like old behavior)
*/
protected boolean rotatable = true;
Should we defer inclusion of the date stamp in the file
name until rotate time? Default is false.
/**
* Should we defer inclusion of the date stamp in the file
* name until rotate time? Default is false.
*/
protected boolean renameOnRotate = false;
Buffered logging.
/**
* Buffered logging.
*/
private boolean buffered = true;
The suffix that is added to log file filenames.
/**
* The suffix that is added to log file filenames.
*/
protected volatile String suffix = "";
The PrintWriter to which we are currently logging, if any.
/**
* The PrintWriter to which we are currently logging, if any.
*/
protected PrintWriter writer = null;
A date formatter to format a Date using the format
given by fileDateFormat
.
/**
* A date formatter to format a Date using the format
* given by <code>fileDateFormat</code>.
*/
protected SimpleDateFormat fileDateFormatter = null;
The current log file we are writing to. Helpful when checkExists
is true.
/**
* The current log file we are writing to. Helpful when checkExists
* is true.
*/
protected File currentLogFile = null;
Instant when the log daily rotation was last checked.
/**
* Instant when the log daily rotation was last checked.
*/
private volatile long rotationLastChecked = 0L;
Do we check for log file existence? Helpful if an external
agent renames the log file so we can automagically recreate it.
/**
* Do we check for log file existence? Helpful if an external
* agent renames the log file so we can automagically recreate it.
*/
private boolean checkExists = false;
Date format to place in log file name.
/**
* Date format to place in log file name.
*/
protected String fileDateFormat = ".yyyy-MM-dd";
Character set used by the log file. If it is null
, the
system default character set will be used. An empty string will be
treated as null
when this property is assigned.
/**
* Character set used by the log file. If it is <code>null</code>, the
* system default character set will be used. An empty string will be
* treated as <code>null</code> when this property is assigned.
*/
protected volatile String encoding = null;
The number of days to retain the access log files before they are
removed.
/**
* The number of days to retain the access log files before they are
* removed.
*/
private int maxDays = -1;
private volatile boolean checkForOldLogs = false;
// ------------------------------------------------------------- Properties
public int getMaxDays() {
return maxDays;
}
public void setMaxDays(int maxDays) {
this.maxDays = maxDays;
}
Returns: the directory in which we create log files.
/**
* @return the directory in which we create log files.
*/
public String getDirectory() {
return directory;
}
Set the directory in which we create log files.
Params: - directory – The new log file directory
/**
* Set the directory in which we create log files.
*
* @param directory The new log file directory
*/
public void setDirectory(String directory) {
this.directory = directory;
}
Check for file existence before logging.
Returns: true
if file existence is checked first
/**
* Check for file existence before logging.
* @return <code>true</code> if file existence is checked first
*/
public boolean isCheckExists() {
return checkExists;
}
Set whether to check for log file existence before logging.
Params: - checkExists – true meaning to check for file existence.
/**
* Set whether to check for log file existence before logging.
*
* @param checkExists true meaning to check for file existence.
*/
public void setCheckExists(boolean checkExists) {
this.checkExists = checkExists;
}
Returns: the log file prefix.
/**
* @return the log file prefix.
*/
public String getPrefix() {
return prefix;
}
Set the log file prefix.
Params: - prefix – The new log file prefix
/**
* Set the log file prefix.
*
* @param prefix The new log file prefix
*/
public void setPrefix(String prefix) {
this.prefix = prefix;
}
Should we rotate the access log.
Returns: true
if the access log should be rotated
/**
* Should we rotate the access log.
*
* @return <code>true</code> if the access log should be rotated
*/
public boolean isRotatable() {
return rotatable;
}
Configure whether the access log should be rotated.
Params: - rotatable – true if the log should be rotated
/**
* Configure whether the access log should be rotated.
*
* @param rotatable true if the log should be rotated
*/
public void setRotatable(boolean rotatable) {
this.rotatable = rotatable;
}
Should we defer inclusion of the date stamp in the file
name until rotate time.
Returns: true
if the logs file names are time stamped
only when they are rotated
/**
* Should we defer inclusion of the date stamp in the file
* name until rotate time.
* @return <code>true</code> if the logs file names are time stamped
* only when they are rotated
*/
public boolean isRenameOnRotate() {
return renameOnRotate;
}
Set the value if we should defer inclusion of the date
stamp in the file name until rotate time
Params: - renameOnRotate – true if defer inclusion of date stamp
/**
* Set the value if we should defer inclusion of the date
* stamp in the file name until rotate time
*
* @param renameOnRotate true if defer inclusion of date stamp
*/
public void setRenameOnRotate(boolean renameOnRotate) {
this.renameOnRotate = renameOnRotate;
}
Is the logging buffered. Usually buffering can increase performance.
Returns: true
if the logging uses a buffer
/**
* Is the logging buffered. Usually buffering can increase performance.
* @return <code>true</code> if the logging uses a buffer
*/
public boolean isBuffered() {
return buffered;
}
Set the value if the logging should be buffered
Params: - buffered –
true
if buffered.
/**
* Set the value if the logging should be buffered
*
* @param buffered <code>true</code> if buffered.
*/
public void setBuffered(boolean buffered) {
this.buffered = buffered;
}
Returns: the log file suffix.
/**
* @return the log file suffix.
*/
public String getSuffix() {
return suffix;
}
Set the log file suffix.
Params: - suffix – The new log file suffix
/**
* Set the log file suffix.
*
* @param suffix The new log file suffix
*/
public void setSuffix(String suffix) {
this.suffix = suffix;
}
Returns: the date format date based log rotation.
/**
* @return the date format date based log rotation.
*/
public String getFileDateFormat() {
return fileDateFormat;
}
Set the date format date based log rotation.
Params: - fileDateFormat – The format for the file timestamp
/**
* Set the date format date based log rotation.
* @param fileDateFormat The format for the file timestamp
*/
public void setFileDateFormat(String fileDateFormat) {
String newFormat;
if (fileDateFormat == null) {
newFormat = "";
} else {
newFormat = fileDateFormat;
}
this.fileDateFormat = newFormat;
synchronized (this) {
fileDateFormatter = new SimpleDateFormat(newFormat, Locale.US);
fileDateFormatter.setTimeZone(TimeZone.getDefault());
}
}
Return the character set name that is used to write the log file.
Returns: Character set name, or null
if the system default
character set is used.
/**
* Return the character set name that is used to write the log file.
*
* @return Character set name, or <code>null</code> if the system default
* character set is used.
*/
public String getEncoding() {
return encoding;
}
Set the character set that is used to write the log file.
Params: - encoding – The name of the character set.
/**
* Set the character set that is used to write the log file.
*
* @param encoding The name of the character set.
*/
public void setEncoding(String encoding) {
if (encoding != null && encoding.length() > 0) {
this.encoding = encoding;
} else {
this.encoding = null;
}
}
// --------------------------------------------------------- Public Methods
Execute a periodic task, such as reloading, etc. This method will be
invoked inside the classloading context of this container. Unexpected
throwables will be caught and logged.
/**
* Execute a periodic task, such as reloading, etc. This method will be
* invoked inside the classloading context of this container. Unexpected
* throwables will be caught and logged.
*/
@Override
public synchronized void backgroundProcess() {
if (getState().isAvailable() && getEnabled() && writer != null &&
buffered) {
writer.flush();
}
int maxDays = this.maxDays;
String prefix = this.prefix;
String suffix = this.suffix;
if (rotatable && checkForOldLogs && maxDays > 0) {
long deleteIfLastModifiedBefore =
System.currentTimeMillis() - (maxDays * 24L * 60 * 60 * 1000);
File dir = getDirectoryFile();
if (dir.isDirectory()) {
String[] oldAccessLogs = dir.list();
if (oldAccessLogs != null) {
for (String oldAccessLog : oldAccessLogs) {
boolean match = false;
if (prefix != null && prefix.length() > 0) {
if (!oldAccessLog.startsWith(prefix)) {
continue;
}
match = true;
}
if (suffix != null && suffix.length() > 0) {
if (!oldAccessLog.endsWith(suffix)) {
continue;
}
match = true;
}
if (match) {
File file = new File(dir, oldAccessLog);
if (file.isFile() && file.lastModified() < deleteIfLastModifiedBefore) {
if (!file.delete()) {
log.warn(sm.getString(
"accessLogValve.deleteFail", file.getAbsolutePath()));
}
}
}
}
}
}
checkForOldLogs = false;
}
}
Rotate the log file if necessary.
/**
* Rotate the log file if necessary.
*/
public void rotate() {
if (rotatable) {
// Only do a logfile switch check once a second, max.
long systime = System.currentTimeMillis();
if ((systime - rotationLastChecked) > 1000) {
synchronized(this) {
if ((systime - rotationLastChecked) > 1000) {
rotationLastChecked = systime;
String tsDate;
// Check for a change of date
tsDate = fileDateFormatter.format(new Date(systime));
// If the date has changed, switch log files
if (!dateStamp.equals(tsDate)) {
close(true);
dateStamp = tsDate;
open();
}
}
}
}
}
}
Rename the existing log file to something else. Then open the
old log file name up once again. Intended to be called by a JMX
agent.
Params: - newFileName – The file name to move the log file entry to
Returns: true if a file was rotated with no error
/**
* Rename the existing log file to something else. Then open the
* old log file name up once again. Intended to be called by a JMX
* agent.
*
* @param newFileName The file name to move the log file entry to
* @return true if a file was rotated with no error
*/
public synchronized boolean rotate(String newFileName) {
if (currentLogFile != null) {
File holder = currentLogFile;
close(false);
try {
holder.renameTo(new File(newFileName));
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
log.error(sm.getString("accessLogValve.rotateFail"), e);
}
/* Make sure date is correct */
dateStamp = fileDateFormatter.format(
new Date(System.currentTimeMillis()));
open();
return true;
} else {
return false;
}
}
// -------------------------------------------------------- Private Methods
private File getDirectoryFile() {
File dir = new File(directory);
if (!dir.isAbsolute()) {
dir = new File(getContainer().getCatalinaBase(), directory);
}
return dir;
}
Create a File object based on the current log file name.
Directories are created as needed but the underlying file
is not created or opened.
Params: - useDateStamp – include the timestamp in the file name.
Returns: the log file object
/**
* Create a File object based on the current log file name.
* Directories are created as needed but the underlying file
* is not created or opened.
*
* @param useDateStamp include the timestamp in the file name.
* @return the log file object
*/
private File getLogFile(boolean useDateStamp) {
// Create the directory if necessary
File dir = getDirectoryFile();
if (!dir.mkdirs() && !dir.isDirectory()) {
log.error(sm.getString("accessLogValve.openDirFail", dir));
}
// Calculate the current log file name
File pathname;
if (useDateStamp) {
pathname = new File(dir.getAbsoluteFile(), prefix + dateStamp
+ suffix);
} else {
pathname = new File(dir.getAbsoluteFile(), prefix + suffix);
}
File parent = pathname.getParentFile();
if (!parent.mkdirs() && !parent.isDirectory()) {
log.error(sm.getString("accessLogValve.openDirFail", parent));
}
return pathname;
}
Move a current but rotated log file back to the unrotated
one. Needed if date stamp inclusion is deferred to rotation
time.
/**
* Move a current but rotated log file back to the unrotated
* one. Needed if date stamp inclusion is deferred to rotation
* time.
*/
private void restore() {
File newLogFile = getLogFile(false);
File rotatedLogFile = getLogFile(true);
if (rotatedLogFile.exists() && !newLogFile.exists() &&
!rotatedLogFile.equals(newLogFile)) {
try {
if (!rotatedLogFile.renameTo(newLogFile)) {
log.error(sm.getString("accessLogValve.renameFail", rotatedLogFile, newLogFile));
}
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
log.error(sm.getString("accessLogValve.renameFail", rotatedLogFile, newLogFile), e);
}
}
}
Close the currently open log file (if any)
Params: - rename – Rename file to final name after closing
/**
* Close the currently open log file (if any)
*
* @param rename Rename file to final name after closing
*/
private synchronized void close(boolean rename) {
if (writer == null) {
return;
}
writer.flush();
writer.close();
if (rename && renameOnRotate) {
File newLogFile = getLogFile(true);
if (!newLogFile.exists()) {
try {
if (!currentLogFile.renameTo(newLogFile)) {
log.error(sm.getString("accessLogValve.renameFail", currentLogFile, newLogFile));
}
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
log.error(sm.getString("accessLogValve.renameFail", currentLogFile, newLogFile), e);
}
} else {
log.error(sm.getString("accessLogValve.alreadyExists", currentLogFile, newLogFile));
}
}
writer = null;
dateStamp = "";
currentLogFile = null;
}
Log the specified message to the log file, switching files if the date
has changed since the previous log call.
Params: - message – Message to be logged
/**
* Log the specified message to the log file, switching files if the date
* has changed since the previous log call.
*
* @param message Message to be logged
*/
@Override
public void log(CharArrayWriter message) {
rotate();
/* In case something external rotated the file instead */
if (checkExists) {
synchronized (this) {
if (currentLogFile != null && !currentLogFile.exists()) {
try {
close(false);
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
log.info(sm.getString("accessLogValve.closeFail"), e);
}
/* Make sure date is correct */
dateStamp = fileDateFormatter.format(
new Date(System.currentTimeMillis()));
open();
}
}
}
// Log this message
try {
message.write(System.lineSeparator());
synchronized(this) {
if (writer != null) {
message.writeTo(writer);
if (!buffered) {
writer.flush();
}
}
}
} catch (IOException ioe) {
log.warn(sm.getString(
"accessLogValve.writeFail", message.toString()), ioe);
}
}
Open the new log file for the date specified by dateStamp
.
/**
* Open the new log file for the date specified by <code>dateStamp</code>.
*/
protected synchronized void open() {
// Open the current log file
// If no rotate - no need for dateStamp in fileName
File pathname = getLogFile(rotatable && !renameOnRotate);
Charset charset = null;
if (encoding != null) {
try {
charset = B2CConverter.getCharset(encoding);
} catch (UnsupportedEncodingException ex) {
log.error(sm.getString(
"accessLogValve.unsupportedEncoding", encoding), ex);
}
}
if (charset == null) {
charset = StandardCharsets.ISO_8859_1;
}
try {
writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(pathname, true), charset), 128000),
false);
currentLogFile = pathname;
} catch (IOException e) {
writer = null;
currentLogFile = null;
log.error(sm.getString("accessLogValve.openFail", pathname), e);
}
// Rotating a log file will always trigger a new file to be opened so
// when a new file is opened, check to see if any old files need to be
// removed.
checkForOldLogs = true;
}
Start this component and implement the requirements of LifecycleBase.startInternal()
. Throws: - LifecycleException – if this component detects a fatal error
that prevents this component from being used
/**
* Start this component and implement the requirements
* of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
*
* @exception LifecycleException if this component detects a fatal error
* that prevents this component from being used
*/
@Override
protected synchronized void startInternal() throws LifecycleException {
// Initialize the Date formatters
String format = getFileDateFormat();
fileDateFormatter = new SimpleDateFormat(format, Locale.US);
fileDateFormatter.setTimeZone(TimeZone.getDefault());
dateStamp = fileDateFormatter.format(new Date(System.currentTimeMillis()));
if (rotatable && renameOnRotate) {
restore();
}
open();
super.startInternal();
}
Stop this component and implement the requirements of LifecycleBase.stopInternal()
. Throws: - LifecycleException – if this component detects a fatal error
that prevents this component from being used
/**
* Stop this component and implement the requirements
* of {@link org.apache.catalina.util.LifecycleBase#stopInternal()}.
*
* @exception LifecycleException if this component detects a fatal error
* that prevents this component from being used
*/
@Override
protected synchronized void stopInternal() throws LifecycleException {
super.stopInternal();
close(false);
}
}