Logging Abstraction - Tigra-Astronomy/TA.Utilities GitHub Wiki
The TA.Utils.Core.Diagnostics
namespace defines a pair of interfaces, ILog
and IFluentLogBuilder
, that define an abstract logging service.
Libraries can perform logging through these interfaces without ever taking a dependency on any logging imnplementation. This helps to keep code loosely coupled and makes it easy to switch logging frameworks. The actual implementation can be injected at runtime, typically in a constructor parameter. The policy decision about which logging engine to use can be taken in the top level composition root of the application.
The fluent interface defined in IFluentLogBuilder
was modeled on the NLog fluent interface, so it is a very natural fit.
However, the interface has enough flexibility to adapt to other logging backends without too much trouble.
NLog is our preferred logging framework and we have produced an adapter for it, documented elsewhere.
A null implementation is provided in DegenerateLoggerService
and DegenerateLogBuilder
.
The two classes do essentially nothing and produce no output; they are a data sink.
Libraries can choose to use this as their default logging implementation, which is easier than checking
whether the logger is null every time it is used.
One could use Maybe<ILog>
but it is easier to just use a degenerate implementation and not have to check whether it has been configured.
We like fluent interfaces a lot and we also like the Builder Pattern. So no surprise that the abstract interfaces support exactly that.
log.error()
.Message("There was an error")
.Exception(ex)
.Property("value", currentValue)
.Write();
The interface also supports semantic (or structured) logging. You can use a simple format string like so:
log.Info().Message("Sending data {0}", data).Write();
log.Error().Message("Exception {0} occurred with error code {1}", ex.Message, errorCode).Write();
But this leaves useful information on the table. Extra rich information can be included like so:
log.Info().Message("Sending data {data}", data).Write();
log.Error()
.Message("Exception {exception} occurred with error code {error}")
.Property("exception", ex.Message)
.Property("error", errorCode)
.Exception(ex)
.Write();
In both statements, we are adding property-value pairs to the log.
In the first Log.Info()
statement this is implicit, whereas in the Log.Error()
statement it is made explicit.
This extra information may or may not be used by the log renderer, but if its not there then it can't be used!
So if in doubt, include extra information where it is appropriate.
Again, this feature set is native to NLog so makes for a very lightweight adaptor. When developing adaptors for other logging frameworks, every attempt shouldbe made to preserve as much of the information as possible.
If you have only ever rendered log output to the console or a simple text file, then the advantages of semantic logging may be lost on you. If you are using NLog, try rendering your logs to a JSON file, or to a service like Seq.
Think of logging as occurring in two distinct stages.
- You build the log entry using
IFluentLogBuilder
, adding all of the relevant information as Properties of the log entry. - You send the log entry to the backend to be rendered.
The renderer may use none, some or all of the information you provided and it may even augment it with additional metadata. As a library developer, you shouldn't be concerned with how the entry will be rendered, stored or how it will be formatted. You should concentrate only on including as much relevant information as is appropriate.
Multiple renderers may be in use and different renderers will produce different output from the exact same log entry. For example:
- A file renderer may include a timestamp and perform log file rotation so that a new file is created each day.
- A debug output stream may include the name of the class where the log entry originated and print only the message portion.
- A console logger may write different lines in different colours accoring to the severity level.
- A syslog rendere may include the host name of the originating computer.
- A NoSQL database renderer may write out all of the properties as a JSON document.
In most cases, the way in which log data is ultimately rendered is controlled by the application, often using a configuration file. As a library developer, you must accept that you have little to no control over this. Just concentrate on including appropriate and useful information and don't think about formatting or storage too much.