Dieser kurze Artikel befasst sich mit der Verwendung von MDC in einem Spring-Projekt. Der Grund, den Artikel zu schreiben, war ein weiterer kürzlich veröffentlichter Artikel über Habré .
Wir sind ein kleines Team von Backend-Entwicklern, auch ich, die an einem mobilen App-Server-Projekt für eine Organisation arbeiten. Anwendungen werden nur von seinen Mitarbeitern verwendet und wir haben keine nennenswerte hohe Belastung. Aus diesem Grund haben wir den bekanntesten Stack für den Server ausgewählt: Java und Spring Boot für Servlet-Container.
- 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));
}
}
Und ja, wir haben unsere eigene Alternative zu MDC.MDCCloseable geschrieben:
/**
* 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);
}
}
Diese Klasse kann im Anwendungscode separat verwendet werden, um einige zusätzliche Felder festzulegen, die in Zukunft bei der Suche nach Protokollen im Aggregator hilfreich sind, z. B.:
// ... - ...
try (var ignored = LoggingUtils.mdcCloseable(MdcKeys.SOME_EXT_SVC_URL, url) {
/*
MdcKeys.SOME_EXT_SVC_URL url. */
}
// ... - ...
All dies kann sich als die wildeste Überentwicklung herausstellen.
Ich bin mit Reactor und WebFlux nicht sehr vertraut, daher denke ich, dass es etwas schwieriger wäre, einen ähnlichen Ansatz mit ihnen anzuwenden.
Lombok ; var
, Map.of
, Set.of
Und andere Funktionen der neuen Versionen von Java; StreamEx ist alles Feuer.
Schlagen Sie nicht streng für den ersten Artikel über die Ressource.