11

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

我有以下设置:

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.

如您所见,这些日志记录语句都没有按我需要的方式工作。

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

在对评估器、标记器等进行了大量实验后,我终于找到了一个解决方案,虽然相当笨拙,但具有预期的效果。这是一个两步解决方案:

  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>

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

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,并提出以下问题:有没有更好的方法来做到这一点?

PS 作为最后一点,仅配置的解决方案会更可取;如果可能的话,我想避免编写自定义 Java 类(过滤器、标记等)来获得这种效果。原因是,我正在从事的项目是一种“元项目”——它是一个根据用户标准生成其他程序的程序,而这些生成的程序就是 Logback 所在的地方。因此,我编写的任何自定义 Java 代码都必须复制到那些生成的程序中,如果可以避免的话,我宁愿不这样做。


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

4

3 回答 3

7

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

无论如何,我的解决方案是编写我自己的 Converter 类,命名为ConditionalCompositeConverter,用于在编码器/布局模式中表达通用的“if-then”逻辑(例如“仅在 Y 为真时显示 X”)。像%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 来处理空白和非空白消息。转换词告诉 Appender 像往常一样解析提供的%onlyShowIf模式,除非消息是空白的,在这种情况下跳过整个事情。然后在转换词的末尾有一个换行符,以确保无论消息是否为空白都打印换行符。

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

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

于 2013-08-29T13:50:40.750 回答
3

使 logback 在新行中分开的标记是 %n。您可以做什么而不是在模式末尾使用一个 %n ,而是使用两次 %n%n ,如下例所示。

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

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

于 2015-01-15T17:14:04.207 回答
0

MegaJar,我的日志知识是有限的,但由于你的 logback 目标是 System.out 和 System.err,你为什么不使用

System.out.println("\n"); System.err.println("\n");

空行?

另请参阅Java Logger 和 System.out.println 有什么区别。对于将 out/err 重定向到文件,请参阅System.out 到 java 中的文件

于 2019-07-11T03:24:52.587 回答