Author: rjung
Date: Sun Jun 19 10:58:02 2011
New Revision: 1137336

URL: http://svn.apache.org/viewvc?rev=1137336&view=rev
Log:
BZ 49165 and more:
- Allow any time stamp formats supported
  by SimpleDateFormat in AccessLogValve.
- Support logging begin and/or end of request.

Docs to follow.

Modified:
    tomcat/trunk/java/org/apache/catalina/valves/AccessLogValve.java
    tomcat/trunk/webapps/docs/changelog.xml

Modified: tomcat/trunk/java/org/apache/catalina/valves/AccessLogValve.java
URL: 
http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/valves/AccessLogValve.java?rev=1137336&r1=1137335&r2=1137336&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/valves/AccessLogValve.java (original)
+++ tomcat/trunk/java/org/apache/catalina/valves/AccessLogValve.java Sun Jun 19 
10:58:02 2011
@@ -29,6 +29,7 @@ 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.TimeZone;
@@ -76,6 +77,7 @@ import org.apache.tomcat.util.ExceptionU
  * <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
@@ -149,7 +151,7 @@ public class AccessLogValve extends Valv
      * The descriptive information about this implementation.
      */
     protected static final String info =
-        "org.apache.catalina.valves.AccessLogValve/2.1";
+        "org.apache.catalina.valves.AccessLogValve/2.2";
 
 
     /**
@@ -227,39 +229,239 @@ public class AccessLogValve extends Valv
      */
     private static final String timeZoneDST;
     
+    /**
+     * 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;
+
     
     /**
      * The current log file we are writing to. Helpful when checkExists
      * is true.
      */
     protected File currentLogFile = null;
-    private static class AccessDateStruct {
-        private Date currentDate = new Date();
-        private String currentDateString = null;
-        private SimpleDateFormat dayFormatter = new SimpleDateFormat("dd");
-        private SimpleDateFormat monthFormatter = new SimpleDateFormat("MM");
-        private SimpleDateFormat yearFormatter = new SimpleDateFormat("yyyy");
-        private SimpleDateFormat timeFormatter = new 
SimpleDateFormat("HH:mm:ss");
-        public AccessDateStruct() {
-            TimeZone tz = TimeZone.getDefault();
-            dayFormatter.setTimeZone(tz);
-            monthFormatter.setTimeZone(tz);
-            yearFormatter.setTimeZone(tz);
-            timeFormatter.setTimeZone(tz);
+
+    /**
+     * <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>The cache is not threadsafe. It can be used without synchronization
+     * via thread local instances, or with synchronization as a global 
cache.</p>
+     *
+     * <p>This class uses a small thread local first level cache and a bigger
+     * synchronized global second level cache.</p>
+     */
+    private static class DateFormatCache {
+
+        private class Cache {
+
+            /* CLF log format */
+            private static final String cLFFormat = "dd/MMM/yyy:HH:mm:ss";
+
+            /* Second used to retrieve CLF format in most recent invocation */
+            private long previousSeconds = 0L;
+            /* Value of CLF format retrieved in most recent invocation */
+            private String previousFormat = "";
+
+            /* First second contained in cache */
+            private long first = 0L;
+            /* Last second contained in cache */
+            private long last = 0L;
+            /* 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();
+
+            private String cache[];
+            private SimpleDateFormat formatter;
+            private boolean isCLF = false;
+
+            private Cache parent = null;
+
+            private Cache(String format, Cache parent) {
+                cache = new String[cacheSize];
+                for (int i = 0; i < cacheSize; i++) {
+                    cache[i] = null;
+                }
+                if (format == null) {
+                    isCLF = true;
+                    format = cLFFormat;
+                }
+                formatter = new SimpleDateFormat(format);
+                formatter.setTimeZone(TimeZone.getDefault());
+                this.parent = parent;
+            }
+
+            private String getFormat(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;
+                    last = seconds;
+                } else if (seconds < first) {
+                    for (int i = 1; i < first - seconds; i++) {
+                        cache[(index + i) % cacheSize] = null;
+                    }
+                    first = seconds;
+                    last = seconds + cacheSize;
+                }
+
+                /* Last step: format new timestamp either using
+                 * parent cache or locally. */
+                if (parent != null) {
+                    synchronized(parent) {
+                        previousFormat = parent.getFormat(time);
+                    }
+                } else {
+                    currentDate.setTime(time);
+                    previousFormat = formatter.format(currentDate);
+                    if (isCLF) {
+                        StringBuilder current = new StringBuilder(32);
+                        current.append('[');
+                        current.append(previousFormat);
+                        current.append(' ');
+                        current.append(getTimeZone(currentDate));
+                        current.append(']');
+                        previousFormat = current.toString();
+                    }
+                }
+                cache[index] = previousFormat;
+                return previousFormat;
+            }
+        }
+
+        private String type;
+
+        /* Number of cached entries */
+        private int cacheSize = 0;
+
+        private DateFormatCache parent;
+        private Cache cLFCache;
+        private HashMap<String, Cache> formatCache = new HashMap<String, 
Cache>();
+
+        private DateFormatCache(int size, DateFormatCache parent) {
+            if (parent == null) {
+                type = "main";
+            } else {
+                type = "child";
+            }
+            cacheSize = size;
+            this.parent = parent;
+            Cache parentCache = null;
+            if (parent != null) {
+                synchronized(parent) {
+                    parentCache = parent.getCache(null);
+                }
+            }
+            cLFCache = new Cache(null, parentCache);
+        }
+
+        private Cache getCache(String format) {
+            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);
+                        }
+                    }
+                    cache = new Cache(format, parentCache);
+                    formatCache.put(format, cache);
+                }
+            }
+            return cache;
+        }
+
+        public String getFormat(String format, long time) {
+
+            return getCache(format).getFormat(time);
         }
     }
-    
+
+    /**
+     * Global date format cache.
+     */
+    private static final DateFormatCache globalDateCache =
+            new DateFormatCache(globalCacheSize, null);
+
+    /**
+     * Thread local date format cache.
+     */
+    private static final ThreadLocal<DateFormatCache> localDateCache =
+            new ThreadLocal<DateFormatCache>() {
+        protected DateFormatCache initialValue() {
+            return new DateFormatCache(localCacheSize, globalDateCache);
+        }
+    };
+
+
     /**
      * The system time when we last updated the Date that this valve
      * uses for log lines.
      */
-    private static final ThreadLocal<AccessDateStruct> currentDateStruct =
-            new ThreadLocal<AccessDateStruct>() {
+    private static final ThreadLocal<Date> localDate =
+            new ThreadLocal<Date>() {
         @Override
-        protected AccessDateStruct initialValue() {
-            return new AccessDateStruct();
+        protected Date initialValue() {
+            return new Date();
         }
     };
+
+    /**
+     * The list of our format types.
+     */
+    private static enum formatType {
+        CLF, SEC, MSEC, MSEC_FRAC, SDF
+    }
+
     /**
      * Resolve hosts.
      */
@@ -572,7 +774,15 @@ public class AccessLogValve extends Valv
             return;
         }
 
-        Date date = getDate();
+        /**
+         * 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);
+
         StringBuilder result = new StringBuilder(128);
 
         for (int i = 0; i < logElements.length; i++) {
@@ -759,15 +969,10 @@ public class AccessLogValve extends Valv
      * 
      * @return Date
      */
-    private Date getDate() {
-        // Only create a new Date once per second, max.
-        long systime = System.currentTimeMillis();
-        AccessDateStruct struct = currentDateStruct.get(); 
-        if ((systime - struct.currentDate.getTime()) > 1000) {
-            struct.currentDate.setTime(systime);
-            struct.currentDateString = null;
-        }
-        return struct.currentDate;
+    private static Date getDate(long systime) {
+        Date date = localDate.get();
+        date.setTime(systime);
+        return date;
     }
 
 
@@ -830,7 +1035,7 @@ public class AccessLogValve extends Valv
         }
         fileDateFormatter = new SimpleDateFormat(format);
         fileDateFormatter.setTimeZone(timezone);
-        dateStamp = 
fileDateFormatter.format(currentDateStruct.get().currentDate);
+        dateStamp = fileDateFormatter.format(new 
Date(System.currentTimeMillis()));
         open();
         
         setState(LifecycleState.STARTING);
@@ -997,31 +1202,154 @@ public class AccessLogValve extends Valv
     }
 
     /**
-     * write date and time, in Common Log Format - %t
+     * 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 pattern we use to replace "S" millisecond formatting
+         * of SimpleDateFormat by our own handling
+         */
+        private static final String msecPattern = "####";
+
+        /* Our format description string, null if CLF */
+        private String format = null;
+        /* Whether to use begin of request or end of response as the timestamp 
*/
+        private boolean usesBegin = false;
+        /* The format type */
+        private formatType type = formatType.CLF;
+        /* 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 void tidyFormat() {
+            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;
+                }
+            }
+            format = result.toString();
+        }
+
+        protected DateAndTimeElement(String header) {
+            format = header;
+            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;
+                    tidyFormat();
+                }
+            }
+        }
 
         @Override
         public void addElement(StringBuilder buf, Date date, Request request,
                 Response response, long time) {
-            AccessDateStruct struct = currentDateStruct.get();
-            if (struct.currentDateString == null) {
-                StringBuilder current = new StringBuilder(32);
-                current.append('[');
-                current.append(struct.dayFormatter.format(date));
-                current.append('/');
-                current.append(lookup(struct.monthFormatter.format(date)));
-                current.append('/');
-                current.append(struct.yearFormatter.format(date));
-                current.append(':');
-                current.append(struct.timeFormatter.format(date));
-                current.append(' ');
-                current.append(getTimeZone(date));
-                current.append(']');
-                struct.currentDateString = current.toString();
+            long timestamp = date.getTime();
+            if (usesBegin) {
+                timestamp -= time;
+            }
+            switch (type) {
+            case CLF:
+                buf.append(localDateCache.get().getFormat(null, timestamp));
+                break;
+            case SEC:
+                buf.append(timestamp / 1000);
+                break;
+            case MSEC:
+                buf.append(timestamp);
+                break;
+            case MSEC_FRAC:
+                long frac = timestamp % 1000;
+                if (frac < 100) {
+                    if (frac < 10) {
+                        buf.append("00");
+                    } else {
+                        buf.append("0");
+                    }
+                }
+                buf.append(frac);
+                break;
+            case SDF:
+                String temp = localDateCache.get().getFormat(format, 
timestamp);
+                if (usesMsecs) {
+                    String msec = Long.toString(timestamp % 1000);
+                    temp = temp.replace(msecPattern, msec);
+                }
+                buf.append(temp);
+                break;
             }
-            buf.append(struct.currentDateString);
         }
     }
 
@@ -1449,6 +1777,8 @@ public class AccessLogValve extends Valv
             return new RequestAttributeElement(header);
         case 's':
             return new SessionAttributeElement(header);            
+        case 't':
+            return new DateAndTimeElement(header);
         default:
             return new StringElement("???");
         }

Modified: tomcat/trunk/webapps/docs/changelog.xml
URL: 
http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1137336&r1=1137335&r2=1137336&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/changelog.xml (original)
+++ tomcat/trunk/webapps/docs/changelog.xml Sun Jun 19 10:58:02 2011
@@ -64,6 +64,10 @@
         so only types of interest are reported to a 
ServletContainerInitializer.
         (markt)
       </fix>
+      <fix>
+        <bug>49165</bug>: Allow any time stamp formats supported by 
SimpleDateFormat
+        in AccessLogValve. Support logging begin and/or end of request. (rjung)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Coyote">



---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to