This short article will focus on how you can use MDC in a Spring project. The reason to write the article was another recent article on Habrรฉ .
We're a small team of backend developers, myself included, working on a mobile app server project for an organization. Applications are used only by its employees and we do not have a significant highload. Therefore, we chose the most familiar stack for the server: Java and Spring Boot on servlet containers.
- 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));
}
}
And, yes, we wrote our own alternative to 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);
}
}
This class can be used separately in the application code to set some additional fields that will help in the future to search for logs in the aggregator, for example:
// ... - ...
try (var ignored = LoggingUtils.mdcCloseable(MdcKeys.SOME_EXT_SVC_URL, url) {
/*
MdcKeys.SOME_EXT_SVC_URL url. */
}
// ... - ...
All of the above can turn out to be the wildest over-engineering.
I'm not very familiar with Reactor and WebFlux, so I think it would be a little more difficult to apply a similar approach with them.
Lombok ; var
, Map.of
, Set.of
And other features of new versions of Java; StreamEx is all fire.
Do not beat strictly for the first article on the resource.