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 <= 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.
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:
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() && level <= 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() && level <= 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() && level >= 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() && level >= 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.
RollingFileAppender extends FileAppender with the capability to rollover log files. For example, RollingFileAppender can log to a file named log. txt file and, once a certain condition is met, change its logging target to another file.
Log4j has been defined as java based application with logging utility which is the java framework for logging messages to a different output, which helps enable to locate the problems. Logback is defined as the successor to log4j, which is also a java framework for logging messages in any java based applications.
The data masking in Logback is done in two steps: Define the masking patterns with the help of regular expressions in logback. xml configuration file. Define a custom Layout class that will read the masking patterns and apply those pattern regex on the log message.
GitHub - qos-ch/logback: The reliable, generic, fast and flexible logging framework for Java.
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 <= 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With