この短い記事では、SpringプロジェクトでMDCを使用する方法に焦点を当てます。この記事を書いた理由は、Habréに関する最近の別の記事でした。
私も含めて、バックエンド開発者の小さなチームであり、組織のモバイルアプリサーバープロジェクトに取り組んでいます。アプリケーションは従業員のみが使用し、大きな負荷はありません。そのため、サーバーには最も使い慣れたスタックであるJavaとサーブレットコンテナのSpringBootを選択しました。
- 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. */
}
// ... - ...
上記のすべては、最もワイルドなオーバーエンジニアリングであることが判明する可能性があります。
私はReactorとWebFluxにあまり詳しくないので、同様のアプローチを適用するのは少し難しいと思います。
ロンボク; var
、Map.of
、Set.of
およびJavaの新しいバージョンの他の機能。StreamExはすべて火事です。
リソースの最初の記事を厳密に打ち負かさないでください。