[Java] Кейс использования Mapping Diagnostic Context и @Async
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Речь в этой небольшой заметке пойдёт про то, как с пользой можно использовать MDC в Spring-проекте. Поводом написать статью послужила другая недавняя статья на Хабре.
Мы — небольшая команда backend-разработчиков, включая меня — работаем над проектом сервера для мобильных приложений для некой организации. Приложениями пользуются только её сотрудники и значительного highload-а у нас нет. Поэтому для сервера мы выбрали максимально привычный нам стек: Java и Spring Boot на servlet-контейнерах.Здесь я хотел бы рассказать про свой подход к логированию бизнес-процессов при помощи MDC. Почему именно MDC? Фактически, имеется несколько реплик приложения, которые развёрнуты в Kubernetes, и все логи стекаются в единый агрегатор (Graylog). В конфигурации logback-а добавлен специальный appender, который отправляет все логи по нужному адресу, а также к каждому сообщению добавляет все имеющиеся в MDC поля:logback-graylog.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<springProperty name="graylog_environment"
scope="context"
source="logging.graylog.environment"
defaultValue="local"/>
<springProperty name="graylog_host"
scope="context"
source="logging.graylog.host"
defaultValue="127.0.0.1"/>
<springProperty name="graylog_port"
scope="context"
source="logging.graylog.port"
defaultValue="12201"/>
<springProperty name="graylog_microservice"
scope="context"
source="logging.graylog.microservice"
defaultValue=""/>
<appender name="UDP_GELF"
class="biz.paluch.logging.gelf.logback.GelfLogbackAppender">
<host>${graylog_host}</host>
<port>${graylog_port}</port>
<version>1.1</version>
<extractStackTrace>true</extractStackTrace>
<filterStackTrace>true</filterStackTrace>
<includeFullMdc>true</includeFullMdc>
<additionalFields>environment=${graylog_environment},microservice=${graylog_microservice}</additionalFields>
<additionalFieldTypes>environment=String,microservice=String</additionalFieldTypes>
<timestampPattern>yyyy-MM-dd HH:mm:ss,SSS</timestampPattern>
<maximumMessageSize>8192</maximumMessageSize>
</appender>
<root level="DEBUG">
<appender-ref ref="UDP_GELF"/>
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
Это очень удобно — можно отправить название окружения (боевое или одно из тестовых), название конкретного микросервиса, Spring Cloud Sleuth добавляет в него данные трассировки (traceId и spanId), а мне захотелось отправлять туда такие поля, которые в последствии помогут искать в Graylog-е причины каких-то сбоев именно мне. Если на вашем проекте используется стек ELK, то скорее всего там всё аналогично, просто мне с ним сталкиваться не приходилось.Подавляющая часть входящих запросов проходит через кастомный Security-фильтр, который по заголовкам запроса определяет пользователя. Именно в этот момент удобно подложить в MDC то, что хочется. Сперва описываем класс с теми полями, которые хочется подложить:
@UtilityClass
public class MdcKeys {
/**
* Значение HTTP-заголовка User-Agent из внешнего запроса.
*/
public final String MDC_USER_AGENT = "user-agent";
/**
* Значение заголовка Authorization из внешнего запроса.
*/
public final String MDC_USER_TOKEN = "authorization";
/**
* Логин пользователя, от имени которого поступил запрос.
*/
public final String MDC_USER_LOGIN = "login";
/**
* URL, на который поступил внешний запрос.
*/
public final String MDC_API_URL = "apiUrl";
// ... и некоторые другие ...
}
Если просто устанавливать значения через MDC#put то они не будут удаляться из него после обработки запроса, что чревато проблемами: не все запросы должны быть авторизованы, поэтому часть проскакивает мимо вызова AuthenticationManager-а. Так как потоки, которые обрабатывают запросы, живут в servlet-контейнере и не пересоздаются, старые значения "замусоривают" нам картину. Выход простой — обернуть в try-catch и почистить за собой в блоке finally.Далее возникает вопрос о том, что делать с методами, помеченными аннотацией @Async. Методы, помеченные ей, выполняются на отдельном пуле потоков, а так как значения в MDC локальны по отношению к текущему потоку, нужно их как-то туда скопировать и в конце метода не забыть удалить. Решение подсмотрено опять же у Spring Security. Находим место в конфигурации, где у нас создаётся пул потоков:
/**
* TaskExecutor, используемый для запуска асинхронных методов.
*/
@Bean
@Qualifier("taskExecutor")
TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
// ... код настройки taskExecutor-а опущен ...
taskExecutor.setTaskDecorator(new AsyncTaskCustomDecorator());
return taskExecutor;
}
И устанавливаем декоратор, который будет оборачивать все запускаемые асинхронные методы своим кодом:
private static class AsyncTaskCustomDecorator implements TaskDecorator {
@Override
@NonNull
public Runnable decorate(@NonNull Runnable runnable) {
var runnableWithRestoredMDC = LoggingUtils.decorateMdcCopying(runnable);
return new DelegatingSecurityContextRunnable(runnableWithRestoredMDC);
}
}
Фактически, оборачивание происходит дважды: один раз нашим кодом (LoggingUtils#decorateMdcCopying) и один раз для самой Spring Security (иначе мы сломаем передачу контекста в SecurityContextHolder-е). Можно было бы "сделать всю работу" на месте, но мы вынесли код в утильный класс. Смотрим дальше:
@UtilityClass
public class LoggingUtils {
private final Set<String> COPYABLE_MDC_FIELDS = Set.of(
MdcKeys.MDC_USER_AGENT,
MdcKeys.MDC_USER_TOKEN,
MdcKeys.MDC_USER_LOGIN,
MdcKeys.MDC_API_URL,
MdcKeys.MDC_MOBILE_FEATURE);
/**
* Декорирует Runnable таким образом, что при его вызове в другом потоке
* будут восстановлены значения нескольких определённых полей MDC из
* текущего потока.
*/
public Runnable decorateMdcCopying(Runnable runnable) {
// Получаем значения, которые нужно будет восстановить в MDC.
Map<String, String> mdcMap = getMdcMeaningfulMap();
return () -> {
// Восстанавливаем в значения полей MDC из потока-создателя.
try (var ignored = mdcCloseable(mdcMap)) {
// Вызываем оригинальный код.
runnable.run();
}
};
}
private Map<String, String> getMdcMeaningfulMap() {
return StreamEx.of(COPYABLE_MDC_FIELDS)
.mapToEntry(MDC::get)
.nonNullValues()
.toMap();
}
public MdcCloseable mdcCloseable(Map<String, String> values) {
// Если нечего устанавливать, вернём ничего не закрывающий singleton.
if (MapUtils.isEmpty(values)) {
return MdcCloseable.EMPTY;
}
// Вариант, когда нам не нужно будет ничего восстанавливать в MDC.
var mdcMap = MapUtils.emptyIfNull(MDC.getCopyOfContextMap());
if (MapUtils.isEmpty(mdcMap)) {
return new MdcCloseable(values, Collections.emptyMap());
}
// Значения, которые нужно будет вернуть в MDC
// (если они там были до нас).
Map<String, String> original = EntryStream.of(mdcMap)
.nonNullValues()
.filterKeys(values::containsKey)
.filterKeyValue((k, v) -> Objects.equals(v, mdcMap.get(k)))
.toMap();
return new MdcCloseable(values, original);
}
public MdcCloseable mdcCloseable(String key, String value) {
return mdcCloseable(Map.of(key, value));
}
}
И, да, мы написали собственную альтернативу MDC.MDCCloseable:
/**
* Данный класс отличается от org.slf4j.MDC.MDCCloseable следующим:
* <ol>
* <li>работает со множеством значений,</li>
* <li>умеет восстанавливать предыдущие значения.</li>
* </ol>
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class MdcCloseable implements Closeable {
public static final MdcCloseable EMPTY = new MdcCloseable(
Collections.emptySet(),
Collections.emptyMap());
private final Set<String> values;
private final Map<String, String> original;
MdcCloseable(Map<String, String> values, Map<String, String> original) {
this(values.keySet(), original);
values.forEach(MDC::put);
}
@Override
public void close() {
// Удалили все новые значения при закрытии.
values.forEach(MDC::remove);
// Восстановили все предыдущие значения.
original.forEach(MDC::put);
}
}
Данный класс можно использовать отдельно в коде приложения для установки каких-то дополнительных полей, которые помогут в дальнейшем искать логи в агрегаторе, например:
// ... какой-то код ...
try (var ignored = LoggingUtils.mdcCloseable(MdcKeys.SOME_EXT_SVC_URL, url) {
/* Все логи в агрегаторе из этого блока кода будут содержать
ключ MdcKeys.SOME_EXT_SVC_URL и значение url. */
}
// ... какой-то код ...
Всё вышеописанное может оказаться дичайшим over-engineering-ом.Я не очень близко знаком с Reactor и WebFlux, поэтому мне кажется, что применить с ними аналогичный подход будет немного сложнее.Lombok; var, Map.of, Set.of и другие фичи новых версий Java; StreamEx — это всё огонь.Не бейте строго за первую статью на ресурсе.
===========
Источник:
habr.com
===========
Похожие новости:
- [Тестирование IT-систем, JavaScript] Не используйте фикстуры в Cypress и юнит-тесты — используйте фабричные функции (перевод)
- [Программирование] Дайджест материалов сообщества Deno (01.01 — 31.01)
- [Программирование, Java] Spring Cloud и Spring Boot. Часть 2: использование Zipkin Server для распределенной трассировки (перевод)
- [Java, C#, Учебный процесс в IT, Карьера в IT-индустрии] Как стать разработчиком Java и С#: открываем онлайн-практикум с поддержкой менторов
- [JavaScript, Node.JS, ReactJS] Домашнее IoT-устройство глазами JS-разработчика
- [JavaScript, Программирование, HTML, Браузеры, DIY или Сделай сам] How I create browser applications inside browsers (перевод)
- [Программирование, Java, Scala, Функциональное программирование] Еще раз про try и Try
- [JavaScript, API, ReactJS] Делаем страницу на React с базой сотрудников при помощи Airtable и Quarkly
- [Ненормальное программирование, Программирование, Java, API, C#] Вы всё ещё ловите исключения? Тогда мы к вам
- [Open source, Java] Конфигурация Java систем — как убрать боль
Теги для поиска: #_java, #_java, #_spring_mvc, #_slf4j, #_logback, #_mdc, #_graylog, #_java
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:19
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Речь в этой небольшой заметке пойдёт про то, как с пользой можно использовать MDC в Spring-проекте. Поводом написать статью послужила другая недавняя статья на Хабре. Мы — небольшая команда backend-разработчиков, включая меня — работаем над проектом сервера для мобильных приложений для некой организации. Приложениями пользуются только её сотрудники и значительного highload-а у нас нет. Поэтому для сервера мы выбрали максимально привычный нам стек: Java и Spring Boot на servlet-контейнерах.Здесь я хотел бы рассказать про свой подход к логированию бизнес-процессов при помощи MDC. Почему именно MDC? Фактически, имеется несколько реплик приложения, которые развёрнуты в Kubernetes, и все логи стекаются в единый агрегатор (Graylog). В конфигурации logback-а добавлен специальный appender, который отправляет все логи по нужному адресу, а также к каждому сообщению добавляет все имеющиеся в MDC поля:logback-graylog.xml <?xml version="1.0" encoding="UTF-8"?>
<configuration> <include resource="org/springframework/boot/logging/logback/defaults.xml"/> <include resource="org/springframework/boot/logging/logback/console-appender.xml"/> <springProperty name="graylog_environment" scope="context" source="logging.graylog.environment" defaultValue="local"/> <springProperty name="graylog_host" scope="context" source="logging.graylog.host" defaultValue="127.0.0.1"/> <springProperty name="graylog_port" scope="context" source="logging.graylog.port" defaultValue="12201"/> <springProperty name="graylog_microservice" scope="context" source="logging.graylog.microservice" defaultValue=""/> <appender name="UDP_GELF" class="biz.paluch.logging.gelf.logback.GelfLogbackAppender"> <host>${graylog_host}</host> <port>${graylog_port}</port> <version>1.1</version> <extractStackTrace>true</extractStackTrace> <filterStackTrace>true</filterStackTrace> <includeFullMdc>true</includeFullMdc> <additionalFields>environment=${graylog_environment},microservice=${graylog_microservice}</additionalFields> <additionalFieldTypes>environment=String,microservice=String</additionalFieldTypes> <timestampPattern>yyyy-MM-dd HH:mm:ss,SSS</timestampPattern> <maximumMessageSize>8192</maximumMessageSize> </appender> <root level="DEBUG"> <appender-ref ref="UDP_GELF"/> <appender-ref ref="CONSOLE"/> </root> </configuration> @UtilityClass
public class MdcKeys { /** * Значение HTTP-заголовка User-Agent из внешнего запроса. */ public final String MDC_USER_AGENT = "user-agent"; /** * Значение заголовка Authorization из внешнего запроса. */ public final String MDC_USER_TOKEN = "authorization"; /** * Логин пользователя, от имени которого поступил запрос. */ public final String MDC_USER_LOGIN = "login"; /** * URL, на который поступил внешний запрос. */ public final String MDC_API_URL = "apiUrl"; // ... и некоторые другие ... } /**
* TaskExecutor, используемый для запуска асинхронных методов. */ @Bean @Qualifier("taskExecutor") TaskExecutor taskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); // ... код настройки taskExecutor-а опущен ... taskExecutor.setTaskDecorator(new AsyncTaskCustomDecorator()); return taskExecutor; } private static class AsyncTaskCustomDecorator implements TaskDecorator {
@Override @NonNull public Runnable decorate(@NonNull Runnable runnable) { var runnableWithRestoredMDC = LoggingUtils.decorateMdcCopying(runnable); return new DelegatingSecurityContextRunnable(runnableWithRestoredMDC); } } @UtilityClass
public class LoggingUtils { private final Set<String> COPYABLE_MDC_FIELDS = Set.of( MdcKeys.MDC_USER_AGENT, MdcKeys.MDC_USER_TOKEN, MdcKeys.MDC_USER_LOGIN, MdcKeys.MDC_API_URL, MdcKeys.MDC_MOBILE_FEATURE); /** * Декорирует Runnable таким образом, что при его вызове в другом потоке * будут восстановлены значения нескольких определённых полей MDC из * текущего потока. */ public Runnable decorateMdcCopying(Runnable runnable) { // Получаем значения, которые нужно будет восстановить в MDC. Map<String, String> mdcMap = getMdcMeaningfulMap(); return () -> { // Восстанавливаем в значения полей MDC из потока-создателя. try (var ignored = mdcCloseable(mdcMap)) { // Вызываем оригинальный код. runnable.run(); } }; } private Map<String, String> getMdcMeaningfulMap() { return StreamEx.of(COPYABLE_MDC_FIELDS) .mapToEntry(MDC::get) .nonNullValues() .toMap(); } public MdcCloseable mdcCloseable(Map<String, String> values) { // Если нечего устанавливать, вернём ничего не закрывающий singleton. if (MapUtils.isEmpty(values)) { return MdcCloseable.EMPTY; } // Вариант, когда нам не нужно будет ничего восстанавливать в MDC. var mdcMap = MapUtils.emptyIfNull(MDC.getCopyOfContextMap()); if (MapUtils.isEmpty(mdcMap)) { return new MdcCloseable(values, Collections.emptyMap()); } // Значения, которые нужно будет вернуть в MDC // (если они там были до нас). Map<String, String> original = EntryStream.of(mdcMap) .nonNullValues() .filterKeys(values::containsKey) .filterKeyValue((k, v) -> Objects.equals(v, mdcMap.get(k))) .toMap(); return new MdcCloseable(values, original); } public MdcCloseable mdcCloseable(String key, String value) { return mdcCloseable(Map.of(key, value)); } } /**
* Данный класс отличается от org.slf4j.MDC.MDCCloseable следующим: * <ol> * <li>работает со множеством значений,</li> * <li>умеет восстанавливать предыдущие значения.</li> * </ol> */ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public class MdcCloseable implements Closeable { public static final MdcCloseable EMPTY = new MdcCloseable( Collections.emptySet(), Collections.emptyMap()); private final Set<String> values; private final Map<String, String> original; MdcCloseable(Map<String, String> values, Map<String, String> original) { this(values.keySet(), original); values.forEach(MDC::put); } @Override public void close() { // Удалили все новые значения при закрытии. values.forEach(MDC::remove); // Восстановили все предыдущие значения. original.forEach(MDC::put); } } // ... какой-то код ...
try (var ignored = LoggingUtils.mdcCloseable(MdcKeys.SOME_EXT_SVC_URL, url) { /* Все логи в агрегаторе из этого блока кода будут содержать ключ MdcKeys.SOME_EXT_SVC_URL и значение url. */ } // ... какой-то код ... =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:19
Часовой пояс: UTC + 5