To implement the logging interfaces described earlier, there are some issues we need to consider. There are two different usage patterns we need to support, plus we need to support concurrent use, and avoid code duplication.
Usage Scenarios
The first scenario is when a method controls the context of its logging; the logger reference it needs is immediate to hand and easily used. Different parts of the application will pass around the logger as required.
The second, and more complex, is when a helper method wants to ensure that its logging is properly included in the active scope. We need to have some way for the current logging context to be found when required. Preferably, this would be a technique that doesn’t require adding a logger to every method signature.
For each of these scenarios, we will create a different logging class - a ScopedLogger
for the case where we are keeping explicit references to our current logging context, and a DelegatingLogger
for the case where we need to coordinate with a context created elsewhere. They will share the common base class LoggerBase
to avoid code duplication.
Asynchronous support
To cleanly associate a distinct logger with each asynchronous task, we want something akin to the [ThreadLocal]
attribute; fortunately, we don’t need to look far to find the AsyncLocal<T>
class. We use this to declare a static property to give us access to the logger wherever needed:
With this in place, we can introduce methods to actually write the message out:
For DelegatingLogger
, we implement the abstract WriteLogMessge
using CurrentLogger
:
Conversely, for ScopedLogger
, we already know the necessary context and we can pass it directly:
I’ve thrown in a little sanity check to ensure we’re not continuing to use a scoped logger after the scope has been completed.
Avoiding code duplication
We introduced the base class LoggerBase
that provides functionality common to both logging situations. As a part of that class, let us declare an enumeration that captures the different kinds of logging messages we want to emit:
We can then implement most of our desired logging methods as simple helpers that all delegate to WriteLogMessage()
:
The remaining log method is Action
, which returns a scoped logger after updating CurrentLogger
to match.
For each kind of log message, we define a dictionary of prefixes to use so that they’re easily distinguishable in the output:
It’s very common for text-based loggers to use somewhat cryptic abbreviations for the different log levels. Here we could have instead used DBG
, INF
, STA
, FIN
, SUC
and ERR
- but I wanted something with a little more visual appeal, so I’ve used some Unicode characters instead.
Conclusion
With this text-based logger in place, we can review the use of the application using the Output window of Visual Studio. Later on, we might want to replace this with something built into the application to allow troubleshooting without Visual Studio.
Comments
blog comments powered by Disqus