Tuesday, April 12, 2011

SLF4J Logging + Maven

Just as a "foreword": I assume SLF4J is used for logging, not opening discussion about this, do it elsewhere. Also, I assume you use Apache Maven 3.x for builds. These two facts are NOT about what this post is, I consider them granted and don't want to argue about these above.

This post is more about your SLF4J related artifact dependencies and their scoping. Usually, the things that make you swear (as real productive coder). I really love how slf4j project is "layered". There is not one monolithic artifact with all-or-nothing capability, but you actually have real freedom (see below) as a developer (and even as integrator) to choose actual backend and to "normalize" your logging if needed. But sadly, too many times I see this "layered" structure either unused (backend dragged along as transitive dependency) or misused in some other ways.

This is why I quickly drafted this little post, for quick reference. First the rules:

Rule #0

When you use more than one artifact of some project (SLF4J has 2-3 of them usually needed in an application), define properties for their shared version. Also, with proper use of dependency management section, you ensure you get what you want on your build's classpath. This is general advice.

It relieves you of later changes (bumping to newer version) and also clearly states what you want to expect as dependency.

Rule #1

From SLF4J project, the slf4j-api is the only and the one and only one artifact you want to depend in compile scope. Full stop.








Rule #2

From SLF4J project, the bridge ("-over-" and "-to-") artifacts should be "runtime" scoped, if needed at all. And it's only in some cases when you use another library as dependency, that relies on commons-logging for example (typical example is Apache HttpClient 3.x). In this case, you have to have commons-logging API on classpath (during tests and also runtime), and jcl-over-slf4j does exactly that. But, you don't want to compile against it -- even by mistake. Hence, the "runtime" scope. It will make it transitively dragged, but not interfere with classpath you want to code against.








Rule #3

From SLF4J project, you never ever want to include any backend artifact in a scope that is transitively propagated. Full stop.

To describe these rules above with examples, I'll try to take a look at those from different perspectives.

From SLF4J's artifacts perspective

The slf4j-api is against what your code compiles. Easy-peasy, "compile" scope is what you need.

The slf4j bridges are never used by your code -- otherwise why do you compile against slf4j-api in the first place? So, it must be some dependency. There are some strange cases when you extend some class and you do have to have it on classpath, but those are rare exceptions. Drag these only if you must (like in example above).

As a reusable library publisher, you don't want to assume what backend the project consuming your library use, right? So, do not make you library drag one.

From publishing developer role perspective

As reusable library developer, you want to ease the lives of your consumers, right? But you also love SLF4J, right? This makes your situation the simplest, just add slf4j-api as "compile" scoped dependency (that will be dragged), and use slf4j-simple in your tests, naturally with "test" scoped dependency. Done.

As application developer, you actually pull in multiple libraries, add some "glue" code and you create one or more deliverables. Here, you always want to keep your modules "clean", slf4j-api is the only dependency you want to drag over those modules. Some simple slf4j-simple backend will pop-up in your tests probable, but the "only and the real" logging backend appears only in those modules, that actually build/package/produce the final deliverable (WAR, bundle, App, etc).

From consuming developer role perspective

As library consumer, you really hate excludes, since that's the only way to fight back developers not setting scopes right. So, bummer, that's bad news. But believe me, after writing 5th exclusion in POM (IDE integrations are of great help here!), you'll start giving back patches for POMs and eagerly waiting for next dot releases consuming those same patches. Good work!

From project module hierarchy perspecticve

If we take a Mavenized build, and look at the order of modules built by Maven Reactor, we may -- and this is a very-very huge simplification of things -- that the list beings with some "API-ish" and "Util-ish" modules, next are the "Meat-ish" or "Imple-ish" modules, and the last in the chain is "Deliverable-ish" module(s).

Initial modules like APIs and Utils might drag slf4j-api as dependency, just to clearly advocate "we use SLF4J" and make API users know what is used for logging in here. Usually we do not log in those Util or API modules.

The In-the-middle-modules usually have bridge dependencies (along with the slf4j-api one) mainly because of dependencies needing some other logging API, like commons-logging, JUL or who-knows-what. That's fine.

Modules, usually last in reactor build order, are the one producing deliverables. This is where you, actually intentionally perform the decision about logging backend to use, and add proper SLF4J backend dependencies to POM.


Keeping in mind these simple rules, will make your libraries more consumer friendly, and easier to maintain!


jaxzin said...

Doesn't your example in rule #2 (runtime scope) contradict rule #3?

Tamás Cservenák said...

Rule #2 example is about bridge artifacts (those with "-to-" and "-over-") in their artifactId. They could be considered as "input sink", usually used by your dependencies at runtime, and for your artifact to properly pull it's dependencies, it's a must (otherwise your dependencies would not work, would CNFEx runtime). In short, if your library depends on ApacheHttpClient 3.x, then you do depend on JCL too (runtime).

Rule #3 is about backend artifacts (like slf4j-log4j or logback, a native backend). You don't want to drag them with library -- but you do have to have them in WARs for example. But libraries should leave that choice to the integrator.

Typical example with Nexus: Restlet dependency uses JUL, Apache HttpClient 3.x uses JCL 1.0, Jetty eagerly detects log4j and use it (but it supports slf4j too, but log4j was present on classpath for historical reason) and Nexus uses Plexus logger (again, historical reason) backed with SLF4J, and so on. Using slf4j we managed to "focus and concentrate" all those into SLF4J, and it's single backend. Otherwise, we would need to configure all these subsystems, and probably end up with multiple logfiles...