SeamFramework.orgCommunity Documentation

Chapter 6. Logging, redesigned

6.1. JBoss Logging: The foundation
6.2. Solder Logging: Feature set
6.3. Typed loggers
6.4. Native logger API
6.5. Typed message bundles
6.6. Implementation classes
6.6.1. Generating the implementation classes
6.6.2. Including the implementation classes in Arquillian tests

Seam Solder brings a fresh perspective to the ancient art of logging. Rather than just giving you an injectable version of the same old logging APIs, Solder goes the extra mile by embracing the type-safety of CDI and eliminating brittle, boilerplate logging statements. The best part is, no matter how you decide to roll it out, you still get to keep your logging engine of choice (for the logging wars will never end!).

Before talking about Solder Logging, you need to first be introduced to JBoss Logging 3. The reason is, JBoss Logging provides the foundation on which Solder's declarative programming model for logging is built. Plus, we have to convince you that you aren't tied to JBoss AS by using it.

JBoss Logging acts as a logging bridge. If you don't add any other logging libraries to your project, it will delegate all logging calls it handles to the logging facility built into the Java platform (commonly referred to as JDK logging). That's nice, because it means your deployment headaches caused by missing logging jars are gone. And you accomplish it all through the use of the Logger type. It has the usual level-based log methods and complimentary ones that provide formatting.

Here's an example of how you obtain a logger and log a basic message:

Logger log = Logger.getLogger(Bean.class);

// log a plain text method
log.debug("I'm using JBoss Logging.");

If you want to use another logging engine, such as SLF4J or Log4J, you just have to add the native library to the deployment. Keep in mind, though, if your application server provides one of these frameworks, it will get choosen instead. On JBoss AS, JBoss Logging will prefer the JBoss LogManager because it's the built-in logging engine. (We are looking into more sophisticated runtime selection of the logging engine).

Here are the providers JBoss Logging supports (and the order in which it looks for them):

  • JBoss LogManager
  • Log4J
  • SLF4J
  • JDK logging

So you get that JBoss Logging is an abtraction. What else is it good for?

JBoss Logging has a facility for formatting log messages, using either the printf syntax or MessageFormat. This makes it possible to use positional parameters to build dynamic log messages based on contextual information.

Logger log = Logger.getLogger(Bean.class);

// log a message formatted using printf-style substitutions
log.infof("My name is %s.", "David");
// log a message formatted using MessageFormat-style substitutions
log.errorv("The license for Seam is the {0}", "APL");

The most significant and distinguishing feature of JBoss Logging is support for typed loggers. A typed logger is an interface that defines methods which serve as logging operations. When a method is invoked on one of these interfaces, the message defined in an annotation on the method is interpolated and written to the underlying logging engine.

Here's an example of a typed logger:

import org.jboss.logging.Message;

import org.jboss.logging.LogMessage;
import org.jboss.logging.MessageLogger;
@MessageLogger
public interface CelebritySightingLog {
    @LogMessage @Message("Spotted celebrity %s!")
    void spottedCelebrity(String name);
}

JBoss Logging has is parallel support for typed message bundles, whose methods return a formatted message rather than log it. Combined, these features form the centerpiece of Solder's logging and message bundle programming model (and a foundation for additional support provided by the Seam international module). After looking at the samples provided so far, don't pull out your IDE just yet. We'll get into the details of typed loggers and how to use them in Solder in a later section.

There you have it! JBoss Logging is a low-level API that provides logging abstraction, message formatting and internationalization, and typed loggers. But it doesn't tie you to JBoss AS!

With that understanding, we'll now move on to what Solder does to turn this foundation into a programming model and how to use it in your CDI-based application.

Solder builds on JBoss Logging 3 to provide the following feature set:

Without further discussion, let's get into it.

To define a typed logger, first create an interface, annotated it, then add methods that will act as log operations and configure the message it will print using another annotation:

import org.jboss.seam.solder.messages.Message;

import org.jboss.seam.solder.logging.Log;
import org.jboss.seam.solder.logging.MessageLogger;
@MessageLogger
public interface TrainSpotterLog {
    @Log @Message("Spotted %s diesel trains")
    void dieselTrainsSpotted(int number);
}

We have configured the log messages to use printf-style interpolations of parameters (%s).

You can then inject the typed logger with no further configuration necessary. We use another optional annotation to set the category of the logger to "trains" at the injection point, overriding the default category of the fully-qualified class name of the component receiving the injection:

    @Inject @Category("trains")

    private TrainSpotterLog log;

We log a message by simply invoking a method of the typed logger interface:

   log.dieselTrainsSpotted(7);

The default locale will be used unless overridden. Here we configure the logger to use the UK locale:

    @Inject @Category("trains") @Locale("en_GB")

    private TrainSpotterLog log;

You can also log exceptions.

import org.jboss.seam.solder.messages.Message;

import org.jboss.seam.solder.messages.Cause;
import org.jboss.seam.solder.logging.Log;
import org.jboss.seam.solder.logging.MessageLogger;
@MessageLogger
public interface TrainSpotterLog {
    @Log @Message("Failed to spot train %s")
    void missedTrain(String trainNumber, @Cause Exception exception);
}

You can then log a message with an exception as follows:

try {

    ...
} catch (Exception e) {
    log.missedTrain("RH1", e);
}

The stacktrace of the exception parameter will be written to the log along with the message.

Typed loggers also provide internationalization support. Simply add the @MessageBundle annotation to the logger interface.

If injecting a typed logger seems to "enterprisy" to you, or you need to get a reference to it from outside of CDI, you can use a static accessor method on Logger:

TrainSpotterLog log = Logger.getMessageLogger(TrainSpotterLog.class, "trains");

log.dieselTrainsSpotted(7);

The injected version is a convenience for those who prefer the declarative style of programming. If you are looking for a simpler starting point, you can simply use the Logger directly.

You can also inject a "plain old" Logger (from the JBoss Logging API):

import javax.inject.Inject;


import org.jboss.logging.Logger;
public class LogService {
    @Inject
    private Logger log;
    public void logMessage() {
        log.info("Hey sysadmins!");
    }
}

Log messages created from this Logger will have a category (logger name) equal to the fully-qualified class name of the bean implementation class. You can specify a category explicitly using an annotation.

    @Inject @Category("billing")

    private Logger log;

You can also specify a category using a reference to a type:

    @Inject @TypedCategory(BillingService.class)

    private Logger log;

Often times you need to access a localized message. For example, you need to localize an exception message. Seam Solder let's you retrieve this message from a typed message logger to avoid having to use hard-coded string messages.

To define a typed message bundle, first create an interface, annotated it, then add methods that will act as message retrievers and configure the message to produce using another annotation:

import org.jboss.seam.solder.messages.Message;

import org.jboss.seam.solder.messages.MessageBundle;
@MessageBundle
public interface TrainMessages {
    @Message("No trains spotted due to %s")
    String noTrainsSpotted(String cause);
}

Inject it:

    @Inject @MessageBundle

    private TrainMessages messages;

And use it:

   throw new BadDayException(messages.noTrainsSpotted("leaves on the line"));

You may have noticed that throughout this chapter, we've only defined interfaces. Yet, we are injecting and invoking them as though they are concrete classes. So where's the implementation?

Good news. The typed logger and message bundle implementations are generated automatically! You'll see this strategy used often in Seam 3. It's declarative programming at its finest (or to an extreme, depending on how you look at it). Either way, it saves you from a whole bunch of typing.

So how are they generated? Let's find out!

The first time you need logging in your application, you'll likely start with the more casual approach of using the Logger API directly. There's no harm in that, but it's certainly cleaner to use the typed loggers, and at the same time leverage the parallel benefits of the typed bundles. So we recommend that as your long term strategy.

Once you are ready to move to the the typed loggers and message bundles, you'll need to generate the concrete implementation classes as part of the build. These classes are generated by using an annotation processor that is provided by Solder and based on the JBoss Logging tools project. Don't worry, setting it up is a lot simpler than it sounds. You just need to do these two simple steps:

  • Set the Java compliance to 1.6 (or better)
  • Add the Solder tooling library to the build classpath

Warning

If you forget to add the annotation processor to your build, you'll get an error when you deploy the application that reports: "Invalid bundle interface (implementation not found)". This error occurs because the concrete implementation classes are missing.

Setting the Java compliance to 1.6 enables any annotation processors on the classpath to be activated during compilation.

If you're using Maven, here's how the configuration in your POM file looks:


<dependencies>
    <!-- Annotation processor for generating typed logger and message bundle classes -->
    <dependency>
        <groupId>org.jboss.seam.solder</groupId>
        <artifactId>seam-solder-tooling</artifactId>
        <scope>provided</scope>
        <optional>true</optional>
    </dependency>
    ...
</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>1.6</source>
                <target>1.6</target>
            </configuration>
        </plugin>
    </plugins>
</build>

Note

In the future, you can expect IDE plugins like JBoss Tools to setup this configuration automatically.

Here are the classes that will be generated for the examples above:

TrainSpotterLog_$logger.java
TrainSpotterLog_$logger_en_GB.java
TrainMessages_$bundle.java

Classes are generated for each language referenced by an annotation or if there is a .i18n.properties language file in the same package as the interface and has the same root name. For instance, if we wanted to generate a French version of TrainMessages, we would have to create the following properties file in the same package as the interface:

TrainMessages_fr.i18n.properties

Then populate it with the translations (Note the property key is the method name):

noTrainsSpotted=pas de trains repéré en raison de %s

Now the annotation processor will generate the following class:

TrainMessages_$bundle_fr.java

Now you can add typed loggers and message bundles at will (and you won't have to worry about unsatisified dependencies).