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

Posted

技术标签:

【中文标题】如何使 Logback 记录一个空行,而不包括模式字符串?【英文标题】:How to make Logback log a blank line, without including the pattern string? 【发布时间】:2013-08-25 19:22:13 【问题描述】:

我有一个设置为使用 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>%dHH:mm:ss.SSS [%thread] %-5level %logger36 - %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>%dHH:mm:ss.SSS [%thread] %-5level %logger36 - %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.

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

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

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

    修改每个现有 Appender 中的过滤器,使其仅允许非空消息。 为每个 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>%dHH:mm:ss.SSS [%thread] %-5level %logger36 - %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>%dHH:mm:ss.SSS [%thread] %-5level %logger36 - %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,并提出以下问题:有更好的方法吗?

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

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

【问题讨论】:

这听起来有点小气,但是……为什么?!日志记录应该处理事件,试图让 logback 写一个空行几乎与此相反。这就是为什么你必须竭尽全力才能实现它。我很确定没有只配置的方式来做你想做的事,仅仅是因为它不是日志框架应该做的事情的一部分。话虽这么说......应该有一个使用标记的更优雅的解决方案。您可以为空行指定一个标记,将其提供给您的空事件并过滤它们的附加程序使用情况。 @sheltem:主要是可读性问题。生成的项目使用客户端-服务器系统,双方打印日志消息以显示当前状态。没有空行分隔它们,所有这些消息都混在一起,很难一眼就将它们区分开来。关于你的标记建议,你有一个例子吗?我尝试过使用标记,但最终还是需要复制我的附加程序:一个接受标记(并且只打印一个换行符),一个不接受它(并打印真正的消息)。如果您知道如何使用 ONE Appender 做同样的事情,请分享! 不,抱歉。这几乎就是我对标记想法的想法。那或者能够将 %replace 绑定到一个标记,我没有找到任何可行的解决方案。如果它对您来说足够重要,您可以尝试写入 logback 邮件列表。它可能会让你接触到比我更了解这个主题的人。 ;) 【参考方案1】:

我已经对此进行了更多尝试,并且想出了一种替代方法来实现我想要的效果。现在,这个解决方案涉及编写自定义 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(%dHH:mm:ss.SSS [%thread] %-5level %logger36 - %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(%dHH:mm:ss.SSS [%thread] %-5level %logger36 - %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 中作为括号内的参数传递,而 Evaluator(s) 必须在最后通过 curly 中的选项列表传递- 大括号;这意味着这个“if-then”结构必须在“if”部分之前有“then”部分,这看起来有点不直观。

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

【讨论】:

【参考方案2】:

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

<pattern>%dHH:mm:ss.SSS [%thread] %-5level %logger36 - %msg%n%n</pattern>

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

【讨论】:

【参考方案3】:

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

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

对于空行?

另见What is the difference between Java Logger and System.out.println。对于将 out/err 重定向到文件,请参阅 System.out to a file in java。

【讨论】:

以上是关于如何使 Logback 记录一个空行,而不包括模式字符串?的主要内容,如果未能解决你的问题,请参考以下文章

如何使一个材料表列的编辑模式字段类型依赖于另一列的值,而不影响其他行?

如何在 Python27 中遍历文件而不遇到 ValueError 并用空行完全遍历文件?

Logback,SLF4J,Log4J2。了解它们并学习如何使用。(翻译)

如何配置logback使日志输出到mysql数据库

logback 文件名格式,包括时间格式

如何使用SBT和Scala正确管理开发和生产中的logback配置?