You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2014/01/10 14:09:50 UTC
svn commit: r1557113 [1/2] - in
/tomcat/trunk/java/org/apache/catalina/valves: AbstractAccessLogValve.java
AccessLogValve.java
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-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org