Daniel Cazzulino's Blog

We all need some kind of tracing or logging in our apps. We’d also like third party components to provide useful logging too. And if it integrates with whatever logging framework we happen to use, even better!

There’s a challenge though: we’d all have to agree on using a certain logging framework up-front. Or we could all agree on a common API (much like Common Service Locator did for picking DI containers) and provide specific adapters. The former is impossible, so it’s got to be the latter

There are some efforts in the area, most notably Common.Logging which has quite a following according to the nuget download numbers. So I set to investigate how thin the abstraction was: 28 public types, yuck. Doesn’t look much like a thin wrapper over specific frameworks . One problem I noticed right away is that it already provides a bunch of abstractions to write logger implementations, reading configuration, etc., which I believe should be totally out of scope of such an abstraction.

The abstraction should be about the consuming side, not the bootstrapping/authoring side. Just like common service locator doesn’t dictate how you configure a container, how to initialize or extend it in any way, neither should a logging abstraction: just the API for consumers, nothing more.

I looked across log4net, NLog and EntLib logging, and at the core, they are about just two operations: log a message (with a formatted overload), or log an exception alongside a message (also with a formatted overload). Then they all provide a gazillion overloads in their main logger interface/class for all the permutations of those four by each of the supported severity (or log type): Critical, Error, Warning, Information and Verbose (also called Debug).

That’s IT. There’s nothing more an abstraction for consuming code needs. Here’s all we need:

The BCL already has TraceEventType which can be used to determine the type of entry to create, although some logging library might not support all of the values in the enumeration (although I would love it if they do, because the activity tracing ones are really really useful if leveraged properly, but that’s another post . Of course some usability overloads to do a direct tracer.Warn(….) are nice, but those can be easily placed in a static class as extension methods, allowing us to keep the main interface clean:

This static class would need a static Initialize method where you’d pass the implementation that retrieves the actual tracer implementations for each specific logging framework. The bridge there could be something like an ITracerManager, with the same API as the static facade:

interface ITracerManager
{
ITracer Get(string name);
}

That’s all. A general-purpose logging abstraction does not need anything more than this.

Now, given that this is SUCH a small abstraction, does it justify being a full-blown assembly? I don’t think so, and in the spirit of NETFx, I’ve made it a source-only nuget package. So you can, right now, go and add this package to your own “Common” project, or “Core.Interfaces” that is shared by all of your app components:

Install-Package Tracer

Next, just start using it by retrieving (ideally statically) a tracer for use in your components:

18 Comments

Great initiative, but I believe it must be an assembly. How else could 3rd party libraries use it for logging? How would 3rd party loggers implement it? It dictates a common assembly for all to consume.

It does not.
That will depend on the underlying bootstrapping, not each assembly that consumes it.

See, the adapter forwards to the underlying library, and at that point, the library itself (i.e. Log4net) is configured ONLY from the bootstrapper, so nobody actually needs that.

The way I solved this for the System.Diagnostics-based one, is that TraceSource instances are cached in the AppDomain.SetData/GetData store, meaning that all libraries will share those regardless, and the adapter basically pulls the actual sources from there too. This completely decouples underlying implementation from consumers.

I believe you’ve misunderstood Common.Logging. It’s not a bootstrap code for logger implementations. It is a configuration for wrapper itself (i mean deciding what to wrap). And it could be very useful. For example, if one’s writing a library and only references Common.Logging assembly, he could configure test assembly to redirect logs to System.Diagnostics infrastructure. And, at the same time, user of the library could configure logging to go to log4net.

And that’s the point: I don’t believe abstraction over configuration is useful. Each logging library decides how to do that. And you can see that you don’t need much abstraction to provide a one-liner initialization of the implementation like I’ve done here. No abstractions at all beyond just getting a tracer.

I found Common.Logging too big for what it’s supposed to do. Tracer shows it can be done in a much lighter way, IMHO.

It works really well, but it seems like Tracer and, say, Tracer.NLog are both intended to be added to the same project?
I thought the point of something like Tracer was so that my framework project could be logging framework-agnostic, with the implementation chosen by a separate, consuming project.

I got it working easily with this separation just by making some classes/methods public, but am I missing the point?

Yes, you’re missing the point. Just like your business logic can live (and typically SHOULD) in a separate assembly from (say) the webapp or console app or windows service or whatever, that hosts it, you DON’T need a reference to the implementation from there.

Only the bootstrapping/hosting/startup app needs to initialize the concrete implementation, just like you would with a DI container.

Like @mo above, I don’t understand how Tracer is usable across different, assemblies/projects.

Let’s assume DEP is some dependency DLL/project used by some project MAIN. DEP installs Tracer:Interfaces nuget. The MAIN project installs, say, Tracer:NLog (and I assume also Tracer:Interfaces which provides the interfaces required by the nlog TraceManager?).

At this point, calling Trace.Initialize() within MAIN modifies MAIN’s Tracer.manager, and not DEP’s. To make this work I’d have to manually make DEP’s Tracer class public, and make an explicit call to DEP.Tracer.Initialize() in MAIN’s code, right?

I’m sure I misunderstood something fundamental here, any help would be appreciated…

The source code for the interfaces would need to live in an assembly that both the logic and the bootstrapper share, which is typically already the case (i.e. your “Common.dll” or whatever where you keep interfaces shared by all your projects.

So what you’re saying is that both the bootstrapper and the logic need to live in the same assembly, right? Isn’t this model unsuitable for *3rd-party* logging abstraction (as opposed to abstraction solely within my own project)?

To explain better, here’s my situation at the moment. I’m a library developer (Npgsql), and I’d like to provide my users tracing/logging. I don’t want to force them to use a specific implementation (e.g. NLog), so I’m looking for an abstraction. But my users would be doing the bootstrapping, obviously in their own, separate project – meaning I can’t use your Tracer for this, right?

I think the scenario above (3rd-party library logging abstraction) is mainly what Commons.Logging was created to solved, and not a way to abstract logging solely within my own project – this is probably the source for some of the confusion in the comments above.

Unfortunately this doesn’t seem fixable with a lightweight NetFX approach, since the bootstrapper needs to inject a TraceManager implementation into the library, which requires the implementation to implement an interface defined *inside* that library… Unless I’m mistaken a reflection-based approach would be required…