如何使Logback日志成为空白行,而不包括模式字符串?

时间:2013-08-22 21:17:22

标签: java logging conditional newline logback

我有一个Java应用程序设置为使用SLF4J / Logback。我似乎无法找到一种简单的方法使Logback输出在两个其他日志条目之间完全空行。空白行不应包含编码器的图案;它应该是BLANK。我在网上搜索了一个简单的方法来做到这一点,但是空洞了。

我有以下设置:

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(测试代码)

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();
    }
}

这会产生以下输出:

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.

正如您所看到的,这些日志记录语句都不能按我需要的方式工作。

  • 如果我只是记录一个空字符串,即使消息为空,编码器的模式仍然会添加到消息中。
  • 如果我在字符串的开头或中间嵌入换行符,那么之后的所有内容都将缺少模式前缀,因为该模式仅在消息开头应用一次。
  • 如果我在字符串的末尾嵌入换行符,它会创建所需的空行,但这仍然只是部分解决方案;它仍然不允许我在记录消息之前输出空行。

经过对Evaluators,Markers等的大量实验,我终于找到了一个解决方案,虽然相当笨拙,却具有预期的效果。这是一个两步解决方案:

  1. 修改每个现有Appender中的过滤器,以便它们只允许非空消息。
  2. 创建每个Appender的副本;修改重复项,使其过滤器只允许空消息,并且它们的模式只包含换行符号。
  3. 生成的文件如下所示:

    logback.xml(已修改)

    <?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>
    

    使用此设置,我之前的测试代码会生成以下输出:

    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.
    

    请注意,带有空消息的日志记录语句现在会根据需要创建一个空行。所以此解决方案有效。但是,正如我上面所说的,必须创建每个Appender的副本是非常笨拙的,而且它当然不是很可扩展。更不用说,为了达到如此简单的结果,完成所有这些工作似乎是一种重大的过度杀伤。

    所以,我将问题提交给Stack Overflow,问题是:有更好的方法吗?

    P.S。最后要说明的是,仅配置解决方案更可取;如果可能的话,我想避免编写自定义Java类(过滤器,标记等)以获得此效果。原因是,我正在研究的项目是一种“元项目” - 它是一个根据用户标准生成OTHER程序的程序,而那些生成的程序是Logback将存在的地方。因此,我编写的任何自定义Java代码都必须复制到那些生成的程序中,如果可以避免,我宁愿不这样做。


    编辑:我认为它真正归结为:有没有办法将条件逻辑嵌入到Appender的布局模式中?换句话说,要使用一个使用标准布局模式的Appender,但在某些情况下有条件地修改(或忽略)该模式?基本上,我想告诉我的Appender,“使用这些过滤器和此输出目标,并使用此模式IF条件X为真,否则使用此其他模式。”我知道某些转化术语(例如%caller%exception)允许您将评估者附加到它们,因此只有在评估者返回true时才会显示该术语。问题是,大多数术语不支持该功能,我当然不知道有任何方法可以立即将评估器应用于整个模式。因此,需要将每个Appender分成两个,每个Appender都有自己独立的评估器和模式:一个用于空白消息,一个用于非空消息。

3 个答案:

答案 0 :(得分:6)

我已经玩过这个了,我已经想出了另一种方法来实现我想要的效果。现在,这个解决方案涉及编写自定义Java代码,这意味着它实际上对我的具体情况没有帮助(因为,正如我上面所说,我需要一个仅配置的解决方案)。但是,我认为我也可以发布它,因为(a)它可以帮助其他人处理相同的问题,并且(b)它似乎在许多其他用例中除了添加空行之外也是有用的。

无论如何,我的解决方案是编写我自己的名为ConditionalCompositeConverter的Converter类,用于在编码器/布局模式中表达通用的“if-then”逻辑(例如“仅显示X如果” Y是真的“)。与%replace转换字一样,它会扩展CompositeConverter(因此可能包含子转换器);它还需要一个或多个评估员,它们提供测试条件。源代码如下:

<强> 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);
    }
}

然后我在配置文件中使用此转换器,如下所示:

<强> 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>

我认为这比以前的解决方案更优雅,因为它允许我使用单个Appender来处理空白和非空白消息。 %onlyShowIf转换字告诉Appender像往常一样解析提供的模式,除非消息是空白的,在这种情况下跳过整个事情。然后在转换单词结束后有换行符号,以确保打印换行符,无论消息是否为空。

此解决方案的唯一缺点是主模式(包含子转换器)必须在FIRST中传递,作为括号内的参数,而评估器必须在最后传递,通过卷曲中的选项列表-braces;这意味着这个“if-then”结构必须在“if”部分之前有“then”部分,这看起来有点不直观。

无论如何,我希望这证明对有类似问题的人有帮助。我不会“接受”这个答案,因为我仍然希望有人会提出一个仅适用于我的具体情况的配置解决方案。

答案 1 :(得分:3)

使新行中的logback分开的令牌是%n。 你可以做什么,而不是在模式的最后使用一个%n,你使用两次%n%n,如下例所示。

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

我在这里试过,但效果很好。

答案 2 :(得分:0)

MegaJar, 我对日志的了解有限,但是由于您的登录目标是System.out和System.err,所以为什么不使用

System.out.println(“ \ n”); System.err.println(“ \ n”);

空白行?

另请参见What is the difference between Java Logger and System.out.println。有关重定向出或错误到文件的信息,请参见System.out to a file in java