How to make Logback log a blank line, without including the pattern string?

13,611

Solution 1

I've played around with this some more, and I've come up with an alternative method of achieving the effect I want. Now, this solution involved writing custom Java code, which means it wouldn't actually help with MY specific situation (since, as I said above, I need a configuration-only solution). However, I figured I may as well post it, because (a) it may help others with the same issue, and (b) it seems like it'd be useful in many other use-cases besides just adding blank lines.

Anyway, my solution was to write my own Converter class, named ConditionalCompositeConverter, which is used to express a general-purpose "if-then" logic within the encoder/layout pattern (e.g. "only show X if Y is true"). Like the %replace conversion word, it extends CompositeConverter (and therefore may contain child converters); it also requires one or more Evaluators, which supply the condition(s) to test. The source code is as follows:

ConditionalCompositeConverter.java

package converter;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.boolex.EvaluationException;
import ch.qos.logback.core.boolex.EventEvaluator;
import ch.qos.logback.core.pattern.CompositeConverter;
import ch.qos.logback.core.status.ErrorStatus;

public class ConditionalCompositeConverter extends CompositeConverter<ILoggingEvent>
{
    private List<EventEvaluator<ILoggingEvent>> evaluatorList = null;
    private int errorCount = 0;

    @Override
    @SuppressWarnings("unchecked")
    public void start()
    {
        final List<String> optionList = getOptionList();
        final Map<?, ?> evaluatorMap = (Map<?, ?>) getContext().getObject(CoreConstants.EVALUATOR_MAP);

        for (String evaluatorStr : optionList)
        {
            EventEvaluator<ILoggingEvent> ee = (EventEvaluator<ILoggingEvent>) evaluatorMap.get(evaluatorStr);
            if (ee != null)
            {
                addEvaluator(ee);
            }
        }

        if ((evaluatorList == null) || (evaluatorList.isEmpty()))
        {
            addError("At least one evaluator is expected, whereas you have declared none.");
            return;
        }

        super.start();
    }

    @Override
    public String convert(ILoggingEvent event)
    {
        boolean evalResult = true;
        for (EventEvaluator<ILoggingEvent> ee : evaluatorList)
        {
            try
            {
                if (!ee.evaluate(event))
                {
                    evalResult = false;
                    break;
                }
            }
            catch (EvaluationException eex)
            {
                evalResult = false;

                errorCount++;
                if (errorCount < CoreConstants.MAX_ERROR_COUNT)
                {
                    addError("Exception thrown for evaluator named [" + ee.getName() + "].", eex);
                }
                else if (errorCount == CoreConstants.MAX_ERROR_COUNT)
                {
                    ErrorStatus errorStatus = new ErrorStatus(
                          "Exception thrown for evaluator named [" + ee.getName() + "].",
                          this, eex);
                    errorStatus.add(new ErrorStatus(
                          "This was the last warning about this evaluator's errors. " +
                          "We don't want the StatusManager to get flooded.", this));
                    addStatus(errorStatus);
                }
            }
        }

        if (evalResult)
        {
            return super.convert(event);
        }
        else
        {
            return CoreConstants.EMPTY_STRING;
        }
    }

    @Override
    protected String transform(ILoggingEvent event, String in)
    {
        return in;
    }

    private void addEvaluator(EventEvaluator<ILoggingEvent> ee)
    {
        if (evaluatorList == null)
        {
            evaluatorList = new ArrayList<EventEvaluator<ILoggingEvent>>();
        }
        evaluatorList.add(ee);
    }
}

I then use this converter in my configuration file, like so:

logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <conversionRule conversionWord="onlyShowIf"
                    converterClass="converter.ConditionalCompositeConverter" />

    <evaluator name="NOT_EMPTY_EVAL">
        <expression>!message.isEmpty()</expression>
    </evaluator>

    <!-- STDOUT (System.out) appender for messages with level "INFO" and below. -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
            <evaluator class="ch.qos.logback.classic.boolex.JaninoEventEvaluator">
                <expression>return level &lt;= INFO;</expression>
            </evaluator>
            <OnMatch>NEUTRAL</OnMatch>
            <OnMismatch>DENY</OnMismatch>
        </filter>
        <encoder>
            <pattern>%onlyShowIf(%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg){NOT_EMPTY_EVAL}%n</pattern>
        </encoder>
        <target>System.out</target>
    </appender>

    <!-- STDERR (System.err) appender for messages with level "WARN" and above. -->
    <appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>WARN</level>
        </filter>
        <encoder>
            <pattern>%onlyShowIf(%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg){NOT_EMPTY_EVAL}%n</pattern>
        </encoder>
        <target>System.err</target>
    </appender>

    <!-- Root logger. -->
    <root level="DEBUG">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="STDERR" />
    </root>

</configuration>

I think this is much more elegant than the previous solution, as it lets me use a single Appender to handle both blank and non-blank messages. The %onlyShowIf conversion word tells the Appender to parse the supplied pattern as usual, UNLESS the message is blank, in which case skip the whole thing. Then there's a newline token after the end of the conversion word, to ensure that a linebreak is printed whether the message is blank or not.

The only downside to this solution is that the main pattern (containing child converters) must be passed in FIRST, as arguments within parentheses, whereas the Evaluator(s) must be passed in at the end, via the option list within curly-braces; this means that this "if-then" construct must have the "then" part BEFORE the "if" part, which looks somewhat unintuitive.

Anyway, I hope this proves helpful to anyone with similar issues. I'm not going to "accept" this answer, as I'm still hoping someone will come up with a configuration-only solution that would work in my specific case.

Solution 2

The token which makes the logback separate in new lines is %n. What you can do instead of using one %n in the end of pattern, you use twice %n%n like the example below.

<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n%n</pattern>

I tried here and it worked fine.

Share:
13,611

Related videos on Youtube

MegaJar
Author by

MegaJar

Updated on September 21, 2022

Comments

  • MegaJar
    MegaJar about 1 year

    I have a Java application that's set up to use SLF4J/Logback. I can't seem to find a simple way to make Logback output a completely blank line between two other log entries. The blank line should not include the encoder's pattern; it should just be BLANK. I've searched all over the Web for a simple way to do this, but came up empty.

    I have the following setup:

    logback.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
    
        <!-- STDOUT (System.out) appender for messages with level "INFO" and below. -->
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
                <evaluator class="ch.qos.logback.classic.boolex.JaninoEventEvaluator">
                    <expression>return level &lt;= INFO;</expression>
                </evaluator>
                <OnMatch>NEUTRAL</OnMatch>
                <OnMismatch>DENY</OnMismatch>
            </filter>
            <encoder>
                <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            </encoder>
            <target>System.out</target>
        </appender>
    
        <!-- STDERR (System.err) appender for messages with level "WARN" and above. -->
        <appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
            <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
                <level>WARN</level>
            </filter>
            <encoder>
                <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            </encoder>
            <target>System.err</target>
        </appender>
    
        <!-- Root logger. -->
        <root level="DEBUG">
            <appender-ref ref="STDOUT" />
            <appender-ref ref="STDERR" />
        </root>
    
    </configuration>
    

    LogbackMain.java (test code)

    package pkg;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class LogbackMain
    {
        private static final Logger log = LoggerFactory.getLogger(LogbackMain.class);
    
        public LogbackMain()
        {
            log.info("Message A: Single line message.");
            log.info("Message B: The message after this one will be empty.");
            log.info("");
            log.info("Message C: The message before this one was empty.");
            log.info("\nMessage D: Message with a linebreak at the beginning.");
            log.info("Message E: Message with a linebreak at the end.\n");
            log.info("Message F: Message with\na linebreak in the middle.");
        }
    
        /**
         * @param args
         */
        public static void main(String[] args)
        {
            new LogbackMain();
        }
    }
    

    This produces the following output:

    16:36:14.152 [main] INFO  pkg.LogbackMain - Message A: Single line message.
    16:36:14.152 [main] INFO  pkg.LogbackMain - Message B: The message after this one will be empty.
    16:36:14.152 [main] INFO  pkg.LogbackMain - 
    16:36:14.152 [main] INFO  pkg.LogbackMain - Message C: The message before this one was empty.
    16:36:14.152 [main] INFO  pkg.LogbackMain - 
    Message D: Message with a linebreak at the beginning.
    16:36:14.152 [main] INFO  pkg.LogbackMain - Message E: Message with a linebreak at the end.
    
    16:36:14.152 [main] INFO  pkg.LogbackMain - Message F: Message with
    a linebreak in the middle.
    

    As you can see, none of these logging statements work the way I need.

    • If I just log an empty string, the encoder's pattern is still prepended to the message, even though the message is empty.
    • If I embed a newline character at the beginning or in the middle of the string, everything after that will be missing the pattern prefix, because the pattern is only applied once, at the beginning of the message.
    • If I embed a newline character at the end of the string, it does create the desired blank line, but this is still only a partial solution; it still doesn't allow me to output a blank line BEFORE a logged message.

    After much experimentation with Evaluators, Markers, etc, I finally arrived at a solution that, while quite unwieldy, has the desired effect. It's a two-step solution:

    1. Modify the filters in each existing Appender, so that they only allow non-empty messages.
    2. Create a duplicate of each Appender; modify the duplicates so that their filters only allow empty messages, and their patterns only contain a newline token.

    The resulting file looks like this:

    logback.xml (modified)

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
    
        <!-- STDOUT (System.out) appender for non-empty messages with level "INFO" and below. -->
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
                <evaluator class="ch.qos.logback.classic.boolex.JaninoEventEvaluator">
                    <expression>return !message.isEmpty() &amp;&amp; level &lt;= INFO;</expression>
                </evaluator>
                <OnMatch>NEUTRAL</OnMatch>
                <OnMismatch>DENY</OnMismatch>
            </filter>
            <encoder>
                <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            </encoder>
            <target>System.out</target>
        </appender>
    
        <!-- STDOUT (System.out) appender for empty messages with level "INFO" and below. -->
        <appender name="STDOUT_EMPTY" class="ch.qos.logback.core.ConsoleAppender">
            <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
                <evaluator class="ch.qos.logback.classic.boolex.JaninoEventEvaluator">
                    <expression>return message.isEmpty() &amp;&amp; level &lt;= INFO;</expression>
                </evaluator>
                <OnMatch>NEUTRAL</OnMatch>
                <OnMismatch>DENY</OnMismatch>
            </filter>
            <encoder>
                <pattern>%n</pattern>
            </encoder>
            <target>System.out</target>
        </appender>
    
        <!-- STDERR (System.err) appender for non-empty messages with level "WARN" and above. -->
        <appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
            <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
                <evaluator class="ch.qos.logback.classic.boolex.JaninoEventEvaluator">
                    <expression>return !message.isEmpty() &amp;&amp; level &gt;= WARN;</expression>
                </evaluator>
                <OnMatch>NEUTRAL</OnMatch>
                <OnMismatch>DENY</OnMismatch>
            </filter>
            <encoder>
                <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            </encoder>
            <target>System.err</target>
        </appender>
    
        <!-- STDERR (System.err) appender for empty messages with level "WARN" and above. -->
        <appender name="STDERR_EMPTY" class="ch.qos.logback.core.ConsoleAppender">
            <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
                <evaluator class="ch.qos.logback.classic.boolex.JaninoEventEvaluator">
                    <expression>return message.isEmpty() &amp;&amp; level &gt;= WARN;</expression>
                </evaluator>
                <OnMatch>NEUTRAL</OnMatch>
                <OnMismatch>DENY</OnMismatch>
            </filter>
            <encoder>
                <pattern>%n</pattern>
            </encoder>
            <target>System.err</target>
        </appender>
    
        <!-- Root logger. -->
        <root level="DEBUG">
            <appender-ref ref="STDOUT" />
            <appender-ref ref="STDOUT_EMPTY" />
            <appender-ref ref="STDERR" />
            <appender-ref ref="STDERR_EMPTY" />
        </root>
    
    </configuration>
    

    With this setup, my previous test code produces the following output:

    17:00:37.188 [main] INFO  pkg.LogbackMain - Message A: Single line message.
    17:00:37.188 [main] INFO  pkg.LogbackMain - Message B: The message after this one will be empty.
    
    17:00:37.203 [main] INFO  pkg.LogbackMain - Message C: The message before this one was empty.
    17:00:37.203 [main] INFO  pkg.LogbackMain - 
    Message D: Message with a linebreak at the beginning.
    17:00:37.203 [main] INFO  pkg.LogbackMain - Message E: Message with a linebreak at the end.
    
    17:00:37.203 [main] INFO  pkg.LogbackMain - Message F: Message with
    a linebreak in the middle.
    

    Notice that the logging statement with an empty message now creates a blank line, as desired. So this solution works. However, as I said above, it's quite unwieldy to have to create a duplicate of every Appender, and it's certainly not very scalable. Not to mention, it seems like major overkill to do all this work to achieve such a simple result.

    And so, I submit my problem to Stack Overflow, with the question: Is there a better way to do this?

    P.S. As a final note, a configuration-only solution would be preferable; I'd like to avoid having to write custom Java classes (Filters, Markers, etc) to get this effect, if possible. Reason being, the project I'm working on is a kind of "meta project" -- it's a program that generates OTHER programs, based on user criteria, and those generated programs are where Logback will live. So any custom Java code I write would have to be copied over to those generated programs, and I'd rather not do that if I can avoid it.


    EDIT: I think what it really boils down to is this: Is there a way to embed conditional logic into an Appender's layout pattern? In other words, to have an Appender that uses a standard layout pattern, but conditionally modify (or ignore) that pattern in certain instances? Essentially, I want to tell my Appender, "Use these filter(s) and this output target, and use this pattern IF condition X is true, otherwise use this other pattern." I know that certain conversion terms (like %caller and %exception) allow you to attach an Evaluator to them, so that the term is only displayed if the Evaluator returns true. Problem is, most terms don't support that feature, and I certainly don't know of any way to apply an Evaluator to the ENTIRE pattern at once. Hence, the need for splitting each Appender into two, each with its own separate evaluator and pattern: one for blank messages, and one for non-blank messages.