Solution – What to do with the Correlation ID from the Error Page

Scott ZischerkWhile developing SharePoint 2010 solutions I am constantly going through the ULS logs to find out what happened when an error occurs.  I usually use a tool like ULSViewer to wade through the logs and find my specific correlation id.  Then figure out what happened.  I thought to myself, there must be a better way!  So I tossed around a couple of ideas about creating a web part that would show the log items that had that the correlation id, but that still seemed too cumbersome.  What I really needed was to have it right on the error page.

After a few trial and error runs at it I finally came up with a solution to this problem and it’s totally contained in a single wsp and can be deployed to all my development servers for easier development.  Here’s how I did it.

Create A New Visual Studio Project

The first thing we need to do is create a new project in Visual Studio.  I almost always use the Empty SharePoint Project, it gives me the most flexibility when developing a solution.  Since we will be adding files to the 14 hive make sure you make it a Farm Solution.

SNAGHTML295cb95

After creating the project, we need to map a folder to the layouts directory.

image

Now we should be ready to go!

Create A New Error Page

SharePoint allows you to use custom error pages which is great if you want to give the end user a better experience after encountering an error.  Your company logo, a link on who to contact or other helpful information is a great addition to the standard error page.

The easiest way to do this is to copy the existing error.aspx page in the 14 hive (usually C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\LAYOUTS\error.aspx) and place it in your project in the just created mapped layouts folder, like so:

image

Since we placed it in the IBS.SharePoint2010.CustomError folder, we can leave the name the same. You never want to alter the original SharePoint error.aspx page, always make a copy of it when you copy it into your project,

Using the New Error Page

The next thing to do is tell SharePoint that we want to use ours instead of the out of the box one.  To do this, we need to create a new feature in our project and a feature receiver.

To create the new feature right click the features folder and select Add Feature.

image

We’ll rename the feature from feature1 to IBSCustomError and set some of the properties.

image

Noticed that we selected WebApplication for the scope of the feature.  Error pages can only be assigned at the web application level, so the feature has to be scoped for the web application.

The Feature Receiver

The next thing to do is to create the feature receiver, to do this, right click on the IBSCustomError feature and select Add Event Receiver.

image

In the feature receiver class file we want to implement the FeatureActivated and the FeatureDeactivating methods along with a constant to hold the path to our new error page.

The constant looks like this:

const string CustomErrorPage = "/_layouts/IBS.SharePoint2010.CustomError/error.aspx";

In the FeatureActivated method we’ll grab the web application and update the mapped page to our new error page.  There are other mapped pages you can change in a similar fashion.

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    SPWebApplication webApp = properties.Feature.Parent as SPWebApplication; 
    if (webApp != null) 
    { 
        if (!webApp.UpdateMappedPage(SPWebApplication.SPCustomPage.Error, CustomErrorPage)) 
        { 
            throw new ApplicationException("Cannot create the new error page mapping."); 
        } 
        webApp.Update(true); 
    }
}

In a similar manner, in the FeatureDeactivating method, we’ll set the mapped page to null.  This tells SharePoint to use the out of the box error page.

public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
    SPWebApplication webApp = properties.Feature.Parent as SPWebApplication; 
    if (webApp != null) 
    { 
        if (!webApp.UpdateMappedPage(SPWebApplication.SPCustomPage.Error, null)) 
        { 
            throw new ApplicationException("Cannot reset the default error page mapping."); 
        } 
        webApp.Update(true); 
    }
}

At this point, we should test the error page out, but how?  There’s never a error when you need one, so we’ll create one,  Let’s add a new visual web part to the project and call it “ThrowError”, but we’ll have to remember to remove the web part from the feature later so it’s not available to the end users.

SNAGHTML805e568[4]

This will be real simple, we’ll add the following code to the ascx page.

<asp:Button ID="btnThrowError" runat="server" Text="Throw Error" />

and in the code behind, we’ll use the following code blocks.

protected void Page_Load(object sender, EventArgs e)
{
    btnThrowError.Click += new EventHandler(btnThrowError_Click);
}

void btnThrowError_Click(object sender, EventArgs e)
{
    throw new Exception("Test exception");
}

Rename the new feature1 that was created when adding the visual web part to ThrowError update the properties of the feature.

SNAGHTML80f79ab

Now we can add some text to the error.aspx page.  Open the error.aspx page and add something after the RequestGuidText label, which is the correlation id.  I added IBS Custom Error.

image

Hit the F5 button and see the project add, deploy, and activate the feature on the site.  Then add the new web part to the page.

image

Click on the Throw Error button and check out your new custom error page!

image

image

Great! But we’re not finished.  We still need to show the ULS log items on the page,  We’ll, I need to come clean, the reality is when the error page is rendered to the screen the error has not been written to the ULS log yet so we need to think of another way to show the log items on the screen.  My solution was to put a button on the screen that opens up a secondary page with the log items.  So the next thing on the agenda is to create a new log viewer page.

Creating a Page to View the Log Items

For the Log Viewer page we don’t need a lot, just an aspx page with a code behind.  We don’t need a master page either, just simple html.  The aspx page looks like this:

<%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %>
<%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0,
Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=14.0.0.0,
Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="asp" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Assembly Name="Microsoft.Web.CommandUI, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="LogViewer.aspx.cs" Inherits="IBS.SharePoint2010.CustomError.
Layouts.IBS.SharePoint2010.CustomError.LogViewer" DynamicMasterPageFile="~masterurl/default.master" %>
<html>
<head>
</head>
<body>
    <form id="Form1" runat="server">
        <asp:Button ID="btnRefresh" runat="server" Text="Refresh" />
        <asp:Literal ID="litULSLog" runat="server"></asp:Literal>
    </form>
</body>
</html>

From here we need to read in the ULS logs and pull the log items with the specified correlation id.  Speaking of correlation id’s we’ll need to send it from the error page to the LogViewer page so we’ll accomplish this in a query string.  We’ll also need a domain object to hold the log item, we’ll call this ULSLogItem.

ULSLogItem.cs

using System;

namespace IBS.SharePoint2010.CustomError.Code.Domain
{
    public class ULSLogItem
    {
        public DateTime Time { get; set; }
        public string Server { get; set; }
        public string Process { get; set; }
        public string Thread { get; set; }
        public string Product { get; set; }
        public string Category { get; set; }
        public string EventId { get; set; }
        public string Level { get; set; }
        public Guid CorrelationId { get; set; }
        public string Message { get; set; }
    }
}

Now let’s load the latest ULS log and grab the log items with our correlation id with the following code.

public List<ULSLogItem> GetLogsByCorrelationId(Guid correlationId)
{
    string logPath = SPUtility.GetGenericSetupPath("LOGS");
    DirectoryInfo logDirectory = new DirectoryInfo(logPath);

    //Make sure we can access the ULS Logs!
    if (!logDirectory.Exists) throw new ArgumentException(String.Format("The ULS log location: '{0}' does not exist or is inaccessible.", logPath));

    //Only get the ULS logs, not the upgrade or pscdiagnostics logs.
    string[] logFiles = logDirectory.GetFiles("*.log").Where(u => !u.Name.ToLower().Contains("upgrade") && !u.Name.ToLower().Contains("pscdiagnostics")).OrderByDescending(u => u.LastWriteTime).Select(u => u.FullName).ToArray();
    List<ULSLogItem> logs = new List<ULSLogItem>();

    //If there aren't any logs don't bother trying to open them.
    if (logFiles.Length == 0) return new List<ULSLogItem>();
    
    //We're only looking for the first log, because that's where our error should be.
    FileStream fs = File.Open(logFiles[0], FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
    using (var file = new StreamReader(fs))
    {
        int lineNumber = 0;
        foreach (string line in file.Lines())
        {
            lineNumber++;
            //Skip the first line.  It only contains headers
            if (lineNumber == 1) continue;

            //Pre check, make sure the correlation id is in the line
            if (line.Contains(correlationId.ToString()))
            {
                string[] fields = line.Split('\t');
                
                //Fill the log item
                ULSLogItem log = new ULSLogItem()
                {
                    Time = Convert.ToDateTime(fields[0]),
                    Process = fields[1],
                    Thread = fields[2],
                    Product = fields[3],
                    Category = fields[4],
                    EventId = fields[5],
                    Level = fields[6],
                    Message = fields[7],
                    CorrelationId = new Guid(fields[8])
                };
                

                //Need this to exclude the LogViewer.aspx page on refresh of the LogViewer.aspx page
                if (correlationId == log.CorrelationId)
                    logs.Add(log);
            }                    
        }
    }
    return logs;
}

We’ll also need an extension method to the LogViewer.aspx.cs make the loading a little faster.

public static class TextReaderExt
{
    public static IEnumerable<string> Lines(this TextReader reader)
    {
        while (true)
        {
            string line = reader.ReadLine();

            if (line == null) // EOF?
            {
                break;
            }

            yield return line;
        }
    }
}

Now we just need to output the logs to the screen, I could have used a datagrid, but I was thinking about using jQuery datatables down the line, so I’m just going to use a literal control and write the html as a string.

private string GetHtml(List<ULSLogItem> errorMsgs)
{
    StringBuilder sb = new StringBuilder();
    sb.AppendLine("<table id=\"LogViewer\">");
    sb.AppendLine("<thead><tr>" +
                    "<th>Time</th>" +
                    "<th>Process</th>" +
                    "<th>Product</th>" +
                    "<th>Thread</th>" +
                    "<th>EventId</th>" +
                    "<th>Category</th>" +
                    "<th>Correlation Id</th>" +
                    "<th>Level</th>" +
                    "<th>Message</th>" +
                    "</tr></thead><tbody>");
    foreach (ULSLogItem item in errorMsgs)
    {
        sb.AppendFormat("<tr><td nowrap>{0}</td>" +
                            "<td nowrap>{1}</td>" +
                            "<td nowrap>{2}</td>" +
                            "<td nowrap>{3}</td>" +
                            "<td nowrap>{4}</td>" +
                            "<td nowrap>{5}</td>" +
                            "<td nowrap>{6}</td>" +
                            "<td nowrap>{7}</td>" +
                            "<td>{8}</td></tr>",
            item.Time,
            item.Process,
            item.Product,
            item.Thread,
            item.EventId,
            item.Category,
            item.CorrelationId,
            item.Level,
            item.Message.Replace("    ", "<br />"));
    }
    sb.AppendLine("</tbody></table>");
    return sb.ToString();
}

Finishing Up

Whew! We’re almost there.  The last thing we need to do is change the correlation id text to a link on the error.aspx page.  We’re going to change this:

<p>
    <asp:Label ID="RequestGuidText" Runat="server" />
</p>

to this:

<p>
    <a href="javascript:ViewLog();">
         <asp:Label ID="RequestGuidText" Runat="server" />
    </a>
</p>

and add this javascript to the PlaceHolderAdditionalPageHead placeholder:

    function ViewLog() {
        var correlationId = document.getElementById('ctl00_PlaceHolderMain_RequestGuidText').innerText.substring(16, 100);
        window.open('/_layouts/IBS.SharePoint2010.CustomError/LogViewer.aspx?CorrelationId=' + correlationId);
    }

Hit the F5 button to run the project, then click the Throw Error button.  You should get the following:

image

Click on the correlation id and it should bring up the log items related to that correlation id.

image

From here all you need to do is dress it up a little!  So there it is, a true time saver.  I have included the full source control and the wsp file for your use.

IBS.SharePoint2010.CustomError.wsp

IBS.SharePoint2010.CustomError.zip

One thought on “Solution – What to do with the Correlation ID from the Error Page

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s