Pretty Printing Exceptions

Here are some snippets you might find useful for pretty-printing exception stack traces (using the trace string rather than the StackTrace class).

First, a regular expression to extract namespace, method name, argument and source details from a stack trace string:

\sat\s(?<namespace>.*?)\.(?<method>(\.ctor|<\.ctor>[^(]*?|[^.(]+))\(\s*(((?<type>[^,\s)]+)\s+(?<arg>[^,\s)]+)\s*,?\s*?)*?)\s*\)( in (?<file>.*?):line (?<line>[0-9]*))?

Next, a helper class that uses the regex to return a collection of method details:

public class ExceptionParser
{
    private static Regex stackTraceExpr = new Regex(
        @"\sat\s(?<namespace>.*?)\.(?<method>(\.ctor|<\.ctor>[^(]*?|[^.(]+))\(\s*(((?<type>[^, )]+)\s+(?<arg>[^, )]+)\s*,?\s*?)*?)\s*\)( in (?<file>.*?):line (?<line>[0-9]*))?" );

    /// <summary>
    /// Parses the stack trace.
    /// </summary>
    /// <param name="stackTrace">The stack trace.</param>
    /// <returns></returns>
    public static IEnumerable<Method> ParseStackTrace( string stackTrace )
    {
        var matches = stackTraceExpr.Matches( stackTrace ).Cast<Match>();

        foreach ( var match in matches )
        {
            var line = match.Groups[ "line" ].Value;

            yield return new Method
            {
                Namespace = match.Groups[ "namespace" ].Value,
                Name = match.Groups[ "method" ].Value,
                Arguments = ParseArguments( match ).ToArray(),
                Source = match.Groups[ "source" ].Value,
                Line = !string.IsNullOrEmpty( line ) ? (int?)int.Parse( line ) : null
            };
        }
    }

    /// <summary>
    /// Parses the arguments.
    /// </summary>
    /// <param name="match">The match.</param>
    /// <returns></returns>
    private static IEnumerable<Argument> ParseArguments( Match match )
    {
        var type = match.Groups[ "type" ];
        var arg = match.Groups[ "arg" ];
        var types = type.Captures.Cast<Capture>().Select( c => c.Value ).Union( new[] { type.Value } ).GetEnumerator();
        var args = arg.Captures.Cast<Capture>().Select( c => c.Value ).Union( new[] { arg.Value } ).GetEnumerator();

        while ( types.MoveNext() && args.MoveNext() )
        {
            yield return new Argument
            {
                Type = types.Current,
                Name = args.Current
            };
        }
    }

    public struct Argument
    {
        public string Type;
        public string Name;
    }

    public struct Method
    {
        public string Namespace;
        public string Name;
        public Argument[] Arguments;
        public string Source;
        public int? Line;
    }
}

From there you might want to render it as a WPF FlowDocument or series of Inlines in WPF or Silverlight (you can use this directly or as an IValueConverter):

public class PrettyExceptionConverter : DependencyObject, IValueConverter
{
    #region Dependency properties

    public static readonly DependencyProperty ErrorStyleProperty = RegisterStyle( "ErrorStyle", MakeStyle( Colors.Red, FontStyles.Italic, FontWeights.Bold ) );
    public Style ErrorStyle { get { return (Style)GetValue( ErrorStyleProperty ); } set { SetValue( ErrorStyleProperty, value ); } }
    private Inline Error( string text ) { return new Run( text ) { Style = ErrorStyle }; }

    public static readonly DependencyProperty NamespaceStyleProperty = RegisterStyle( "NamespaceStyle", MakeStyle( Colors.Blue ) );
    public Style NamespaceStyle { get { return (Style)GetValue( NamespaceStyleProperty ); } set { SetValue( NamespaceStyleProperty, value ); } }
    private Inline Namespace( string text ) { return new Run( text ) { Style = NamespaceStyle }; }

    public static readonly DependencyProperty MethodNameStyleProperty = RegisterStyle( "MethodNameStyle", MakeStyle( Colors.Black, null, FontWeights.Bold ) );
    public Style MethodNameStyle { get { return (Style)GetValue( MethodNameStyleProperty ); } set { SetValue( MethodNameStyleProperty, value ); } }
    private Inline MethodName( string text ) { return new Run( text ) { Style = MethodNameStyle }; }

    public static readonly DependencyProperty BracketStyleProperty = RegisterStyle( "BracketStyle", MakeStyle( Colors.DarkRed ) );
    public Style BracketStyle { get { return (Style)GetValue( BracketStyleProperty ); } set { SetValue( BracketStyleProperty, value ); } }
    private Inline Bracket( string text ) { return new Run( text ) { Style = BracketStyle }; }

    public static readonly DependencyProperty SeparatorStyleProperty = RegisterStyle( "SeparatorStyle", MakeStyle( Colors.DarkBlue ) );
    public Style SeparatorStyle { get { return (Style)GetValue( SeparatorStyleProperty ); } set { SetValue( SeparatorStyleProperty, value ); } }
    private Inline Separator( string text ) { return new Run( text ) { Style = SeparatorStyle }; }

    public static readonly DependencyProperty SpaceStyleProperty = RegisterStyle( "SpaceStyle", null );
    public Style SpaceStyle { get { return (Style)GetValue( SpaceStyleProperty ); } set { SetValue( SpaceStyleProperty, value ); } }
    private Inline Space() { return new Run( " " ) { Style = SpaceStyle }; }

    public static readonly DependencyProperty TypeNameStyleProperty = RegisterStyle( "TypeNameStyle", MakeStyle( Colors.Teal ) );
    public Style TypeNameStyle { get { return (Style)GetValue( TypeNameStyleProperty ); } set { SetValue( TypeNameStyleProperty, value ); } }
    private Inline TypeName( string text ) { return new Run( text ) { Style = TypeNameStyle }; }

    public static readonly DependencyProperty ArgumentStyleProperty = RegisterStyle( "ArgumentStyle", MakeStyle( Colors.DarkGreen ) );
    public Style ArgumentStyle { get { return (Style)GetValue( ArgumentStyleProperty ); } set { SetValue( ArgumentStyleProperty, value ); } }
    private Inline Argument( string text ) { return new Run( text ) { Style = ArgumentStyle }; }

    public static readonly DependencyProperty SourceStyleProperty = RegisterStyle( "SourceStyle", MakeStyle( Colors.Gray ) );
    public Style SourceStyle { get { return (Style)GetValue( SourceStyleProperty ); } set { SetValue( SourceStyleProperty, value ); } }
    private Inline Source( string text ) { return new Run( text ) { Style = SourceStyle }; }

    public static readonly DependencyProperty LocationStyleProperty = RegisterStyle( "LocationStyle", MakeStyle( Colors.DarkRed ) );
    public Style LocationStyle { get { return (Style)GetValue( LocationStyleProperty ); } set { SetValue( LocationStyleProperty, value ); } }
    private Inline Location( string text ) { return new Run( text ) { Style = LocationStyle }; }

    public static DependencyProperty ChildIndentProperty = DependencyProperty.Register( "ChildIndent", typeof( double ), typeof( PrettyExceptionConverter ), new PropertyMetadata( 25d ) );
    public double ChildIndent { get { return (double)GetValue( ChildIndentProperty ); } set { SetValue( ChildIndentProperty, value ); } }

    #endregion

    #region Static helpers

    private static DependencyProperty RegisterStyle( string name, Style defaultStyle )
    {
        return DependencyProperty.Register(
            name,
            typeof( Style ),
            typeof( PrettyExceptionConverter ),
            new PropertyMetadata( defaultStyle ) );
    }

    private static Style MakeStyle( Color color )
    {
        return MakeStyle( color, null, null );
    }

    private static Style MakeStyle( Color color, FontStyle? fontStyle )
    {
        return MakeStyle( color, fontStyle, null );
    }

    private static Style MakeStyle( Color color, FontStyle? fontStyle, FontWeight? fontWeight )
    {
        var style = new Style();
        style.Setters.Add( new Setter( Run.ForegroundProperty, new SolidColorBrush( color ) ) );
        if ( fontStyle.HasValue ) style.Setters.Add( new Setter( Run.FontStyleProperty, fontStyle.Value ) );
        if ( fontWeight.HasValue ) style.Setters.Add( new Setter( Run.FontWeightProperty, fontWeight.Value ) );
        return style;
    }

    #endregion

    #region FlowDocument helpers

    /// <summary>
    /// Renders the specified <see cref="Exception"/>.
    /// </summary>
    /// <param name="ex">The ex.</param>
    /// <returns></returns>
    public FlowDocument Render( Exception ex )
    {
        var doc = new FlowDocument
        {
            FontFamily = new FontFamily( "Arial" ),
            FontSize = 14d,
            PagePadding = new Thickness( 0 )
        };

        doc.Blocks.AddRange( RenderExceptions( ex ) );

        return doc;
    }

    private Inline LineBreak()
    {
        return new LineBreak();
    }

    #endregion

    #region Rendering helpers

    /// <summary>
    /// Renders the <see cref="Exception"/> and inner Exceptions.
    /// </summary>
    /// <param name="ex">The ex.</param>
    /// <returns></returns>
    public IEnumerable<Block> RenderExceptions( Exception ex )
    {
        var indent = 0d;
        var hanging = ChildIndent / 2;

        for ( ; ex != null; ex = ex.InnerException )
        {
            foreach ( var block in RenderException( ex, indent, hanging ) ) yield return block;
            indent += ChildIndent;
        }
    }

    /// <summary>
    /// Renders the <see cref="Exception"/>.
    /// </summary>
    /// <param name="ex">The ex.</param>
    /// <param name="indent">The indent.</param>
    /// <param name="hanging">The hanging indent.</param>
    /// <returns></returns>
    public IEnumerable<Block> RenderException( Exception ex, double indent, double hanging )
    {
        var paragraph = new Paragraph
        {
            TextAlignment = TextAlignment.Left,
            IsHyphenationEnabled = false,
            TextIndent = -hanging,
            Margin = new Thickness( indent + hanging, 0, 0, 0 )
        };

        paragraph.Inlines.AddRange( RenderException( ex ) );

        yield return paragraph;
    }

    /// <summary>
    /// Renders the <see cref="Exception"/>.
    /// </summary>
    /// <param name="ex">The ex.</param>
    /// <returns></returns>
    public IEnumerable<Inline> RenderException( Exception ex )
    {
        if ( ex != null )
        {
            yield return Error( ex.Message );

            if ( ex.StackTrace != null )
            {
                yield return LineBreak();
                foreach ( var method in RenderStackTrace( ex.StackTrace ) ) yield return method;
            }
        }
    }

    /// <summary>
    /// Renders the stack trace.
    /// </summary>
    /// <param name="stackTrace">The stack trace.</param>
    /// <returns></returns>
    public IEnumerable<Inline> RenderStackTrace( string stackTrace )
    {
        if ( stackTrace != null )
        {
            var first = true;

            foreach ( var method in ExceptionParser.ParseStackTrace( stackTrace ) )
            {
                if ( !first ) yield return LineBreak();
                else first = false;

                foreach ( var inline in RenderMethod( method ) ) yield return inline;
            }
        }
    }

    /// <summary>
    /// Renders the method.
    /// </summary>
    /// <param name="method">The method.</param>
    /// <returns></returns>
    public IEnumerable<Inline> RenderMethod( ExceptionParser.Method method )
    {
        foreach ( var ns in method.Namespace.Split( '.' ) )
        {
            yield return Namespace( ns );
            yield return Separator( "." );
        }

        yield return MethodName( method.Name );
        yield return Bracket( "(" );

        if ( method.Arguments.Length > 0 )
        {
            yield return Space();

            var first = true;

            foreach ( var arg in method.Arguments )
            {
                if ( !first )
                {
                    yield return Bracket( "," );
                    yield return Space();
                }
                else first = false;

                yield return TypeName( arg.Type );
                yield return Space();
                yield return Argument( arg.Name );
            }

            yield return Space();
        }

        yield return Bracket( ")" );
    }

    #endregion

    #region IValueConverter Members

    public object Convert( object value, Type targetType, object parameter, System.Globalization.CultureInfo culture )
    {
        return ( value is Exception )
            ? (object)Render( (Exception)value )
            : RenderStackTrace( value as string );
    }

    public object ConvertBack( object value, Type targetType, object parameter, System.Globalization.CultureInfo culture )
    {
        throw new NotImplementedException();
    }

    #endregion
}

You can set the Style properties to change the appearance.  Here’s an example that puts the stack traces inside nested Expanders:

image

Maybe you’d prefer to render as HTML (useful for health monitoring emails etc):

public class PrettyExceptionPrint
{
    #region Properties

    public static readonly string ErrorStyle = "PEP_Error";
    private string Error( string text ) { return Span( text, ErrorStyle ); }

    public static readonly string NamespaceStyle = "PEP_Namespace";
    private string Namespace( string text ) { return Span( text, NamespaceStyle ); }

    public static readonly string MethodNameStyle = "PEP_MethodName";
    private string MethodName( string text ) { return Span( text, MethodNameStyle ); }

    public static readonly string BracketStyle = "PEP_Bracket";
    private string Bracket( string text ) { return Span( text, BracketStyle ); }

    public static readonly string SeparatorStyle = "PEP_Separator";
    private string Separator( string text ) { return Span( text, SeparatorStyle ); }

    public static readonly string SpaceStyle = "PEP_Space";
    private string Space() { return Span( " ", SpaceStyle ); }

    public static readonly string TypeNameStyle = "PEP_TypeName";
    private string TypeName( string text ) { return Span( text, TypeNameStyle ); }

    public static readonly string ArgumentStyle = "PEP_Argument";
    private string Argument( string text ) { return Span( text, ArgumentStyle ); }

    public static readonly string SourceStyle = "PEP_Source";
    private string Source( string text ) { return Span( text, SourceStyle ); }

    public static readonly string LocationStyle = "PEP_Location";
    private string Location( string text ) { return Span( text, LocationStyle ); }

    public double ChildIndent { get; set; }

    #endregion

    #region HTML helpers

    private string Span( object content )
    {
        return Span( content, null );
    }

    private string Span( object content, string css )
    {
        return string.Format( "<span{0}>{1}</span>", string.IsNullOrEmpty( css ) ? null : " class='" + css + "'", content );
    }

    private string LineBreak()
    {
        return "<br/>\n";
    }

    #endregion

    #region Rendering helpers

    /// <summary>
    /// Renders the <see cref="Exception"/> and inner Exceptions.
    /// </summary>
    /// <param name="ex">The ex.</param>
    /// <returns></returns>
    public IEnumerable<string> RenderExceptions( Exception ex )
    {
        var indent = 0d;
        var hanging = ChildIndent / 2;

        for ( ; ex != null; ex = ex.InnerException )
        {
            foreach ( var block in RenderException( ex, indent, hanging ) ) yield return block;
            indent += ChildIndent;
        }
    }

    /// <summary>
    /// Renders the <see cref="Exception"/>.
    /// </summary>
    /// <param name="ex">The ex.</param>
    /// <param name="indent">The indent.</param>
    /// <param name="hanging">The hanging indent.</param>
    /// <returns></returns>
    public IEnumerable<string> RenderException( Exception ex, double indent, double hanging )
    {
        foreach ( var markup in RenderException( ex ) ) yield return markup;
    }

    /// <summary>
    /// Renders the <see cref="Exception"/>.
    /// </summary>
    /// <param name="ex">The ex.</param>
    /// <returns></returns>
    public IEnumerable<string> RenderException( Exception ex )
    {
        if ( ex != null )
        {
            yield return Error( ex.Message );

            if ( ex.StackTrace != null )
            {
                yield return LineBreak();
                foreach ( var method in RenderStackTrace( ex.StackTrace ) ) yield return method;
            }
        }
    }

    /// <summary>
    /// Renders the stack trace.
    /// </summary>
    /// <param name="stackTrace">The stack trace.</param>
    /// <returns></returns>
    public IEnumerable<string> RenderStackTrace( string stackTrace )
    {
        if ( stackTrace != null )
        {
            var first = true;

            foreach ( var method in ExceptionParser.ParseStackTrace( stackTrace ) )
            {
                if ( !first ) yield return LineBreak();
                else first = false;

                foreach ( var inline in RenderMethod( method ) ) yield return inline;
            }
        }
    }

    /// <summary>
    /// Renders the method.
    /// </summary>
    /// <param name="method">The method.</param>
    /// <returns></returns>
    public IEnumerable<string> RenderMethod( ExceptionParser.Method method )
    {
        foreach ( var ns in method.Namespace.Split( '.' ) )
        {
            yield return Namespace( ns );
            yield return Separator( "." );
        }

        yield return MethodName( method.Name );
        yield return Bracket( "(" );

        if ( method.Arguments.Length > 0 )
        {
            yield return Space();

            var first = true;

            foreach ( var arg in method.Arguments )
            {
                if ( !first )
                {
                    yield return Bracket( "," );
                    yield return Space();
                }
                else first = false;

                yield return TypeName( arg.Type );
                yield return Space();
                yield return Argument( arg.Name );
            }

            yield return Space();
        }

        yield return Bracket( ")" );
    }

    #endregion
}

Then just add some CSS for the PEP_* classes to change the appearance:

<style>
    html, body { font-family: Arial; }
    .PEP_Namespace { color: darkblue; }
    .PEP_MethodName { color: black; font-weight: bold; }
    .PEP_Bracket { color: darkgreen; }
    .PEP_TypeName { color: teal; }
    .PEP_Argument { color: darkred; }
</style>

It’ll look something like this:

image

Rendering to XML is even easier:

public class PrettyExceptionPrint
{
    /// <summary>
    /// Renders the <see cref="Exception"/> and inner Exceptions.
    /// </summary>
    /// <param name="ex">The ex.</param>
    /// <returns></returns>
    public static IEnumerable<XElement> RenderExceptions( Exception ex )
    {
        for ( ; ex != null; ex = ex.InnerException ) yield return RenderException( ex );
    }

    /// <summary>
    /// Renders the <see cref="Exception"/>.
    /// </summary>
    /// <param name="ex">The ex.</param>
    /// <returns></returns>
    public static XElement RenderException( Exception ex )
    {
        return new XElement( "Exception",
            new XAttribute( "Type", ex.GetType() ),
            new XAttribute( "Message", ex.Message ),
            new XElement( "StackTrace", RenderStackTrace( ex.StackTrace ) ) );
    }

    /// <summary>
    /// Renders the stack trace.
    /// </summary>
    /// <param name="stackTrace">The stack trace.</param>
    /// <returns></returns>
    public static IEnumerable<XElement> RenderStackTrace( string stackTrace )
    {
        return from method in ExceptionParser.ParseStackTrace( stackTrace )
               select RenderMethod( method );
    }

    /// <summary>
    /// Renders the method.
    /// </summary>
    /// <param name="method">The method.</param>
    /// <returns></returns>
    public static XElement RenderMethod( ExceptionParser.Method method )
    {
        return new XElement( "Method",
            new XAttribute( "Namespace", method.Namespace ),
            new XAttribute( "Name", method.Name ),
            new XElement( "Arguments",
                from arg in method.Arguments
                select new XElement( "Argument",
                    new XAttribute( "Type", arg.Type ),
                    new XAttribute( "Name", arg.Name ) ) ) );
    }
}

Then you could transform it to HTML with some XSLT like this:

<?xml version="1.0" encoding="UTF-8" ?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" exclude-result-prefixes="xsl">

    <xsl:output method="html"/>

    <xsl:variable name="lineFeed" select="'&#xD;&#xA;'"/>

    <xsl:template match="/">

        <style>
            p.exception { border: 1px solid #AAAAAA; background-color: #FFAEB9; padding: 4px; }
            table.error { border: 1px solid lightsteelblue; border-collapse: collapse; font-size: 9pt; margin-bottom: 8px; }
            table.error th { text-align: left; background-color: lightsteelblue; padding: 4px 4px 4px 4px; }
            table.error td { border: 1px solid lightsteelblue; padding: 4px 4px 4px 4px; }
            .title { background-color: white; }
            .namespace { color: navy; }
            .method { font-weight: bold; }
            .type { color: green; }
            .var { color: gray; }
            .trace { padding-left: 2em; text-indent: -2em; font-size: 8pt; }
        </style>

        <xsl:copy-of select="Exceptions/Header/*"/>

        <!--
        <Exceptions>
            <Exception Type="type" Message="message">
                <StackTrace>
                    <Method Namespace="namespace" Type="type" Method="method">
                        <Argument Type="type" Name="name"/>
                    </Method>
                </StackTrace>
            </Exception>
        </Exceptions>
        -->
        <xsl:for-each select="Exceptions/Exception">
            <table class="error">
                <tr>
                    <th>
                        <xsl:value-of select="@Type"/>
                    </th>
                </tr>
                <tr>
                    <td>
                        <xsl:value-of select="@Message"/>
                    </td>
                </tr>
                <xsl:if test="StackTrace/Method">
                    <tr>
                        <td colspan="2">
                            <xsl:for-each select="StackTrace/Method">
                                <div class="trace">
                                    <span class="namespace">
                                        <xsl:if test="@Namespace">
                                            <xsl:value-of select="@Namespace"/>
                                            <xsl:text>.</xsl:text>
                                        </xsl:if>
                                        <xsl:if test="@Type">
                                            <xsl:value-of select="@Type"/>
                                            <xsl:text>.</xsl:text>
                                        </xsl:if>
                                        <span class="method">
                                            <xsl:value-of select="@Method"/>
                                        </span>
                                    </span>
                                    <xsl:text>(</xsl:text>
                                    <xsl:for-each select="Parameter">
                                        <xsl:if test="position()>1">,</xsl:if>
                                        <xsl:if test="@Type">
                                            <xsl:text> </xsl:text>
                                            <span class="type">
                                                <xsl:value-of select="@Type"/>
                                            </span>
                                        </xsl:if>
                                        <xsl:text> </xsl:text>
                                        <span class="var">
                                            <xsl:value-of select="@Name"/>
                                        </span>
                                    </xsl:for-each>
                                    <xsl:text> )</xsl:text>
                                </div>
                            </xsl:for-each>
                        </td>
                    </tr>
                </xsl:if>
            </table>
        </xsl:for-each>

    </xsl:template>

</xsl:stylesheet>

One Comment

Leave a comment