如何使Logback记录一个空行,而不包括模式字符串?

11
我有一个使用SLF4J/Logback的Java应用程序。我似乎找不到一种简单的方法来使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.

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

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

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

  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,并提出问题:有更好的方法吗?

P.S. 最终注释,最好是仅使用配置就能解决的方法;如果可能的话,我希望避免编写自定义Java类(过滤器、标记等)来实现此效果。原因是,我正在开发的项目是一种“元项目”--它是一个根据用户条件生成其他程序的程序,而那些生成的程序就是Logback的位置。因此,我编写的任何自定义Java代码都必须复制到那些生成的程序中,如果可能的话,我宁愿避免这样做。


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


这可能听起来有点琐碎,但是...为什么?日志记录应该处理事件,试图让logback写入空行几乎与此相反。这就是为什么你不得不去做一些扭曲的努力来实现它的原因。我非常确定没有仅通过配置即可完成你想要的操作,因为这不是日志框架应该执行的部分。话虽如此...使用标记应该有一个更优雅的解决方案。您可以为空行指定一个标记,将其给到您的空事件,并在它们上过滤appender的使用。 - sheltem
@sheltem:主要是可读性问题。生成的项目使用客户端-服务器系统,双方都会打印日志消息以显示当前状态。如果没有空行将它们分开,所有这些消息就会混在一起,很难一眼区分它们。关于您的标记建议,您有示例吗?我尝试过使用标记,但最终仍然需要复制我的Appenders:一个用于接受标记(并只打印换行符),另一个不接受标记(并打印真实消息)。如果您知道如何使用一个Appender完成相同的事情,请分享! - MegaJar
不好意思,这基本上就是我对标记点子的想法了。或者是能够将%replace与标记点绑定在一起,但我没有找到任何可行的解决方案。如果这对你很重要,你可以尝试写信给logback邮件列表。这可能会让你接触到比我更熟悉这个主题的人。 ;) - sheltem
3个回答

7
我已经对此进行了进一步的操作,并想出了一种实现我想要效果的替代方法。现在,这个解决方案涉及编写自定义 Java 代码,这意味着它实际上无法帮助我解决特定的问题(因为如上所述,我需要一个仅基于配置的解决方案)。然而,我认为我可能会发帖,因为 (a) 它可能帮助其他遇到相同问题的人,(b) 它看起来似乎在许多其他用例中都很有用,而不仅仅是添加空白行。

无论如何,我的解决方案是编写自己的转换器类,命名为 ConditionalCompositeConverter,用于在编码器/布局模式中表示通用的“如果-则”逻辑(例如,“仅在 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来处理空白和非空白消息。转换词%onlyShowIf告诉Appender按照通常的方式解析提供的模式,除非消息为空,否则跳过整个过程。然后,在转换词结束后有一个换行符号,以确保无论消息是否为空都打印换行符。
这种解决方案唯一的缺点是主模式(包含子转换器)必须首先作为括号内的参数传递,而评估器必须通过花括号中的选项列表在最后传递;这意味着这个“if-then”结构必须在“if”部分之前具有“then”部分,看起来有些不直观。
总之,我希望这对于有类似问题的人们有所帮助。我不会“接受”这个答案,因为我仍然希望有人能想出一个适用于我的特定情况的仅配置解决方案。

3

使logback在新行中分隔的令牌是%n。

您可以使用两个%n%n来代替在模式末尾使用一个%n,如下面的示例所示。

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

我在这里尝试过,结果很好。


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接