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.
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
Adding some simple reflection code, we can expose a
public static IEnumerable<LogEvent> AllEvents
.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:
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
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.
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.