Search code examples
javamavenlog4j2open-telemetry

SLF4J2 and Log4J1 to Log4J2


I have been scratching my head over logger dependencies. I want my own code to use the SLF4J 2 API and in tests and runtime use Log4J2 as the provider. However, I have a dependency on OpenLr's binary package that uses Log4J1. Additionally, I want to setup telemetry and have my logs exported.

I found these docs:

It seemed like the correct dependencies would be:

<dependencyManagement>
        <dependencies>
            <!-- Set SLF4J 2 API version -->
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>${slf4j.version}</version>
            </dependency>
            <!-- Set Log4J 2 API version -->
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-api</artifactId>
                <version>${log4j.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
...
        <!-- Use SLF4J API so logging framework is code independent. -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <!-- Redirect dependencies using Log4j 1 to Log4j 2 instead. -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-1.2-api</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <!-- Add logging framework. -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j2-impl</artifactId>
            <version>${log4j.version}</version>
            <!-- includes log4j-api of same version -->
        </dependency>
        <!-- OpenTelemetry to produce metrics and logs -->
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-api</artifactId>
            <version>${opentelemetry.version}</version>
        </dependency>
        <!-- Dependencies needed for Geneva export with OTLP agent on docker -->
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-sdk-extension-autoconfigure</artifactId>
            <version>${opentelemetry.version}</version>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-exporter-otlp</artifactId>
            <version>${opentelemetry.version}</version>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry.instrumentation</groupId>
            <artifactId>opentelemetry-log4j-appender-2.17</artifactId>
            <version>${opentelemetry.version}-alpha</version>
            <scope>runtime</scope>
        </dependency>

In another module without opentelemetry these settings seem to work, but in the main project SLF4J can't find a binding. A teammate told me that the following (setting logging to use log4j-reload) works:

        <!-- Use SLF4J API so logging framework is code independent. -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <!-- Add logging framework. -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-reload4j</artifactId>
            <version>${slf4j.version}</version>
        </dependency>

I'm just not sure why the log4j 2 provider is not working. Is there an incompatability with OpenTelemetry? Please help me understand.


Solution

  • Eventually we started looking deeper and realized that there were other logging frameworks and other subprojects causing some problems. We made sure to set the desired versions via dependency management and ended up with the following logging bridges:

    <!-- Use SLF4J API so logging framework is code independent -->
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>${slf4j.version}</version>
                <scope>provided</scope>
            </dependency>
            <!-- Use Log4j 2 as logging provider -->
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-core</artifactId>
                <version>${log4j.version}</version>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-slf4j2-impl</artifactId>
                <version>${log4j.version}</version>
                <scope>test</scope>
                <!-- Includes log4j-api of same version -->
            </dependency>
            <!-- Redirect dependencies using Log4j 1 to Log4j 2 instead -->
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-1.2-api</artifactId>
                <version>${log4j.version}</version>
                <scope>test</scope>
            </dependency>
            <!-- Redirect dependencies using JCL to Log4j 2 instead. -->
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-jcl</artifactId>
                <version>${log4j.version}</version>
                <scope>test</scope>
            </dependency>
    

    To ensure the logging worked in local execution we override the configuration once in the initialization of an object's lazy val (singleton) like this:

        val loggerContext = LogManager.getContext(false).asInstanceOf[LoggerContext]
        // load our custom configuration
        val customLog4j2Config = new XmlConfigurationFactory().getConfiguration(
          loggerContext,
          this.getClass.getName,
          log4j2ConfigFile
        )
        customLog4j2Config.initialize()
    
        // transfer missing appenders to our config
        loggerContext.getConfiguration
          .getAppenders
          .values()
          .forEach(
            appender => {
              if (
                !customLog4j2Config
                  .getAppenders
                  .containsKey(appender.getName)
              ) {
                customLog4j2Config.addAppender(appender)
              }
            }
          )
    
        // transfer missing loggers to our config
        loggerContext.getConfiguration
          .getLoggers
          .values()
          .forEach(
            loggerConfig => {
              if (
                !customLog4j2Config
                  .getLoggers
                  .containsKey(loggerConfig.getName)
              ) {
                customLog4j2Config.addLogger(loggerConfig.getName, loggerConfig)
              }
            }
          )
    
        // update the context to use our custom config
        loggerContext.reconfigure(customLog4j2Config)
        loggerContext.updateLoggers()
      }