Index: tests/src/java/org/apache/log4j/PatternLayoutTestCase.java =================================================================== --- tests/src/java/org/apache/log4j/PatternLayoutTestCase.java (revision 691542) +++ tests/src/java/org/apache/log4j/PatternLayoutTestCase.java Wed Sep 03 11:13:51 BST 2008 @@ -23,8 +23,6 @@ import org.apache.log4j.Logger; import org.apache.log4j.Level; -import org.apache.log4j.MDC; -import org.apache.log4j.NDC; import org.apache.log4j.util.LineNumberFilter; import org.apache.log4j.util.Transformer; @@ -38,6 +36,10 @@ import org.apache.log4j.util.SunReflectFilter; import org.apache.log4j.util.JunitTestRunnerFilter; +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; + public class PatternLayoutTestCase extends TestCase { static String TEMP = "output/temp"; @@ -285,6 +287,35 @@ assertTrue(Compare.compare(FILTERED, "witness/patternLayout.14")); } + public void testExtendedStackTrace() throws Exception { + PropertyConfigurator.configure("input/patternLayoutExtendedStackTrace.properties"); + common(); + + BufferedReader in1 = new BufferedReader(new FileReader(TEMP)); + try { + int count = 0; + while (true) { + String line = in1.readLine(); + if (line == null) { + break; + } + else { + if (line.contains("[") && line.contains("]")) { + count++; + } + } + } + assertTrue("Should have found at least one line with extended versioning information", count > 0); + } + finally { + try { + in1.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + void common() { String oldThreadName = Thread.currentThread().getName(); Thread.currentThread().setName("main"); Index: tests/src/java/org/apache/log4j/spi/ThrowableInformationTest.java =================================================================== --- tests/src/java/org/apache/log4j/spi/ThrowableInformationTest.java (revision 691542) +++ tests/src/java/org/apache/log4j/spi/ThrowableInformationTest.java Wed Sep 03 10:08:46 BST 2008 @@ -20,7 +20,9 @@ import java.io.PrintWriter; +import org.apache.log4j.config.PropertySetterException; + /** * Unit tests for ThrowableInformation. */ @@ -299,4 +301,20 @@ String[] rep2 = ti.getThrowableStrRep(); assertEquals("Hello, World", rep2[0]); } + + public void testStackTracePackageName() throws Exception { + ThrowableInformation ti = new ThrowableInformation( + new PropertySetterException("Hello")); + String[] rep = ti.getThrowableStrRep(true); + boolean found = false; + for (int i = 0, size = rep.length; i < size; i++) { + String line = rep[i]; + //System.out.println(line); + if (line.indexOf("[") > 0) { + found = true; -} + } + } + assertTrue("Found a package version for the JDK", found); + } + +} Index: src/main/java/org/apache/log4j/spi/ThrowableInformation.java =================================================================== --- src/main/java/org/apache/log4j/spi/ThrowableInformation.java (revision 691542) +++ src/main/java/org/apache/log4j/spi/ThrowableInformation.java Wed Sep 03 10:16:08 BST 2008 @@ -19,6 +19,7 @@ import java.io.*; import java.util.ArrayList; +import java.net.URL; /** * ThrowableInformation is log4j's internal representation of @@ -64,6 +65,15 @@ public String[] getThrowableStrRep() { + return getThrowableStrRep(false); + } + + /** + * If true is specified then an extended format is used which returns + * the jar file of the class and its package version if they can be deduced from the class loaders + */ + public + String[] getThrowableStrRep(boolean extendedFormat) { if(rep == null) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); @@ -75,6 +85,9 @@ try { String line = reader.readLine(); while(line != null) { + if (extendedFormat) { + line = createExtendedExceptionLine(line); + } lines.add(line); line = reader.readLine(); } @@ -90,6 +103,127 @@ } return (String[]) rep.clone(); } + + /** + * Appends the optional text [jarName:versionNumber] to each line of the stack trace to + * indicate what name of jar the code comes from along with which version was found on the classpath + */ + protected + String createExtendedExceptionLine(String line) { + String trimmed = line.trim(); + + // TODO note that the JVM hard codes 'at ' currently + // we might want to consider using reflection on Java 1.4 or later + // to use the throwable.getStackFrames() to get the class names + if (trimmed.startsWith("at ")) { + // lets try append the package version to this line + String classAndMethod = trimmed.substring(3).trim(); + int idx = classAndMethod.indexOf('('); + if (idx > 0) { + classAndMethod = classAndMethod.substring(0, idx); -} + } + idx = classAndMethod.lastIndexOf('.'); + String className = classAndMethod; + if (idx > 0) { + //method = classAndMethod.substring(idx + 1); + className = className.substring(0, idx); + } + String packageName = ""; + idx = className.lastIndexOf('.'); + if (idx > 0) { + packageName = className.substring(0, idx); + } + if (packageName.length() > 0) { + String version = getVersion(packageName); + String jarFile = getJarNameOfClass(className); + // lets always include a colon in between so we can determine + // if a [] string is a version or jar name + // + // e.g. [foo:] would be a jar name and [:foo] a version + if (notNullOrBlank(version) || notNullOrBlank(jarFile)) { + return line + " [" + blankIfNull(jarFile) + ":" + blankIfNull(version) + "]"; + } + } + } + return line; + } + protected static String blankIfNull(String text) { + return text != null ? text : ""; + } + + protected static boolean notNullOrBlank(String text) { + return text != null && text.length() > 0; + } + + /** + * Attempts to deduce the package version number of the given package + * that is currently on the classpath + */ + protected + String getVersion(String packageName) { + try { + Package aPackage = Package.getPackage(packageName); + if (aPackage != null) { + return aPackage.getImplementationVersion(); + } + } catch (Exception e) { + // ignore exception + } + return ""; + } + + /** + * Uses the context class path or the current global class loader to + * deduce the file that the given class name comes from + */ + protected + String getJarNameOfClass(String className) { + try { + Class type = findClass(className); + if (type != null) { + URL resource = type.getClassLoader().getResource(type.getName().replace('.', '/') + ".class"); + if (resource != null) { + String text = resource.toString(); + int idx = text.lastIndexOf('!'); + if (idx > 0) { + text = text.substring(0, idx); + // now lets remove all but the file name + idx = text.lastIndexOf('/'); + if (idx > 0) { + text = text.substring(idx + 1); + } + idx = text.lastIndexOf('\\'); + if (idx > 0) { + text = text.substring(idx + 1); + } + return text; + } + } + } + } + catch (Exception e) { + // ignore + } + return ""; + } + + private Class findClass(String className) { + try { + return Thread.currentThread().getContextClassLoader().loadClass(className); + } catch (ClassNotFoundException e) { + try { + return Class.forName(className); + } catch (ClassNotFoundException e1) { + try { + return getClass().getClassLoader().loadClass(className); + } catch (ClassNotFoundException e2) { + return null; + } + } + } + } +} + + Index: src/main/java/org/apache/log4j/PatternLayout.java =================================================================== --- src/main/java/org/apache/log4j/PatternLayout.java (revision 691542) +++ src/main/java/org/apache/log4j/PatternLayout.java Wed Sep 03 10:46:53 BST 2008 @@ -19,6 +19,7 @@ import org.apache.log4j.Layout; import org.apache.log4j.spi.LoggingEvent; +import org.apache.log4j.spi.ThrowableInformation; import org.apache.log4j.helpers.PatternParser; import org.apache.log4j.helpers.PatternConverter; @@ -417,6 +418,11 @@ private PatternConverter head; + // I'd really really like this enabled by default but given the + // zillions of tests it'd break without hacking them and the possibility + // of breaking other projects, I guess disabled by default is safer :( + private boolean extendedStackTrace = false; + /** Constructs a PatternLayout using the DEFAULT_LAYOUT_PATTERN. @@ -454,7 +460,23 @@ return pattern; } + public + boolean isExtendedStackTrace() { + return extendedStackTrace; + } + /** + * Enables or disables the extended stack trace printing which also includes the jar + * name and the version information of classes on the exception stack if they can be deduced + * + * @param extendedStackTrace the flag to enable or disable extended stack frames + */ + public + void setExtendedStackTrace(boolean extendedStackTrace) { + this.extendedStackTrace = extendedStackTrace; + } + + /** Does not do anything as options become effective */ public @@ -470,7 +492,7 @@ @since 0.8.4 */ public boolean ignoresThrowable() { - return true; + return !isExtendedStackTrace(); } /** @@ -502,6 +524,20 @@ c.format(sbuf, event); c = c.next; } + if (isExtendedStackTrace()) { + ThrowableInformation information = event.getThrowableInformation(); + if (information != null) { + String[] s = information.getThrowableStrRep(true); + if (s != null) { + int len = s.length; + for (int i = 0; i < len; i++) { + sbuf.append(s[i]); + sbuf.append(Layout.LINE_SEP); + } + } + } + } return sbuf.toString(); } + } Index: tests/input/patternLayoutExtendedStackTrace.properties =================================================================== --- tests/input/patternLayoutExtendedStackTrace.properties Wed Sep 03 11:14:29 BST 2008 +++ tests/input/patternLayoutExtendedStackTrace.properties Wed Sep 03 11:14:29 BST 2008 @@ -0,0 +1,22 @@ +# 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. + +log4j.rootCategory=TRACE, testAppender +log4j.appender.testAppender=org.apache.log4j.FileAppender +log4j.appender.testAppender.File= output/temp +log4j.appender.testAppender.Append=false +log4j.appender.testAppender.layout=org.apache.log4j.MyPatternLayout +log4j.appender.testAppender.layout.ConversionPattern=%5p %-4# - %m%n +log4j.appender.testAppender.layout.ExtendedStackTrace=true