Is there a Logback appender that redirects to JUL (java.util.logging)?
I have a problem similar to this question. My applications are running on a Java application server (WebSphere) and use Logback, because I want to write log messages to a separate log file as an addition to the application server's default log file. This allows configuring different log levels for administrators and developers. Admins look at the app server log and developers at their separate log file.
WebSphere internally uses JUL. So, the common solution seems to be ConsoleAppender
. At first this seems to solve the issue. But unfortunately the log level gets lost and instead of a meaningful log level codes (E
, W
, I
, ...) you always get O
, as pointed out in the linked SO question above. This is because ConsoleAppender
just writes to System.out and System.err.
Everything I can find about such a JUL appender is either out dated or is about the jul-to-slf4j-bridge. The bridge is not a solution for me. As its name says it redirects JUL messages to slf4j where we have the opposite requirement - redirecting slf4j to JUL. Although you probably could solve the problem with the bridge, it'll cause other issues in an app server environment, where JUL is managed by the core.
As there seems to be no solution, we made our own. IIRC it's based on this Log4j solution.
The great thing about it is that we can even enable tracing in the WebSphere console. That means you can use Logback methods isDebugEnabled()
or isTraceEnabled()
to check whether JUL trace level is enabled in WebSphere.
The appender:
package com.example.logging;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import java.util.logging.Level;
import java.util.logging.Logger;
public class JulAppender extends AppenderBase<ILoggingEvent> {
@Override
protected void append(ILoggingEvent event) {
Logger logger = Logger.getLogger(event.getLoggerName());
if (logger == null) {
System.out.println("Cannot obtain JUL " + event.getLoggerName() + ".");
return;
}
ch.qos.logback.classic.Level logbackLevel = event.getLevel();
Level level = JulLogbackEventConverter.convertLogbackToJulLevel(logbackLevel);
logger.log(level, event.getFormattedMessage());
}
}
Convert Logback to JUL log level:
package com.example.logging;
import java.util.logging.Level;
public class JulLogbackEventConverter {
public static Level convertLogbackToJulLevel(ch.qos.logback.classic.Level level) {
if (level.equals(ch.qos.logback.classic.Level.OFF)) {
return Level.OFF;
} else if (level.equals(ch.qos.logback.classic.Level.ERROR)) {
return Level.SEVERE;
} else if (level.equals(ch.qos.logback.classic.Level.WARN)) {
return Level.WARNING;
} else if (level.equals(ch.qos.logback.classic.Level.INFO)) {
return Level.INFO;
} else if (level.equals(ch.qos.logback.classic.Level.DEBUG)) {
return Level.FINER;
} else if (level.equals(ch.qos.logback.classic.Level.TRACE)) {
return Level.FINEST;
} else if (level.equals(ch.qos.logback.classic.Level.ALL)) {
return Level.ALL;
}
return Level.FINER;
}
}
Logback turbo filter for isDebugEnabled()
etc. support
package com.example.logging;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.turbo.TurboFilter;
import ch.qos.logback.core.spi.FilterReply;
import org.slf4j.Marker;
public class JulLogbackFilter extends TurboFilter {
@Override
public FilterReply decide(Marker marker, Logger logger, Level level, String s, Object[] objects,
Throwable throwable
) {
java.util.logging.Logger julLogger = java.util.logging.Logger.getLogger(logger.getName());
java.util.logging.Level julLevel = JulLogbackEventConverter.convertLogbackToJulLevel(level);
if (julLogger.isLoggable(julLevel)) {
return FilterReply.NEUTRAL;
}
return FilterReply.DENY;
}
}
Example configuration. Our goal with this configuration was to write INFO messages in a separate log file instead of "spamming" WebSphere's SystemOut.log. Warnings and errors should appear in SystemOut.log. If trace is enabled the messages should be written to WebSphere's trace.log:
<configuration>
<turboFilter class="com.example.logging.JulLogbackFilter" />
<appender name="julAppender" class="com.example.logging.JulAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
</appender>
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/myapp/myapp-web.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/myapp/myapp-web-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>60</maxHistory>
<totalSizeCap>8GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<root level="TRACE">
<appender-ref ref="julAppender" />
<appender-ref ref="file" />
</root>
</configuration>