Extending ELMAH on Windows Azure Table Storage

ELMAH and Windows Azure go together like peanut butter and jelly. If you’ve been using both, you’re probably familiar with a Nuget package that hooks ELMAH up to Table Storage. But, you may hit a snag with large exceptions. In this post, I’ll take you through how to get ELMAH and Table Storage settle some of their differences.

If you’ve been building asp.net apps for Windows Azure, you may recognize ELMAH. It’s a fantastic logging add-on to any application. If you’ve never heard of it, check it out - it makes catching unhandled (and handled) exceptions a breeze.

This is especially helpful when you start deploying applications to Windows Azure, where having easy access to exceptions makes your life much less stressful when something inevitably goes wrong. Thankfully, Wade Wegner (@wadewegner) has made it super easy to get this working in Azure with his package that uses Table Storage to store exceptions. Wade gives a great write up on it in more detail.

A Little Problem

Logging the exceptions works great however, you may encounter the following exception at some point. {“ The property value is larger than allowed by the Table Service. “} This is because the Table Storage Service limits the size of any string value property to 64KB. When you further examine the ErrorEntity class, you will find that this is actually quite easy to accomplish with exceptions that have large stack traces. A little examination reveals that the error caught by ELMAH is encoded to an XML string and saved to the SerializedError property.

Have No Fear

My solution to this is to store the serialized error in Blob Storage, and add a key (pointer) to it on the ErrorEntity. So, lets get right to the code…

public class ErrorEntity : TableServiceEntity
{
     public string BlobId { get; set; }
     private Error OriginalError { get; set; }

     public ErrorEntity() { }
     public ErrorEntity(Error error) : base(...)
     {
          this.OriginalError = error;
     }

     public Error GetOriginalError()
     {
          return this.OriginalError;
     }
}

I’ve added two new properties. BlobId - which will be our pointer to our blob record. OriginalError - which is needed, but we will use later. One thing you will notice is that SerializedError is gone, I will get to this a little later.

The other important things to capture is that we assign our Error that we constructed the ErrorEntity with to our new property and we also created a function to obtain it later on. The key here is that we mark the OriginalError property private. This is to prevent the property from being persisted as part of the Table Storage entity.

The Dirty Work

Now comes the fun part; getting the error into blob storage. Lets start with where we save the entity to Table Storage.

public override string Log(Error error)
{
     var entity = new ErrorEntity(error);
     var context = CloudStorageAccount.Parse(connectionString).CreateCloudTableClient().GetDataServiceContext();
     entity.BlobId = SerializeErrorToBlob(entity);

     context.AddObject("elmaherrors", entity);
     context.SaveChangesWithRetries();
     return entity.RowKey;
}

What we are doing here is simply making a call to a function (which we will define in a second) that will persist our error to blob storage and give us back and ID we can use to obtain it later.

private string SerializeErrorToBlob(ErrorEntity error)
{
      string id = Guid.NewGuid().ToString();
      string xml = ErrorXml.EncodeString(error.GetOriginalError());

      var container = CloudStorageAccount.Parse(this.connectionString).CreateCloudBlobClient().GetContainerReference("elmaherrors");
      var blob = container.GetBlobReference(id);
      blob.UploadText(xml);
      return id;
}

Pretty simple eh? Now that we know how we are saving to blob storage, we can simply extract it via the reverse process.

private string GetErrorFromBlob(string blobId)
{
     var container = CloudStorageAccount.Parse(this.connectionString).CreateCloudBlobClient().GetContainerReference("elmaherrors");

    var blob = container.GetBlobReference(blobId);
    return blob.DownloadText();
}

public override ErrorLogEntry GetError(string id)
{
     var error = CloudStorageAccount.Parse(connectionString).CreateCloudTableClient().GetDataServiceContext().CreateQuery<ErrorEntity>("elmaherrors").Where(e => e.PartitionKey == string.Empty && e.RowKey == id).Single();


    return new ErrorLogEntry(this, id, ErrorXml.DecodeString(GetErrorFromBlob(error.BlobId)));


}

public override int GetErrors(int pageIndex, int pageSize, IList errorEntryList)
{
     ....
          if (!String.IsNullOrEmpty(error.BlobId))
          {
              var e = ErrorXml.DecodeString(GetErrorFromBlob(error.BlobId));
              errorEntryList.Add(new ErrorLogEntry(this, error.RowKey, e));
              count += 1;
          }
     }
     return count;
}

The rest is pretty self-explanatory. You can now throw exceptions with full (large) stack traces and ELMAH should be able to handle them all day long.