You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tapestry.apache.org by hl...@apache.org on 2014/05/31 01:57:29 UTC

[2/3] git commit: Integrate an exception reporting service that will write exception report text files to the file system

Integrate an exception reporting service that will write exception report text files to the file system


Project: http://git-wip-us.apache.org/repos/asf/tapestry-5/repo
Commit: http://git-wip-us.apache.org/repos/asf/tapestry-5/commit/511813f3
Tree: http://git-wip-us.apache.org/repos/asf/tapestry-5/tree/511813f3
Diff: http://git-wip-us.apache.org/repos/asf/tapestry-5/diff/511813f3

Branch: refs/heads/master
Commit: 511813f381c7724024cc818977e9a85f7eade22a
Parents: 08807a1
Author: Howard M. Lewis Ship <hl...@apache.org>
Authored: Fri May 30 16:55:07 2014 -0700
Committer: Howard M. Lewis Ship <hl...@apache.org>
Committed: Fri May 30 16:57:01 2014 -0700

----------------------------------------------------------------------
 54_RELEASE_NOTES.md                             |   4 +
 .../org/apache/tapestry5/SymbolConstants.java   |  14 +-
 .../corelib/pages/ExceptionReport.java          |  42 +--
 .../internal/TapestryInternalUtils.java         |  31 +-
 .../DefaultRequestExceptionHandler.java         |  56 +--
 .../exceptions/ExceptionReporterImpl.java       | 340 +++++++++++++++++++
 .../tapestry5/modules/TapestryModule.java       |   3 +
 .../services/exceptions/ExceptionReporter.java  |  33 ++
 .../services/exceptions/package-info.java       |   4 +
 .../tapestry5/corelib/pages/ExceptionReport.tml |   4 +-
 .../DefaultRequestExceptionHandlerTest.java     | 117 ++++---
 11 files changed, 535 insertions(+), 113 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/54_RELEASE_NOTES.md
----------------------------------------------------------------------
diff --git a/54_RELEASE_NOTES.md b/54_RELEASE_NOTES.md
index 23fc912..eda173b 100644
--- a/54_RELEASE_NOTES.md
+++ b/54_RELEASE_NOTES.md
@@ -159,6 +159,10 @@ resurface in the future as a CSS expression, but is currently not supported.
 
 The default exception report page has been modified to display a list of threads.
 
+## ExceptionReporter Service
+
+A new service, `ExceptionReporter`, will now create a text file on the file system for each runtime request processing exception.
+
 ## Symbol tapestry.asset-path-prefix
 
 The definition of the symbol 'tapestry.asset-path-prefix' has changed; it no longer includes the leading and trailing

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
index d2526ae..6ab3759 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/SymbolConstants.java
@@ -1,5 +1,3 @@
-// Copyright 2008-2014 The Apache Software Foundation
-//
 // Licensed 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
@@ -474,15 +472,25 @@ public class SymbolConstants
      * @since 5.4
      */
     public static final String FORM_FIELD_CSS_CLASS = "tapestry.form-field-css-class";
-    
+
     /**
      * Defines whether {@link java.text.DateFormat} instances created by Tapestry should be
      * lenient or not by default. The default value is <code>false</code>.
+     *
      * @since 5.4
      */
     public static final String LENIENT_DATE_FORMAT = "tapestry.lenient-date-format";
 
     /**
+     * The directory to which exception report files should be written. The default is appropriate
+     * for development: {@code build/exceptions}, and should be changed for production.
+     *
+     * @see org.apache.tapestry5.services.exceptions.ExceptionReporter
+     * @since 5.4
+     */
+    
+    public static final String EXCEPTION_REPORTS_DIR = "tapestry.exception-reports-dir";
+    /**
      * Defines whether {@link CSSURLRewriter} will throw an exception when a CSS file
      * references an URL which doesn't exist. The default value is <code>false</code>.
      * 

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
index 1ca7e25..bdc853a 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/pages/ExceptionReport.java
@@ -1,5 +1,3 @@
-// Copyright 2006-2013 The Apache Software Foundation
-//
 // Licensed 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
@@ -24,6 +22,7 @@ import org.apache.tapestry5.annotations.UnknownActivationContextCheck;
 import org.apache.tapestry5.func.F;
 import org.apache.tapestry5.func.Mapper;
 import org.apache.tapestry5.internal.InternalConstants;
+import org.apache.tapestry5.internal.TapestryInternalUtils;
 import org.apache.tapestry5.internal.services.PageActivationContextCollector;
 import org.apache.tapestry5.internal.services.ReloadHelper;
 import org.apache.tapestry5.ioc.annotations.Inject;
@@ -34,7 +33,6 @@ import org.apache.tapestry5.services.*;
 
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.Arrays;
 import java.util.List;
 import java.util.regex.Pattern;
 
@@ -115,14 +113,15 @@ public class ExceptionReport implements ExceptionReporter
 
     public class ThreadInfo implements Comparable<ThreadInfo>
     {
-        public final String className, name, flags;
+        public final String className, name, state, flags;
 
         public final ThreadGroup group;
 
-        public ThreadInfo(String className, String name, String flags, ThreadGroup group)
+        public ThreadInfo(String className, String name, String state, String flags, ThreadGroup group)
         {
             this.className = className;
             this.name = name;
+            this.state = state;
             this.flags = flags;
             this.group = group;
         }
@@ -212,39 +211,9 @@ public class ExceptionReport implements ExceptionReporter
         return getPropertyValue().split(pathSeparator);
     }
 
-    private Thread[] assembleThreads()
-    {
-        ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
-
-        while (true)
-        {
-            ThreadGroup parentGroup = rootGroup.getParent();
-            if (parentGroup == null)
-            {
-                break;
-            }
-            rootGroup = parentGroup;
-        }
-
-        Thread[] threads = new Thread[rootGroup.activeCount()];
-
-        while (true)
-        {
-            // A really ugly API. threads.length must be larger than
-            // the actual number of threads, just so we can determine
-            // if we're done.
-            int count = rootGroup.enumerate(threads, true);
-            if (count < threads.length)
-            {
-                return Arrays.copyOf(threads, count);
-            }
-            threads = new Thread[threads.length * 2];
-        }
-    }
-
     public List<ThreadInfo> getThreads()
     {
-        return F.flow(assembleThreads()).map(new Mapper<Thread, ThreadInfo>()
+        return F.flow(TapestryInternalUtils.getAllThreads()).map(new Mapper<Thread, ThreadInfo>()
         {
             @Override
             public ThreadInfo map(Thread t)
@@ -271,6 +240,7 @@ public class ExceptionReport implements ExceptionReporter
 
                 return new ThreadInfo(Thread.currentThread() == t ? "active-thread" : "",
                         t.getName(),
+                        t.getState().name(),
                         InternalUtils.join(flags),
                         t.getThreadGroup());
             }

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/internal/TapestryInternalUtils.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/TapestryInternalUtils.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/TapestryInternalUtils.java
index 6c6138b..3435d64 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/TapestryInternalUtils.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/TapestryInternalUtils.java
@@ -1,5 +1,3 @@
-// Copyright 2006-2013 The Apache Software Foundation
-//
 // Licensed 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
@@ -33,6 +31,7 @@ import java.io.OutputStream;
 import java.lang.annotation.Annotation;
 import java.lang.ref.Reference;
 import java.lang.reflect.Type;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.regex.Pattern;
@@ -587,5 +586,33 @@ public class TapestryInternalUtils
 
         return ref == null ? null : ref.get();
     }
+
+    /**
+     * Gathers together an array containing all the threads.
+     * @since 5.4 */
+    public static Thread[] getAllThreads() {
+        ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
+
+        while (true) {
+            ThreadGroup parentGroup = rootGroup.getParent();
+            if (parentGroup == null) {
+                break;
+            }
+            rootGroup = parentGroup;
+        }
+
+        Thread[] threads = new Thread[rootGroup.activeCount()];
+
+        while (true) {
+            // A really ugly API. threads.length must be larger than
+            // the actual number of threads, just so we can determine
+            // if we're done.
+            int count = rootGroup.enumerate(threads, true);
+            if (count < threads.length) {
+                return Arrays.copyOf(threads, count);
+            }
+            threads = new Thread[threads.length * 2];
+        }
+    }
 }
 

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandler.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandler.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandler.java
index 70f72cb..de6b50b 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandler.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandler.java
@@ -1,5 +1,3 @@
-// Copyright 2006-2013 The Apache Software Foundation
-//
 // Licensed 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
@@ -44,14 +42,14 @@ import java.util.Map.Entry;
  * servlet spec's standard error handling, the default exception handler allows configuring handlers for specific types of
  * exceptions. The error-page/exception-type configuration in web.xml does not work in Tapestry application as errors are
  * wrapped in Tapestry's exception types (see {@link OperationException} and {@link ComponentEventException} ).
- *
+ * <p/>
  * Configurations are flexible. You can either contribute a {@link ExceptionHandlerAssistant} to use arbitrary complex logic
  * for error handling or a page class to render for the specific exception. Additionally, exceptions can carry context for the
  * error page. Exception context is formed either from the name of Exception (e.g. SmtpNotRespondingException -> ServiceFailure mapping
  * would render a page with URL /servicefailure/smtpnotresponding) or they can implement {@link ContextAwareException} interface.
- *
+ * <p/>
  * If no configured exception type is found, the default exception page {@link SymbolConstants#EXCEPTION_REPORT_PAGE} is rendered.
- * This fallback exception page must implement the {@link ExceptionReporter} interface.
+ * This fallback exception page must implement the {@link org.apache.tapestry5.services.ExceptionReporter} interface.
  */
 public class DefaultRequestExceptionHandler implements RequestExceptionHandler
 {
@@ -71,30 +69,28 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
 
     private final LinkSource linkSource;
 
+    private final org.apache.tapestry5.services.exceptions.ExceptionReporter exceptionReporter;
+
     // should be Class<? extends Throwable>, Object but it's not allowed to configure subtypes
     private final Map<Class, Object> configuration;
 
     /**
-     * @param pageCache
-     * @param renderer
-     * @param logger
-     * @param pageName
-     * @param request
-     * @param response
-     * @param componentClassResolver
-     * @param linkSource
-     * @param serviceResources
-     * @param configuration A map of Exception class and handler values. A handler is either a page class or an ExceptionHandlerAssistant. ExceptionHandlerAssistant can be a class
-     * in which case the instance is autobuilt.
+     * @param configuration
+     *         A map of Exception class and handler values. A handler is either a page class or an ExceptionHandlerAssistant. ExceptionHandlerAssistant can be a class
      */
     @SuppressWarnings("rawtypes")
-    public DefaultRequestExceptionHandler(RequestPageCache pageCache, PageResponseRenderer renderer, Logger logger,
-
+    public DefaultRequestExceptionHandler(RequestPageCache pageCache,
+                                          PageResponseRenderer renderer,
+                                          Logger logger,
                                           @Symbol(SymbolConstants.EXCEPTION_REPORT_PAGE)
                                           String pageName,
-
-                                          Request request, Response response, ComponentClassResolver componentClassResolver,
-                                          LinkSource linkSource, ServiceResources serviceResources, Map<Class, Object> configuration)
+                                          Request request,
+                                          Response response,
+                                          ComponentClassResolver componentClassResolver,
+                                          LinkSource linkSource,
+                                          ServiceResources serviceResources,
+                                          org.apache.tapestry5.services.exceptions.ExceptionReporter exceptionReporter,
+                                          Map<Class, Object> configuration)
     {
         this.pageCache = pageCache;
         this.renderer = renderer;
@@ -104,6 +100,7 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
         this.response = response;
         this.componentClassResolver = componentClassResolver;
         this.linkSource = linkSource;
+        this.exceptionReporter = exceptionReporter;
 
         Map<Class<ExceptionHandlerAssistant>, ExceptionHandlerAssistant> handlerAssistants = new HashMap<Class<ExceptionHandlerAssistant>, ExceptionHandlerAssistant>();
 
@@ -131,14 +128,14 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
 
     /**
      * Handles the exception thrown at some point the request was being processed
-     *
+     * <p/>
      * First checks if there was a specific exception handler/page configured for this exception type, it's super class or super-super class.
      * Renders the default exception page if none was configured.
      *
-     * @param exception The exception that was thrown
-     *
+     * @param exception
+     *         The exception that was thrown
      */
-    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @SuppressWarnings({"rawtypes", "unchecked"})
     public void handleRequestException(Throwable exception) throws IOException
     {
         // skip handling of known exceptions if there are none configured
@@ -235,6 +232,10 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
     {
         logger.error(String.format("Processing of request failed with uncaught exception: %s", exception), exception);
 
+        // In the case where one of the contributed rules, above, changes the behavior, then we don't report the
+        // exception. This is just for exceptions that are going to be rendered, real failures.
+        exceptionReporter.reportException(exception);
+
         // TAP5-233: Make sure the client knows that an error occurred.
 
         response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
@@ -249,7 +250,7 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
 
         Page page = pageCache.get(pageName);
 
-        ExceptionReporter rootComponent = (ExceptionReporter) page.getRootComponent();
+        org.apache.tapestry5.services.ExceptionReporter rootComponent = (org.apache.tapestry5.services.ExceptionReporter) page.getRootComponent();
 
         // Let the page set up for the new exception.
 
@@ -262,7 +263,8 @@ public class DefaultRequestExceptionHandler implements RequestExceptionHandler
      * Form exception context either from the name of the exception, or the context the exception contains if it's of type
      * {@link ContextAwareException}
      *
-     * @param exception The exception that the context is formed for
+     * @param exception
+     *         The exception that the context is formed for
      * @return Returns an array of objects to be used as the exception context
      */
     @SuppressWarnings({"unchecked", "rawtypes"})

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/exceptions/ExceptionReporterImpl.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/exceptions/ExceptionReporterImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/exceptions/ExceptionReporterImpl.java
new file mode 100644
index 0000000..f3dd3c5
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/exceptions/ExceptionReporterImpl.java
@@ -0,0 +1,340 @@
+// Licensed 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.tapestry5.internal.services.exceptions;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.tapestry5.SymbolConstants;
+import org.apache.tapestry5.func.F;
+import org.apache.tapestry5.func.Flow;
+import org.apache.tapestry5.func.Mapper;
+import org.apache.tapestry5.func.Reducer;
+import org.apache.tapestry5.internal.TapestryInternalUtils;
+import org.apache.tapestry5.ioc.annotations.Inject;
+import org.apache.tapestry5.ioc.annotations.Symbol;
+import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
+import org.apache.tapestry5.ioc.internal.util.InternalUtils;
+import org.apache.tapestry5.ioc.services.ExceptionAnalysis;
+import org.apache.tapestry5.ioc.services.ExceptionAnalyzer;
+import org.apache.tapestry5.ioc.services.ExceptionInfo;
+import org.apache.tapestry5.ioc.util.ExceptionUtils;
+import org.apache.tapestry5.services.Request;
+import org.apache.tapestry5.services.RequestGlobals;
+import org.apache.tapestry5.services.exceptions.ExceptionReporter;
+import org.slf4j.Logger;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.reflect.Array;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@SuppressWarnings("ResultOfMethodCallIgnored")
+public class ExceptionReporterImpl implements ExceptionReporter
+{
+    private static final Reducer<Integer, Integer> MAX = new Reducer<Integer, Integer>()
+    {
+        @Override
+        public Integer reduce(Integer accumulator, Integer element)
+        {
+            return Math.max(accumulator, element);
+        }
+    };
+
+    private static final Mapper<String, Integer> STRING_TO_LENGTH = new Mapper<String, Integer>()
+    {
+        @Override
+        public Integer map(String element)
+        {
+            return element.length();
+        }
+    };
+
+    @Inject
+    @Symbol(SymbolConstants.EXCEPTION_REPORTS_DIR)
+    private File logDir;
+
+    @Inject
+    @Symbol(SymbolConstants.CONTEXT_PATH)
+    private String contextPath;
+
+    @Inject
+    private ExceptionAnalyzer analyzer;
+
+    private final AtomicInteger uid = new AtomicInteger();
+
+    @Inject
+    private Logger logger;
+
+    @Inject
+    private RequestGlobals requestGlobals;
+
+    @Override
+    public void reportException(Throwable exception)
+    {
+        Date date = new Date();
+        String folderName = String.format("%tY/%<tm/%<td/%<tH/%<tM", date);
+        String fileName = String.format(
+                "exception-%tY%<tm%<td-%<tH%<tM%<tS-%<tL.%d.txt", date,
+                uid.getAndIncrement());
+
+        try
+        {
+            File folder = new File(logDir, folderName);
+            folder.mkdirs();
+
+            File log = new File(folder, fileName);
+
+            writeExceptionToFile(exception, log);
+
+            logger.warn(String.format("Wrote exception report to %s", toURI(log)));
+        } catch (Exception ex)
+        {
+            logger.error(String.format("Unable to write exception report %s: %s",
+                    fileName, ExceptionUtils.toMessage(ex)));
+
+            logger.error("Original exception:", exception);
+        }
+    }
+
+    private String toURI(File file)
+    {
+        try
+        {
+            return file.toURI().toString();
+        } catch (Exception e)
+        {
+            return file.toString();
+        }
+    }
+
+    private void writeExceptionToFile(Throwable exception, File log) throws IOException
+    {
+        log.createNewFile();
+        ExceptionAnalysis analysis = analyzer.analyze(exception);
+        PrintWriter writer = null;
+        try
+        {
+            writer = new PrintWriter(log);
+            writeException(writer, analysis);
+        } finally
+        {
+            IOUtils.closeQuietly(writer);
+        }
+    }
+
+    interface PropertyWriter
+    {
+        void write(String name, Object value);
+    }
+
+    private final static Mapper<ExceptionInfo, Flow<String>> EXCEPTION_INFO_TO_PROPERTY_NAMES =
+            new Mapper<ExceptionInfo, Flow<String>>()
+            {
+                @Override
+                public Flow<String> map(ExceptionInfo element)
+                {
+                    return F.flow(element.getPropertyNames());
+                }
+            };
+
+    private void writeException(final PrintWriter writer, ExceptionAnalysis analysis)
+    {
+        final Formatter f = new Formatter(writer);
+        writer.print("EXCEPTION STACK:\n\n");
+        Request request = requestGlobals.getRequest();
+
+        // Figure out what all the property names are so that we can set the width of the column that lists
+        // property names.
+        Flow<String> propertyNames = F.flow(analysis.getExceptionInfos())
+                .mapcat(EXCEPTION_INFO_TO_PROPERTY_NAMES).append("Exception type", "Message");
+
+        if (request != null)
+        {
+            propertyNames = propertyNames.concat(request.getParameterNames()).concat(request.getHeaderNames());
+        }
+
+        final int maxPropertyNameLength = propertyNames.map(STRING_TO_LENGTH).reduce(MAX, 0);
+
+        final String propertyNameFormat = "  %" + maxPropertyNameLength + "s: %s\n";
+
+        PropertyWriter pw = new PropertyWriter()
+        {
+            @SuppressWarnings("rawtypes")
+            @Override
+            public void write(String name, Object value)
+            {
+                if (value.getClass().isArray())
+                {
+                    write(name, toList(value));
+                    return;
+                }
+
+                if (value instanceof Iterable)
+                {
+                    boolean first = true;
+                    Iterable iterable = (Iterable) value;
+                    Iterator i = iterable.iterator();
+                    while (i.hasNext())
+                    {
+                        if (first)
+                        {
+                            f.format(propertyNameFormat, name, i.next());
+                            first = false;
+                        } else
+                        {
+                            for (int j = 0; j < maxPropertyNameLength + 4; j++)
+                                writer.write(' ');
+
+                            writer.println(i.next());
+                        }
+                    }
+                    return;
+                }
+
+                // TODO: Handling of arrays & collections
+                f.format(propertyNameFormat, name, value);
+            }
+
+            @SuppressWarnings({"rawtypes", "unchecked"})
+            private List toList(Object array)
+            {
+                int count = Array.getLength(array);
+                List result = new ArrayList(count);
+                for (int i = 0; i < count; i++)
+                {
+                    result.add(Array.get(array, i));
+                }
+                return result;
+            }
+        };
+
+        boolean first = true;
+
+        for (ExceptionInfo info : analysis.getExceptionInfos())
+        {
+            if (first)
+            {
+                writer.println();
+                first = false;
+            }
+            pw.write("Exception type", info.getClassName());
+            pw.write("Message", info.getMessage());
+            for (String name : info.getPropertyNames())
+            {
+                pw.write(name, info.getProperty(name));
+            }
+            if (!info.getStackTrace().isEmpty())
+            {
+                writer.write("\n  Stack trace:\n");
+                for (StackTraceElement e : info.getStackTrace())
+                {
+                    f.format("  - %s\n", e.toString());
+                }
+            }
+            writer.println();
+        }
+
+        if (request != null)
+        {
+            writer.print("REQUEST:\n\nBasic Information:\n");
+            List<String> flags = CollectionFactory.newList();
+            if (request.isXHR())
+            {
+                flags.add("XHR");
+            }
+            if (request.isRequestedSessionIdValid())
+            {
+                flags.add("requestedSessionIdValid");
+            }
+            if (request.isSecure())
+            {
+                flags.add("secure");
+            }
+            pw.write("contextPath", contextPath);
+            if (!flags.isEmpty())
+            {
+                pw.write("flags", InternalUtils.joinSorted(flags));
+            }
+            pw.write("method", request.getMethod());
+            pw.write("path", request.getPath());
+            pw.write("locale", request.getLocale());
+            pw.write("serverName", request.getServerName());
+            writer.print("\nHeaders:\n");
+            for (String name : request.getHeaderNames())
+            {
+                pw.write(name, request.getHeader(name));
+            }
+            if (!request.getParameterNames().isEmpty())
+            {
+                writer.print("\nParameters:\n");
+                for (String name : request.getParameterNames())
+                {
+                    // TODO: Support multi-value parameters
+                    pw.write(name, request.getParameters(name));
+                }
+            }
+            // TODO: Session if it exists
+        }
+
+        writer.print("\nSYSTEM INFORMATION:");
+
+        Runtime runtime = Runtime.getRuntime();
+
+        f.format("\n\nMemory:\n  %,15d bytes free\n  %,15d bytes total\n  %,15d bytes max\n",
+                runtime.freeMemory(),
+                runtime.totalMemory(),
+                runtime.maxMemory());
+
+        Thread[] threads = TapestryInternalUtils.getAllThreads();
+
+        int maxThreadNameLength = 0;
+
+        for (Thread t : threads)
+        {
+            maxThreadNameLength = Math.max(maxThreadNameLength, t.getName().length());
+        }
+
+        String format = "\n%s %" + maxThreadNameLength + "s %s";
+
+        f.format("\n%,d Threads:", threads.length);
+
+        for (Thread t : threads)
+        {
+            f.format(format,
+                    Thread.currentThread() == t ? "*" : " ",
+                    t.getName(),
+                    t.getState().name());
+            if (t.isDaemon())
+            {
+                writer.write(", daemon");
+            }
+            if (!t.isAlive())
+            {
+                writer.write(", NOT alive");
+            }
+            if (t.isInterrupted())
+            {
+                writer.write(", interrupted");
+            }
+            if (t.getPriority() != Thread.NORM_PRIORITY)
+            {
+                f.format(", priority %d", t.getPriority());
+            }
+        }
+        writer.println();
+
+        f.close();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
index d13c974..78e1523 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/modules/TapestryModule.java
@@ -38,6 +38,7 @@ import org.apache.tapestry5.internal.services.*;
 import org.apache.tapestry5.internal.services.ajax.AjaxFormUpdateFilter;
 import org.apache.tapestry5.internal.services.ajax.AjaxResponseRendererImpl;
 import org.apache.tapestry5.internal.services.ajax.MultiZoneUpdateEventResultProcessor;
+import org.apache.tapestry5.internal.services.exceptions.ExceptionReporterImpl;
 import org.apache.tapestry5.internal.services.linktransform.LinkTransformerImpl;
 import org.apache.tapestry5.internal.services.linktransform.LinkTransformerInterceptor;
 import org.apache.tapestry5.internal.services.messages.PropertiesFileParserImpl;
@@ -370,6 +371,7 @@ public final class TapestryModule
         binder.bind(PathConstructor.class, PathConstructorImpl.class);
         binder.bind(DateUtilities.class, DateUtilitiesImpl.class);
         binder.bind(PartialTemplateRenderer.class, PartialTemplateRendererImpl.class);
+        binder.bind(org.apache.tapestry5.services.exceptions.ExceptionReporter.class, ExceptionReporterImpl.class);
     }
 
     // ========================================================================
@@ -2126,6 +2128,7 @@ public final class TapestryModule
         // TAP5-2187
         configuration.add(SymbolConstants.STRICT_CSS_URL_REWRITING, "false");
 
+        configuration.add(SymbolConstants.EXCEPTION_REPORTS_DIR, "build/exceptions");
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/ExceptionReporter.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/ExceptionReporter.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/ExceptionReporter.java
new file mode 100644
index 0000000..97a94ec
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/ExceptionReporter.java
@@ -0,0 +1,33 @@
+// Licensed 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.tapestry5.services.exceptions;
+
+/**
+ * Services used to report request handling exceptions. This is invoked <em>before</em> the exception report page is rendered.
+ * The default implementation converts the exception into a well formatted text file, with content similar to the default
+ * {@link org.apache.tapestry5.corelib.pages.ExceptionReport} page, and stores this file on the file system.
+ * <p/>
+ * Exception report files are stored beneath a root directory, with intermediate folders for year, month, day, hour, and minute.
+ * <p/>
+ * Directories are created as necessary; however, there is nothing in place to delete exceptions.
+ *
+ * @see org.apache.tapestry5.SymbolConstants#EXCEPTION_REPORTS_DIR
+ * @since 5.4
+ */
+public interface ExceptionReporter
+{
+    /**
+     * Records the exception.
+     */
+    void reportException(Throwable exception);
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/package-info.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/package-info.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/package-info.java
new file mode 100644
index 0000000..20dfa5f
--- /dev/null
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/exceptions/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Services related to the generation of exception reports.
+ */
+package org.apache.tapestry5.services.exceptions;
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml
index 0190b80..9bd1f25 100644
--- a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml
+++ b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/pages/ExceptionReport.tml
@@ -96,11 +96,12 @@
 
                 <h3>Threads</h3>
 
-                <table class="table table-compact table-striped exception-report-threads">
+                <table class="table table-condensed table-hover table-striped exception-report-threads">
                     <thread>
                         <tr>
                             <th>Thread Name</th>
                             <th>Group Name</th>
+                            <th>State</th>
                             <th>Flags</th>
                         </tr>
                     </thread>
@@ -109,6 +110,7 @@
                         <tr t:type="loop" source="threads" value="thread" class="${thread.className}">
                             <td class="thread-name">${thread.name}</td>
                             <td>${thread.group?.name}</td>
+                            <td>${thread.state}</td>
                             <td>${thread.flags}</td>
                         </tr>
                     </tbody>

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/511813f3/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandlerTest.java
----------------------------------------------------------------------
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandlerTest.java b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandlerTest.java
index 4370020..ddab492 100644
--- a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandlerTest.java
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/DefaultRequestExceptionHandlerTest.java
@@ -1,5 +1,3 @@
-// Copyright 2006, 2008, 2010, 2011 The Apache Software Foundation
-//
 // Licensed 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
@@ -14,12 +12,6 @@
 
 package org.apache.tapestry5.internal.services;
 
-import java.io.IOException;
-import java.security.AccessControlException;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
 import org.apache.tapestry5.ContextAwareException;
 import org.apache.tapestry5.ExceptionHandlerAssistant;
 import org.apache.tapestry5.Link;
@@ -28,13 +20,21 @@ import org.apache.tapestry5.ioc.ServiceResources;
 import org.apache.tapestry5.services.ComponentClassResolver;
 import org.apache.tapestry5.services.Request;
 import org.apache.tapestry5.services.Response;
+import org.apache.tapestry5.services.exceptions.ExceptionReporter;
 import org.easymock.EasyMock;
 import org.slf4j.Logger;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import java.io.IOException;
+import java.security.AccessControlException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 @SuppressWarnings("serial")
-public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase {
+public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase
+{
     private Map<Class, Object> mockConfiguration = new HashMap<Class, Object>();
     RequestPageCache pageCache;
     PageResponseRenderer renderer;
@@ -44,25 +44,31 @@ public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase {
     ComponentClassResolver componentClassResolver;
     LinkSource linkSource;
     ServiceResources serviceResources;
+
     private DefaultRequestExceptionHandler exceptionHandler;
 
-    private static class MyContextAwareException extends Throwable implements ContextAwareException {
+
+    private static class MyContextAwareException extends Throwable implements ContextAwareException
+    {
         private Object[] context;
 
-        public MyContextAwareException(Object[] context) {
+        public MyContextAwareException(Object[] context)
+        {
             this.context = context;
         }
 
-        public Object[] getContext() {
+        public Object[] getContext()
+        {
             return context;
         }
 
     }
-    
-    private static class MyPage {
-        
+
+    private static class MyPage
+    {
+
     }
-    
+
     @BeforeMethod
     public void setup_tests() throws Exception
     {
@@ -75,28 +81,45 @@ public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase {
         componentClassResolver = mockComponentClassResolver();
         linkSource = mockLinkSource();
         serviceResources = mockServiceResources();
-          mockConfiguration.put(AccessControlException.class, MyPage.class);
-        mockConfiguration.put(MyContextAwareException.class, new ExceptionHandlerAssistant() {
-                      public Object handleRequestException(Throwable exception, List<Object> exceptionContext)
-                          throws IOException {
-                      return null;
-                  }
-              });
-        exceptionHandler = new DefaultRequestExceptionHandler(pageCache, renderer, logger, "exceptionpage", request, response, componentClassResolver, linkSource, serviceResources, mockConfiguration);
+        mockConfiguration.put(AccessControlException.class, MyPage.class);
+        mockConfiguration.put(MyContextAwareException.class, new ExceptionHandlerAssistant()
+        {
+            public Object handleRequestException(Throwable exception, List<Object> exceptionContext)
+                    throws IOException
+            {
+                return null;
+            }
+        });
+
+        ExceptionReporter noopExceptionReporter = new ExceptionReporter()
+        {
+            @Override
+            public void reportException(Throwable exception)
+            {
+
+            }
+        };
+
+        exceptionHandler = new DefaultRequestExceptionHandler(pageCache, renderer, logger, "exceptionpage", request, response, componentClassResolver, linkSource, serviceResources, noopExceptionReporter, mockConfiguration);
     }
-    
+
 
     @Test
-    public void noContextWhenExceptionDoesntContainMessage() {
-        Object[] context = exceptionHandler.formExceptionContext(new RuntimeException() {
+    public void noContextWhenExceptionDoesntContainMessage()
+    {
+        Object[] context = exceptionHandler.formExceptionContext(new RuntimeException()
+        {
         });
         assertEquals(context.length, 0);
     }
 
     @Test
-    public void contextIsExceptionMessage() {
-        Object[] context = exceptionHandler.formExceptionContext(new RuntimeException() {
-            public String getMessage() {
+    public void contextIsExceptionMessage()
+    {
+        Object[] context = exceptionHandler.formExceptionContext(new RuntimeException()
+        {
+            public String getMessage()
+            {
                 return "HelloWorld";
             }
         });
@@ -105,7 +128,8 @@ public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase {
     }
 
     @Test
-    public void contextIsExceptionType() {
+    public void contextIsExceptionType()
+    {
         Object[] context = exceptionHandler.formExceptionContext(new IllegalArgumentException("Value not allowed"));
         assertEquals(context.length, 1);
         assertTrue(context[0] instanceof String);
@@ -113,42 +137,47 @@ public class DefaultRequestExceptionHandlerTest extends InternalBaseTestCase {
     }
 
     @Test
-    public void contextIsProvidedByContextAwareException() {
-        Object[] sourceContext = new Object[] { new Integer(10), this };
+    public void contextIsProvidedByContextAwareException()
+    {
+        Object[] sourceContext = new Object[]{new Integer(10), this};
 
-        Object[] context = exceptionHandler.formExceptionContext(new MyContextAwareException(sourceContext) {
+        Object[] context = exceptionHandler.formExceptionContext(new MyContextAwareException(sourceContext)
+        {
         });
         assertEquals(context, sourceContext);
 
     }
-    
+
     @Test
-    public void handleRequestExceptionWithConfiguredPage() throws IOException {
-        train_resolvePageClassNameToPageName(componentClassResolver, MyPage.class.getName(), "mypage" );
+    public void handleRequestExceptionWithConfiguredPage() throws IOException
+    {
+        train_resolvePageClassNameToPageName(componentClassResolver, MyPage.class.getName(), "mypage");
         Link link = mockLink();
         expect(linkSource.createPageRenderLink("mypage", false, new Object[]{"accesscontrol"})).andReturn(link);
         expect(request.isXHR()).andReturn(false);
         response.sendRedirect(link);
         EasyMock.expectLastCall();
         replay();
-        
+
         exceptionHandler.handleRequestException(new AccessControlException("No permission"));
     }
-    
+
     @Test
-    public void handleRequestExceptionWithConfiguredAssistant() throws IOException {
-        ExceptionHandlerAssistant assistant = new ExceptionHandlerAssistant() {
+    public void handleRequestExceptionWithConfiguredAssistant() throws IOException
+    {
+        ExceptionHandlerAssistant assistant = new ExceptionHandlerAssistant()
+        {
             public Object handleRequestException(Throwable exception, List<Object> exceptionContext)
                     throws IOException
             {
                 return null;
             }
         };
-        
+
         mockConfiguration.put(MyContextAwareException.class, assistant);
         replay();
-        
+
         exceptionHandler.handleRequestException(new MyContextAwareException(new Object[]{}));
     }
-    
+
 }