In this week’s episode of Cloud Cover, Steve and I covered Logging, Tracing, and ELMAH in Windows Azure. Steve explored the first two topics while I looked into ELMAH in Windows Azure. You should make sure and take a look at his posts – they’re useful:

ELMAH (Error Logging Modules and Handlers) itself is extremely useful, and with a few simple modifications can provide a very effective way to handle application-wide error logging for your ASP.NET web applications. If you aren’t already familiar with ELMAH, take a look at the ELMAH project page.

NuGet

Before going any further, I thought I’d let you know that I’ve created a NuGet package that makes this extremely easy to try. You can take a look at ELMAH with Windows Azure Table Storage on the NuGet gallery or immediately try this out with the following command:

       Install-Package WindowsAzure.ELMAH.Tables

By default this NuGet package is configured to use the local storage emulator. If you want to use your actual Windows Azure storage account you can uncomment the following line in the Web.Config file:

<!--
<errorLog 
    type="WebRole1.TableErrorLog, WebRole1" 
    connectionString="DefaultEndpointsProtocol=https;AccountName=YOURSTORAGEACCOUNT;
      AccountKey=YOURSTORAGEKEY" />
-->

Incidentally, if you like NuGet, then you should check out Cory Fowler’s post on must have NuGet packages for Windows Azure development.

Demo

For those of you unfamiliar with ELMAH, I put together a simple demo. You can try it out on http://elmahdemo.cloudapp.net/. Just enter a message (keep it clean, please!) and throw an exception.

ELMAHDemo

Click the ELMAH button to then load the handler. You’ll see all the errors logged with a lot of great detail.

ELMAHHandler

The great part is that these files are getting serialized into Windows Azure table storage. The benefit of this is you can read them from anywhere – in fact, you don’t have to even deploy the elmah.axd handler with your web application! You could run it locally.

Here’s what the files look like in table storage:

ELMAHInTables

How Does it Work?

The nice part is you can easily grab the NuGet package to view all the source code. There are two items of interest: ErrorEntity.cs and Web.Config.

In ErrorEntity.cs we first create our ErrorEntity:

public class ErrorEntity : TableServiceEntity
{
    public string SerializedError { get; set; }

    public ErrorEntity() { }
    public ErrorEntity(Error error)
        : base(string.Empty, (DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks).ToString("d19"))
    {
        this.SerializedError = ErrorXml.EncodeString(error);
    }
}

Then we implement the ErrorLog abstract class from ELMAH to create a TableErrorLog class with all the implementation details.

public class TableErrorLog : ErrorLog
{
    private string connectionString;

    public override ErrorLogEntry GetError(string id)
    {
        return new ErrorLogEntry(this, id, ErrorXml.DecodeString(CloudStorageAccount.Parse(
            connectionString).CreateCloudTableClient().GetDataServiceContext()
            .CreateQuery<ErrorEntity>("elmaherrors").Where(e => e.PartitionKey == string.Empty 
                && e.RowKey == id).Single().SerializedError));
    }

    public override int GetErrors(int pageIndex, int pageSize, IList errorEntryList)
    {
        var count = 0;
        foreach (var error in CloudStorageAccount.Parse(connectionString).
            CreateCloudTableClient().GetDataServiceContext()
            .CreateQuery<ErrorEntity>("elmaherrors")
            .Where(e => e.PartitionKey == string.Empty).AsTableServiceQuery()
            .Take((pageIndex + 1) * pageSize).ToList().Skip(pageIndex * pageSize))
        {
            errorEntryList.Add(new ErrorLogEntry(this, error.RowKey, 
                ErrorXml.DecodeString(error.SerializedError)));
            count += 1;
        }
        return count;
    }

    public override string Log(Error error)
    {
        var entity = new ErrorEntity(error);
        var context = CloudStorageAccount.Parse(connectionString)
            .CreateCloudTableClient().GetDataServiceContext();
        context.AddObject("elmaherrors", entity);
        context.SaveChangesWithRetries();
        return entity.RowKey;
    }

    public TableErrorLog(IDictionary config)
    {
        connectionString = (string)config["connectionString"] ?? RoleEnvironment
            .GetConfigurationSettingValue((string)config["connectionStringName"]);
        Initialize();
    }

    public TableErrorLog(string connectionString)
    {
        this.connectionString = connectionString;
        Initialize();
    }

    void Initialize()
    {
        CloudStorageAccount.Parse(connectionString).CreateCloudTableClient()
            .CreateTableIfNotExist("elmaherrors");
    }
}

Now, to leverage these assets, we update the Web.Config file to include an <elmah> … </elmah> section that specifies our custom error log (and also allows remote access to the handler:

<elmah>
  <security allowRemoteAccess="yes" />
  <errorLog type="WebRole1.TableErrorLog, WebRole1" 
            connectionString="UseDevelopmentStorage=true" />
</elmah>

That’s all there’s to it!

Of course, there are many other ways you could define your ErrorEntity and implement the TableErrorLog (i.e. you could extract more details into additional entities within your table), but this way is pretty effective.

I hope this helps!

  • http://debugmode.net/ Dhananjay Kumar

    Thaks for this informative post

  • Pingback: Episode 56 – Logging, Tracing, and ELMAH in Windows Azure - Windows Azure Blog

  • Ray Fan

    Thank you wade, this is very helpful! One thing I’d like to ask is that currently using System.Diagnostics.TraceSource logging to the WADLogsTable, it also logs DeploymentId, Role, RoleInstance, EventId, could you add these info to your package for a future release please? Thanks a lot for your work!

  • Matt

    Hi Wade,

    Do you know if this can be used with the Windows Azure Accelerator for Web Roles? (for the record, it doesn’t appear too)

    Thanks!

  • Matt

    Disregard, I got it working.

  • Andrew

    I was planning on just using ELMAH’s SQL storage engine with SQL Azure. What is the benefit in using Azure Table Storage instead?

    Unrelated, but I also made a patch that allows ELMAH to include or exclude more information in a pretty granular fashion: http://code.google.com/p/elmah/issues/detail?id=124

  • Jason Frakes

    Thanks for the NuGet update, this was very easy. I would only suggest one thing that you point out that you need to change the error log type depending on your WebRole, I am running MVC so I had to make one small change.

    Thanks again

  • Pingback: Azure: Links, News and Resources (2) « Angel “Java” Lopez on Blog

  • Jason

    I can’t get this to work. Do you have a sample project?

  • Ian Mckay

    Hey,

    I just noticed that the paging logic looks a little suspect. Is this assuming that the table is ordered by timestamp default?

    Shouldn’t
    .Take((pageIndex + 1) * pageSize).ToList().Skip(pageIndex * pageSize))

    Be
    .Skip((pageIndex + 1) * pageSize)).Take(pageSize)

    Also the count returned will simply be the pageSize where as it needs to be the totalRowCount so the elmah client paging can work correctly. When we used the above code we got a single page of unordered results. We added an ordering and the above paging changes and now it works as expected.

    Thanks

    • Ian Mckay

      Sorry i meant to post

      .Skip(pageIndex * pageSize).Take(pageSize)

  • http://andreykuzmenko.com/ Andrey

    Great post, but there is a limitation – String values may be up to 64 KB in size. That does not work.

  • Pingback: Reading Notes 2012-03-26 | Matricis

  • Seth

    Wade, thanks for providing this, it is a great way to get started quickly. Unfortunately, there is a pretty severe limitation to the “GetErrors” method. The paging is really just totally wrong, and I don’t see any great way to fix it. The way it is implemented you will never get more than one page of data, and there is no way to get to more than max 1000 errors (due to Azure limit of 1000 records). I wasn’t even able to get elmah to increase the page size over 100, meaning I could only view the last 100 errors at most.

    I believe you would have to use continuation tokens in the query to properly implement paging, but I don’t know of any good way to get the elmah web interface to remember those tokens.

    I might take a stab at a workaround, has anyone else tried to resolve this problem?

  • Simon

    I’ve come across a similar (or same probably) thing to Seth.

    Now there are so many error logs I can’t even navigate to the elmah page because the request times out, I imagine it’s getting all of the errors at once.

  • http://cuptrek.com Andrey

    I’ve created a log provider for Blob storage based on XmlFileErrorLog. We use it in our projects. Here is a link https://github.com/AndreyKuzmenko/Blob-Elmah

  • Cheeta

    Hi.
    Thank you for this code!
    I got your sample working, only when I call the elmah.axd page and want to view the details of an error, I get the yellow screen of death you normally see with an asp.net page, instead of the extensive error details, that also contain the Server Variables.
    Did I do something wrong?
    Is there a way to ‘turn on’ the extensive details view?
    Thanks! :-)

  • Cheeta

    Update to my previous post: an error was occuring in the above code of the TableErrorLog. Once I fixed/worked around that, the extensive error reporting had magically reappeared…
    This line: .Single(e => e.PartitionKey == string.Empty && e.RowKey == id) was giving this error: “the method ‘Single’ is not supported”.
    The error disappeard when I changed it to .Where(e => e.PartitionKey == string.Empty && e.RowKey == id).Single().

  • Thej

    Please post sample code for this

  • http://twitter.com/RyanAnde Ryan Anderson

    Important to note here that changes to Microsoft.WindowsdAzure.Storage v2 this code is broken. Refer to the breaking changes of v2 and how to fix here; http://stackoverflow.com/questions/13647479/where-is-the-tableclient-createtableifnotexist-in-azurestorage-library-v2