如何使Logback日志成为空行,而不包括模式字符串? [英] How to make Logback log a blank line, without including the pattern string?

查看:797
本文介绍了如何使Logback日志成为空行,而不包括模式字符串?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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.

我有以下设置:

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.

正如您所看到的,这些日志记录语句都不能满足我的需要。

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:


  1. 修改每个现有Appender中的过滤器,以便它们只允许非空消息。

  2. 创建每个Appender的副本;修改重复项,使其过滤器只允许空消息,并且它们的模式只包含换行令牌。

生成的文件如下所示:

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>

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

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.

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

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.

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

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

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

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.



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


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.

推荐答案

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

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.

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

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>

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

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.

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

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.

这篇关于如何使Logback日志成为空行,而不包括模式字符串?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆