As Remy pointed out earlier today, we tend to trust the people that we vote in as committers here. There is no need to pre-post a commit: We allow people to scratch their itches here, and nobody is afraid to -1 a really bad commit. Just do it.
Personally, I'd make the fileDateFormatter configurable (it is, admittedly low, on my to-do list to make this configurable for Catalina loggers -- It's a 3.3 feature that I miss :). ----- Original Message ----- From: "Tim Funk" <[EMAIL PROTECTED]> To: "Tomcat Developers List" <[EMAIL PROTECTED]> Sent: Saturday, May 31, 2003 5:22 PM Subject: [5] Extended AccessLog Valve > For one of my projects, I am working on an extended access log valve as > defined by http://www.w3.org/TR/WD-logfile.html. When (if) I finish this, > does anyone else have interst in this? > > It will have the following config options: > directory - Where to put the logs (like AccessLogValve) > pattern - The log pattern > prefix - File Name prefix (like AccessLogValve) > rotatable - Is this a rotating log (like AccessLogValve) > suffix - Ending text (like AccessLogValve) > condition - Conditionally log requests. This is a String value. If set, > then the value in condition will be the key in a lookup > for a ServletRequest attribute. If the result is non-null > then the logging is skipped for this entry. This can be an > easy foundation to allow users to omit monitoring software > from the access logs. > checkExists - If set, if will check for the log files existence on the > event an external tool moved it. If the file doesn't exist > the close takes place and attempt to reopen the log file > occurs. > > For app-specific logging, I made this (so far): > x-A(...) - Pull ... from the servletConext.getAttribute() > x-C(...) - Pull cookie named ... > x-R(...) - Pull ... from the servletRequest.getAttribute() > x-S(...) - Pull ... from the session.getAttribute() > x-H(...) - Call the appriopriate get or is method in the > HttpServletRequest for any of the following based on ... > authType > characterEncoding > contentLength > locale > protocol > remoteUser > requestedSessionId > requestedSessionIdFromCookie > requestedSessionIdValid > scheme > secure > > > I also expose a JMX operation called rotate. This will allow an external > "thing" be able move the log file to a new name, then reopen the old filename. > > With any luck, the code is attached (2 attachments). It builds (and seems to > run) fine off of tomcat 5. Comments? Would this be OK of I added this to 5? > If so, are there changes (style/...) which need made first? > > > > > -Tim > > > ---------------------------------------------------------------------------- ---- > /* > * $Header: Exp $ > * $Revision: $ > * $Date: $ > * ==================================================================== > * > * The Apache Software License, Version 1.1 > * > * Copyright (c) 1999-2001 The Apache Software Foundation. All rights > * reserved. > * > * Redistribution and use in source and binary forms, with or without > * modification, are permitted provided that the following conditions > * are met: > * > * 1. Redistributions of source code must retain the above copyright > * notice, this list of conditions and the following disclaimer. > * > * 2. Redistributions in binary form must reproduce the above copyright > * notice, this list of conditions and the following disclaimer in > * the documentation and/or other materials provided with the > * distribution. > * > * 3. The end-user documentation included with the redistribution, if > * any, must include the following acknowlegement: > * "This product includes software developed by the > * Apache Software Foundation (http://www.apache.org/)." > * Alternately, this acknowlegement may appear in the software itself, > * if and wherever such third-party acknowlegements normally appear. > * > * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software > * Foundation" must not be used to endorse or promote products derived > * from this software without prior written permission. For written > * permission, please contact [EMAIL PROTECTED] > * > * 5. Products derived from this software may not be called "Apache" > * nor may "Apache" appear in their names without prior written > * permission of the Apache Group. > * > * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED > * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES > * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE > * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR > * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, > * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT > * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF > * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND > * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, > * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT > * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF > * SUCH DAMAGE. > * ==================================================================== > * > * This software consists of voluntary contributions made by many > * individuals on behalf of the Apache Software Foundation. For more > * information on the Apache Software Foundation, please see > * <http://www.apache.org/>. > * > * [Additional notices, if required by prior licensing conditions] > * > */ > > > package org.apache.catalina.valves; > > > import java.io.File; > import java.io.FileWriter; > import java.io.IOException; > import java.io.PrintWriter; > import java.net.InetAddress; > import java.text.SimpleDateFormat; > import java.util.Date; > import java.util.Iterator; > import java.util.LinkedList; > import java.util.TimeZone; > import javax.servlet.ServletException; > import javax.servlet.ServletRequest; > import javax.servlet.ServletResponse; > import javax.servlet.http.Cookie; > import javax.servlet.http.HttpServletRequest; > import javax.servlet.http.HttpServletResponse; > import javax.servlet.http.HttpSession; > import org.apache.catalina.HttpResponse; > import org.apache.catalina.Lifecycle; > import org.apache.catalina.LifecycleEvent; > import org.apache.catalina.LifecycleException; > import org.apache.catalina.LifecycleListener; > import org.apache.catalina.Request; > import org.apache.catalina.Response; > import org.apache.catalina.ValveContext; > import org.apache.catalina.util.LifecycleSupport; > import org.apache.catalina.util.ServerInfo; > import org.apache.catalina.util.StringManager; > > import org.apache.commons.logging.LogFactory; > import org.apache.commons.logging.Log; > > > > /** > * An implementation of the W3c Extended Log File Format. See > * http://www.w3.org/TR/WD-logfile.html for more information about the format. > * > * The following fields are supported: > * <ul> > * <li><code>c-dns</code>: Client hostname</li> > * <li><code>c-ip</code>: Client ip address</li> > * <li><code>bytes</code>: bytes served</li> > * <li><code>cs-method</code>: request method</li> > * <li><code>cs-uri</code>: The full uri requested</li> > * <li><code>cs-uri-query</code>: The query string</li> > * <li><code>cs-uri-stem</code>: The uri without query string</li> > * <li><code>date</code>: The date in yyyy-mm-dd format for GMT</li> > * <li><code>s-dns</code>: The server dns entry </li> > * <li><code>s-ip</code>: The server ip address</li> > * <li><code>cs(XXX)</code>: The value of header XXX from client to server</li> > * <li><code>sc(XXX)</code>: The value of header XXX from server to client </li> > * <li><code>sc-status</code>: The status code</li> > * <li><code>time</code>: Time the request was served</li> > * <li><code>time-taken</code>: Time to server the request</li> > * <li><code>x-A(XXX)</code>: Pull XXX attribute from the servlet context </li> > * <li><code>x-C(XXX)</code>: Pull the first cookie of the name XXX </li> > * <li><code>x-R(XXX)</code>: Pull XXX attribute from the servlet request </li> > * <li><code>x-S(XXX)</code>: Pull XXX attribute from the session </li> > * <li>For any of the x-H(...) the following method will be called from the > * HttpServletRequestObject </li> > * <li><code>x-H(authType)</code>: getAuthType </li> > * <li><code>x-H(characterEncoding)</code>: getCharacterEncoding </li> > * <li><code>x-H(contentLength)</code>: getContentLength </li> > * <li><code>x-H(locale)</code>: getLocale</li> > * <li><code>x-H(protocol)</code>: getProtocol </li> > * <li><code>x-H(remoteUser)</code>: getRemoteUser</li> > * <li><code>x-H(requestedSessionId)</code>: getGequestedSessionId</li> > * <li><code>x-H(requestedSessionIdFromCookie)</code>: > * isRequestedSessionIdFromCookie </li> > * <li><code>x-H(requestedSessionIdValid)</code>: > * isRequestedSessionIdValid</li> > * <li><code>x-H(scheme)</code>: getScheme</li> > * <li><code>x-H(secure)</code>: isSecure</li> > * </ul> > * > * > * > * <p> > * Log rotation can be on or off. This is dictated by the rotatable > * property. > * </p> > * > * <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 start writing a new log file. > * </p> > * > * <p> > * Conditional logging is also supported. This can be done with the > * <code>condition</code> property. > * If the value returned from ServletRequest.getAttribute(condition) > * yields a non-null value. The logging will be skipped. > * </p> > * > * > * @author Tim Funk > * @version $Revision: $ $Date: $ > */ > > public final class ExtendedAccessLogValve > extends ValveBase > implements Lifecycle { > > > // ----------------------------------------------------------- Constructors > > > /** > * Construct a new instance of this class with default property values. > */ > public ExtendedAccessLogValve() { > > super(); > > > } > > > // ----------------------------------------------------- Instance Variables > private static Log log = LogFactory.getLog(ExtendedAccessLogValve.class); > > > /** > * The descriptive information about this implementation. > */ > protected static final String info = > "org.apache.catalina.valves.ExtendedAccessLogValve/1.0"; > > > /** > * The lifecycle event support for this component. > */ > protected LifecycleSupport lifecycle = new LifecycleSupport(this); > > > > /** > * The string manager for this package. > */ > private StringManager sm = > StringManager.getManager(Constants.Package); > > > /** > * Has this component been started yet? > */ > private boolean started = false; > > > /** > * The as-of date for the currently open log file, or a zero-length > * string if there is no open log file. > */ > private String dateStamp = ""; > > > /** > * The PrintWriter to which we are currently logging, if any. > */ > private PrintWriter writer = null; > > > /** > * The formatter for the date contained in the file name. > */ > private SimpleDateFormat fileDateFormatter = null; > > > /** > * A date formatter to format a Date into a date in the format > * "yyyy-MM-dd". > */ > private SimpleDateFormat dateFormatter = null; > > > /** > * A date formatter to format a Date into a time in the format > * "kk:mm:ss" (kk is a 24-hour representation of the hour). > */ > private SimpleDateFormat timeFormatter = null; > > > /** > * My ip address. Look it up once and remember it. Dump this if we can > * determine another reliable way to get server ip address since this > * server may have many ip's. > */ > private String myIpAddress = null; > > > /** > * My dns name. Look it up once and remember it. Dump this if we can > * determine another reliable way to get server name address since this > * server may have many ip's. > */ > private String myDNSName = null; > > > /** > * Holder for all of the fields to log after the pattern is decoded. > */ > private FieldInfo[] fieldInfos; > > > /** > * The current log file we are writing to. Helpful when checkExists > * is true. > */ > private File currentLogFile = null; > > > > /** > * The system time when we last updated the Date that this valve > * uses for log lines. > */ > private Date currentDate = null; > > > /** > * Instant when the log daily rotation was last checked. > */ > private long rotationLastChecked = 0L; > > > /** > * The directory in which log files are created. > */ > private String directory = "logs"; > > > /** > * The pattern used to format our access log lines. > */ > private String pattern = null; > > > /** > * The prefix that is added to log file filenames. > */ > private String prefix = "access_log."; > > > /** > * Should we rotate our log file? Default is true (like old behavior) > */ > private boolean rotatable = true; > > > /** > * The suffix that is added to log file filenames. > */ > private String suffix = ""; > > > /** > * Are we doing conditional logging. default false. > */ > private String condition = null; > > > /** > * 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; > > > > > // ------------------------------------------------------------- Properties > > > /** > * Return the directory in which we create log files. > */ > public String getDirectory() { > > return (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; > > } > > > /** > * Return descriptive information about this implementation. > */ > public String getInfo() { > > return (this.info); > > } > > > /** > * Return the format pattern. > */ > public String getPattern() { > > return (this.pattern); > > } > > > /** > * Set the format pattern, first translating any recognized alias. > * > * @param pattern The new pattern pattern > */ > public void setPattern(String pattern) { > > FieldInfo[] f= decodePattern(pattern); > if (f!=null) { > this.pattern = pattern; > this.fieldInfos = f; > } > } > > > /** > * Return the log file prefix. > */ > public String getPrefix() { > > return (prefix); > > } > > > /** > * Set the log file prefix. > * > * @param prefix The new log file prefix > */ > public void setPrefix(String prefix) { > > this.prefix = prefix; > > } > > > /** > * Return true if logs are automatically rotated. > */ > public boolean isRotatable() { > > return rotatable; > > } > > > /** > * Set the value is we should we rotate the logs > * > * @param rotatable true is we should rotate. > */ > public void setRotatable(boolean rotatable) { > > this.rotatable = rotatable; > > } > > > /** > * Return the log file suffix. > */ > public String getSuffix() { > > return (suffix); > > } > > > /** > * Set the log file suffix. > * > * @param suffix The new log file suffix > */ > public void setSuffix(String suffix) { > > this.suffix = suffix; > > } > > > > /** > * Return whether the attribute name to look for when > * performing conditional loggging. If null, every > * request is logged. > */ > public String getCondition() { > > return condition; > > } > > > /** > * Set the ServletRequest.attribute to look for to perform > * conditional logging. Set to null to log everything. > * > * @param condition Set to null to log everything > */ > public void setCondition(String condition) { > > this.condition = condition; > > } > > > > /** > * Check for file existence before logging. > */ > public boolean isCheckExists() { > > return checkExists; > > } > > > /** > * 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; > > } > > // --------------------------------------------------------- Public Methods > > > /** > * Log a message summarizing the specified request and response, according > * to the format specified by the <code>pattern</code> property. > * > * @param request Request being processed > * @param response Response being processed > * @param context The valve context used to invoke the next valve > * in the current processing pipeline > * > * @exception IOException if an input/output error has occurred > * @exception ServletException if a servlet error has occurred > */ > public void invoke(Request request, Response response, > ValveContext context) > throws IOException, ServletException { > > // Pass this request on to the next valve in our pipeline > long endTime; > long runTime; > long startTime=System.currentTimeMillis(); > > context.invokeNext(request, response); > > endTime = System.currentTimeMillis(); > runTime = endTime-startTime; > > if (fieldInfos==null || condition!=null && > null!=request.getRequest().getAttribute(condition)) { > return; > } > > > Date date = getDate(endTime); > StringBuffer result = new StringBuffer(); > > for (int i=0; fieldInfos!=null && i<fieldInfos.length; i++) { > switch(fieldInfos[i].type) { > case FieldInfo.DATA_CLIENT: > if (FieldInfo.FIELD_IP==fieldInfos[i].location) > result.append(request.getRequest().getRemoteAddr()); > else if (FieldInfo.FIELD_DNS==fieldInfos[i].location) > result.append(request.getRequest().getRemoteHost()); > else > result.append("?WTF?"); /* This should never happen! */ > break; > case FieldInfo.DATA_SERVER: > if (FieldInfo.FIELD_IP==fieldInfos[i].location) > result.append(myIpAddress); > else if (FieldInfo.FIELD_DNS==fieldInfos[i].location) > result.append(myDNSName); > else > result.append("?WTF?"); /* This should never happen! */ > break; > case FieldInfo.DATA_REMOTE: > result.append('?'); /* I don't know how to handle these! */ > break; > case FieldInfo.DATA_CLIENT_TO_SERVER: > result.append(getClientToServer(fieldInfos[i], request)); > break; > case FieldInfo.DATA_SERVER_TO_CLIENT: > result.append(getServerToClient(fieldInfos[i], response)); > break; > case FieldInfo.DATA_SERVER_TO_RSERVER: > result.append('-'); > break; > case FieldInfo.DATA_RSERVER_TO_SERVER: > result.append('-'); > break; > case FieldInfo.DATA_APP_SPECIFIC: > result.append(getAppSpecific(fieldInfos[i], request)); > break; > case FieldInfo.DATA_SPECIAL: > if (FieldInfo.SPECIAL_DATE==fieldInfos[i].location) > result.append(dateFormatter.format(date)); > else if (FieldInfo.SPECIAL_TIME_TAKEN==fieldInfos[i].location) > result.append(runTime/1000d); > else if (FieldInfo.SPECIAL_TIME==fieldInfos[i].location) > result.append(timeFormatter.format(date)); > else if (FieldInfo.SPECIAL_BYTES==fieldInfos[i].location) > result.append(dateFormatter.format(date)); > else if (FieldInfo.SPECIAL_CACHED==fieldInfos[i].location) > result.append('-'); /* I don't know how to evaluate this! */ > else > result.append("?WTF?"); /* This should never happen! */ > break; > default: > result.append("?WTF?"); /* This should never happen! */ > } > > if (fieldInfos[i].postWhiteSpace!=null) { > result.append(fieldInfos[i].postWhiteSpace); > } > } > log(result.toString(), date); > > } > > > /** > * 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(); > try { > holder.renameTo(new File(newFileName)); > } catch(Throwable e){ > log.error("rotate failed", e); > } > > /* Make sure date is correct */ > currentDate = new Date(); > fileDateFormatter = new SimpleDateFormat("yyyy-MM-dd"); > dateStamp = dateFormatter.format(currentDate); > > open(); > return true; > } else { > return false; > } > } > > // -------------------------------------------------------- Private Methods > > > /** > * Return the client to server data. > * @param fieldInfo The field to decode. > * @param request The object we pull data from. > * @return The appropriate value. > */ > private String getClientToServer(FieldInfo fieldInfo, Request request) { > > ServletRequest sr = request.getRequest(); > HttpServletRequest hsr = null; > if (sr instanceof HttpServletRequest) > hsr = (HttpServletRequest)sr; > > switch(fieldInfo.location) { > case FieldInfo.FIELD_METHOD: > return hsr.getMethod(); > case FieldInfo.FIELD_URI: > if (null==hsr.getQueryString()) > return hsr.getRequestURI(); > else > return hsr.getRequestURI() + "?" + hsr.getQueryString(); > case FieldInfo.FIELD_URI_STEM: > return hsr.getRequestURI(); > case FieldInfo.FIELD_URI_QUERY: > if (null==hsr.getQueryString()) > return "-"; > return hsr.getQueryString(); > case FieldInfo.FIELD_HEADER: > return wrap(hsr.getHeader(fieldInfo.value)); > default: > ; > } > return "-"; > } > > > /** > * Return the server to client data. > * @param fieldInfo The field to decode. > * @param response The object we pull data from. > * @return The appropriate value. > */ > private String getServerToClient(FieldInfo fieldInfo, Response response) { > HttpResponse r = (HttpResponse)response; > switch(fieldInfo.location) { > case FieldInfo.FIELD_STATUS: > return "" + r.getStatus(); > case FieldInfo.FIELD_COMMENT: > return "?"; /* Not coded yet*/ > case FieldInfo.FIELD_HEADER: > return wrap(r.getHeader(fieldInfo.value)); > default: > ; > } > > return "-"; > } > > /** > * Get app specific data. > * @param fieldInfo The field to decode > * @param request Where we will pull the data from. > * @return The appropriate value > */ > private String getAppSpecific(FieldInfo fieldInfo, Request request) { > > ServletRequest sr = request.getRequest(); > HttpServletRequest hsr = null; > if (sr instanceof HttpServletRequest) > hsr = (HttpServletRequest)sr; > > switch(fieldInfo.xType) { > case FieldInfo.X_REQUEST: > return wrap(sr.getAttribute(fieldInfo.value)); > case FieldInfo.X_SESSION: > HttpSession session = null; > if (hsr!=null){ > session = hsr.getSession(false); > if (session!=null) > return wrap(session.getAttribute(fieldInfo.value)); > } > break; > case FieldInfo.X_COOKIE: > Cookie[] c = hsr.getCookies(); > for (int i=0; c != null && i < c.length; i++){ > if (fieldInfo.value.equals(c[i].getName())){ > return wrap(c[i].getValue()); > } > } > case FieldInfo.X_APP: > return wrap(request.getContext().getServletContext() > .getAttribute(fieldInfo.value)); > case FieldInfo.X_SERVLET_REQUEST: > if (fieldInfo.location==FieldInfo.X_LOC_AUTHTYPE) { > return wrap(hsr.getAuthType()); > } else if (fieldInfo.location==FieldInfo.X_LOC_REMOTEUSER) { > return wrap(hsr.getRemoteUser()); > } else if (fieldInfo.location== > FieldInfo.X_LOC_REQUESTEDSESSIONID) { > return wrap(hsr.getRequestedSessionId()); > } else if (fieldInfo.location== > FieldInfo.X_LOC_REQUESTEDSESSIONIDFROMCOOKIE) { > return wrap(""+hsr.isRequestedSessionIdFromCookie()); > } else if (fieldInfo.location== > FieldInfo.X_LOC_REQUESTEDSESSIONIDVALID) { > return wrap(""+hsr.isRequestedSessionIdValid()); > } else if (fieldInfo.location==FieldInfo.X_LOC_CONTENTLENGTH) { > return wrap(""+hsr.getContentLength()); > } else if (fieldInfo.location== > FieldInfo.X_LOC_CHARACTERENCODING) { > return wrap(hsr.getCharacterEncoding()); > } else if (fieldInfo.location==FieldInfo.X_LOC_LOCALE) { > return wrap(hsr.getLocale()); > } else if (fieldInfo.location==FieldInfo.X_LOC_PROTOCOL) { > return wrap(hsr.getProtocol()); > } else if (fieldInfo.location==FieldInfo.X_LOC_SCHEME) { > return wrap(hsr.getScheme()); > } else if (fieldInfo.location==FieldInfo.X_LOC_SECURE) { > return wrap(""+hsr.isSecure()); > } > break; > default: > ; > } > > return "-"; > } > > > private String wrap(Object value) { > String svalue; > // Does the value contain a " ? If so must encode it > if (value==null || "-".equals(value)) > return "-"; > > > try { > svalue = value.toString(); > if ("".equals(svalue)) > return "-"; > } catch(Throwable e){ > /* Log error */ > return "-"; > } > > /* Wrap all quotes in double quotes. */ > StringBuffer buffer = new StringBuffer(svalue.length()+2); > buffer.append('"'); > int i=0; > while (i<svalue.length()) { > int j = svalue.indexOf('"', i); > if (j==-1) { > buffer.append(svalue.substring(i)); > i=svalue.length(); > } else { > buffer.append(svalue.substring(i, j+1)); > buffer.append('"'); > i=j+2; > } > } > > buffer.append('"'); > return buffer.toString(); > } > > > /** > * Close the currently open log file (if any) > */ > private synchronized void close() { > > if (writer == null) > return; > writer.flush(); > writer.close(); > writer = null; > currentLogFile = null; > > } > > > /** > * 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 > * @param date the current Date object (so this method doesn't need to > * create a new one) > */ > private void log(String message, Date date) { > > if (rotatable){ > // Only do a logfile switch check once a second, max. > long systime = System.currentTimeMillis(); > if ((systime - rotationLastChecked) > 1000) { > > // We need a new currentDate > currentDate = new Date(systime); > rotationLastChecked = systime; > > // Check for a change of date > String tsDate = fileDateFormatter.format(currentDate); > > // If the date has changed, switch log files > if (!dateStamp.equals(tsDate)) { > synchronized (this) { > if (!dateStamp.equals(tsDate)) { > close(); > dateStamp = tsDate; > open(); > } > } > } > > } > } > > /* In case something external rotated the file instead */ > if (checkExists){ > synchronized (this) { > if (currentLogFile!=null && !currentLogFile.exists()) { > try { > close(); > } catch (Throwable e){ > log.info("at least this wasn't swallowed", e); > } > > /* Make sure date is correct */ > currentDate = new Date(System.currentTimeMillis()); > fileDateFormatter = new SimpleDateFormat("yyyy-MM-dd"); > dateStamp = dateFormatter.format(currentDate); > > open(); > } > } > } > > // Log this message > if (writer != null) { > writer.println(message); > } > > } > > > /** > * Open the new log file for the date specified by <code>dateStamp</code>. > */ > private synchronized void open() { > > // Create the directory if necessary > File dir = new File(directory); > if (!dir.isAbsolute()) > dir = new File(System.getProperty("catalina.base"), directory); > dir.mkdirs(); > > // Open the current log file > try { > String pathname; > > // If no rotate - no need for dateStamp in fileName > if (rotatable){ > pathname = dir.getAbsolutePath() + File.separator + > prefix + dateStamp + suffix; > } else { > pathname = dir.getAbsolutePath() + File.separator + > prefix + suffix; > } > > currentLogFile = new File(pathname); > writer = new PrintWriter(new FileWriter(pathname, true), true); > if (currentLogFile.length()==0) { > writer.println("#Fields: " + pattern); > writer.println("#Version: 1.0"); > writer.println("#Software: " + ServerInfo.getServerInfo()); > } > > > } catch (IOException e) { > writer = null; > currentLogFile = null; > } > > } > > > /** > * This method returns a Date object that is accurate to within one > * second. If a thread calls this method to get a Date and it's been > * less than 1 second since a new Date was created, this method > * simply gives out the same Date again so that the system doesn't > * spend time creating Date objects unnecessarily. > */ > private Date getDate(long systime) { > /* Avoid extra call to System.currentTimeMillis(); */ > if (0==systime) { > systime = System.currentTimeMillis(); > } > > // Only create a new Date once per second, max. > if ((systime - currentDate.getTime()) > 1000) { > currentDate.setTime(systime); > } > > return currentDate; > > } > > > // ------------------------------------------------------ Lifecycle Methods > > > /** > * Add a lifecycle event listener to this component. > * > * @param listener The listener to add > */ > public void addLifecycleListener(LifecycleListener listener) { > > lifecycle.addLifecycleListener(listener); > > } > > > /** > * Get the lifecycle listeners associated with this lifecycle. If this > * Lifecycle has no listeners registered, a zero-length array is returned. > */ > public LifecycleListener[] findLifecycleListeners() { > > return lifecycle.findLifecycleListeners(); > > } > > > /** > * Remove a lifecycle event listener from this component. > * > * @param listener The listener to add > */ > public void removeLifecycleListener(LifecycleListener listener) { > > lifecycle.removeLifecycleListener(listener); > > } > > > /** > * Prepare for the beginning of active use of the public methods of this > * component. This method should be called after <code>configure()</code>, > * and before any of the public methods of the component are utilized. > * > * @exception LifecycleException if this component detects a fatal error > * that prevents this component from being used > */ > public void start() throws LifecycleException { > > // Validate and update our current component state > if (started) > throw new LifecycleException > (sm.getString("extendedAccessLogValve.alreadyStarted")); > lifecycle.fireLifecycleEvent(START_EVENT, null); > started = true; > > // Initialize the timeZone, Date formatters, and currentDate > TimeZone tz = TimeZone.getTimeZone("GMT"); > dateFormatter = new SimpleDateFormat("yyyy-MM-dd"); > dateFormatter.setTimeZone(tz); > timeFormatter = new SimpleDateFormat("HH:mm:ss"); > timeFormatter.setTimeZone(tz); > currentDate = new Date(System.currentTimeMillis()); > fileDateFormatter = new SimpleDateFormat("yyyy-MM-dd"); > dateStamp = fileDateFormatter.format(currentDate); > > /* Everybody say ick ... ick */ > try { > InetAddress inetAddress = InetAddress.getLocalHost(); > myIpAddress = inetAddress.getHostAddress(); > myDNSName = inetAddress.getHostName(); > } catch(Throwable e){ > myIpAddress="127.0.0.1"; > myDNSName="localhost"; > } > > open(); > > } > > > /** > * Gracefully terminate the active use of the public methods of this > * component. This method should be the last one called on a given > * instance of this component. > * > * @exception LifecycleException if this component detects a fatal error > * that needs to be reported > */ > public void stop() throws LifecycleException { > > // Validate and update our current component state > if (!started) > throw new LifecycleException > (sm.getString("extendedAccessLogValve.notStarted")); > lifecycle.fireLifecycleEvent(STOP_EVENT, null); > started = false; > > close(); > > } > > /** > * Decode the given pattern. Is public so a pattern may > * allows to be validated. > * @param fields The pattern to decode > * @return null on error. Otherwise array of decoded fields > */ > public FieldInfo[] decodePattern(String fields) { > if (log.isDebugEnabled()) > log.debug("decodePattern, fields=" + fields); > > LinkedList list = new LinkedList(); > > //Ignore leading whitespace. > int i=0; > for (;i<fields.length() && Character.isWhitespace(fields.charAt(i));i++); > > if (i>=fields.length()) { > log.info("fields was just empty or whitespace"); > return null; > } > > int j; > while(i<fields.length()) { > if (log.isDebugEnabled()) > log.debug("fields.substring(i)=" + fields.substring(i)); > > FieldInfo currentFieldInfo = new FieldInfo(); > > > if (fields.startsWith("date",i)) { > currentFieldInfo.type = FieldInfo.DATA_SPECIAL; > currentFieldInfo.location = FieldInfo.SPECIAL_DATE; > i+="date".length(); > } else if (fields.startsWith("time-taken",i)) { > currentFieldInfo.type = FieldInfo.DATA_SPECIAL; > currentFieldInfo.location = FieldInfo.SPECIAL_TIME_TAKEN; > i+="time-taken".length(); > } else if (fields.startsWith("time",i)) { > currentFieldInfo.type = FieldInfo.DATA_SPECIAL; > currentFieldInfo.location = FieldInfo.SPECIAL_TIME; > i+="time".length(); > } else if (fields.startsWith("bytes",i)) { > currentFieldInfo.type = FieldInfo.DATA_SPECIAL; > currentFieldInfo.location = FieldInfo.SPECIAL_BYTES; > i+="bytes".length(); > } else if (fields.startsWith("cached",i)) { > currentFieldInfo.type = FieldInfo.DATA_SPECIAL; > currentFieldInfo.location = FieldInfo.SPECIAL_CACHED; > i+="cached".length(); > } else if (fields.startsWith("c-ip",i)) { > currentFieldInfo.type = FieldInfo.DATA_CLIENT; > currentFieldInfo.location = FieldInfo.FIELD_IP; > i+="c-ip".length(); > } else if (fields.startsWith("c-dns",i)) { > currentFieldInfo.type = FieldInfo.DATA_CLIENT; > currentFieldInfo.location = FieldInfo.FIELD_DNS; > i+="c-dns".length(); > } else if (fields.startsWith("s-ip",i)) { > currentFieldInfo.type = FieldInfo.DATA_SERVER; > currentFieldInfo.location = FieldInfo.FIELD_IP; > i+="s-ip".length(); > } else if (fields.startsWith("s-dns",i)) { > currentFieldInfo.type = FieldInfo.DATA_SERVER; > currentFieldInfo.location = FieldInfo.FIELD_DNS; > i+="s-dns".length(); > } else if (fields.startsWith("cs",i)) { > i = decode(fields, i+2, currentFieldInfo, > FieldInfo.DATA_CLIENT_TO_SERVER); > if (i<0) > return null; > } else if (fields.startsWith("sc",i)) { > i = decode(fields, i+2, currentFieldInfo, > FieldInfo.DATA_SERVER_TO_CLIENT); > if (i<0) > return null; > } else if (fields.startsWith("sr",i)) { > i = decode(fields, i+2, currentFieldInfo, > FieldInfo.DATA_SERVER_TO_RSERVER); > if (i<0) > return null; > } else if (fields.startsWith("rs",i)) { > i = decode(fields, i+2, currentFieldInfo, > FieldInfo.DATA_RSERVER_TO_SERVER); > if (i<0) > return null; > } else if (fields.startsWith("x",i)) { > i = decodeAppSpecific(fields, i, currentFieldInfo); > } else { > // Unable to decode ... > log.error("unable to decode with rest of chars being: " + > fields.substring(i)); > return null; > } > > // By this point we should have the field, get the whitespace > j=i; > for (;j<fields.length() && Character.isWhitespace(fields.charAt(j));j++); > > if (j>=fields.length()) { > if (j==i) { > // Special case - end of string > currentFieldInfo.postWhiteSpace = ""; > } else { > currentFieldInfo.postWhiteSpace = fields.substring(i); > i=j; > } > } else { > currentFieldInfo.postWhiteSpace = fields.substring(i,j); > i=j; > } > > list.add(currentFieldInfo); > } > > i=0; > FieldInfo[] f = new FieldInfo[list.size()]; > for (Iterator k = list.iterator(); k.hasNext();) > f[i++] = (FieldInfo)k.next(); > > if (log.isDebugEnabled()) > log.debug("finished decoding with length of: " + i); > > return f; > } > > /** > * Decode the cs or sc fields. > * Returns negative on error. > * > * @param fields The pattern to decode > * @param i The string index where we are decoding. > * @param fieldInfo Where to store the results > * @param type The type we are decoding. > * @return -1 on error. Otherwise the new String index. > */ > private int decode(String fields, int i, FieldInfo fieldInfo, short type) { > > if (fields.startsWith("-status",i)) { > fieldInfo.location = FieldInfo.FIELD_STATUS; > i+="-status".length(); > } else if (fields.startsWith("-comment",i)) { > fieldInfo.location = FieldInfo.FIELD_COMMENT; > i+="-comment".length(); > } else if (fields.startsWith("-uri-query",i)) { > fieldInfo.location = FieldInfo.FIELD_URI_QUERY; > i+="-uri-query".length(); > } else if (fields.startsWith("-uri-stem",i)) { > fieldInfo.location = FieldInfo.FIELD_URI_STEM; > i+="-uri-stem".length(); > } else if (fields.startsWith("-uri",i)) { > fieldInfo.location = FieldInfo.FIELD_URI; > i+="-uri".length(); > } else if (fields.startsWith("-method",i)) { > fieldInfo.location = FieldInfo.FIELD_METHOD; > i+="-method".length(); > } else if (fields.startsWith("(",i)) { > fieldInfo.location = FieldInfo.FIELD_HEADER; > i++; /* Move past the ( */ > int j = fields.indexOf(')', i); > if (j==-1) { /* Not found */ > log.error("No closing ) found for in decode"); > return -1; > } > fieldInfo.value = fields.substring(i,j); > i=j+1; // Move pointer past ) */ > } else { > log.error("The next characters couldn't be decoded: " + fields.substring(i)); > return -1; > } > > fieldInfo.type = type; > return i; > } > > > /** > * Decode app specific log entry. > * > * Special fields are of the form: > * x-C(...) - For cookie > * x-A(...) - Value in servletContext > * x-S(...) - Value in session > * x-R(...) - Value in servletRequest > * @param fields The pattern to decode > * @param i The string index where we are decoding. > * @param fieldInfo Where to store the results > * @return -1 on error. Otherwise the new String index. > */ > private int decodeAppSpecific(String fields, int i, FieldInfo fieldInfo) { > fieldInfo.type = FieldInfo.DATA_APP_SPECIFIC; > /* Move past 'x-' */ > i+=2; > > if (i>=fields.length()) { > log.error("End of line reached before decoding x- param"); > return -1; > } > > switch(fields.charAt(i)) { > case 'A': > fieldInfo.xType = FieldInfo.X_APP; > break; > case 'C': > fieldInfo.xType = FieldInfo.X_COOKIE; > break; > case 'R': > fieldInfo.xType = FieldInfo.X_REQUEST; > break; > case 'S': > fieldInfo.xType = FieldInfo.X_SESSION; > break; > case 'H': > fieldInfo.xType = FieldInfo.X_SERVLET_REQUEST; > break; > default: > return -1; > } > > /* test that next char is a ( */ > if (i+1!=fields.indexOf('(',i)) { > log.error("x param in wrong format. Needs to be 'x-#(...)' read the docs!"); > return -1; > } > i+=2; /* Move inside of the () */ > > /* Look for ending ) and return error if not found. */ > int j = fields.indexOf(')',i); > if (j==-1) { > log.error("x param in wrong format. No closing ')'!"); > return -1; > } > > fieldInfo.value = fields.substring(i,j); > > if (fieldInfo.xType == FieldInfo.X_SERVLET_REQUEST) { > if ("authType".equals(fieldInfo.value)){ > fieldInfo.location = FieldInfo.X_LOC_AUTHTYPE; > } else if ("remoteUser".equals(fieldInfo.value)){ > fieldInfo.location = FieldInfo.X_LOC_REMOTEUSER; > } else if ("requestedSessionId".equals(fieldInfo.value)){ > fieldInfo.location = FieldInfo.X_LOC_REQUESTEDSESSIONID; > } else if ("requestedSessionIdFromCookie".equals(fieldInfo.value)){ > fieldInfo.location = FieldInfo.X_LOC_REQUESTEDSESSIONIDFROMCOOKIE; > } else if ("requestedSessionIdValid".equals(fieldInfo.value)){ > fieldInfo.location = FieldInfo.X_LOC_REQUESTEDSESSIONID; > } else if ("contentLength".equals(fieldInfo.value)){ > fieldInfo.location = FieldInfo.X_LOC_CONTENTLENGTH; > } else if ("characterEncoding".equals(fieldInfo.value)){ > fieldInfo.location = FieldInfo.X_LOC_CHARACTERENCODING; > } else if ("locale".equals(fieldInfo.value)){ > fieldInfo.location = FieldInfo.X_LOC_LOCALE; > } else if ("protocol".equals(fieldInfo.value)){ > fieldInfo.location = FieldInfo.X_LOC_PROTOCOL; > } else if ("scheme".equals(fieldInfo.value)){ > fieldInfo.location = FieldInfo.X_LOC_SCHEME; > } else if ("secure".equals(fieldInfo.value)){ > fieldInfo.location = FieldInfo.X_LOC_SECURE; > } else { > log.error("x param for servlet request, couldn't decode value: " + > fieldInfo.location); > return -1; > } > } > > return j+1; > } > > > > } > > /** > * A simple helper for decoding the pattern. > */ > class FieldInfo { > /* > The goal of the constants listed below is to make the construction of the log > entry as quick as possible via numerci decodings of the methods to call instead > of performing many String comparisons on each logging request. > */ > > /* Where the data is located. */ > static final short DATA_CLIENT = 0; > static final short DATA_SERVER = 1; > static final short DATA_REMOTE = 2; > static final short DATA_CLIENT_TO_SERVER = 3; > static final short DATA_SERVER_TO_CLIENT = 4; > static final short DATA_SERVER_TO_RSERVER = 5; /* Here to honor the spec. */ > static final short DATA_RSERVER_TO_SERVER = 6; /* Here to honor the spec. */ > static final short DATA_APP_SPECIFIC = 7; > static final short DATA_SPECIAL = 8; > > /* The type of special fields. */ > static final short SPECIAL_DATE = 1; > static final short SPECIAL_TIME_TAKEN = 2; > static final short SPECIAL_TIME = 3; > static final short SPECIAL_BYTES = 4; > static final short SPECIAL_CACHED = 5; > > /* Where to pull the data for prefixed values */ > static final short FIELD_IP = 1; > static final short FIELD_DNS = 2; > static final short FIELD_STATUS = 3; > static final short FIELD_COMMENT = 4; > static final short FIELD_METHOD = 5; > static final short FIELD_URI = 6; > static final short FIELD_URI_STEM = 7; > static final short FIELD_URI_QUERY = 8; > static final short FIELD_HEADER = 9; > > > /* Application Specific parameters */ > static final short X_REQUEST = 1; /* For x app specific */ > static final short X_SESSION = 2; /* For x app specific */ > static final short X_COOKIE = 3; /* For x app specific */ > static final short X_APP = 4; /* For x app specific */ > static final short X_SERVLET_REQUEST = 5; /* For x app specific */ > > static final short X_LOC_AUTHTYPE = 1; > static final short X_LOC_REMOTEUSER = 2; > static final short X_LOC_REQUESTEDSESSIONID = 3; > static final short X_LOC_REQUESTEDSESSIONIDFROMCOOKIE = 4; > static final short X_LOC_REQUESTEDSESSIONIDVALID = 5; > static final short X_LOC_CONTENTLENGTH = 6; > static final short X_LOC_CHARACTERENCODING = 7; > static final short X_LOC_LOCALE = 8; > static final short X_LOC_PROTOCOL = 9; > static final short X_LOC_SCHEME = 10; > static final short X_LOC_SECURE = 11; > > > > /** The field type */ > short type; > > /** Where to pull the data from? Icky variable name. */ > short location; > > /** The x- specific place to pull the data from. */ > short xType; > > /** The field value if needed. Needed for headers and app specific. */ > String value; > > /** Any white space after this field? Put it here. */ > String postWhiteSpace = null; > > } > ---------------------------------------------------------------------------- ---- > Index: mbeans-descriptors.xml > =================================================================== > RCS file: /home/cvspublic/jakarta-tomcat-catalina/catalina/src/share/org/apache/catali na/valves/mbeans-descriptors.xml,v > retrieving revision 1.1 > diff -u -r1.1 mbeans-descriptors.xml > --- mbeans-descriptors.xml 17 Mar 2003 06:45:33 -0000 1.1 > +++ mbeans-descriptors.xml 1 Jun 2003 00:16:40 -0000 > @@ -108,19 +108,79 @@ > <attribute name="debug" > description="The debugging detail level for this component" > type="int"/> > - > + > </mbean> > - > + > + <mbean name="ExtendedAccessLogValve" > + description="Valve that generates a web server access log" > + domain="Catalina" > + group="Valve" > + type="org.apache.catalina.valves.ExtendedAccessLogValve"> > + > + <attribute name="className" > + description="Fully qualified class name of the managed object" > + type="java.lang.String" > + writeable="false"/> > + > + <attribute name="debug" > + description="The debugging detail level for this component" > + type="int"/> > + > + <attribute name="containerName" > + description="Object name of the container" > + type="javax.management.ObjectName"/> > + > + <attribute name="directory" > + description="The directory in which log files are created" > + type="java.lang.String"/> > + > + <attribute name="pattern" > + description="The pattern used to format our access log lines" > + type="java.lang.String"/> > + > + <attribute name="prefix" > + description="The prefix that is added to log file filenames" > + type="java.lang.String"/> > + > + <attribute name="rotatable" > + description="Rotate log" > + is="true" > + type="boolean"/> > + > + <attribute name="condition" > + description="The value to look for conditional logging." > + type="java.lang.String"/> > + > + <attribute name="checkExists" > + description="Check for file existence before each logging." > + is="true" > + type="boolean"/> > + > + <attribute name="suffix" > + description="The suffix that is added to log file filenames" > + type="java.lang.String"/> > + > + <operation name="rotate" > + description="Move the existing log file to a new name" > + impact="ACTION" > + returnType="boolean"> > + <parameter name="newFileName" > + description="File name to move the log file to." > + type="java.lang.String"/> > + </operation> > + > + </mbean> > + > <mbean name="RemoteAddrValve" > description="Concrete implementation of RequestFilterValve that filters based on the string representation of the remote client's IP address" > domain="Catalina" > group="Valve" > type="org.apache.catalina.valves.RemoteAddrValve"> > - > + > <attribute name="allow" > description="The comma-delimited set of allow expressions" > type="java.lang.String"/> > - > + > <attribute name="containerName" > description="Object name of the container" > type="javax.management.ObjectName"/> > > ---------------------------------------------------------------------------- ---- > --------------------------------------------------------------------- > To unsubscribe, e-mail: [EMAIL PROTECTED] > For additional commands, e-mail: [EMAIL PROTECTED] --------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]