If you’re writing .NET apps or libraries for your own use, well, you can choose and use whatever logging facility you like.
If you want to write packages for others to consume, your best bet is to avoid API designs that call for logging at all. Strong invariants, exceptions and unsurprising behavior will inform users a lot more than log messages, which they may not always collect or see.
Some kinds of libraries, especially framework-style ones that call user code rather than the other way around, can’t make this simplification. If you’re writing the next NancyFx, Topshelf, or MassTransit, you’ll face the problem of how to write log events for developers to consume.
Applications get to choose the logging subsystem, and your library has play well with that.
In the past, we’ve dealt with this through abstractions like Common.Logging. The abstraction defines an interface, and a framework-specific adapter implements this in terms of whatever logging library is in use:
While logging abstractions have been a great asset to the ecosystem, they suffer some serious downsides.
First, abstractions target the lowest common denominator. Why do you choose one logging library over another? Often it’s because you value some power that its API affords. Abstractions usually have to wipe away these advantages in order to fit a variety of implementations.
Second, abstractions bring dependencies. A single-assembly or single-package library that supports a shared abstraction now becomes three or more dependencies in actual usage: the library, the abstraction library, the binding between the abstraction and the logging library, and the logging library itself. All of these need to be maintained and versioned.
Third, abstractions proliferate. Because of the first problem, someone’s always looking to create a better abstraction, and because of the second problem, this leaves applications peppered with implementations of multiple similar abstractions.
Given this unsatisfactory situation, most popular libraries end up defining an interface that they expose for the user to plug logging in. The library can define the abstraction at the level it considers “ideal”, and since the abstraction is part of the library at least some dependency proliferation is avoided.
Still, this isn’t a great place for applications to be. Libraries get to stay relatively “clean”, but now since every library potentially defines its own logging interface, the job of binding all of these and wiring the result up is a chore. Making matters worse, IntelliSense for `ILog…` in Visual Studio now presents a dozen suggested types to choose from. Every time. Ick.
Enter LibLog.
@ThomasArdal LibLog within libs. Serilog for apps. For the record, Log4net died with the signing key fiasco
— Adam Ralph (@adamralph) April 25, 2016
LibLog is not Serilog, so why am I writing about it? Well, it’s a bit cynical of you to ask but since you did: LibLog is different. Serilog is for apps. LibLog is built specifically for libraries, to address the pain points I mentioned above. I’m an admirer because we’ve lived with this problem for so long, I’d given up on an elegant solution ever emerging.
When you install the LibLog package, a source file is embedded directly into your project. There’s no third-party assembly or runtime dependency at all. The source file defines an ILog
that is internal to your project: there’s nothing polluting your public API surface. The source file also includes some reflection-based code to sniff out which concrete logging library is actually in use, and some Expression
-based code generation to efficiently connect to it.
All of this happens automatically, behind the scenes, without a user of your library having to know or care how things are wired up. It’s like magic! Its the future.
LibLog is also smart: you can use message templates with it to do structured logging a-la Serilog, Microsoft.Framework.Logging, Logary 4, and maybe even a future version of NLog.
namespace ClassLibrary1
{
public class Class1
{
public static void SayHello(int number)
{
var log = LogProvider.For<Class1>();
log.InfoFormat("Hello, the number is {Number}", number);
}
}
}
Used in an application with Serilog, this event has the Number
property and its value attached. If the underlying logger doesn’t support it, the message is translated into a regular .NET format string and passes through with full fidelity.
If you want something more from your logging interface – say, you want to be able to pass an integer event type with every message to cater to some new framework – it’s just a matter of tweaking the code file that’s already part of your app. Admittedly there’s got to be a common denominator, but you get to choose what it is, and it doesn’t have to be the lowest.
What are the downsides of using LibLog? Your library gains a few kilobytes of adapter code internally that need to be carried around. The weight of the code is comparable to what an external abstraction would cost, so it’s zero-sum in reality.
Why is LibLog the future? From where we stand right now, amidst a mature ecosystem of .NET software, no one – not Microsoft, nor you, nor me – can define the one logging abstraction to rule them all. We can’t put the genie back into the bottle! We have so much code between us in libraries and applications that all of the logging libraries we have, the abstractions, adapters and other gunk, will live on practically forever. Any new abstraction, no matter how clean, will just add one more to the long list we already drag along with us.
So, how about letting your library’s users off the treadmill? LibLog for libraries, Serilog (if you like it) for apps. Couldn’t have said it better myself :-).
To get started with LibLog check out the wiki or this brief introduction from Christos Matskas.