Log4j2 RoutingAppender с ListAppenders в нескольких потоках

(Это продолжение моего первоначального вопроса: Пользовательское приложение Log4j2 в Maven Surefire: возможно ли повторное использование в разных потоках?)

Я пытаюсь выполнить модульное тестирование результатов любого журнала, поступающего из Log4j2, в моем приложении Spring через JUnit5. Чтобы помочь в этом, я создал расширение JUnit5, которое создает WriterAppender перед каждым тестом, который я могу использовать для проверки любых зарегистрированных сообщений.

Maven используется вместе с плагином Surefire. По умолчанию Surefire разделяет несколько потоков для ускорения тестирования. Однако было отмечено, что каждое из этих тестовых приложений также будет содержать выходные данные других тестов, которые выполняются в то же время.

(Я на себе убедился в этом.)

Было предложено использовать RoutingAppender, который создает ListAppender на основе значения, хранящегося в ThreadContext, что я и пытаюсь сделать.

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

Я попробовал следующую конфигурацию XML (и ее варианты):

log4j2.xml

<?xml version = "1.0" encoding = "UTF-8"?>
<Configuration status = "WARN" name = "RoutingForTests">
    <Appenders>
        <Routing name = "Routing">
            <Routes pattern = "$${ctx:appenderName}">
                <Route>
                    <List name = "$${ctx:appenderName}" />
                </Route>
            </Routes>
        </Routing>
    </Appenders>
    <Loggers>
        <Root level = "info">
            <AppenderRef ref = "Routing" />
        </Root>
    </Loggers>
</Configuration>

ThreadContext настраивается (правильно или неправильно) через зарегистрированное расширение JUnit5, например.

LoggerExtension.java

public class LoggerExtension implements BeforeEachCallback, AfterEachCallback {
    // ...

    @Override
    public void beforeEach(ExtensionContext context) {
        // ...
        ThreadContext.put("appenderName", context.getUniqueId()); // I've even tried hardcoding this value
        // ...
    }

    public void verifyMessage(String message) {
        ListAppender appender = ListAppender.getListAppender(ThreadContext.get("appenderName")); // appender ends up null
        // ...
    }
}

Я также пробовал использовать жестко запрограммированное имя ListAppender и ту же переменную поиска, но с одним $ вместо двух (но я думаю, что это приведет к пустому/нулевому значению, поскольку я не думаю, что ThreadContext было правильно настройки на этом этапе, хотя я не могу быть уверен). Я также пытался установить key вместо Route, но безрезультатно.

Я даже пытался использовать ThreadId из event, но все попытки найти ListAppender также не увенчались успехом.

Я предполагаю, что я либо что-то упускаю, либо неправильно понимаю, как это следует применять.

Я также пытался сделать это программно, но вышло что-то вроде путаницы.

Любая помощь будет оценена по достоинству, и я заранее извиняюсь, если это что-то простое, что я каким-то образом умудрился упустить из виду.

Заранее спасибо!

Источники:

Зачем вы ведете журналы модульного тестирования?

aled 18.05.2024 20:49

Журналы являются важной частью любой системы, будь то сообщения, ориентированные на бизнес, или сообщения, ориентированные на поддержку/производительность. Как и любая другая подсистема, важно убедиться, что они работают правильно при развертывании. Последнее, что мне (или кому-либо еще) нужно, — это устранить проблему или иным образом предоставить ответы, которые можно получить из журналов, но их нет на месте. Береженого Бог бережет.

BtySgtMajor 18.05.2024 21:31

Я использовал Spring OutputCaptureExtension и отлично работает. Почему у вас это не работает?

silver_mx 18.05.2024 23:28

Потому что я не хочу менять сигнатуру каждого модульного теста, который хочу проверить, и потому, что при этом фиксируются все выходные данные. Я хочу изолировать сами журналы и быть уверенным, что они поступают из самого Log4j. OutputCaptureExtension — мое последнее средство.

BtySgtMajor 18.05.2024 23:34
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
4
112
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Приложение маршрутизации немного своеобразно, когда дело доходит до замены свойств:

  • Атрибуты pattern элемента Routes оцениваются дважды (во время настройки и каждый раз при регистрации события) и поэтому требуют экранирования $, как вы это сделали,
  • Однако содержимое элемента Route оценивается только один раз, когда pattern оценивается как новое значение. Поэтому не требуется экранировать $.

Вам следует использовать:

        <Routing name = "Routing">
            <Routes pattern = "$${ctx:appenderName}">
                <Route>
                    <List name = "${ctx:appenderName}" />
                </Route>
            </Routes>
        </Routing>

Чтобы получить приложение, специфичное для теста, используйте:

LoggerContext context = LoggerContext.getContext(false);
RoutingAppender routing = context.getConfiguration().getAppender("Routing");
AppenderControl appenderRef = routing.getAppenders().get(extensionContext.getUniqueId());
ListAppender list = (ListAppender) appenderRef.getAppender();

Примечание: приложения к спискам создаются лениво, если ни одно сообщение не было зарегистрировано, приложение не будет присутствовать.

Я попробовал использовать одиночный $ для имени приложения к списку, но это не сработало, поэтому я предполагаю, что проблема в том, как я пытаюсь получить приложение к списку. Сегодня я отсутствую у компьютера, но попробую завтра и тогда отчитаюсь. Еще раз спасибо за вашу помощь!

BtySgtMajor 18.05.2024 23:22

Приложение маршрутизации не регистрирует свои подприложения в основном контейнере приложения, поэтому ListAppender.getListAppender не работает. Получить их можно только позвонив RoutingAppender.getAppenders().

Piotr P. Karwasz 19.05.2024 09:37

Я обновил XML, и мне пришлось изменить последнюю строку на ListAppender listAppender = (ListAppender) routingAppender.getAppenders().get(ThreadContext.get("append‌​erName")).getAppende‌​r();, но я получил сообщение об ошибке, сообщающее, что get(...) возвращает значение null. Я проверил карту, возвращенную getAppenders(), и она пуста. Есть ли что-нибудь еще, что я могу проверить? Я явно делаю что-то не так, но не знаю, что именно.

BtySgtMajor 19.05.2024 21:56

Я исправил код в ответе согласно вашему комментарию, и он должен работать. Обратите внимание, что appenderRef будет null, если ни одно сообщение не было зарегистрировано, поэтому вы можете добавить к этому утверждение.

Piotr P. Karwasz 20.05.2024 06:25

Я проверил, что сообщение регистрируется, выдав сообщение на консоль сразу после вызова log.info в тестируемом классе. Он все еще показывает, что list.getMessages() пуст. Я также подтвердил, что сам ListAppender возвращается правильно, поскольку проверка его имени показывает правильное имя. Было бы нормально, если бы я связался с вами через DM здесь, чтобы поделиться более подробной информацией о настройке? Я уважаю, что вы, вероятно, очень заняты, поэтому я пойму, если это невозможно. В любом случае спасибо!

BtySgtMajor 20.05.2024 18:29
ListAppender может работать двумя способами: если вы указали макет в своей конфигурации (например, <PatternLayout pattern = "%m"/>, то ListAppender.getMessages() будет непустым, в противном случае ListAppender.getEvents() будет непустым. Напишите мне в Директ.
Piotr P. Karwasz 20.05.2024 23:52

Ах, вот это было! getEvents() содержало то, что мне было нужно. Я не знал о разнице между этим и getMessages(). Огромное спасибо за вашу помощь!

BtySgtMajor 21.05.2024 19:33

Другие вопросы по теме