Как сделать журнал журнала пустой строкой, не включая строку шаблона?

У меня есть приложение 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.

Как вы можете видеть, ни одно из этих операторов регистрации не работает так, как мне нужно.

  • Если я просто зарегистрирую пустую строку, шаблон кодировки все еще добавляется к сообщению, хотя сообщение пусто.
  • Если я вставляю символ новой строки в начале или в середине строки, все после этого будет отсутствовать префикс шаблона, потому что шаблон применяется только один раз в начале сообщения.
  • Если я вставляю символ новой строки в конец строки, он создает желаемую пустую строку, но это все еще только частичное решение; он по-прежнему не позволяет мне выводить пустую строку перед зарегистрированным сообщением.

После долгих экспериментов с оценщиками, маркерами и т.д. я, наконец, пришел к решению, которое, хотя и довольно громоздко, имеет желаемый эффект. Это двухэтапное решение:

  • Измените фильтры в каждом существующем 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>%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, и он, конечно, не очень масштабируемый. Не говоря уже о том, что для достижения такого простого результата кажется серьезным излишеством сделать эту работу.

Итак, я отправляю свою проблему в Qaru с вопросом: Есть ли лучший способ сделать это?

P.S. В качестве заключительного замечания было бы предпочтительным решение только для конфигурации; Я хотел бы избежать необходимости писать собственные классы Java (фильтры, маркеры и т.д.), Чтобы получить этот эффект, если это возможно. Разумеется, проект, над которым я работаю, является своего рода "метапроектом" - это программа, которая генерирует ДРУГИЕ программы, основанные на пользовательских критериях, и те сгенерированные программы, где будет работать Logback. Поэтому любой пользовательский код Java, который я пишу, должен быть скопирован в эти сгенерированные программы, и я бы предпочел не делать этого, если бы мог его избежать.


EDIT: Я думаю, что это действительно сводится к следующему: есть ли способ вставить условную логику в шаблон макета Appender? Другими словами, иметь Appender, который использует стандартный шаблон макета, но условно модифицировать (или игнорировать) этот шаблон в определенных случаях? По сути, я хочу сказать моему Appender: "Используйте эти фильтры (фильтры) и эту целевую точку вывода и используйте этот шаблон IF условие X истинно, иначе используйте этот другой шаблон". Я знаю, что определенные термины преобразования (например, %caller и %exception) позволяют вам присоединить к ним Оценщик, так что этот термин отображается только в том случае, если Evaluator возвращает true. Проблема в том, что большинство терминов не поддерживают эту функцию, и я, конечно, не знаю, как применить метод Evaluator к шаблону ENTIRE сразу. Следовательно, необходимо разделить каждого Appender на два, каждый со своим собственным отдельным оценщиком и шаблоном: один для пустых сообщений и один для непустых сообщений.

Ответ 1

Я играл с этим еще немного, и я придумал альтернативный метод достижения эффекта, который я хочу. Теперь это решение связано с написанием специального кода Java, что означает, что на самом деле это не помогло бы с моей конкретной ситуацией (так как, как я уже сказал, мне нужно только решение для конфигурации). Тем не менее, я полагал, что могу опубликовать его, потому что (а) он может помочь другим с той же проблемой, и (б) кажется, что он был бы полезен во многих других случаях использования, помимо добавления пустых строк.

В любом случае, моим решением было написать собственный класс конвертера с именем ConditionalCompositeConverter, который используется для выражения логики "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 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" должна иметь "затем" часть ПЕРЕД частью "если", которая выглядит несколько неинтуитивной.

В любом случае, я надеюсь, что это окажется полезным для всех, у кого есть подобные проблемы. Я не собираюсь "принимать" этот ответ, так как я все еще надеюсь, что кто-то придумает конфигурационное решение, которое будет работать в моем конкретном случае.

Ответ 2

Токен, который делает логарифм отдельно в новых строках, равен% n. Что вы можете сделать вместо того, чтобы использовать один% n в конце шаблона, вы используете дважды% n% n, как в примере ниже.

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

Я пробовал здесь, и он работал нормально.

Ответ 3

MegaJar, мои познания в журналах ограничены, но поскольку вашими целями регистрации являются System.out и System.err, почему бы вам не использовать

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

для пустых строк?

Также см. В чем разница между Java Logger и System.out.println. А для перенаправления /err в файл см. System.out в файл в java.