Andrei Petrenko
Andrei Petrenko

Reputation: 3950

Separate error logging in Log4j 2

Log4j 2 allows class and line information to be included into log entries. However, authors claim that it takes 4-5 times more time to create the entry.

99.5% of my log consists of normal operation entries (info) where line and class aren't really necessary. I wonder if there's any way to configure log4j 2 that it only includes file, class and line into entries with 'warn' or higher level?

Upvotes: 1

Views: 3485

Answers (2)

Augustus Kling
Augustus Kling

Reputation: 3333

This does not seem to be supported natively. You can however use an own filter implementation and 2 appenders to archive the desired outcome.

Each appender only outputs a range of levels and the appender for the severer events can add the line numbers. The logger needs to use both appenders to save you from handling the distinction in your code, that is logging looks like:

// Will have line numbers in output.
LogManager.getLogger().error("bla");
// Won't have line numbers in output.
LogManager.getLogger().info("blub");

The Log4j 2 configuration looks similar to (note %line for ConsoleMoreSevere):

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" packages="yield.log4j2">
    <Appenders>
        <Console name="ConsoleLessSevere" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{ISO8601} [%t] %-5level %logger{36} - %msg%n" />
            <LevelRangeFilter maxLevel="INFO"/>
        </Console>
        <Console name="ConsoleMoreSevere" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{ISO8601} [%t] %-5level %logger{36}:%line - %msg%n" />
            <LevelRangeFilter minLevel="WARN"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="WARN">
            <AppenderRef ref="ConsoleLessSevere" />
            <AppenderRef ref="ConsoleMoreSevere" />
        </Root>
    </Loggers>
</Configuration>

This leaves the need to define the custom filter implementation, called yield.log4j2.LevelRangeFilter in my example. The implementation is modeled after the example in Log4j 2's documentation and can be found on from https://github.com/AugustusKling/yield/blob/master/src/main/java/yield/log4j2/LevelRangeFilter.java. Feel free to copy the class.

To make this answer complete without external sources, the implementation of LevelRangeFilter follows:

package yield.log4j2;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.filter.AbstractFilter;
import org.apache.logging.log4j.message.Message;

/**
 * Filters log events within given bounds.
 */
@Plugin(name = "LevelRangeFilter", category = "Core", elementType = "filter", printObject = true)
public class LevelRangeFilter extends AbstractFilter {
    private final Level minLevel;
    private final Level maxLevel;

    private LevelRangeFilter(final Level minLevel, final Level maxLevel,
            final Result onMatch, final Result onMismatch) {
        super(onMatch, onMismatch);
        this.minLevel = minLevel;
        this.maxLevel = maxLevel;
    }

    @Override
    public Result filter(final Logger logger, final Level level,
            final Marker marker, final String msg, final Object... params) {
        return filter(level);
    }

    @Override
    public Result filter(final Logger logger, final Level level,
            final Marker marker, final Object msg, final Throwable t) {
        return filter(level);
    }

    @Override
    public Result filter(final Logger logger, final Level level,
            final Marker marker, final Message msg, final Throwable t) {
        return filter(level);
    }

    @Override
    public Result filter(final LogEvent event) {
        return filter(event.getLevel());
    }

    private Result filter(final Level level) {
        if (maxLevel.intLevel() <= level.intLevel()
                && minLevel.intLevel() >= level.intLevel()) {
            return onMatch;
        } else {
            return onMismatch;
        }
    }

    /**
     * @param minLevel
     *            Minimum log Level.
     * @param maxLevel
     *            Maximum log level.
     * @param onMatch
     *            Action to take on a match.
     * @param onMismatch
     *            Action to take on a mismatch.
     * @return The created filter.
     */
    @PluginFactory
    public static LevelRangeFilter createFilter(
            @PluginAttribute(value = "minLevel", defaultString = "TRACE") final Level minLevel,
            @PluginAttribute(value = "maxLevel", defaultString = "FATAL") final Level maxLevel,
            @PluginAttribute(value = "onMatch", defaultString = "NEUTRAL") final Result onMatch,
            @PluginAttribute(value = "onMismatch", defaultString = "DENY") final Result onMismatch) {
        return new LevelRangeFilter(minLevel, maxLevel, onMatch, onMismatch);
    }
}

Upvotes: 5

Remko Popma
Remko Popma

Reputation: 36754

As of RC2, Log4j2 does not provide such a feature. You could raise a feature request on the log4j2 Jira issue tracker for this.

Meanwhile, the control you have over whether to include location information or not is on a per-logger basis. So one idea could be to have a single system-wide FATAL-level logger that is configured with includeLocation="true".

Example config snippet:

  ...
  <Loggers>
    <AsyncLogger name="FATAL_LOGGER" level="fatal" includeLocation="true" additivity="false">
      <AppenderRef ref="someAppender"/>
    </AsyncLogger>
    <Root level="trace" includeLocation="false">
      <AppenderRef ref="someAppender"/>
    </Root>
  </Loggers>
</Configuration>

Example usage:

public class MyApp {
  private static final Logger logger = LogManager.getLogger(MyApp.class);

  public void someMethod(int value) {
    // normal trace-level logging does not include location info
    logger.trace("Doing some work with param {}", value);
    try {
      callAnotherMethod();
    } catch (Throwable t) {
      // use the shared fatal logger to include location info
      LogManager.getLogger("FATAL_LOGGER").fatal("Unexpected error", t);
    }
  }
}

Upvotes: 2

Related Questions