Getting Log4j2 to work in an OSGi context

The Log4j2 framework is a drastic improvement over Log4j. Rolling file appenders work well across multiple JVMs, it offers improved reliability and it has a nice plugin system that enables you to extend the framework.

Those, and the fact that Log4j will not work in Java 9, are good reasons to migrate to Log4j2.

For regular webapps, setting up Log4j2 is easy. Just drop log4j2-api.jar and log4j2-core.jar on your classpath (using your favourite build system or otherwise), put a log4j2.xml configuration on the classpath, and you’re ready to go.

The problem with Log4j2 in OSGi

In an OSGi context however, things become a little more tricky. To understand why, we need to look at how OSGi classloading works.

Log4j2 OSGi patch fix

In an OSGi system, each bundle (typically a JAR with extra information in its manifest) has its own classloader. Consider a bundle B that depends on packageA present in bundle A. For bundle B to access packageA, bundle B must explicitly declare a dependency in its own manifest:

Import-Package:  mypackageA;version="x.x.x"

Bundle A must in its turn specify that it exports packageA:
Export-Package:  mypackageA;version="x.x.x"

This allows OSGi frameworks like Apache Felix or Eclipse Equinox to provide different class versions to different client bundles. Different versions of the same class are located in separate bundles, each with their own classloader.

This presents a problem for Log4j2, however, because of the way Log4j2 configurates itself.

The first step Log4j2 takes is log4j-api instantiating a LoggerContextFactory implementation. Which one it should instantiate is specified in a file called log4j-provider.properties, located in META-INF in log4j-core.

In a regular application, META-INF is on the classpath so it can be found without any problems. But in OSGi, this file resides in the classloader of log4j-core. The META-INF directory is not exported by log4j-core so log4j-api has no way of accessing it.

This typically results in the error:

log4j2.xml not found by org.apache.logging.log4j.core
ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console.

The second problem is finding the log4j2.xml configuration file and custom plugins, like Appenders you have written yourself. As described In the Log4j2 plugin manual, log4j2 searches active OSGi bundles and tries to load plugin metadata from there.

However, log4j2 can be initialized quite early in the process, which means not all bundles are active yet.

 

The solution: use OSGi fragments!

What if there was a way to:

  1. Make the log4j-provider.properties available to the log4j-api artifact, and
  2. Make the configuration file and plugins available to the log4j-core.

That would solve our problems, right?

Thankfully, we can do this. We’re going to build two OSGi fragments.

An OSGi fragment is like an OSGi bundle (eg a JAR file with some metadata in its manifest), but doesn’t have its own Activator. Instead it is part of another OSGi bundle.

In order to specify what OSGi bundle a fragment belongs to, specify the fragment host in the fragment’s manifest:

Manifest-Version: 1.0
    Bundle-ManifestVersion: 2
    Bundle-Name: Log4j API configurator
    Bundle-SymbolicName: org.apache.logging.log4j.apiconf
    Bundle-Version: 1.0.0
    Bundle-Vendor: None
    Bundle-RequiredExecutionEnvironment: OSGi/Minimum-1.2
    Fragment-Host: org.apache.logging.log4j.api
    DynamicImport-Package: *

In this case, the fragment’s host bundle is the Log4j2 api artifact. This tells the OSGi framework to merge the contents of your fragment JAR with the Log4j2 Api OSGi bundle.

The fragment-host value references the Bundle-SymbolicName in the manifest of the log4j2-api artifact, which is org.apache.logging.log4j.api. Note that the Bundle-SymbolicName of the fragment can be anything you want it to be.

You can build an OSGi fragment with Maven, for example. A typical pom file for the example above would look like this:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>my.group</groupId>
    <artifactId>log4j2-api-config</artifactId>
    <version>1.0</version>
    <name>log4j2-api-config</name>
    <packaging>bundle</packaging>
    <properties>
        <java-version>1.7</java-version>
    </properties>

    <dependencies>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <version>2.0.0</version>
                <extensions>true</extensions>
                <configuration>
                    <instructions>
                        <Bundle-SymbolicName>org.apache.logging.log4j.apiconf</Bundle-SymbolicName>
                        <Bundle-Name>Log4j API Configurator</Bundle-Name>
                        <Bundle-Version>1.0.0</Bundle-Version>
                        <Fragment-Host>org.apache.logging.log4j.api</Fragment-Host>
                        <DynamicImport-Package>
                            *;resolution:=optional
                        </DynamicImport-Package>
                    </instructions>
                </configuration>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>.</directory>
                <includes>
                    <include>log4j-provider.properties</include>
                </includes>
                <targetPath>META-INF</targetPath>
            </resource>
        </resources>
    </build>
</project>

 

Fixing the api OSGi bundle

Now, all we need to do is to include everything we missed in the log4j2-api artifact. In this case, it’s just the log4j-provider.properties file (you can just copy it from the one in the log4j2-core.jar). So, the final fragment will look like this:

META-INF/
   MANIFEST.MF
   log4j-provider.properties

Fixing the core OSGi bundle

To register plugins, we can also make a fragment that attaches itself to the log4j2-core bundle, in the same way we did with the api bundle:

Manifest-Version: 1.0
    Bundle-ManifestVersion: 2
    Bundle-Name: Log4j API configurator
    Bundle-SymbolicName: org.apache.logging.log4j.coreconf
    Bundle-Version: 1.0.0
    Bundle-Vendor: None
    Bundle-RequiredExecutionEnvironment: OSGi/Minimum-1.2
    Fragment-Host: org.apache.logging.log4j.core
    DynamicImport-Package: *

Now, we can add a log4j2.xml configuration file and needed plugins. For example, the following plugin appender and configuration file:
@Plugin(name = "MyAppender", category = "Core", elementType = "appender", printObject = true)
public class MyAppender extends AbstractAppender {

    public MyAppender(String name, Filter filter,
            Layout<? extends Serializable> layout, boolean ignoreExceptions) {
        super(name, filter, layout);
    }

    public void append(LogEvent logEvent) {
        // do something with the logevent
    }

    @PluginFactory
    public static MyAppender createAppender(@PluginAttribute("name") String name,
            @PluginElement("Layout") Layout<? extends Serializable> layout,
            @PluginElement("Filter") final Filter filter,
            @PluginAttribute("ignoreExceptions") boolean ignoreExceptions) {
        return new MyAppender(name, filter, layout, ignoreExceptions);
    }
}

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="debug" monitorInterval="60">
    <Appenders>
        <MyAppender name="myAppender">
            <PatternLayout pattern="%c %d{ISO8601} -- %p -- %m%n"/>
        </MyAppender>
    </Appenders>
    <Loggers>
        <Root level="debug">
            <AppenderRef ref="myAppender"/>
        </Root>
    </Loggers>
</Configuration>

In order to make the plugin work, you need to generate a log4jPlugins.dat file that contains information about the plugin. The .dat file is automatically generated by the log4j plugin annotation processor when you compile the plugin with log4j2-core on the classpath.

All you need to do is to package these the files in an OSGi fragment. The final fragment looks like this:

META-INF/
   MANIFEST.MF
   org/apache/logging/log4j/core/plugins/Log4j2Plugins.dat
org/myplugins/MyAppender.class    
log4j2.xml

Now, just drop the two fragment JARs where the other OSGi bundles in your application reside. When you start your OSGi application you will see all log4j2 calls being handled as expected.

Since the log4j2 OSGi bundles have no explicit coupling with your fragments, you can leave the original bundles untouched.

Summary

Getting Log4j2 to work in an OSGi context can be a little tricky. Thankfully we can use OSGi fragments to extend the Log4j2 bundles and make things work:

  • Extend the Log4j2 api bundle with an OSGi fragment containing the log4j-provider.properties. Find a GitHub example here.
  • Extend the Log4j2 core bundle with an OSGi fragment containing the log4j2 configuration file, and optionally some custom plugins. A GitHub example can be found here.

2 thoughts on "Getting Log4j2 to work in an OSGi context"

  1. Pedro Lamarão says:

    Thanks for you post! I’ve found that log4j 2.8 no longer requires the fragment for log4j-api with the providers. It is enough to provide the fragment for log4j-core with the configuration.

  2. Amit Phadke says:

    Thanks! Took me some time, but finally got it working! Perfect solution!

    Also thanks to Pedro Lamarao for suggesting that “It is enough to provide the fragment for log4j-core with the configuration.”!!

Leave a Reply

Your email address will not be published. Required fields are marked *