An Approach for More Structured Enums

The Need

I encountered a scenario today where a team need to increase the structure of their logging data. Currently, logging is unstructured text – log.Error("something broke") – whereas the operations team would like clearer information about error codes, descriptions and accompanying guidance.

The first proposed solution was a fairly typical one: we would define error codes, use them in the code, then document them in a spreadsheet somewhere. This is a very common solution, and demonstrated to work, but I wanted to table an alternative.

This blog post is written in the context of logging, but you can potentially extend this idea to anywhere that you’re using an enum right now.

My Goals

I wanted to:

  • support the operations team with clear guidance
  • keep the guidance in the codebase, so that it ages at the same rate as the code
  • keep it easy for developers to write log entries
  • make it easy for developers to invent new codes, so that we don’t just re-use previous ones

A Proposed Solution

Instead of an enum, let’s define our logging events like this:

public static class LogEvents
{
    public const long ExpiredAuthenticationContext = 1234;
    public const long CorruptAuthenticationContext = 5678;
}

So far, we haven’t added any value with this approach, but now let’s change the type and add some more information:

public static class LogEvents
{
    public static readonly LogEvent ExpiredAuthenticationContext = new LogEvent
    {
        EventId = 1234,
        ShortDescription = "The authentication context is beyond its expiry date and can't be used.",
        OperationalGuidance = "Check the time coordination between the front-end web servers and the authentication tier."
    };
 
    public static readonly LogEvent CorruptAuthenticationContext = new LogEvent
    {
        EventId = 5678,
        ShortDescription = "The authentication token failed checksum prior to decryption.",
        OperationalGuidance = "Use the authentication test helper script to validate the raw tokens being returned by the authentication tier."
    };
}

From a consumer perspective, we can still refer to these individual items akin to how we would enums – logger.Error(LogEvent.CorruptAuthenticationContext), however we can now get more detail with simple calls like LogEvent.CorruptAuthenticationContext.EventId and LogEvent.CorruptAuthenticationContext.OperationalGuidance.

More Opportunities

Adding some simple reflection code, we can expose a LogEvents.AllEvents property:

public static IEnumerable<LogEvent> AllEvents
{
    get
    {
        return typeof(LogEvents)
            .GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly)
            .Where(f => f.FieldType == typeof(LogEvent))
            .Select(f => (LogEvent)f.GetValue(null));
    }
}

This then allows us to enforce conventions as unit tests, like saying that all of our log events should have at least a sentence of so of operational guidance:

[Test]
[TestCaseSource(typeof(LogEvents), "AllEvents")]
public void AllEventsShouldHaveAtLeast50CharactersOfOperationalGuidance(LogEvents.LogEvent logEvent)
{
    Assert.IsTrue(logEvent.OperationalGuidance.Length >= 50);
}

Finally, it’s incredibly easy to either list the guidance on an admin page, or generate it to static documentation during build: just enumerate the LogEvents.AllEvents property.

The Code

I’ve posted some sample code to https://github.com/tathamoddie/LoggingPoc

Something interesting things in that repository are:

  • I’ve split the ‘framework’ code like the AllEvents property into a partial class so that LogEvents.cs stays cleaner.
  • I’ve written some convention tests that cover uniqueness of ids and validation of operational guidance.

Wrap Up

There’s absolutely nothing about this solution that is technically interesting. It’s flat out boring, but sometimes those are the most elegant solutions. Jimmy already wrote about enumeration classes 5 years ago.

One comment

Comments are closed.