Author: markt Date: Fri Jan 10 13:09:50 2014 New Revision: 1557113 URL: http://svn.apache.org/r1557113 Log: Fix https://issues.apache.org/bugzilla/show_bug.cgi?id=55893 Split AccessLogValve and extract the formatting logic in an AbstractAccessLogValve to facilitate other implementations that output to destinations other than files. Patch provided by Cyrille Le Clerc
Added: tomcat/trunk/java/org/apache/catalina/valves/AbstractAccessLogValve.java (with props) Modified: tomcat/trunk/java/org/apache/catalina/valves/AccessLogValve.java Added: tomcat/trunk/java/org/apache/catalina/valves/AbstractAccessLogValve.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/valves/AbstractAccessLogValve.java?rev=1557113&view=auto ============================================================================== --- tomcat/trunk/java/org/apache/catalina/valves/AbstractAccessLogValve.java (added) +++ tomcat/trunk/java/org/apache/catalina/valves/AbstractAccessLogValve.java Fri Jan 10 13:09:50 2014 @@ -0,0 +1,1565 @@ +/* + * 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.CharArrayWriter; +import java.io.IOException; +import java.net.InetAddress; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpSession; + +import org.apache.catalina.AccessLog; +import org.apache.catalina.Globals; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Session; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.coyote.RequestInfo; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.collections.SynchronizedStack; + + +/** + * <p>Abstract implementation of the <b>Valve</b> interface that generates a web + * server access log with the detailed line contents matching a configurable + * pattern. The syntax of the available patterns is similar to that supported by + * the <a href="http://httpd.apache.org/">Apache HTTP Server</a> + * <code>mod_log_config</code> module.</p> + * + * <p>Patterns for the logged message may include constant text or any of the + * following replacement strings, for which the corresponding information + * from the specified Response is substituted:</p> + * <ul> + * <li><b>%a</b> - Remote IP address + * <li><b>%A</b> - Local IP address + * <li><b>%b</b> - Bytes sent, excluding HTTP headers, or '-' if no bytes + * were sent + * <li><b>%B</b> - Bytes sent, excluding HTTP headers + * <li><b>%h</b> - Remote host name (or IP address if + * <code>enableLookups</code> for the connector is false) + * <li><b>%H</b> - Request protocol + * <li><b>%l</b> - Remote logical username from identd (always returns '-') + * <li><b>%m</b> - Request method + * <li><b>%p</b> - Local port + * <li><b>%q</b> - Query string (prepended with a '?' if it exists, otherwise + * an empty string + * <li><b>%r</b> - First line of the request + * <li><b>%s</b> - HTTP status code of the response + * <li><b>%S</b> - User session ID + * <li><b>%t</b> - Date and time, in Common Log Format format + * <li><b>%t{format}</b> - Date and time, in any format supported by SimpleDateFormat + * <li><b>%u</b> - Remote user that was authenticated + * <li><b>%U</b> - Requested URL path + * <li><b>%v</b> - Local server name + * <li><b>%D</b> - Time taken to process the request, in millis + * <li><b>%T</b> - Time taken to process the request, in seconds + * <li><b>%I</b> - current Request thread name (can compare later with stacktraces) + * </ul> + * <p>In addition, the caller can specify one of the following aliases for + * commonly utilized patterns:</p> + * <ul> + * <li><b>common</b> - <code>%h %l %u %t "%r" %s %b</code> + * <li><b>combined</b> - + * <code>%h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"</code> + * </ul> + * + * <p> + * There is also support to write information from the cookie, incoming + * header, the Session or something else in the ServletRequest.<br> + * It is modeled after the + * <a href="http://httpd.apache.org/">Apache HTTP Server</a> log configuration + * syntax:</p> + * <ul> + * <li><code>%{xxx}i</code> for incoming headers + * <li><code>%{xxx}o</code> for outgoing response headers + * <li><code>%{xxx}c</code> for a specific cookie + * <li><code>%{xxx}r</code> xxx is an attribute in the ServletRequest + * <li><code>%{xxx}s</code> xxx is an attribute in the HttpSession + * <li><code>%{xxx}t</code> xxx is an enhanced SimpleDateFormat pattern + * (see Configuration Reference document for details on supported time patterns) + * </ul> + * + * <p> + * Conditional logging is also supported. This can be done with the + * <code>conditionUnless</code> and <code>conditionIf</code> properties. + * If the value returned from ServletRequest.getAttribute(conditionUnless) + * yields a non-null value, the logging will be skipped. + * If the value returned from ServletRequest.getAttribute(conditionIf) + * yields the null value, the logging will be skipped. + * The <code>condition</code> attribute is synonym for + * <code>conditionUnless</code> and is provided for backwards compatibility. + * </p> + * + * <p> + * For extended attributes coming from a getAttribute() call, + * it is you responsibility to ensure there are no newline or + * control characters. + * </p> + * + * @author Craig R. McClanahan + * @author Jason Brittain + * @author Remy Maucherat + * @author Takayuki Kaneko + * @author Peter Rossbach + * + * @version $Id$ + */ +public abstract class AbstractAccessLogValve extends ValveBase implements AccessLog { + + private static final Log log = LogFactory.getLog(AbstractAccessLogValve.class); + + //------------------------------------------------------ Constructor + public AbstractAccessLogValve() { + super(true); + } + + // ----------------------------------------------------- Instance Variables + + + /** + * enabled this component + */ + protected boolean enabled = true; + + /** + * The pattern used to format our access log lines. + */ + protected String pattern = null; + + /** + * The size of our global date format cache + */ + private static final int globalCacheSize = 300; + + /** + * The size of our thread local date format cache + */ + private static final int localCacheSize = 60; + + /** + * <p>Cache structure for formatted timestamps based on seconds.</p> + * + * <p>The cache consists of entries for a consecutive range of + * seconds. The length of the range is configurable. It is + * implemented based on a cyclic buffer. New entries shift the range.</p> + * + * <p>There is one cache for the CLF format (the access log standard + * format) and a HashMap of caches for additional formats used by + * SimpleDateFormat.</p> + * + * <p>Although the cache supports specifying a locale when retrieving a + * formatted timestamp, each format will always use the locale given + * when the format was first used. New locales can only be used for new formats. + * The CLF format will always be formatted using the locale + * <code>en_US</code>.</p> + * + * <p>The cache is not threadsafe. It can be used without synchronization + * via thread local instances, or with synchronization as a global cache.</p> + * + * <p>The cache can be created with a parent cache to build a cache hierarchy. + * Access to the parent cache is threadsafe.</p> + * + * <p>This class uses a small thread local first level cache and a bigger + * synchronized global second level cache.</p> + */ + protected static class DateFormatCache { + + protected class Cache { + + /* CLF log format */ + private static final String cLFFormat = "dd/MMM/yyyy:HH:mm:ss Z"; + + /* Second used to retrieve CLF format in most recent invocation */ + private long previousSeconds = Long.MIN_VALUE; + /* Value of CLF format retrieved in most recent invocation */ + private String previousFormat = ""; + + /* First second contained in cache */ + private long first = Long.MIN_VALUE; + /* Last second contained in cache */ + private long last = Long.MIN_VALUE; + /* Index of "first" in the cyclic cache */ + private int offset = 0; + /* Helper object to be able to call SimpleDateFormat.format(). */ + private final Date currentDate = new Date(); + + protected final String cache[]; + private SimpleDateFormat formatter; + private boolean isCLF = false; + + private Cache parent = null; + + private Cache(Cache parent) { + this(null, parent); + } + + private Cache(String format, Cache parent) { + this(format, null, parent); + } + + private Cache(String format, Locale loc, Cache parent) { + cache = new String[cacheSize]; + for (int i = 0; i < cacheSize; i++) { + cache[i] = null; + } + if (loc == null) { + loc = cacheDefaultLocale; + } + if (format == null) { + isCLF = true; + format = cLFFormat; + formatter = new SimpleDateFormat(format, Locale.US); + } else { + formatter = new SimpleDateFormat(format, loc); + } + formatter.setTimeZone(TimeZone.getDefault()); + this.parent = parent; + } + + private String getFormatInternal(long time) { + + long seconds = time / 1000; + + /* First step: if we have seen this timestamp + during the previous call, and we need CLF, return the previous value. */ + if (seconds == previousSeconds) { + return previousFormat; + } + + /* Second step: Try to locate in cache */ + previousSeconds = seconds; + int index = (offset + (int)(seconds - first)) % cacheSize; + if (index < 0) { + index += cacheSize; + } + if (seconds >= first && seconds <= last) { + if (cache[index] != null) { + /* Found, so remember for next call and return.*/ + previousFormat = cache[index]; + return previousFormat; + } + + /* Third step: not found in cache, adjust cache and add item */ + } else if (seconds >= last + cacheSize || seconds <= first - cacheSize) { + first = seconds; + last = first + cacheSize - 1; + index = 0; + offset = 0; + for (int i = 1; i < cacheSize; i++) { + cache[i] = null; + } + } else if (seconds > last) { + for (int i = 1; i < seconds - last; i++) { + cache[(index + cacheSize - i) % cacheSize] = null; + } + first = seconds - (cacheSize - 1); + last = seconds; + offset = (index + 1) % cacheSize; + } else if (seconds < first) { + for (int i = 1; i < first - seconds; i++) { + cache[(index + i) % cacheSize] = null; + } + first = seconds; + last = seconds + (cacheSize - 1); + offset = index; + } + + /* Last step: format new timestamp either using + * parent cache or locally. */ + if (parent != null) { + synchronized(parent) { + previousFormat = parent.getFormatInternal(time); + } + } else { + currentDate.setTime(time); + previousFormat = formatter.format(currentDate); + if (isCLF) { + StringBuilder current = new StringBuilder(32); + current.append('['); + current.append(previousFormat); + current.append(']'); + previousFormat = current.toString(); + } + } + cache[index] = previousFormat; + return previousFormat; + } + } + + /* Number of cached entries */ + private int cacheSize = 0; + + private final Locale cacheDefaultLocale; + private final DateFormatCache parent; + protected final Cache cLFCache; + private final HashMap<String, Cache> formatCache = new HashMap<>(); + + protected DateFormatCache(int size, Locale loc, DateFormatCache parent) { + cacheSize = size; + cacheDefaultLocale = loc; + this.parent = parent; + Cache parentCache = null; + if (parent != null) { + synchronized(parent) { + parentCache = parent.getCache(null, null); + } + } + cLFCache = new Cache(parentCache); + } + + private Cache getCache(String format, Locale loc) { + Cache cache; + if (format == null) { + cache = cLFCache; + } else { + cache = formatCache.get(format); + if (cache == null) { + Cache parentCache = null; + if (parent != null) { + synchronized(parent) { + parentCache = parent.getCache(format, loc); + } + } + cache = new Cache(format, loc, parentCache); + formatCache.put(format, cache); + } + } + return cache; + } + + public String getFormat(long time) { + return cLFCache.getFormatInternal(time); + } + + public String getFormat(String format, Locale loc, long time) { + return getCache(format, loc).getFormatInternal(time); + } + } + + /** + * Global date format cache. + */ + private static final DateFormatCache globalDateCache = + new DateFormatCache(globalCacheSize, Locale.getDefault(), null); + + /** + * Thread local date format cache. + */ + private static final ThreadLocal<DateFormatCache> localDateCache = + new ThreadLocal<DateFormatCache>() { + @Override + protected DateFormatCache initialValue() { + return new DateFormatCache(localCacheSize, Locale.getDefault(), globalDateCache); + } + }; + + + /** + * The system time when we last updated the Date that this valve + * uses for log lines. + */ + private static final ThreadLocal<Date> localDate = + new ThreadLocal<Date>() { + @Override + protected Date initialValue() { + return new Date(); + } + }; + + /** + * The list of our format types. + */ + private static enum FormatType { + CLF, SEC, MSEC, MSEC_FRAC, SDF + } + + /** + * Are we doing conditional logging. default null. + * It is the value of <code>conditionUnless</code> property. + */ + protected String condition = null; + + /** + * Are we doing conditional logging. default null. + * It is the value of <code>conditionIf</code> property. + */ + protected String conditionIf = null; + + /** + * Name of locale used to format timestamps in log entries and in + * log file name suffix. + */ + protected String localeName = Locale.getDefault().toString(); + + + /** + * Locale used to format timestamps in log entries and in + * log file name suffix. + */ + protected Locale locale = Locale.getDefault(); + + /** + * Array of AccessLogElement, they will be used to make log message. + */ + protected AccessLogElement[] logElements = null; + + /** + * @see #setRequestAttributesEnabled(boolean) + */ + protected boolean requestAttributesEnabled = false; + + /** + * Buffer pool used for log message generation. Pool used to reduce garbage + * generation. + */ + private SynchronizedStack<CharArrayWriter> charArrayWriters = + new SynchronizedStack<>(); + + /** + * Log message buffers are usually recycled and re-used. To prevent + * excessive memory usage, if a buffer grows beyond this size it will be + * discarded. The default is 256 characters. This should be set to larger + * than the typical access log message size. + */ + private int maxLogMessageBufferSize = 256; + + // ------------------------------------------------------------- Properties + + /** + * {@inheritDoc} + */ + @Override + public void setRequestAttributesEnabled(boolean requestAttributesEnabled) { + this.requestAttributesEnabled = requestAttributesEnabled; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean getRequestAttributesEnabled() { + return requestAttributesEnabled; + } + + /** + * @return Returns the enabled. + */ + public boolean getEnabled() { + return enabled; + } + + /** + * @param enabled + * The enabled to set. + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * Return the format pattern. + */ + public String getPattern() { + return (this.pattern); + } + + + /** + * Set the format pattern, first translating any recognized alias. + * + * @param pattern The new pattern + */ + public void setPattern(String pattern) { + if (pattern == null) { + this.pattern = ""; + } else if (pattern.equals(Constants.AccessLog.COMMON_ALIAS)) { + this.pattern = Constants.AccessLog.COMMON_PATTERN; + } else if (pattern.equals(Constants.AccessLog.COMBINED_ALIAS)) { + this.pattern = Constants.AccessLog.COMBINED_PATTERN; + } else { + this.pattern = pattern; + } + logElements = createLogElements(); + } + + /** + * Return whether the attribute name to look for when + * performing conditional logging. 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; + } + + + /** + * Return whether the attribute name to look for when + * performing conditional logging. If null, every + * request is logged. + */ + public String getConditionUnless() { + return getCondition(); + } + + + /** + * 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 setConditionUnless(String condition) { + setCondition(condition); + } + + /** + * Return whether the attribute name to look for when + * performing conditional logging. If null, every + * request is logged. + */ + public String getConditionIf() { + return conditionIf; + } + + + /** + * 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 setConditionIf(String condition) { + this.conditionIf = condition; + } + + /** + * Return the locale used to format timestamps in log entries and in + * log file name suffix. + */ + public String getLocale() { + return localeName; + } + + + /** + * Set the locale used to format timestamps in log entries and in + * log file name suffix. Changing the locale is only supported + * as long as the AccessLogValve has not logged anything. Changing + * the locale later can lead to inconsistent formatting. + * + * @param localeName The locale to use. + */ + public void setLocale(String localeName) { + this.localeName = localeName; + locale = findLocale(localeName, locale); + } + + // --------------------------------------------------------- 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 + * + * @exception IOException if an input/output error has occurred + * @exception ServletException if a servlet error has occurred + */ + @Override + public void invoke(Request request, Response response) throws IOException, + ServletException { + getNext().invoke(request, response); + } + + + @Override + public void log(Request request, Response response, long time) { + if (!getState().isAvailable() || !getEnabled() || logElements == null + || condition != null + && null != request.getRequest().getAttribute(condition) + || conditionIf != null + && null == request.getRequest().getAttribute(conditionIf)) { + return; + } + + /** + * XXX This is a bit silly, but we want to have start and stop time and + * duration consistent. It would be better to keep start and stop + * simply in the request and/or response object and remove time + * (duration) from the interface. + */ + long start = request.getCoyoteRequest().getStartTime(); + Date date = getDate(start + time); + + CharArrayWriter result = charArrayWriters.pop(); + if (result == null) { + result = new CharArrayWriter(128); + } + + for (int i = 0; i < logElements.length; i++) { + logElements[i].addElement(result, date, request, response, time); + } + + log(result); + + if (result.size() <= maxLogMessageBufferSize) { + result.reset(); + charArrayWriters.push(result); + } + } + + // -------------------------------------------------------- Protected Methods + + /** + * Log the specified message. + * + * @param message Message to be logged. This object will be recycled by + * the calling method. + */ + protected abstract void log(CharArrayWriter message); + + // -------------------------------------------------------- Private Methods + + /** + * 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. + * + * @return Date + */ + private static Date getDate(long systime) { + Date date = localDate.get(); + date.setTime(systime); + return date; + } + + + /** + * Find a locale by name + */ + protected static Locale findLocale(String name, Locale fallback) { + if (name == null || name.isEmpty()) { + return Locale.getDefault(); + } else { + for (Locale l: Locale.getAvailableLocales()) { + if (name.equals(l.toString())) { + return(l); + } + } + } + log.error(sm.getString("accessLogValve.invalidLocale", name)); + return fallback; + } + + + /** + * 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 { + + setState(LifecycleState.STARTING); + } + + + /** + * 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 { + + setState(LifecycleState.STOPPING); + } + + /** + * AccessLogElement writes the partial message into the buffer. + */ + protected interface AccessLogElement { + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time); + + } + + /** + * write thread name - %I + */ + protected static class ThreadNameElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + RequestInfo info = request.getCoyoteRequest().getRequestProcessor(); + if(info != null) { + buf.append(info.getWorkerThreadName()); + } else { + buf.append("-"); + } + } + } + + /** + * write local IP address - %A + */ + protected static class LocalAddrElement implements AccessLogElement { + + private static final String LOCAL_ADDR_VALUE; + + static { + String init; + try { + init = InetAddress.getLocalHost().getHostAddress(); + } catch (Throwable e) { + ExceptionUtils.handleThrowable(e); + init = "127.0.0.1"; + } + LOCAL_ADDR_VALUE = init; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + buf.append(LOCAL_ADDR_VALUE); + } + } + + /** + * write remote IP address - %a + */ + protected class RemoteAddrElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + if (requestAttributesEnabled) { + Object addr = request.getAttribute(REMOTE_ADDR_ATTRIBUTE); + if (addr == null) { + buf.append(request.getRemoteAddr()); + } else { + buf.append(addr.toString()); + } + } else { + buf.append(request.getRemoteAddr()); + } + } + } + + /** + * write remote host name - %h + */ + protected class HostElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + String value = null; + if (requestAttributesEnabled) { + Object host = request.getAttribute(REMOTE_HOST_ATTRIBUTE); + if (host != null) { + value = host.toString(); + } + } + if (value == null || value.length() == 0) { + value = request.getRemoteHost(); + } + if (value == null || value.length() == 0) { + value = "-"; + } + buf.append(value); + } + } + + /** + * write remote logical username from identd (always returns '-') - %l + */ + protected static class LogicalUserNameElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + buf.append('-'); + } + } + + /** + * write request protocol - %H + */ + protected class ProtocolElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + if (requestAttributesEnabled) { + Object proto = request.getAttribute(PROTOCOL_ATTRIBUTE); + if (proto == null) { + buf.append(request.getProtocol()); + } else { + buf.append(proto.toString()); + } + } else { + buf.append(request.getProtocol()); + } + } + } + + /** + * write remote user that was authenticated (if any), else '-' - %u + */ + protected static class UserElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + if (request != null) { + String value = request.getRemoteUser(); + if (value != null) { + buf.append(value); + } else { + buf.append('-'); + } + } else { + buf.append('-'); + } + } + } + + /** + * write date and time, in configurable format (default CLF) - %t or %t{format} + */ + protected class DateAndTimeElement implements AccessLogElement { + + /** + * Format prefix specifying request start time + */ + private static final String requestStartPrefix = "begin"; + + /** + * Format prefix specifying response end time + */ + private static final String responseEndPrefix = "end"; + + /** + * Separator between optional prefix and rest of format + */ + private static final String prefixSeparator = ":"; + + /** + * Special format for seconds since epoch + */ + private static final String secFormat = "sec"; + + /** + * Special format for milliseconds since epoch + */ + private static final String msecFormat = "msec"; + + /** + * Special format for millisecond part of timestamp + */ + private static final String msecFractionFormat = "msec_frac"; + + /** + * The patterns we use to replace "S" and "SSS" millisecond + * formatting of SimpleDateFormat by our own handling + */ + private static final String msecPattern = "{#}"; + private static final String trippleMsecPattern = + msecPattern + msecPattern + msecPattern; + + /* Our format description string, null if CLF */ + private final String format; + /* Whether to use begin of request or end of response as the timestamp */ + private final boolean usesBegin; + /* The format type */ + private final FormatType type; + /* Whether we need to postprocess by adding milliseconds */ + private boolean usesMsecs = false; + + protected DateAndTimeElement() { + this(null); + } + + /** + * Replace the millisecond formatting character 'S' by + * some dummy characters in order to make the resulting + * formatted time stamps cacheable. We replace the dummy + * chars later with the actual milliseconds because that's + * relatively cheap. + */ + private String tidyFormat(String format) { + boolean escape = false; + StringBuilder result = new StringBuilder(); + int len = format.length(); + char x; + for (int i = 0; i < len; i++) { + x = format.charAt(i); + if (escape || x != 'S') { + result.append(x); + } else { + result.append(msecPattern); + usesMsecs = true; + } + if (x == '\'') { + escape = !escape; + } + } + return result.toString(); + } + + protected DateAndTimeElement(String header) { + String format = header; + boolean usesBegin = false; + FormatType type = FormatType.CLF; + + if (format != null) { + if (format.equals(requestStartPrefix)) { + usesBegin = true; + format = ""; + } else if (format.startsWith(requestStartPrefix + prefixSeparator)) { + usesBegin = true; + format = format.substring(6); + } else if (format.equals(responseEndPrefix)) { + usesBegin = false; + format = ""; + } else if (format.startsWith(responseEndPrefix + prefixSeparator)) { + usesBegin = false; + format = format.substring(4); + } + if (format.length() == 0) { + type = FormatType.CLF; + } else if (format.equals(secFormat)) { + type = FormatType.SEC; + } else if (format.equals(msecFormat)) { + type = FormatType.MSEC; + } else if (format.equals(msecFractionFormat)) { + type = FormatType.MSEC_FRAC; + } else { + type = FormatType.SDF; + format = tidyFormat(format); + } + } + this.format = format; + this.usesBegin = usesBegin; + this.type = type; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + long timestamp = date.getTime(); + long frac; + if (usesBegin) { + timestamp -= time; + } + switch (type) { + case CLF: + buf.append(localDateCache.get().getFormat(timestamp)); + break; + case SEC: + buf.append(Long.toString(timestamp / 1000)); + break; + case MSEC: + buf.append(Long.toString(timestamp)); + break; + case MSEC_FRAC: + frac = timestamp % 1000; + if (frac < 100) { + if (frac < 10) { + buf.append('0'); + buf.append('0'); + } else { + buf.append('0'); + } + } + buf.append(Long.toString(frac)); + break; + case SDF: + String temp = localDateCache.get().getFormat(format, locale, timestamp); + if (usesMsecs) { + frac = timestamp % 1000; + StringBuilder trippleMsec = new StringBuilder(4); + if (frac < 100) { + if (frac < 10) { + trippleMsec.append('0'); + trippleMsec.append('0'); + } else { + trippleMsec.append('0'); + } + } + trippleMsec.append(frac); + temp = temp.replace(trippleMsecPattern, trippleMsec); + temp = temp.replace(msecPattern, Long.toString(frac)); + } + buf.append(temp); + break; + } + } + } + + /** + * write first line of the request (method and request URI) - %r + */ + protected static class RequestElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + if (request != null) { + String method = request.getMethod(); + if (method == null) { + // No method means no request line + buf.append('-'); + } else { + buf.append(request.getMethod()); + buf.append(' '); + buf.append(request.getRequestURI()); + if (request.getQueryString() != null) { + buf.append('?'); + buf.append(request.getQueryString()); + } + buf.append(' '); + buf.append(request.getProtocol()); + } + } else { + buf.append('-'); + } + } + } + + /** + * write HTTP status code of the response - %s + */ + protected static class HttpStatusCodeElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + if (response != null) { + // This approach is used to reduce GC from toString conversion + int status = response.getStatus(); + if (100 <= status && status < 1000) { + buf.append((char) ('0' + (status / 100))) + .append((char) ('0' + ((status / 10) % 10))) + .append((char) ('0' + (status % 10))); + } else { + buf.append(Integer.toString(status)); + } + } else { + buf.append('-'); + } + } + } + + /** + * write local port on which this request was received - %p + */ + protected class LocalPortElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + if (requestAttributesEnabled) { + Object port = request.getAttribute(SERVER_PORT_ATTRIBUTE); + if (port == null) { + buf.append(Integer.toString(request.getServerPort())); + } else { + buf.append(port.toString()); + } + } else { + buf.append(Integer.toString(request.getServerPort())); + } + } + } + + /** + * write bytes sent, excluding HTTP headers - %b, %B + */ + protected static class ByteSentElement implements AccessLogElement { + private final boolean conversion; + + /** + * if conversion is true, write '-' instead of 0 - %b + */ + public ByteSentElement(boolean conversion) { + this.conversion = conversion; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + // Don't need to flush since trigger for log message is after the + // response has been committed + long length = response.getBytesWritten(false); + if (length <= 0) { + // Protect against nulls and unexpected types as these values + // may be set by untrusted applications + Object start = request.getAttribute( + Globals.SENDFILE_FILE_START_ATTR); + if (start instanceof Long) { + Object end = request.getAttribute( + Globals.SENDFILE_FILE_END_ATTR); + if (end instanceof Long) { + length = ((Long) end).longValue() - + ((Long) start).longValue(); + } + } + } + if (length <= 0 && conversion) { + buf.append('-'); + } else { + buf.append(Long.toString(length)); + } + } + } + + /** + * write request method (GET, POST, etc.) - %m + */ + protected static class MethodElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + if (request != null) { + buf.append(request.getMethod()); + } + } + } + + /** + * write time taken to process the request - %D, %T + */ + protected static class ElapsedTimeElement implements AccessLogElement { + private final boolean millis; + + /** + * if millis is true, write time in millis - %D + * if millis is false, write time in seconds - %T + */ + public ElapsedTimeElement(boolean millis) { + this.millis = millis; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + if (millis) { + buf.append(Long.toString(time)); + } else { + // second + buf.append(Long.toString(time / 1000)); + buf.append('.'); + int remains = (int) (time % 1000); + buf.append(Long.toString(remains / 100)); + remains = remains % 100; + buf.append(Long.toString(remains / 10)); + buf.append(Long.toString(remains % 10)); + } + } + } + + /** + * write time until first byte is written (commit time) in millis - %F + */ + protected static class FirstByteTimeElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { + long commitTime = response.getCoyoteResponse().getCommitTime(); + if (commitTime == -1) { + buf.append('-'); + } else { + long delta = commitTime - request.getCoyoteRequest().getStartTime(); + buf.append(Long.toString(delta)); + } + } + } + + /** + * write Query string (prepended with a '?' if it exists) - %q + */ + protected static class QueryElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + String query = null; + if (request != null) { + query = request.getQueryString(); + } + if (query != null) { + buf.append('?'); + buf.append(query); + } + } + } + + /** + * write user session ID - %S + */ + protected static class SessionIdElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + if (request == null) { + buf.append('-'); + } else { + Session session = request.getSessionInternal(false); + if (session == null) { + buf.append('-'); + } else { + buf.append(session.getIdInternal()); + } + } + } + } + + /** + * write requested URL path - %U + */ + protected static class RequestURIElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + if (request != null) { + buf.append(request.getRequestURI()); + } else { + buf.append('-'); + } + } + } + + /** + * write local server name - %v + */ + protected static class LocalServerNameElement implements AccessLogElement { + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + buf.append(request.getServerName()); + } + } + + /** + * write any string + */ + protected static class StringElement implements AccessLogElement { + private final String str; + + public StringElement(String str) { + this.str = str; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + buf.append(str); + } + } + + /** + * write incoming headers - %{xxx}i + */ + protected static class HeaderElement implements AccessLogElement { + private final String header; + + public HeaderElement(String header) { + this.header = header; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + Enumeration<String> iter = request.getHeaders(header); + if (iter.hasMoreElements()) { + buf.append(iter.nextElement()); + while (iter.hasMoreElements()) { + buf.append(',').append(iter.nextElement()); + } + return; + } + buf.append('-'); + } + } + + /** + * write a specific cookie - %{xxx}c + */ + protected static class CookieElement implements AccessLogElement { + private final String header; + + public CookieElement(String header) { + this.header = header; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + String value = "-"; + Cookie[] c = request.getCookies(); + if (c != null) { + for (int i = 0; i < c.length; i++) { + if (header.equals(c[i].getName())) { + value = c[i].getValue(); + break; + } + } + } + buf.append(value); + } + } + + /** + * write a specific response header - %{xxx}o + */ + protected static class ResponseHeaderElement implements AccessLogElement { + private final String header; + + public ResponseHeaderElement(String header) { + this.header = header; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + if (null != response) { + Iterator<String> iter = response.getHeaders(header).iterator(); + if (iter.hasNext()) { + buf.append(iter.next()); + while (iter.hasNext()) { + buf.append(',').append(iter.next()); + } + return; + } + } + buf.append('-'); + } + } + + /** + * write an attribute in the ServletRequest - %{xxx}r + */ + protected static class RequestAttributeElement implements AccessLogElement { + private final String header; + + public RequestAttributeElement(String header) { + this.header = header; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + Object value = null; + if (request != null) { + value = request.getAttribute(header); + } else { + value = "??"; + } + if (value != null) { + if (value instanceof String) { + buf.append((String) value); + } else { + buf.append(value.toString()); + } + } else { + buf.append('-'); + } + } + } + + /** + * write an attribute in the HttpSession - %{xxx}s + */ + protected static class SessionAttributeElement implements AccessLogElement { + private final String header; + + public SessionAttributeElement(String header) { + this.header = header; + } + + @Override + public void addElement(CharArrayWriter buf, Date date, Request request, + Response response, long time) { + Object value = null; + if (null != request) { + HttpSession sess = request.getSession(false); + if (null != sess) { + value = sess.getAttribute(header); + } + } else { + value = "??"; + } + if (value != null) { + if (value instanceof String) { + buf.append((String) value); + } else { + buf.append(value.toString()); + } + } else { + buf.append('-'); + } + } + } + + + /** + * parse pattern string and create the array of AccessLogElement + */ + protected AccessLogElement[] createLogElements() { + List<AccessLogElement> list = new ArrayList<>(); + boolean replace = false; + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < pattern.length(); i++) { + char ch = pattern.charAt(i); + if (replace) { + /* + * For code that processes {, the behavior will be ... if I do + * not encounter a closing } - then I ignore the { + */ + if ('{' == ch) { + StringBuilder name = new StringBuilder(); + int j = i + 1; + for (; j < pattern.length() && '}' != pattern.charAt(j); j++) { + name.append(pattern.charAt(j)); + } + if (j + 1 < pattern.length()) { + /* the +1 was to account for } which we increment now */ + j++; + list.add(createAccessLogElement(name.toString(), + pattern.charAt(j))); + i = j; /* Since we walked more than one character */ + } else { + // D'oh - end of string - pretend we never did this + // and do processing the "old way" + list.add(createAccessLogElement(ch)); + } + } else { + list.add(createAccessLogElement(ch)); + } + replace = false; + } else if (ch == '%') { + replace = true; + list.add(new StringElement(buf.toString())); + buf = new StringBuilder(); + } else { + buf.append(ch); + } + } + if (buf.length() > 0) { + list.add(new StringElement(buf.toString())); + } + return list.toArray(new AccessLogElement[0]); + } + + /** + * create an AccessLogElement implementation which needs header string + */ + protected AccessLogElement createAccessLogElement(String header, char pattern) { + switch (pattern) { + case 'i': + return new HeaderElement(header); + case 'c': + return new CookieElement(header); + case 'o': + return new ResponseHeaderElement(header); + case 'r': + return new RequestAttributeElement(header); + case 's': + return new SessionAttributeElement(header); + case 't': + return new DateAndTimeElement(header); + default: + return new StringElement("???"); + } + } + + /** + * create an AccessLogElement implementation + */ + protected AccessLogElement createAccessLogElement(char pattern) { + switch (pattern) { + case 'a': + return new RemoteAddrElement(); + case 'A': + return new LocalAddrElement(); + case 'b': + return new ByteSentElement(true); + case 'B': + return new ByteSentElement(false); + case 'D': + return new ElapsedTimeElement(true); + case 'F': + return new FirstByteTimeElement(); + case 'h': + return new HostElement(); + case 'H': + return new ProtocolElement(); + case 'l': + return new LogicalUserNameElement(); + case 'm': + return new MethodElement(); + case 'p': + return new LocalPortElement(); + case 'q': + return new QueryElement(); + case 'r': + return new RequestElement(); + case 's': + return new HttpStatusCodeElement(); + case 'S': + return new SessionIdElement(); + case 't': + return new DateAndTimeElement(); + case 'T': + return new ElapsedTimeElement(false); + case 'u': + return new UserElement(); + case 'U': + return new RequestURIElement(); + case 'v': + return new LocalServerNameElement(); + case 'I': + return new ThreadNameElement(); + default: + return new StringElement("???" + pattern + "???"); + } + } +} Propchange: tomcat/trunk/java/org/apache/catalina/valves/AbstractAccessLogValve.java ------------------------------------------------------------------------------ svn:eol-style = native --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org