http://blogs.clariusconsulting.net/kzu

Daniel Cazzulino's Blog

Go Back to
kzu′s Latest post

Tracer: the unified, dead-simple API for all logging frameworks in existence

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:

interface ITracer
{
    void Trace(TraceEventType type, object message);
    void Trace(TraceEventType type, string format, params object[] args);
    void Trace(TraceEventType type, Exception exception, object message);
    void Trace(TraceEventType type, Exception exception, string format, params object[] args);
}


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:

public static void Error(this ITracer tracer, object message)
{
    tracer.Trace(TraceEventType.Error, message);
}

Note that the message parameter is an object. This allows each implementation to support their own fancy formatting/rendering of arbitrary objects.

We’ll also need a way to retrieve tracers for use in our app, and that’s another very small piece we need for the abstraction to work:

static class Tracer
{
    public static ITracer Get(string name) { ... }
    // Convenience...
    public static ITracer Get<T>() { ... }
    public static ITracer Get(Type type) { ... }
}


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:

public class MyComponent
{
    private static readonly ITracer tracer = Tracer.Get<MyComponent>();

    public void DoSomething()
    {
        tracer.Info("Doing something...");
        ...
    }
}


Then your bootstrapper/application startup code, can take a dependency on a specific implementation of the tracer, and initialize it appropriately. Choose your option of System.Diagnostisc, log4net, NLog or Enterprise Library:

Install-Package Tracer.SystemDiagnostics
Install-Package Tracer.log4net
Install-Package Tracer.NLog
Install-Package Tracer.EntLib


and perform  the one-liner initialization:

Tracer.Initialize(new TracerManager());

Done!

 

The source is BSD-licensed, so you can do whatever you want with it.

 

Happy tracing/logging!

Comments

18 Comments

  1. 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.

    • Oh, btw, anyone could make it a DLL :) . Should I also publish a nuget which is not source?

  2. Hey, that is nice, good job. It solves the problem nicely where you want to be able to trace inside your own application/code base.

  3. Amazing simple.
    Why is tracer.IsDebuggerEnabled property and Tracer.GetCurrentClassTracer() method is missing here?

    • What would you use IsDebuggerEnabled for?

      The GetCurrentClassTracer is what Tracer.Get() does :) . Just pass your own class as the T.

  4. The most useful logging I’ve encountered in an application allowed specifying “classesToLog”.

    This would require 1 more argument to Trace, a string className. This enables the logger to filter what log messages are actually logged by this className if it so desires.

    This enables you to put logging everywhere w/o worrying about logging too much since it can be filtered out except when it is actually desired.

  5. 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.

  6. 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.

  7. FYI it seems like the latest ELAMH adapter nugget package is empty (no class). I had the use a previous version…is this just for me or general issue?

  8. I meant the Enterprise Library adapter…

  9. 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.

  10. Thanks for answering.

    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…

  11. I dug a little deeper into the problem and didn’t find a very satisfactory result, if anyone interested: http://www.roji.org/logging-from-net-library/