Wednesday, May 21, 2008

Logging Workflow Errors in the Workflow History List

By overriding the HandleFault method and using the SPWorkflowActivationProperties.Workflow.CreateHistoryEvent method, it's possible to write unhandled exceptions (without much coding overhead) to the history log to assist in debugging long running workflows.

The code at the bottom of this post demonstrates the idea. A potential issue is that the description field of the workflow history view displayed at the bottom of the workflow status page will be truncated if it's longer that 255 characters - making it not ideal to show long exception messages and/or stack traces. One solution is to modify the code the WrkStat.aspx page to display a link to the workflow history list item, for example by adding the following code to override the ViewFields used by the idHistoryView ListViewByQuery (near the very bottom of the page), so that a link to the item (in this case a DocIcon field) is displayed. Alternatively, you could code a url into the description, but that would require information about the workflow history item (ID, etc) that's not available by using the CreateHistoryEvent method.

<%
if (this.idHistoryView.Visible == true) {
    this.idHistoryView.Query.ViewFields = "<FieldRef Name=\"Occurred\" /><FieldRef Name=\"Event\" /><FieldRef Name=\"User\" /><FieldRef Name=\"DocIcon\" /><FieldRef Name=\"Description\" /><FieldRef Name=\"Outcome\" />";
}
%>
<SharePoint:ListViewByQuery
runat="server" DisableSort="true" DisableFilter="true" ID="idHistoryView" />


...

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Workflow.Activities;
using System.Workflow.ComponentModel;
using Microsoft.SharePoint.Workflow;
 
namespace HubKey.DevelopmentHole
{
    public sealed partial class MyWorkflow: SequentialWorkflowActivity
    {
        //assign workflowProperties in the OnWorkflowActivated activity
        public SPWorkflowActivationProperties workflowProperties = new SPWorkflowActivationProperties();
 
        protected override ActivityExecutionStatus HandleFault(ActivityExecutionContext executionContext, Exception exception)
        {
            try
            {
                workflowProperties.Workflow.CreateHistoryEvent(
                    (int)SPWorkflowHistoryEventType.WorkflowError,
                    0,
                    workflowProperties.OriginatorUser,
                    "Nothing good",
                    string.Format("An exception occured during activity {0}. Check the item data for more information.", executionContext.Activity.Name),
                    Misc.FormatException(exception));
            }
            catch { }
            return base.HandleFault(executionContext, exception);
        }
    }
 
    public static class Misc
    {
        static Regex stackRegex = new Regex(@"\r\n   at ", RegexOptions.Compiled);
 
        public static string FormatException(Exception ex)
        {
            return FormatException(ex, new StringBuilder(), 0);
        }
 
        private static string FormatException(Exception ex, StringBuilder sb, int tab)
        {
            if (ex == null)
                return sb.ToString();
            string sTab = new string('\t', tab);
            sb.AppendFormat("{0}Source: {1}\r\n", sTab, ex.Source);
            sb.AppendFormat("{0}Message: {1}\r\n", sTab, ex.Message);
            sb.AppendFormat("{0}StackTrace: {1}\r\n", sTab, FormatStackTrace(ex, sTab));
            sb.AppendLine();
            return FormatException(ex.InnerException, sb, ++tab);
        }
 
        private static string FormatStackTrace(Exception ex, string sTab)
        {
            string stack = ex.StackTrace;
            if (stack == null)
                return null;
            stack = stackRegex.Replace(ex.StackTrace, delegate(Match m)
            {
                return string.Format("\r\n{0}   at ", sTab);
            });
            return stack;
        }
    }
}