Azure Developer Challenge – YouConf – Day 11 (May 9th)

Quite a bit to report on today….

Setting up a Dupal blog website

Since about day 3 I’d been thinking of moving the posts on my daily progress into a separate blog, as there’s enough information to make some of them worth an entire entry. I was also aware that one of the competition points for this section was around what we do with our other 9 websites. So I figured I’d see if it really was as easy to setup a blog as they made out in http://www.windowsazure.com/en-us/develop/php/tutorials/website-from-gallery/.

I found the above post, and a few others, which setup WordPress blogs, so I thought why not try a different one to make things a bit more interesting. In the end I went with Drupal, as an old workmate of mine used to rave about it. I found an article for guidance on installing WordPress at http://www.windowsazure.com/en-us/develop/php/tutorials/website-from-gallery/, so used this as a guide. Here’s what I did:

  1. Selected the web sites note in the Azure management screen and clicked New
  2. Selected Compute > Web site > From gallery
  3. Selected Acquia Drupal 7 (Note that later I realized there were specific blog applications, so if doing this again I would use one of those…)
    DrupalSelectFromGallery
  4. Chose the url for my blog – youconfblog – and chose to create a new mysql database.
    DrupalConfigureUrl
  5. Followed the rest of the prompts and provided my email address etc, and let it complete. I was then able to browse to my vanilla Drupal installation at http://youconfblog.azurewebsites.net/
  6. I then wanted to install a blog theme, and I found a nice looking one at http://drupal.org/project/responsive_blog
  7. To install it on my site, I found the .tar.gz url for the installation package – http://ftp.drupal.org/files/projects/responsive_blog-7.x-1.6.tar.gz – and in the admin section of my Drupal site, selected Appearance from the top menu, then Install new theme.
  8. I provided the url to the responsive blog package, and then let Drupal do its thing and complete the installation.
  9. I then configured the theme by going to the Settings page for the theme, and added my own YouConfBlog logo, and disabled the slideshow on the homepage.
    DrupalConfigureTheme

And now I have a nice themed Drupal site! http://youconfblog.azurewebsites.net

I then added a couple of blog entries for day one & two, by copying & pasting the html code from my CodeProject article into the blog entry.

What, wait a minute, aren’t we supposed to avoid duplication?

After getting my second day’s progress blogpost into my Drupal site, I realized that if I was to copy & paste all the articles:

  1. It could take a while
  2. I’d have to do the same in future for all my other daily progress updates
  3. If I changed one, I’d have to update the other
  4. I wouldn’t be keeping in line with the CodeProject terms, which discourage you from posting content from CodeProject elsewhere
  5. I might make it harder for the judges to assess me article, as now they’d have to look in two places

In light of the above, I left my two initial blog posts intact, and decided that for now I’ll only post updates in my CodeProject article, since the goal of setting up the blog was to see if it really was as easy as others had made out (whilst learning along the way), which indeed it was. I’ll leave the blog in place though, as it deserves to be part of my entry for challenge two as one of the other 9 websites.

Error Logging

Usually one of the first things I do when creating a project is setting up Error Logging. Sometimes it’s to a text file, sometimes to xml, sometimes to a database, depending on the application requirements. My favourite logging framework for .Net web apps is Elmah, as it takes care of catching unhandled exceptions and logging them to a local directory right out-of-the-box. It has an extension for MVC too, which is awesome.

Elmah allows you to specify the route url you’re like to use for viewing errors in your web.config. It also allows you to restrict access to the log viewer page if needed, using an authorization filter so you can specify which user roles should have access. At this stage I haven’t implemented membership, and so can’t restrict access via roles. Thus I’m going to leave remote access to the logs off (which it is by default). For part 3 when I implement membership I’ll update this. Note that for any production application I’d never leave the error log page open to the public, as it would give away far too much to anyone who happens to come snooping.

Right – to setup Elmah logging I did the following:

  1. Opened the nuget package manager for my YouConf project in Visual Studio, and searched for Elmah as below
    ElmahNuget
  2. Selected the Elmah.Mvc package and installed it. This added an section to my web.config, and also some appsettings for configuring Elmah.
  3. Opened up my web.config and (using the ol’ security through obfuscation mantra) updated my elmah.mvc.route appsetting value to be a long complicated url – superdupersecretlogdirectorythatwillbeprotectedonceweimplementregistrationwithSqlandsimplemembership
  4. Fired up the local debugger and navigated to http://localhost:60539/superdupersecretlogdirectorythatwillbeprotectedonceweimplementregistrationwithSqlandsimplemembership
  5. Voila – we have an error viewer!
    ElmahLogViewer
  6. Now if I trigger an error by going to a dodgy url e.g. http://localhost:60539/// I should see an error appear in my list.
    ElmahLogViewerError
  7. And voila – there it is!
Logging to persistent storage

By default Elmah logs exceptions in-memory, which is great when you’re developing, but not so good when you deploy to another environment and want to store your errors so you can analyze them later. So, how do we setup persistent storage?

In the past I’ve used local xml file, which is really easy to configure in Elmah by adding the following line to the section of your web.config as follows:

<elmah>
  <errorLog type="Elmah.XmlFileErrorLog, Elmah" logPath="~/App_Data" />
</elmah>

This is fine if you’re working on a single server, or can log to a SAN or similar and then aggregate your log files for analysis. However, in our case we’re deploying to Azure, which means there are no guarantees that our site will stay on a single server for its whole lifetime. Not to mention that the site will be cleared each time we redeploy, along with any local log files. So what can we do?

One option is to setup Local Storage in our Azure instance. This will give us access to persistent storage will not be affected by things like web role recycles or redeployments. To use this, we would need to:

  1. Setup local storage as per the following article (http://msdn.microsoft.com/en-us/library/windowsazure/ee758708.aspx)
  2. Configure our error logger to use this directory instead of App_Data.
  3. Sit back and relax

The above solution would work fine, however, since I’m already using Azure Table storage, I thought why not use it for storing errors as well? After some googling I came upon the following package for using table storage with Elmah, but upon downloading the code realized it wasn’t up-to-date with the Azure Storage v2 SDK. It was easy to modify though, with the end result being the class below.

namespace YouConf.Infrastructure.Logging
{
    /// <summary>
	/// Based on http://www.wadewegner.com/2011/08/using-elmah-in-windows-azure-with-table-storage/
    /// Updated for Azure Storage v2 SDK
    /// </summary>
	public class TableErrorLog : ErrorLog
    {
        private string connectionString;
        public const string TableName = "Errors";
        private CloudTableClient GetTableClient()
        {
            // Retrieve the storage account from the connection string.
            CloudStorageAccount storageAccount = CloudStorageAccount.Parse(
               CloudConfigurationManager.GetSetting("StorageConnectionString"));
            // Create the table client.
            return storageAccount.CreateCloudTableClient();
        }
        private CloudTable GetTable(string tableName)
        {
            var tableClient = GetTableClient();
            return tableClient.GetTableReference(tableName);
        }
        public override ErrorLogEntry GetError(string id)
        {
            var table = GetTable(TableName);
            TableQuery<ErrorEntity> query = new TableQuery<ErrorEntity>();
            TableOperation retrieveOperation = TableOperation.Retrieve<ErrorEntity>("", id);
            TableResult retrievedResult = table.Execute(retrieveOperation);
            if (retrievedResult.Result == null)
            {
                return null;
            }
            return new ErrorLogEntry(this, id,
              ErrorXml.DecodeString(((ErrorEntity)retrievedResult.Result).SerializedError));
        }
        public override int GetErrors(int pageIndex, int pageSize, IList errorEntryList)
        {
            var count = 0;
            var table = GetTable(TableName);
            TableQuery<ErrorEntity> query = new TableQuery<ErrorEntity>()
            .Where(TableQuery.GenerateFilterCondition(
              "PartitionKey", QueryComparisons.Equal, TableName))
            .Take((pageIndex + 1) * pageSize);
            //NOTE: Ideally we'd use a continuation token
            // for paging, as currently we're retrieving all errors back
            //then paging in-memory. Running out of time though
            // so have to leave it as-is for now (which is how it was originally)
            var errors = table.ExecuteQuery(query)
                .Skip(pageIndex * pageSize);
            foreach (var error in errors)
            {
                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 table = GetTable(TableName);
            TableOperation upsertOperation = TableOperation.InsertOrReplace(entity);
            table.Execute(upsertOperation);
            return entity.RowKey;
        }
        public TableErrorLog(IDictionary config)
        {
            Initialize();
        }
        public TableErrorLog(string connectionString)
        {
            this.connectionString = connectionString;
            Initialize();
        }
        void Initialize()
        {
            CloudStorageAccount storageAccount = CloudStorageAccount.Parse(
               CloudConfigurationManager.GetSetting("StorageConnectionString"));
            var tableClient = storageAccount.CreateCloudTableClient();
            CloudTable table = tableClient.GetTableReference("Errors");
            table.CreateIfNotExists();
        }
    }
    public class ErrorEntity : TableEntity
    {
        public string SerializedError { get; set; }
        public ErrorEntity() { }
        public ErrorEntity(Error error)
            : base(TableErrorLog.TableName,
              (DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks).ToString("d19"))
        {
            PartitionKey = TableErrorLog.TableName;
            RowKey = (DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks).ToString("d19");
            this.SerializedError = ErrorXml.EncodeString(error);
        }
    }
} 

This will log all errors to the Errors table in Azure table storage, and also take care of reading them back out again.

I also had to update my web.config to use the new logger class as follows:

<elmah>
    <errorLog type="YouConf.Infrastructure.Logging.TableErrorLog, YouConf" />
</elmah>

Now if I generate an error I’ll still see it on the Elmah log viewpage, but I can also see it in my table storage. I’m using dev storage locally, so I can fire up the wonderful Azure Storage Explorer and view my Error Log table as shown below:

AzureStorageExplorerErrors

and also on-screen:

ElmahLogViewerWithTableStorage

Lovely!

Advertisements

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