Should I Make a Log Wrapper Class?
Update December 2020: This article discusses creating your own log wrapper class to sit between your code and a logging system, and why that may not be a good idea under most circumstances. At the time of writing this piece, there was not an easy solution for creating an abstraction, and the need to do so was not as common. Now though, Microsoft.Extensions.Logging exists, and we recommend most .NET developers use it. Microsoft.Extension.Logging is a .NET native logging framework that can be used like a log wrapper class, but is capable of much more.
If you would like to read more on why you should consider using Microsoft.Extensions.Logging, we have written about it here.
There are a few related questions we get from many folks when they’re getting started with Loupe:
- Should I Use NLog/Log4Net/Trace/Something to talk to Loupe? Since Loupe can be the log store for many other log systems, folks often thing they’re supposed to pick one of those systems to use instead of using Loupe directly
- Should I Make My Own Wrapper Class? Instead of talking to any logging system directly should I pre-emptively isolate my code so I can swap them out later.
Loupe Is a Logging System
We’ve worked with many teams since shipping Loupe over five years ago and we can confidently report the best experience comes from using Loupe directly whenever you can. You get an API that gives high fidelity logging with relatively compact and absolutely safe calls. With a minimum of configuration you’re up and running. Internally, we use Loupe as our logging system throughout products (Including VistaDB) and have even made it easy to convert your code from Trace to Loupe.
When Should I use Loupe through another Log System?
The primary reason is because you already are using another log system. If you have a wealth of code that is already using NLog, by all means you don’t need to change that to adopt Loupe. If you love a feature that NLog has which Loupe doesn’t (like say buffered logging) then you should keep using that. There’s no requirement to convert your existing code or abandon patterns you love to use Loupe.
The second reason is when you can’t take a dependency directly on Loupe. For example, if you’re fitting into an environment that already uses Log4Net and you have to take a reference on that anyway, it may be best to just stick with that, at least for logging (but don’t forget that Loupe does Metrics too
Logging in Libraries
The best scenario we’ve seen yet for not logging directly to Loupe is in the case where you’re creating an independent library that is going to be integrated into any application. Picking a logging system is really an application-level decision - you want one logging system running for everything in that application so a user can view all of the diagnostic information at once. Users that pick your library for what it does shouldn’t be constrained in what kind of logging infrastructure they’re required to use. Traditionally, this pushed libraries to use Trace since that’s built into the framework. However, Trace has a number of limitations and is generally poor performing (because it uses global locks by default). A nice alternative to this we’ve seen is LibLog available on NuGet. it gives you a way to automatically use whatever of a number of common log systems are present (or any log system a user specifies). If you’re making an independent library we’d encourage you to check out this approach so your users can pick their favorite system.
This guidance only applies to libraries you’re shipping outside of your organization - if you’re consuming it internally you can make your own constraints on what logging system to use and have the benefit of the most tailored API.
What about Wrapper Classes?
A reflexive thought many teams have when looking at logging is to write their own logging class so their code all calls their own wrapper which in turn calls the logging system. The primary drivers discussed tend to be:
- What if we change our log system: By creating a wrapper we only have one place to update our code when we move from system A to B.
- Easier adoption because our developers only have to learn our system: The wrapper will often have a simple API with the idea that it’s less for people to learn and fewer options to set.
It’s our experience that these advantages don’t come to pass or are dramatically overshadowed by disadvantages of this approach so we don’t recommend it. This isn’t just because we like our API - We’d recommend that you find a logging API you love and use it, which is why we’ve made sure Loupe can work with just about any of them. When we step back and think about it, there are a few key reasons to isolate yourself from a dependency:
- You’re going to have multiple implementations from day one.
- You can dramatically simplify the experience because you’re narrowing the scope of the feature set.
- It’ll be a lot less effort to do up front than handle later if you have to change the dependency.
Let’s take these in order:
Multiple Logger Implementations
If you are planning on having multiple logging implementations from day one, we’d recommend looking at LibLog (see above). Outside of library developers this isn’t common - organizations typically have one log system they’re using at a time. If you’re going to introduce a second log system solely for the purposes of creating a mock system so you can unit test your log messages we’d suggest you reconsider. You want to have a low friction logging experience or developers will generally stop putting in logging. If you insist on unit testing each log message you’re creating a strong disincentive to create the very diagnostics you’ll want when you ship. There are some exceptions to this rule - say logging around security failures and the like - but it holds up well in practice. We recommend asking developers to use the logs to solve problems as soon as their code leaves their desk - this creates an incentive to have reasonably useful diagnostics.
Simplifying the Logging Experience
We’ve found it’s unlikely you’re going to simplify the coding experience of using a log system because most are fairly trim, providing methods you really are going to use. More importantly, you want them to be available to your team for the cases when they do come up. This isn’t like a statistics library where you might use just 1/10th of what it can do; bigger systems will use just about every feature of a log system. If you wrap the log system you’ll end up incrementally building that capability yourself instead of just using it off the shelf the way the designers intended.
Swapping out a Logger
That leaves us to the third reason - is it less effort to do up front than later? In our experience, no - it’s actually *less* work when deferred. Why? Let’s say you’ve adopted Loupe and want to switch to NLog. At worst, what would you have to do? Well, you’d make a copy of our Log class (easy) to your own static implementation that would in turn forward on the calls you’re already making to NLog. You could then decide over time whether to keep calling this wrapper or start calling NLog directly. The best part is you’ll be doing the minimum amount of work to move from one system to another - not trying to fit to an abstract notion of logging you created in your simple wrapper class - and you’re doing the work only when you make the decision to migrate.
This is pretty much the same work you’d need to do to create your own wrapper class, possibly less because you’re not having to invent your own abstraction. In short:
- Same or Less Effort
- Only incurred when you make the decision to move
Hey, we know it’s fun to design API’s - it’s something we love doing too! But we’d suggest you’re going to get a better return putting that time elsewhere in your system. After all, there’s no user story for logging - so delegate it down to someone who’s thinking through the problem for you and focus on creating value for your users instead.
Need logging in your independent library? Check out LibLog for a logger-neutral approach that will automatically integrate with whatever your users have!